From 505416a4e8bdd8ea2b1df48a970ca9b347381a2c Mon Sep 17 00:00:00 2001 From: Jeany Date: Thu, 15 Oct 2026 23:35:27 -0600 Subject: [PATCH 001/238] TrigScript 4: the plan, and A0 as built The lifted game as the checker (A), the language (B), the game pushed (C). A0 ran the same day: every probe plays headless in scm-sim's lifted game. Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 500 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 docs/v4-plan.md diff --git a/docs/v4-plan.md b/docs/v4-plan.md new file mode 100644 index 0000000..3ed9215 --- /dev/null +++ b/docs/v4-plan.md @@ -0,0 +1,500 @@ +# TrigScript 4 — the plan + +Written 2026-10-15. Status: nothing built. The question it answers: now that there is a +lifted copy of the game's own code (scm-sim's exact engine) beside eudplib, what does +TrigScript become if it uses the game itself as its checker, and how far can the language +go towards the TypeScript people actually write. + +The plan has three parts, in the order they would be built: + +- **A. The game as the checker.** Tests, the address check, desyncs, costs and the + debugger, run in the lifted game instead of (or beside) the simulator. +- **B. The language.** `async`/`await`, events, function values, `try`/`catch`, + `Local` and `sync()`, fixed-point numbers, typed memory. +- **C. The game, pushed.** What a map can do with that language, one probe at a time. + +A comes first because it makes every later slice cheaper to check: most of what a probe +answers today, the lifted game answers in the editor. It cannot answer everything (see +*What the lifted game cannot tell us*), so probes stay, fewer and narrower. + +## Where 3.12 stands + +TrigScript 3.12 compiles a `program()` to an IR, `python/trigscript.py` lowers the IR +with eudplib inside the eudplib plugin, and Save writes the built map. The language has +signed numbers, arrays and a heap, records, `Map`/`Set`, strings, classes, called +functions, recursion, callbacks, `Unit` objects, input through MSQC and chatEvent, and +`test()` blocks run by `compiler/simulateIr.ts` on the game's trigger schedule. + +Two costs have stayed with every slice since 3.0: + +1. **Each slice that touches the game has a probe the user plays by hand.** The probe's + lines say "(expect …)" and a person reads them in the game. Sixteen probes so far. +2. **The simulator is a model.** It answers what it was written to answer. A read it + does not model gives 0, and 9½ made that visible rather than fixing it. + +## What scm-sim changes + +scm-sim's exact engine is StarCraft 1.16.1's executable and storm.dll lifted from x86 to +TypeScript (`src/exact/`, the generated module under `data/lifted/`, never committed). +What is already measured there, and what this plan leans on: + +- It plays maps as well as replays: single player, UMS, the game's own computers, and + several people seated on one game instance (`src/exact/net.ts`). Node runs 2,200–4,100 + frames a second; a start takes 0.7 s. +- The whole of its state is one `DataView` at the game's own addresses (`LiftedGame.m`). + Reading a variable of a built map is a read at an address. +- Snapshots cost about 45 ms and 0.6 MB each, and `rewind.ts` seeks backwards from the + nearest one. That is time travel, already built for the replay viewer. +- Routines can be hooked by address when the module is generated (`liftgame.py --hook`), + as the two sound routines are, and any routine can be called between turns + (`L.callAt`), as the placement check is. +- Trigger text, countdown and outcome are read from memory (`ExactLiveEngine.triggerView`). +- **EUD, measured 2026-10-12:** extended-player Deaths *reads* work; `SetDeaths` *writes* + past the table are refused by 1.16.1's own bounds check. So an eudplib-built map, + which writes past the table from its first frame, does not run in the lifted game as + it stands. Lifting that check is the first job (slice 0). + +### What the lifted game cannot tell us + +The lifted game is 1.16.1. TrigScript programs build for Remastered, whose EUD support +is an emulation of 1.16.1's memory, not the memory itself. The two differ in ways we +have already met: + +| Probe | 1.16.1 meaning | Remastered | +| --- | --- | --- | +| Magenta 6, I | a unit's position written | ends the game ("EUD not supported") | +| Magenta 10, 1–3 | the tile buffer written through the MTXM pointer | nothing changes on screen | +| Magenta 10, 4 | sprite image flags (tint) | ends the game | +| Magenta 6, J K | a unit's own top speed | no effect; only the type's table counts | +| Magenta 6, Y H | the cloak status flags | no visible cloak | +| Magenta 5, 9 | the game speed written | no effect | +| Magenta 7 | a key held | Remastered sends one event a press, no key-up | + +Remastered's executable is packed and encrypted on disk, so its emulation table cannot +be read from the file. The user agreed on 2026-10-15 that Remastered may be taken +through Ghidra to settle rules the compiler needs, provided none of it ships. The way in +would be a dump of the running process, once it has unpacked itself, read in Ghidra; the +rules found go into the allowlist with "Remastered, disassembled" as their evidence, and +a probe still confirms each family before a library offers it. Until that is done, the +lifted game tells us **what an address means and what the triggers did with it**; +only a played probe tells us **whether Remastered allows it**. Part A turns that split +into a file (the allowlist) and a check, so the gap is seen at the line that falls into +it rather than in a game that ends. + +## Principles + +1. **It is TypeScript** (unchanged from the Remastered plan). A construct valid in + TypeScript with a sensible meaning in the game works; one without says why and what + to write instead. +2. **The simulator is the fast path; the game is the judge.** Every test still runs in + the simulator on each compile. *Run in the game* runs the same tests in the lifted + game. When the two disagree, that is a bug in one of them, and the Testing view says + both answers. +3. **The allowlist is a file of facts with sources.** Every address a built map touches + is on it with the probe and date that showed it working in Remastered, or the build + says so at the line. An `unsafe` block can go past it, with a reason the author writes. +4. **A desync is a compile error where it can be, a test failure where it cannot.** +5. **Only verified facts ship** (unchanged). A new family of addresses still gets a probe + the user plays; the lifted game shrinks what the probe has to ask. +6. **The engine stays behind the plugin edge.** The engine is a separate work (the + decision of 2026-10-12): TrigScript never vendors or imports it. TrigScript uses + `scm-sim.engine` when that plugin is installed and the person's own copy of the game is + there, and works as 3.12 does when it is not. CI has neither, so engine tests skip + there, as the game-data suites in scm-js do. + +## Part A — the game as the checker + +### A0. Spike: a built map runs in the lifted game + +What it has to show, in order: + +1. **A lifted game that lets EUD writes through.** The bounds check is code, and the + lifted code is TypeScript, so patching bytes in memory changes nothing. (As built, the + routine is replaced at run time; no second build was needed.) +2. **eudplib's output loads in 1.16.1.** Things to check: the CHK version the build + writes, `STRx` against `STR`, anything eudplib does only for Remastered. If 1.16.1 + refuses the map, can a load-time rewrite (as scm-sim does for 1.21 replays) fix it? +3. **eudTurbo holds.** It writes the countdown to 0 after each pass; 1.16.1 sets it to 30 + before running the pass, so every frame should run (verified for 3.12's schedule). +4. **The probes run headless.** `probes/spike.ts` first, then the other fifteen. Their + printed lines are read from `triggerView` and checked against their own "(expect …)" + text. A probe that has no input steps can then run without a person. + +Done when: every probe without input steps prints what it expects in the lifted game, and +each one that does not has a written reason (a 1.16.1/Remastered difference, or a bug). + +Size: 2–4 days, most of it in scm-sim. **No release.** + +**As built (2026-10-15, one day; scm-sim branch `eud-spike`, uncommitted).** Every probe +runs headless in the lifted game and says what it expected. + +- **No second build of the module.** The lifted module keeps a table of the routines + reached through a pointer, and `setEntry` replaces one at run time. The trigger loop + reaches the Deaths condition (0x42CB60) and the Set Deaths action (0x4C6CC0) that way, so + `src/exact/eud.ts` replaces those two and nothing is regenerated. Replays and Test Map + never see the replacements. +- **The scenario.** eudplib writes `VER` 206, `STRx`, and an eight-byte section of + negative length at the end. `classicScenario` writes `VER` 205, turns `STRx` into a `STR` + of the same size with every byte past the header in place, and drops the last section. +- **Where the strings are.** eudplib counts on Remastered keeping the strings section at + 0x191943C8. Its payload is relocated against that address, and it reads string offsets + from the four-byte table there. The game reads a two-byte table from wherever 0x5993D4 + points. `placeStrings` puts the `STRx` bytes at 0x191943C8 and a two-byte table for the + game just below them, both pointing at the same text. Then a text eudplib writes in place + is the text the game shows. Two layouts that seemed simpler were each wrong in one half: + table strings came out empty under one, made texts under the other. +- **The mask rule** (Remastered's `'SC'` records, which eudplib uses everywhere). The field + is `value & mask` and the number is `number & mask`. Set To writes the field, Add adds to + it and drops the carry, Subtract stops at 0. A condition compares `value & mask` with the + unmasked number. Two eudplib tricks fix this rule, and `tests/exact-eud.test.ts` holds + both: its divide-by-four (Subtract 3 << t under 7 << t) and its text decoding (Add a key + under 0xAA, then under 0x55, which is an exclusive or). The first rule I tried added the + unmasked number, and every made text came out scrambled. +- **Results.** All 15 probes were rebuilt with the 3.12 compiler and run for 3,000 frames, + 1–5 s each. The runner (`scripts/exact-eud.ts`) checks every "(expect …)" by its numbers: + **137 hold**. Of the 9 it flags, 7 hold when read by hand and 2 are one wrong + expectation (both below). It leaves 12 lines for a person, those with words in the + expectation (a name, "about 21"); all 12 read right. Not one address fell outside the + machine. +- **Read by hand, and right:** numbers I, reads E and input A put words between the value + and the bracket. The input probe stops after A, because nobody presses its keys. +- **One expectation is wrong, and the lifted game found it:** units K expects F's Marine at + 15 as the weakest of the Men. But the Zergling handed over at H is a Man too, and the + Marines hurt it to 5 during G. A person found a first version of the same mistake on + 2026-09-18; the fix made then was never replayed. +- **Not checked headless:** made texts in the objectives, a leaderboard label and a unit + name (strings L, M, O). The game shows them, but the runner reads only the text lines. + Anything that needs a person (the input probe, "select a Marine") is not checked either. +- **Found on the way:** scm-sim's reader of the game's text lines missed a line identical + to the one it overwrote ("Tick" each second, once the 13-line ring had gone round). It + now counts the slots the ring moved over as new. That was a bug in Test Map in Browser + too. + +### A1. The address tracer and the allowlist (4.0.0) + +**Tracing.** Hook the routines that run a trigger's conditions and actions. On entry, the +hook reads the condition or action record and works out the address it touches; for the +Deaths family, that is the table's base plus 4 × (player + 12 × unit). It records read or +write, the address, the frame and the trigger's own address. This costs a hook call per +condition, not a check per memory access. + +**Back to the line.** A trigger address has to become an IR node, and then a line. +Candidate: `trigscript.py` records, per IR node, the eudplib objects it made; after the +build eudplib gives each object its address (`GetObjectAddr`), and the build returns a +table `{ start, end, at }` beside the map. Addresses that eudplib's own runtime touches +(the triggers rewriting each other's next pointers) are kept as their own kind, which +Remastered is known to allow. + +**The allowlist.** `data/allowlist.json` in this repository: address ranges, read and/or +write, what the range is (from the disassembly), and the evidence (`"magenta 10, key 4, +2026-09-15: ends the game"`). It is filled first from Magenta's played probes and the 16 +TrigScript probes, so it starts with what is already known. + +**The check.** *Run in the game* traces every address the run touches and holds each one +against the allowlist: + +- on the list, allowed: nothing said; +- on the list, refused: an error at the line ("Remastered ends the game on this write: + Magenta probe 6, key I"); +- not on the list: a warning at the line ("not seen in Remastered: needs a probe"). The + map still builds. + +The trace only sees code the run reached, so the check is only as complete as the tests' +coverage. A1 therefore also shows coverage: lines that ran in the game, lines that did not. + +Ships as 4.0.0, the first version with *Run in the game*, because it adds a whole mode. + +### A2. Tests in the game (4.0.0, with A1) + +`test()` blocks run in the lifted game as well as the simulator. + +- **A test becomes a program.** The test body, with its world set-up, is compiled into a + test copy of the map as one more program, and its assertions record into an array the + runner reads from memory when the run ends. So the test runs with the game's own + trigger schedule, unit table and rules, and no new mechanism is needed inside the game. +- **Input.** `sim.type` becomes the text a chat command carries; `sim.click` becomes the + game commands a click sends; `sim.press` becomes a write of the key state the local + machine reads (MSQC reads keys locally). All three need checking against what MSQC + actually reads. +- **What the Testing view shows.** Each test gets two marks, simulator and game. When + they differ, the view says both answers, at the frame they parted. +- **Where it runs.** In the editor through `scm-sim.engine`; the service needs a new + method, roughly `run(map, { frames, seat, inputs, watch, trace })`, which means + contract version 2. Locally from a terminal: `npm run test:game` in this repository, + using `data/lifted/` when it is on the machine. + +A headless run draws nothing, and is allowed from any copy that unlocks the editor +(decided 2026-10-15, see *Open decisions*). + +### A3. The desync detector (4.1.0) + +Run two to eight copies of the game seated as different local players +(`seatPlayers`, local-player words set per copy, as `net.ts` does for multiplayer), feed +them identical turns, and hash each game's state every frame. At the first frame the +hashes differ, report the frame, the last traced trigger in each copy, and its line. + +A test can ask for it: `test("shop", { players: 4 }, …)`. The Testing view then shows +"desync at frame 312, line 48: the copies took different branches here". + +To check first: UMS with several people on one instance (melee is measured, UMS is not). + +### A4. What a frame costs (4.1.0) + +From A1's hooks: conditions and actions run per frame, by IR node and by line. Lifted +wall time is not the game's, so the count is the measure. The editor shows it per line +like the existing hints ("~1,200 actions a frame"), and as a graph over a test's run. + +Open: what count, on what machine, actually lags Remastered. A probe with a dial (run +k × 1,000 actions a frame, the user says when it stutters) gives the first number. + +### A5. The debugger in the game (4.2.0) + +9½'s debugger gets a second backend. + +- **Variables** are read from memory: the build's address table (A1) knows where each + `EUDVariable` and array lives. +- **Back as well as forward.** `rewind.ts` keeps a snapshot every N frames; a step back + restores the nearest snapshot and plays forward to the frame. +- **Watchpoints.** "Stop when this changes" compares a dword once a frame, which is cheap. +- **The game beside the code.** scm-sim renders the frame the debugger stands at. + +## Part B — the language + +Each slice states what changes in the IR, the simulator and the Python, and has a probe. +After A2, a probe without input steps also runs headless, and the user plays only what +needs Remastered's answer. + +### B1. `async` / `await` (4.3.0) + +The biggest change to the model since 3.0. Today a `sleep()` may only stand in a +program's own body or in an inlined function; a called function never sleeps. + +```ts +async function wave(n: number) { + createUnit(P8, units.ZergZergling, n * 4, locations.Gate); + await until(() => countUnits(P8, units.ZergZergling) === 0); + print(`Wave ${n} cleared`); +} + +program(async () => { + for (let n = 1; n <= 10; n++) { + const done = await Promise.race([wave(n), sleep(seconds(90)).then(() => false)]); + if (done === false) defeat(); + } +}); +``` + +- **A task** is a frame record on the heap (its locals, and which `await` it is parked at) + plus a resume point. The lowering is the one 3.0 uses for a program's top-level + `sleep`, a switch on the resume point, applied to every `async` function. +- **The scheduler** keeps the live tasks in a list on the heap, and each frame runs every + ready one until it awaits or ends. A `program()` is a task, so its meaning does not + change. +- **What can be awaited:** `sleep(…)`, `until(() => condition)`, any `async` call, + `Promise.all`, `Promise.race`, `Promise.any`. +- **Generators** (`function*`, `yield`) use the same machinery, for wave scripts and + for anything that hands out one step at a time. +- **Recursion with `await`** — the frame is already on the heap, so the recursion stack + of slice 8 may not be needed for an async function. To decide while building. +- **Errors:** `sleep()` outside an `async` function stays an error, with a fix that adds + `async`. An `async` function that never awaits is a hint. + +Open: whether `Promise.race` cancels the tasks that lost. JavaScript does not cancel +them, and a wave that keeps running after its race was lost is rarely what the author +meant. See *Open decisions*. + +### B2. Events (4.4.0) + +```ts +on.created(units.TerranMarine, (u) => { u.energy = 50; }); +on.died({ owner: P8 }, async (u) => { await sleep(frames(24)); createUnit(P8, u.type, 1, locations.Spawn); }); +on.order(units.ProtossHighTemplar, orders.CastPsionicStorm, (u) => …); +on.chat("-give {n}", (p, { n }) => …); +``` + +- **created**: eudplib's `EUDLoopNewCUnit` walks the units made since the last frame. +- **died**: a unit is kept in a table by the identity 8½'s unit keys already use (pointer + plus the slot's use count, which the leftovers probe showed holding in the game) and is + found gone when its slot no longer matches. Who killed it: scm-sim's analysis found a + killer's kill count rises one frame before the victim leaves the lists, which may give + `u.killedBy` on the same rule. That needs its own check. +- **order**: compares each unit's order with the last frame's; the cost is a scan, and the + hint says so. +- A handler is a task (B1), so it may await. + +### B3. Function values (4.5.0) + +```ts +const effects: ((u: Unit) => void)[] = [heal, slow, (u) => { u.hp -= 10; }]; +effects[random(effects.length)](target); +``` + +- A function value is a number, a function id. A call through it is a switch on the id + (or `EUDTypedFuncPtr`, which eudplib 0.81 has; the build may choose). +- **What a closure captures:** a `const` captured by value is exactly JavaScript, since + it cannot change. A captured `let` is the hard part: there is no garbage collector, and + the heap's rule is "the holder owns the block". Proposed: a `let` may be captured only by + a function that does not outlive the frame it was made in (passed down, never stored); + otherwise an error that says to make it `const` or a field. See *Open decisions*. +- This also gives 8½'s callbacks a way to work when they are not inlined: a callback + passed through two functions. + +### B4. `try` / `catch` / `throw` (4.5.0, with B3) + +- `throw` unwinds the frames on the heap, task frames and recursion frames both, to the + nearest `catch`. +- The faults the game reports today (out of memory, recursion too deep, an index out of + range) become thrown `RangeError`s that a program can catch. When nothing catches one, + it is said in red, as now. +- `finally` runs on every way out, `await` included. + +### B5. `Local` and `sync()` (4.6.0) + +Some values are true only on one machine: the local player, where its screen is, what it +has selected, what it is typing. TrigScript gives none of them out today; all input goes +through MSQC, which sends it to every machine. B5 offers them, typed so that a desync +cannot compile: + +```ts +const sel = localSelection(); // Local +centerView(sel[0]); // fine: the camera is the local machine's own +units.push(...sel); // error: a local value cannot change the game +const picked = await sync(sel[0]); // Unit, the same on every machine, a few frames later +``` + +- **The check** is a taint pass over the IR. `Local` reaches what only the local machine + sees (the camera, text shown to the current player, a sound for one player) and nothing + that changes the game state for all: a store into a shared variable, an action's target, + a condition of a branch that does either. +- **`sync()`** sends the value as a game command, as chatEvent's numbers already go + through MSQC's `val`, and resolves when every machine has it. It is `async` by nature, + which is why B5 comes after B1. +- **What can be local:** each read needs its address from the disassembly and a probe in + Remastered. A3 checks the taint pass's answers in practice. + +### B6. `fixed` and `Math` (4.7.0) + +- `fixed` is a 16.16 number with `+ - * /` and comparisons; mixing it with `number` + converts as TypeScript would round. +- `Math.sin`, `Math.cos`, `Math.atan2` work on the game's 256 directions. The tables are + computed at build time from the formula, and A2 checks them against the game's own + table. `Math.sqrt` and `Math.hypot` use an integer square root. +- Movement uses the actions Remastered allows: a location centred on a point and Move + Unit to it. A unit's position is never written; that ends the game. + +### B7. Typed memory (4.8.0) + +```ts +unsafe("the guard's cooldown, read for the HUD; probe 23", () => { + const cd = memory.unit(u).groundWeaponCooldown; // u8, at +0x… +}); +``` + +- The declarations for `memory.unit`, `memory.player`, `memory.sprite` and the others + are generated from the disassembly's structs, with each field's comment and its + allowlist state ("read: probe 12", "write: not seen", "write: ends the game"). +- Reading a field that is on the allowlist needs no `unsafe`. Writing one, or reading one + that is not on it, needs `unsafe` with a reason, and the reason is shown in the + map-requirements view (3.12). +- This is the door the crazy tier walks through, so it comes after the allowlist and the + tracer are settled. + +### B8. Built from the game (4.9.0) + +Tables a map could not afford to compute while it runs, computed by running the lifted +game when the map is built: + +- `ground.distance(a, b)` and `ground.region(x, y)`, from the game's own regions for this + map (Walkability already reads them out of the lifted engine). +- Anything else a function of the map: `atBuild(() => …)` runs in the lifted game at build + time and its result becomes a constant table. + +Cost: map size. The hint says how many bytes each table adds. + +## Part C — the game, pushed + +Each is a library over Part B, offered only after its probe. "Known" means a played probe +already says so. + +| Idea | What it needs | Known | +| --- | --- | --- | +| Terrain as a framebuffer | tile buffer writes | **No.** Magenta 10: Remastered does not draw them. Dropped. (My first answer on 2026-10-15 proposed it; that was wrong.) | +| Units moved freely every frame (bullet hell, orbits) | location + Move Unit, B6 for the paths | position writes end the game (Magenta 6); moving by action is ordinary, so the probe is how many a frame before it lags (A4's dial) | +| Panels of text: objectives, leaderboard label, unit names as live UI | made texts in those fields | **Yes**, the strings spike (2026-09-19): re-read every draw, written only on the target's machine | +| Custom command buttons | button set data in memory | unknown: probe | +| Custom spells | B2's `on.order`, Magenta's damage/heal verb | partly: the verb is verified, the order watch is not | +| Save codes | strings, a checksum, `chatted()` | needs no new address; a probe for the round trip | +| Unit stats changed while the game runs | the type tables | Magenta-verified fields only | +| Computer players written in TrigScript | B1 tasks, unit orders through the scratch-box trick | the trick is verified (Magenta); a behaviour library is new | +| Camera work for one player | `centerView`, B5 | ordinary actions; B5 makes them safe | + +## Slices + +| # | Slice | What it proves | Size | +| --- | --- | --- | --- | +| A0 | Spike: an eudplib map in the lifted game (scm-sim) | EUD writes through a hooked bounds check; 1.16.1 loads the built map; probes print their expectations headless | 2–4 days, no release | +| A1 + A2 | Address tracer, allowlist, tests in the game (4.0.0) | every address a run touches held against Remastered's known answers at the line; each test judged twice; coverage | 6–8 days, plus scm-sim's `run` (contract 2) | +| A3 + A4 | Desync detector, the cost of a frame (4.1.0) | several seated copies hashed every frame; actions per frame per line; the first lag number from a probe | 4–5 days, one probe (the dial) | +| A5 | The debugger in the game (4.2.0) | variables from memory, step back, watchpoints, the frame drawn | 3–4 days, no probe | +| B1 | `async` / `await`, generators (4.3.0) | tasks on the heap, a scheduler, `Promise.all`/`race`/`any`, `until()` | 6–8 days, probe | +| B2 | Events (4.4.0) | created / died / order / chat, handlers as tasks | 3–4 days, probe | +| B3 + B4 | Function values, `try` / `catch` (4.5.0) | calls through an id, the capture rule, unwinding through tasks | 5–6 days, probe | +| B5 | `Local` and `sync()` (4.6.0) | the taint pass; local reads, each probed; A3 confirms | 4–5 days, probe per read | +| B6 | `fixed` and `Math` (4.7.0) | 16.16 arithmetic, the direction tables against the game's | 2–3 days, probe | +| B7 | Typed memory (4.8.0) | generated structs with allowlist states; `unsafe` with a reason | 3–4 days, no probe of its own | +| B8 | Built from the game (4.9.0) | ground distances and regions baked in; `atBuild` | 3 days, probe | +| C | Libraries, one probe each (4.10 on) | the table above | per idea | + +Ship order for every release, as for 3.x: plugin commit and tag → scm-js pin, vendor, +guide and catalogue note → registry → ai-server prompt. Anything scm-sim needs (A0's +build, contract 2) ships first, then plugin-scm-sim, then TrigScript. + +## Work outside this repository + +- **scm-sim:** the EUD-permitting module build (A0); the trigger hooks (A1); `run(…)` with + seating, inputs, a trace and memory reads (A2, A3); UMS with several people on one + instance (A3). +- **plugin-scm-sim:** contract version 2 for `run`; the `present` and `status()` answers + saying whether the person's copy of the game allows a run. +- **plugin-eudplib:** the build returns the object address table (A1), if `trigscript.py` + cannot get it on its own. +- **scm-js:** nothing until the guide pages; `docs/trigscript.md` gains *Run in the game*. + +## Open decisions + +- ~~Does a headless test run count as play?~~ Decided 2026-10-15 (the user: "headless + test runs seem fine as long as theres no rendering"): a run in the game for tests, + traces, desyncs and costs is allowed from any copy that unlocks the editor, provided + nothing of the game is drawn. A5's "the game beside the code" draws, so it needs the + person's own copy, as Test Map does. +- **Does `Promise.race` cancel the losers?** JavaScript: no. Proposed: no, as JavaScript, + plus an explicit `task.cancel()` on what `spawn(…)` returns. +- **What a closure may capture.** Proposed above (B3): `const` by value, `let` only when + the closure does not outlive its frame. The alternative is reference-counted + environments: closer to JavaScript, more Python, and a new kind of fault. +- **How a trigger becomes a line.** The candidate is the object address table (A1). If + eudplib does not keep objects in one piece, the fallback is a marker action per IR node, + which costs triggers. +- ~~Where the hook for the EUD bounds check lives.~~ Settled by A0 (2026-10-15): nowhere + in the build. The two routines are replaced at run time through the module's table of + 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. + +## Facts this plan leans on + +- scm-sim, `src/exact/` (2026-10-12 to 14): lifted 1.16.1 with storm.dll; maps, replays, + several people on one instance; 2,200–4,100 frames a second in Node; snapshots ~45 ms / + 0.6 MB; hooks by address at generation; `callAt` between turns; `triggerView`; + extended Deaths reads work and writes are refused by 1.16.1's check. +- Remastered's executable is encrypted; no disassembly of it is planned. +- Magenta's played probes 5–11 (2026-09-14/15): position, tint and image writes end the + game; tile buffer writes are not drawn; per-unit speed and cloak flags do nothing; key + hold does not exist; the scratch-box order trick and the damage/heal verb work. +- The strings spike (2026-09-19): objectives, leaderboard label, transmission line and + unit type name show a made text and are re-read every draw. +- eudplib 0.81 (checked in the wheel 2026-10-15): `EUDFuncPtr`, `EUDTypedFuncPtr`, + `EUDLoopNewUnit` / `EUDLoopNewCUnit`, `EUDLoopBullet`, `EUDLoopSprite`, + `QueueGameCommand` and its select / right-click forms. +- TrigScript 3.12 (2026-10-16): the simulator keeps the game's trigger schedule; + programs first run on frame 2; eudTurbo writes 0x6509A0 := 0 after the pass. From 21aa7a038c9c1cf473bce834df8efed94a5e3903 Mon Sep 17 00:00:00 2001 From: Jeany Date: Thu, 15 Oct 2026 23:55:16 -0600 Subject: [PATCH 002/238] TrigScript 4, A1: a run in the lifted game held against Remastered, by line - python/trigscript.py, with the triggerMap setting: every trigger tagged with the statement it was lowered from (or eudplib's function, or "eudplib"), and its place in the payload logged once laid out. Off by default; a build without it is unchanged. - scripts/build-fixture.mts: TRIGGER_MAP=1 writes .triggers.json, from eudplib's log read whole from a file. - compiler/allowlist.ts: the allowlist's shape and checkTrace, byte by byte under each access's mask; findings by line, gravest first, said in words. - data/allowlist.json from scripts/allowlist.mts: Magenta's catalogue, three failures seen in the game, and what the 15 played probes touched. - scripts/game-check.mts: build, play in scm-sim's lifted game, check. - docs/v4-plan.md: A1 as built. Co-Authored-By: Claude Opus 5.5 --- compiler/allowlist.ts | 168 +++ compiler/generated/trigscriptPy.ts | 2 +- data/allowlist.json | 2073 ++++++++++++++++++++++++++++ docs/v4-plan.md | 44 + python/trigscript.py | 77 ++ scripts/allowlist.mts | 112 ++ scripts/build-fixture.mts | 20 +- scripts/game-check.mts | 47 + tests/allowlist.test.ts | 65 + 9 files changed, 2604 insertions(+), 4 deletions(-) create mode 100644 compiler/allowlist.ts create mode 100644 data/allowlist.json create mode 100644 scripts/allowlist.mts create mode 100644 scripts/game-check.mts create mode 100644 tests/allowlist.test.ts diff --git a/compiler/allowlist.ts b/compiler/allowlist.ts new file mode 100644 index 0000000..b9fe158 --- /dev/null +++ b/compiler/allowlist.ts @@ -0,0 +1,168 @@ +/** + * What Remastered is known to allow, and a run in the lifted game held against it (TrigScript 4, A1). + * + * The lifted game is 1.16.1: it says what an address means and what the triggers did with it, never whether + * Remastered's emulation of 1.16.1's memory would have let them. The allowlist says that, entry by entry, each with + * the evidence it rests on: a probe played in Remastered, Magenta's catalogue (itself verified by probes), or a + * failure seen in the game. `checkTrace` takes what a run touched (scm-sim's `scripts/exact-eud.ts --trace`) and + * says, by line, what Remastered was seen to refuse or ignore and what it has not been seen to do at all. + * + * A trace row is a dword and the bits that were looked at or changed (the record's mask); a row is held byte by + * byte, so a write of one byte under a mask needs only that byte allowed. + */ + +/** What Remastered does with a read or a write of an entry: lets it, ends the game, does nothing, or not known. */ +export type Verdict = "allowed" | "ends" | "ignored" | "unknown"; + +export interface AllowEntry { + /** What lies there ("unit table: hit points", "deaths table"). */ + name: string; + /** The first byte; with `stride`, the first byte of the first of `count` rows. */ + base: number; + stride?: number; + count?: number; + /** Bytes from `base` (within each row). */ + width: number; + read: Verdict; + write: Verdict; + /** What each verdict rests on, in words a person can follow up. */ + evidence: string[]; + /** A value that differs between the players' computers (the mouse, the screen): never to change the game itself. */ + local?: boolean; +} + +export interface Allowlist { + version: 1; + entries: AllowEntry[]; +} + +/** [origin, address, write (1) or read (0), mask, times] — origin as the build's map of triggers names it. */ +export type TraceRow = [string, number, number, number, number]; + +export interface Trace { + map?: string; + frames: number; + /** The map's own strings and eudplib's payload behind them, where Remastered keeps them: the map's own memory. */ + strings: { from: number; to: number }; + /** The lifted game's heap (what the game allocated: its trigger lists among it); absent in older traces. */ + heap?: { from: number; to: number }; + rows: TraceRow[]; +} + +export interface Finding { + /** "12:5" or "file.ts:12:5"; "fn:" for eudplib's own functions, "" for a program's frame, "eudplib", or "?". */ + origin: string; + line: number | null; + write: boolean; + verdict: Exclude; + /** The first address of the kind found, how many distinct dwords and how many times in all. */ + address: number; + addresses: number; + times: number; + /** The entry that decided it, or the table of the game it lies in when no entry covers it. */ + entry: string | null; + evidence: string[]; +} + +export interface TraceCheck { + findings: Finding[]; + /** Accesses (rows) by what decided them. */ + counted: { own: number; heap: number; allowed: number; ends: number; ignored: number; unknown: number }; +} + +/** 1.16.1's tables by name, so that what no entry covers is still said in words, and a loop over every unit is one finding. */ +const TABLES = [ + { name: "unit table", base: 0x59cca8, stride: 336, count: 1700 }, + { name: "deaths table", base: 0x58a364, stride: 4, count: 12 * 228 }, +]; +function placeOf(address: number): string { + for (const t of TABLES) { + const off = address - t.base; + if (off >= 0 && off < t.stride * t.count) return t.stride > 4 ? `${t.name} +0x${(off % t.stride).toString(16)}` : t.name; + } + return ""; +} + +/** Whether a line of the script made the access (and not eudplib's own code, or a program's frame). */ +export function isScriptOrigin(origin: string): boolean { + return /^(.+:)?\d+:\d+$/.test(origin); +} + +function lineOf(origin: string): number | null { + const m = /(\d+):\d+$/.exec(origin); + return m && isScriptOrigin(origin) ? Number(m[1]) : null; +} + +/** The bytes of the dword at `address` that `mask` names. */ +function bytesOf(address: number, mask: number): number[] { + const out: number[] = []; + for (let k = 0; k < 4; k++) if ((mask >>> (8 * k)) & 0xff) out.push(address + k); + return out; +} + +function covers(e: AllowEntry, byte: number): boolean { + const off = byte - e.base; + if (off < 0) return false; + if (!e.stride) return off < e.width; + return off % e.stride < e.width && Math.floor(off / e.stride) < (e.count ?? 1); +} + +/** How grave a verdict is when a row is summed up from its bytes: one byte not known makes the row not known. */ +const RANK: Record = { ends: 3, ignored: 2, unknown: 1, allowed: 0 }; +/** Which of several entries over one byte decides: a failure seen beats a success, a success beats no word at all. */ +const WEIGHT: Record = { ends: 3, ignored: 2, allowed: 1, unknown: 0 }; + +/** What the allowlist says of one byte, for a read or a write, and the entry that says it. */ +export function verdictOf(list: Allowlist, byte: number, write: boolean): { verdict: Verdict; entry: AllowEntry | null } { + let best: { verdict: Verdict; entry: AllowEntry | null } = { verdict: "unknown", entry: null }; + for (const e of list.entries) { + if (!covers(e, byte)) continue; + const v = write ? e.write : e.read; + if (best.entry === null || WEIGHT[v] > WEIGHT[best.verdict]) best = { verdict: v, entry: e }; + } + return best; +} + +/** + * Holds every row of a trace against the allowlist. The map's own memory is always allowed; the game's heap is + * allowed to eudplib's own code (its trigger lists live there, reached through the game's pointers) and unknown to + * a line of the script. Findings are grouped by origin, direction, verdict and the entry that decided them. + */ +export function checkTrace(trace: Trace, list: Allowlist): TraceCheck { + const counted = { own: 0, heap: 0, allowed: 0, ends: 0, ignored: 0, unknown: 0 }; + const groups = new Map }>(); + for (const [origin, address, write, mask, times] of trace.rows) { + if (address >= trace.strings.from && address < trace.strings.to) { counted.own++; continue; } + if (trace.heap && address >= trace.heap.from && address < trace.heap.to) { + if (!isScriptOrigin(origin)) { counted.heap++; continue; } + add(origin, address, write === 1, "unknown", null, ["the game's heap, read or written by a line of the script"], times); + continue; + } + let worst: { verdict: Verdict; entry: AllowEntry | null } = { verdict: "allowed", entry: null }; + for (const byte of bytesOf(address, mask)) { + const v = verdictOf(list, byte, write === 1); + if (RANK[v.verdict] > RANK[worst.verdict]) worst = v; + } + if (worst.verdict === "allowed") { counted.allowed++; continue; } + add(origin, address, write === 1, worst.verdict, worst.entry, worst.entry?.evidence ?? [], times); + } + function add(origin: string, address: number, write: boolean, verdict: Verdict, entry: AllowEntry | null, evidence: string[], times: number) { + counted[verdict as "ends" | "ignored" | "unknown"]++; + const place = entry?.name ?? (placeOf(address) || null); + const key = `${origin}|${write ? 1 : 0}|${verdict}|${place ?? address >>> 12}`; + const g = groups.get(key); + if (g) { g.times += times; if (!g.seen.has(address)) { g.seen.add(address); g.addresses++; } if (address < g.address) g.address = address; return; } + groups.set(key, { origin, line: lineOf(origin), write, verdict: verdict as Finding["verdict"], address, addresses: 1, times, entry: place, evidence, seen: new Set([address]) }); + } + const findings = [...groups.values()].map(({ seen: _seen, ...f }) => f) + .sort((a, b) => RANK[b.verdict] - RANK[a.verdict] || (a.line ?? 1e9) - (b.line ?? 1e9) || a.address - b.address); + return { findings, counted }; +} + +/** A finding as one line for a person: where, what, and what it rests on. */ +export function describeFinding(f: Finding): string { + const where = f.line !== null ? `line ${f.line}` : f.origin === "" ? "a program's frame" : f.origin.startsWith("fn:") ? `eudplib's ${f.origin.slice(3)}` : f.origin === "?" ? "eudplib's start" : f.origin; + const what = `${f.write ? "writes" : "reads"} ${f.entry ?? "0x" + f.address.toString(16)}${f.addresses > 1 ? ` (${f.addresses} dwords from 0x${f.address.toString(16)})` : f.entry ? ` at 0x${f.address.toString(16)}` : ""}`; + const verdict = f.verdict === "ends" ? "Remastered ends the game on this" : f.verdict === "ignored" ? "Remastered does nothing with this" : "not seen in Remastered: needs a probe"; + return `${where}: ${what} — ${verdict}${f.evidence.length ? ` (${f.evidence.join("; ")})` : ""}`; +} diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index 8f06e84..9b306e6 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 14\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# 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 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# 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\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\nclass Storage:\n \"\"\"A variable's cell: plain, or a 12-slot row of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n self.store = EUDArray([0] * 12) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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, per_player, player_of):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.bits = decl.get(\"bits\")\n values = decl.get(\"values\")\n self.rowed = per_player and not decl.get(\"shared\") and values is None\n self.player_of = player_of\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 self.store = EUDArray([int(v) & U32 for v in values] if values is not None else [0] * (self.length * (12 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 player's row first.\"\"\"\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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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\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\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.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 12-slot rows of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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\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 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 self.arrays = {a[\"id\"]: SliceStorage(a, self) if a.get(\"slice\") else InnerListStorage(a, self) if a.get(\"through\") else (ListStorage if a.get(\"dynamic\") else ArrayStorage)(a, self.per_player, self.player_of) for a in program.get(\"arrays\", [])}\n self.state = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.wait = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.resumes = [] # (index, Forward) for every sleep\n self.frame_end = None\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 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 declare(self, decl):\n if decl.get(\"kind\") == \"unit\":\n s = UnitStorage(decl, self.per_player, self.player_of)\n elif decl.get(\"kind\") == \"text\" and decl.get(\"text\") != \"id\":\n s = TextStorage(decl, self.per_player, self.player_of)\n else:\n s = Storage(decl, self.per_player, self.player_of)\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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n def get_state(self):\n return self.state[self.player] if self.per_player else self.state\n\n def set_state(self, value):\n if self.per_player:\n self.state[self.player] = value\n else:\n self.state << value\n\n def get_wait(self):\n return self.wait[self.player] if self.per_player else self.wait\n\n def set_wait(self, value):\n if self.per_player:\n self.wait[self.player] = value\n else:\n self.wait << value\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 == \"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 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\"] == \"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] = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: the latch\n latch = self.latches[key]\n\n def get():\n return latch[self.player] if self.per_player else latch\n\n def put(v):\n if self.per_player:\n latch[self.player] = v\n else:\n latch << v\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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"action\":\n self.action(st)\n elif k == \"print\":\n self.print_(st)\n elif k == \"centerLocation\":\n self.center_location(st)\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 index = len(self.resumes) + 1\n resume = Forward()\n self.resumes.append((index, resume))\n self.set_state(index)\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 self.set_wait(frames - 1)\n EUDJump(self.frame_end)\n resume << 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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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 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 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 = [], []\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\":\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 else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n group, room, first = [], 0, [True]\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 <= T[\"scratch\"] + TEXT_BYTES):\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 << T[\"append\"](pos, value.addr)\n else:\n if EUDIf()(pos <= T[\"scratch\"] + TEXT_BYTES):\n pos << T[\"append\"](pos, value.addr)\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 return pos, temps\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 == \"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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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.rowed))\n elif isinstance(s, TextStorage):\n for cell in (s.addr, s.block, s.len):\n pairs.append(self.cell_pair(cell, s.rowed))\n elif isinstance(s, Storage):\n pairs.append(self.cell_pair(s.store, s.rowed))\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.rowed))\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, rowed):\n if rowed:\n return (lambda: cell[self.player_of()]), (lambda v: cell.__setitem__(self.player_of(), 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 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 self.set_state(DONE)\n EUDJump(self.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 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 self.frame_end = Forward()\n start = Forward()\n skip = Forward()\n # Still waiting: count down and leave.\n EUDJumpIfNot(as_var(self.get_wait()) >= 1, skip)\n self.set_wait(as_var(self.get_wait()) - 1)\n EUDJump(self.frame_end)\n skip << NextTrigger()\n # The body once, resumes recorded as sleeps are met; the jump table is filled in after.\n table = Forward()\n EUDJump(table)\n start << NextTrigger()\n try:\n self.block(self.p[\"body\"], {})\n except Leave:\n pass\n # The body ended: the program stops for good, as the classic backend's does.\n self.set_state(DONE)\n EUDJump(self.frame_end)\n table << NextTrigger()\n EUDJumpIf(as_var(self.get_state()) == DONE, self.frame_end)\n for index, resume in self.resumes:\n EUDJumpIf(as_var(self.get_state()) == index, resume)\n EUDJump(start)\n self.frame_end << NextTrigger()\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 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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 14\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# 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 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# 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\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\nclass Storage:\n \"\"\"A variable's cell: plain, or a 12-slot row of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n self.store = EUDArray([0] * 12) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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, per_player, player_of):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.bits = decl.get(\"bits\")\n values = decl.get(\"values\")\n self.rowed = per_player and not decl.get(\"shared\") and values is None\n self.player_of = player_of\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 self.store = EUDArray([int(v) & U32 for v in values] if values is not None else [0] * (self.length * (12 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 player's row first.\"\"\"\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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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\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\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.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 12-slot rows of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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\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 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 self.arrays = {a[\"id\"]: SliceStorage(a, self) if a.get(\"slice\") else InnerListStorage(a, self) if a.get(\"through\") else (ListStorage if a.get(\"dynamic\") else ArrayStorage)(a, self.per_player, self.player_of) for a in program.get(\"arrays\", [])}\n self.state = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.wait = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.resumes = [] # (index, Forward) for every sleep\n self.frame_end = None\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 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 declare(self, decl):\n if decl.get(\"kind\") == \"unit\":\n s = UnitStorage(decl, self.per_player, self.player_of)\n elif decl.get(\"kind\") == \"text\" and decl.get(\"text\") != \"id\":\n s = TextStorage(decl, self.per_player, self.player_of)\n else:\n s = Storage(decl, self.per_player, self.player_of)\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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n def get_state(self):\n return self.state[self.player] if self.per_player else self.state\n\n def set_state(self, value):\n if self.per_player:\n self.state[self.player] = value\n else:\n self.state << value\n\n def get_wait(self):\n return self.wait[self.player] if self.per_player else self.wait\n\n def set_wait(self, value):\n if self.per_player:\n self.wait[self.player] = value\n else:\n self.wait << value\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 == \"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 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\"] == \"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] = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: the latch\n latch = self.latches[key]\n\n def get():\n return latch[self.player] if self.per_player else latch\n\n def put(v):\n if self.per_player:\n latch[self.player] = v\n else:\n latch << v\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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"action\":\n self.action(st)\n elif k == \"print\":\n self.print_(st)\n elif k == \"centerLocation\":\n self.center_location(st)\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 index = len(self.resumes) + 1\n resume = Forward()\n self.resumes.append((index, resume))\n self.set_state(index)\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 self.set_wait(frames - 1)\n EUDJump(self.frame_end)\n resume << 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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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 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 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 = [], []\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\":\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 else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n group, room, first = [], 0, [True]\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 <= T[\"scratch\"] + TEXT_BYTES):\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 << T[\"append\"](pos, value.addr)\n else:\n if EUDIf()(pos <= T[\"scratch\"] + TEXT_BYTES):\n pos << T[\"append\"](pos, value.addr)\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 return pos, temps\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 == \"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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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.rowed))\n elif isinstance(s, TextStorage):\n for cell in (s.addr, s.block, s.len):\n pairs.append(self.cell_pair(cell, s.rowed))\n elif isinstance(s, Storage):\n pairs.append(self.cell_pair(s.store, s.rowed))\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.rowed))\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, rowed):\n if rowed:\n return (lambda: cell[self.player_of()]), (lambda v: cell.__setitem__(self.player_of(), 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 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 self.set_state(DONE)\n EUDJump(self.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 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 self.frame_end = Forward()\n start = Forward()\n skip = Forward()\n # Still waiting: count down and leave.\n EUDJumpIfNot(as_var(self.get_wait()) >= 1, skip)\n self.set_wait(as_var(self.get_wait()) - 1)\n EUDJump(self.frame_end)\n skip << NextTrigger()\n # The body once, resumes recorded as sleeps are met; the jump table is filled in after.\n table = Forward()\n EUDJump(table)\n start << NextTrigger()\n try:\n self.block(self.p[\"body\"], {})\n except Leave:\n pass\n # The body ended: the program stops for good, as the classic backend's does.\n self.set_state(DONE)\n EUDJump(self.frame_end)\n table << NextTrigger()\n EUDJumpIf(as_var(self.get_state()) == DONE, self.frame_end)\n for index, resume in self.resumes:\n EUDJumpIf(as_var(self.get_state()) == index, resume)\n EUDJump(start)\n self.frame_end << NextTrigger()\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 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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\n"; diff --git a/data/allowlist.json b/data/allowlist.json new file mode 100644 index 0000000..df05cdd --- /dev/null +++ b/data/allowlist.json @@ -0,0 +1,2073 @@ +{ + "version": 1, + "entries": [ + { + "name": "max hit points of a unit type", + "base": 6693712, + "width": 4, + "stride": 4, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.maxHp, verified by its probes" + ] + }, + { + "name": "max shields of a unit type", + "base": 6688256, + "width": 2, + "stride": 2, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.maxShields, verified by its probes" + ] + }, + { + "name": "armor of a unit type", + "base": 6684360, + "width": 1, + "stride": 1, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.armor, verified by its probes" + ] + }, + { + "name": "build time of a unit type", + "base": 6685736, + "width": 2, + "stride": 2, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.buildTime, verified by its probes" + ] + }, + { + "name": "mineral cost of a unit type", + "base": 6699144, + "width": 2, + "stride": 2, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.mineralCost, verified by its probes" + ] + }, + { + "name": "gas cost of a unit type", + "base": 6683904, + "width": 2, + "stride": 2, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.gasCost, verified by its probes" + ] + }, + { + "name": "supply used by a unit type", + "base": 6700264, + "width": 1, + "stride": 1, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.supplyRequired, verified by its probes" + ] + }, + { + "name": "supply provided by a unit type", + "base": 6702792, + "width": 1, + "stride": 1, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.supplyProvided, verified by its probes" + ] + }, + { + "name": "sight range of a unit type", + "base": 6697528, + "width": 1, + "stride": 1, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.sightRange, verified by its probes" + ] + }, + { + "name": "target acquisition range of a unit type", + "base": 6696376, + "width": 1, + "stride": 1, + "count": 228, + "read": "unknown", + "write": "unknown", + "evidence": [ + "Magenta unit.seekRange, not verified" + ] + }, + { + "name": "ground weapon of a unit type", + "base": 6698680, + "width": 1, + "stride": 1, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.groundWeapon, verified by its probes" + ] + }, + { + "name": "air weapon of a unit type", + "base": 6690528, + "width": 1, + "stride": 1, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.airWeapon, verified by its probes" + ] + }, + { + "name": "speed of a unit type", + "base": 7118584, + "width": 4, + "stride": 4, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.speed, verified by its probes" + ] + }, + { + "name": "name of a unit type", + "base": 6685280, + "width": 2, + "stride": 2, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.name, verified by its probes" + ] + }, + { + "name": "size of a unit type", + "base": 6693248, + "width": 1, + "stride": 1, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.sizeClass, verified by its probes" + ] + }, + { + "name": "looks of a unit type", + "base": 6702328, + "width": 1, + "stride": 1, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.graphics, verified by its probes" + ] + }, + { + "name": "detector flag of a unit type", + "base": 6701184, + "width": 4, + "stride": 4, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.detector, verified by its probes" + ] + }, + { + "name": "permanent cloak flag of a unit type", + "base": 6701184, + "width": 4, + "stride": 4, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.permanentCloak, verified by its probes" + ] + }, + { + "name": "cloak ability flag of a unit type", + "base": 6701184, + "width": 4, + "stride": 4, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.cloakable, verified by its probes" + ] + }, + { + "name": "burrow ability flag of a unit type", + "base": 6701184, + "width": 4, + "stride": 4, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.burrowable, verified by its probes" + ] + }, + { + "name": "regeneration flag of a unit type", + "base": 6701184, + "width": 4, + "stride": 4, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.regeneratesHp, verified by its probes" + ] + }, + { + "name": "invincible flag of a unit type", + "base": 6701184, + "width": 4, + "stride": 4, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.invincible, verified by its probes" + ] + }, + { + "name": "hero flag of a unit type", + "base": 6701184, + "width": 4, + "stride": 4, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.hero, verified by its probes" + ] + }, + { + "name": "organic flag of a unit type", + "base": 6701184, + "width": 4, + "stride": 4, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.organic, verified by its probes" + ] + }, + { + "name": "mechanical flag of a unit type", + "base": 6701184, + "width": 4, + "stride": 4, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.mechanical, verified by its probes" + ] + }, + { + "name": "robotic flag of a unit type", + "base": 6701184, + "width": 4, + "stride": 4, + "count": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta unit.robotic, verified by its probes" + ] + }, + { + "name": "damage of a weapon", + "base": 6647472, + "width": 2, + "stride": 2, + "count": 130, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta weapon.damage, verified by its probes" + ] + }, + { + "name": "damage bonus per upgrade of a weapon", + "base": 6649464, + "width": 2, + "stride": 2, + "count": 130, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta weapon.bonus, verified by its probes" + ] + }, + { + "name": "cooldown of a weapon", + "base": 6647736, + "width": 1, + "stride": 1, + "count": 130, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta weapon.cooldown, verified by its probes" + ] + }, + { + "name": "damage factor of a weapon", + "base": 6644960, + "width": 1, + "stride": 1, + "count": 130, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta weapon.factor, verified by its probes" + ] + }, + { + "name": "range of a weapon", + "base": 6648944, + "width": 4, + "stride": 4, + "count": 130, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta weapon.range, verified by its probes" + ] + }, + { + "name": "minimum range of a weapon", + "base": 6646296, + "width": 4, + "stride": 4, + "count": 130, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta weapon.minRange, verified by its probes" + ] + }, + { + "name": "mineral cost of an upgrade", + "base": 6641472, + "width": 2, + "stride": 2, + "count": 61, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta upgrade.mineralCost, verified by its probes" + ] + }, + { + "name": "gas cost of an upgrade", + "base": 6641728, + "width": 2, + "stride": 2, + "count": 61, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta upgrade.gasCost, verified by its probes" + ] + }, + { + "name": "research time of an upgrade", + "base": 6642560, + "width": 2, + "stride": 2, + "count": 61, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta upgrade.time, verified by its probes" + ] + }, + { + "name": "maximum level of an upgrade", + "base": 6641408, + "width": 1, + "stride": 1, + "count": 61, + "read": "unknown", + "write": "unknown", + "evidence": [ + "Magenta upgrade.maxLevel, not verified" + ] + }, + { + "name": "mineral cost of a technology", + "base": 6644296, + "width": 2, + "stride": 2, + "count": 44, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta tech.mineralCost, verified by its probes" + ] + }, + { + "name": "gas cost of a technology", + "base": 6644208, + "width": 2, + "stride": 2, + "count": 44, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta tech.gasCost, verified by its probes" + ] + }, + { + "name": "research time of a technology", + "base": 6644696, + "width": 2, + "stride": 2, + "count": 44, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta tech.time, verified by its probes" + ] + }, + { + "name": "energy cost of a technology", + "base": 6644608, + "width": 2, + "stride": 2, + "count": 44, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta tech.energy, verified by its probes" + ] + }, + { + "name": "minerals of a player", + "base": 5763312, + "width": 4, + "stride": 4, + "count": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta player.minerals, verified by its probes" + ] + }, + { + "name": "gas of a player", + "base": 5763360, + "width": 4, + "stride": 4, + "count": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta player.gas, verified by its probes" + ] + }, + { + "name": "upgrade level of a player", + "base": 5821104, + "width": 552, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta player.upgradeLevel, verified by its probes" + ] + }, + { + "name": "technology researched by a player", + "base": 5820228, + "width": 288, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta player.techResearched, verified by its probes" + ] + }, + { + "name": "alliance of a player toward another", + "base": 5822004, + "width": 144, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta player.alliance, verified by its probes" + ] + }, + { + "name": "shared vision between players", + "base": 5763564, + "width": 4, + "stride": 4, + "count": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta player.vision, verified by its probes" + ] + }, + { + "name": "colour of a player", + "base": 5774710, + "width": 1, + "stride": 1, + "count": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta player.color, verified by its probes" + ] + }, + { + "name": "supply provided to a player", + "base": 5775684, + "width": 336, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta player.supplyProvided, verified by its probes" + ] + }, + { + "name": "supply used by a player", + "base": 5775732, + "width": 336, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta player.supplyUsed, verified by its probes" + ] + }, + { + "name": "supply cap of a player", + "base": 5775780, + "width": 336, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta player.supplyMax, verified by its probes" + ] + }, + { + "name": "slot of a player", + "base": 5763508, + "width": 1, + "stride": 1, + "count": 12, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta player.slotType, verified by its probes" + ] + }, + { + "name": "race of a player", + "base": 5763520, + "width": 1, + "stride": 1, + "count": 12, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta player.race, verified by its probes" + ] + }, + { + "name": "whether a player has left", + "base": 5774690, + "width": 1, + "stride": 1, + "count": 8, + "read": "unknown", + "write": "unknown", + "evidence": [ + "Magenta player.left, not verified" + ] + }, + { + "name": "game speed", + "base": 7135188, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta game.speed, verified by its probes" + ] + }, + { + "name": "trigger timer", + "base": 6621600, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta game.triggerTimer, verified by its probes" + ] + }, + { + "name": "local player", + "base": 5318276, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta game.localPlayer, verified by its probes" + ], + "local": true + }, + { + "name": "mouse x on screen", + "base": 7134660, + "width": 2, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta game.mouseX, verified by its probes" + ], + "local": true + }, + { + "name": "mouse y on screen", + "base": 7134662, + "width": 2, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta game.mouseY, verified by its probes" + ], + "local": true + }, + { + "name": "screen x on the map", + "base": 6456460, + "width": 4, + "read": "unknown", + "write": "unknown", + "evidence": [ + "Magenta game.screenX, not verified" + ], + "local": true + }, + { + "name": "screen y on the map", + "base": 6456488, + "width": 4, + "read": "unknown", + "write": "unknown", + "evidence": [ + "Magenta game.screenY, not verified" + ], + "local": true + }, + { + "name": "keyboard key state", + "base": 5859864, + "width": 1, + "stride": 1, + "count": 256, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta game.key, verified by its probes" + ], + "local": true + }, + { + "name": "frames the game has run", + "base": 5763644, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta game.frames, verified by its probes" + ] + }, + { + "name": "seconds on the game clock", + "base": 5822200, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta game.seconds, verified by its probes" + ] + }, + { + "name": "hit points of a placed unit", + "base": 5885104, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta cunit.hp, verified by its probes" + ] + }, + { + "name": "shields of a placed unit", + "base": 5885192, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta cunit.shields, verified by its probes" + ] + }, + { + "name": "energy of a placed unit", + "base": 5885258, + "width": 2, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta cunit.energy, verified by its probes" + ] + }, + { + "name": "owner of a placed unit", + "base": 5885172, + "width": 1, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta cunit.owner, verified by its probes" + ] + }, + { + "name": "type of a placed unit", + "base": 5885196, + "width": 2, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta cunit.type, verified by its probes" + ] + }, + { + "name": "x position of a placed unit", + "base": 5885136, + "width": 2, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta cunit.x, verified by its probes" + ] + }, + { + "name": "y position of a placed unit", + "base": 5885138, + "width": 2, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta cunit.y, verified by its probes" + ] + }, + { + "name": "invincibility of a placed unit", + "base": 5885316, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta cunit.invincible, verified by its probes" + ] + }, + { + "name": "hallucination flag of a placed unit", + "base": 5885316, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Magenta cunit.hallucination, verified by its probes" + ] + }, + { + "name": "cloak state of a placed unit", + "base": 5885316, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Magenta cunit.cloaked, verified by its probes" + ] + }, + { + "name": "unit table: position", + "base": 5885136, + "stride": 336, + "count": 1700, + "width": 4, + "read": "allowed", + "write": "ends", + "evidence": [ + "Magenta probe 6, key I (2026-09-14): writing a unit's position ends the game, \"EUD not supported\"" + ] + }, + { + "name": "unit table: cloak flags", + "base": 5885317, + "stride": 336, + "count": 1700, + "width": 1, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Magenta probe 6, keys Y and H (2026-09-14): status flags 0x100 and 0x200 written, no visible cloak" + ] + }, + { + "name": "game speed", + "base": 7135188, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Magenta probe 5, key 9 (2026-09-14): written, the game kept its speed" + ] + }, + { + "name": "0x512684", + "base": 5318276, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x512685", + "base": 5318277, + "width": 3, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x51a284", + "base": 5350020, + "width": 8, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x51a290", + "base": 5350032, + "width": 8, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x51a29c", + "base": 5350044, + "width": 8, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x51a2a8", + "base": 5350056, + "width": 8, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x51a2b4", + "base": 5350068, + "width": 8, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x51a2c0", + "base": 5350080, + "width": 8, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x51a2cc", + "base": 5350092, + "width": 8, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x51a2d8", + "base": 5350104, + "width": 8, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x57eeeb", + "base": 5762795, + "width": 7, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), strings (2026-09-19), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x57ef0f", + "base": 5762831, + "width": 11, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes classes (2026-09-19), reads (2026-09-18), strings (2026-09-19), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x57f0d8", + "base": 5763288, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x57f0f0", + "base": 5763312, + "width": 8, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x57f120", + "base": 5763360, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x57f1b4", + "base": 5763508, + "width": 3, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x57f1c0", + "base": 5763520, + "width": 2, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x57f23c", + "base": 5763644, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x581d60", + "base": 5774688, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x581d76", + "base": 5774710, + "width": 1, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes reads (2026-09-18), units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x581d7e", + "base": 5774718, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x581dd6", + "base": 5774806, + "width": 1, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x582178", + "base": 5775736, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x5821d4", + "base": 5775828, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x582204", + "base": 5775876, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x582234", + "base": 5775924, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x587f94", + "base": 5799828, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x58a364", + "base": 5808996, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x58a388", + "base": 5809032, + "width": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x58cf45", + "base": 5820229, + "width": 1, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x58d2b7", + "base": 5821111, + "width": 1, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x58dc60", + "base": 5823584, + "width": 16, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes classes (2026-09-19), leftovers (2026-09-19), units (2026-09-18), arrays (2026-09-19), callbacks (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x58dc70", + "base": 5823600, + "width": 4, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x58dc74", + "base": 5823604, + "width": 16, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x58dc84", + "base": 5823620, + "width": 1204, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x58e138", + "base": 5824824, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x58e14c", + "base": 5824844, + "width": 3820, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x58f038", + "base": 5828664, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x596a49", + "base": 5859913, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x596a5d", + "base": 5859933, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x596a69", + "base": 5859945, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x596a6f", + "base": 5859951, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x596a8e", + "base": 5859982, + "width": 2, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6283f8", + "base": 6456312, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x628438", + "base": 6456376, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x62848c", + "base": 6456460, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6284a8", + "base": 6456488, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6284b8", + "base": 6456504, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x640b60", + "base": 6556512, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x640c3a", + "base": 6556730, + "width": 2, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x640d14", + "base": 6556948, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x640dee", + "base": 6557166, + "width": 2, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x640ec8", + "base": 6557384, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x640fa2", + "base": 6557602, + "width": 2, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x64107c", + "base": 6557820, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x641156", + "base": 6558038, + "width": 2, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x641230", + "base": 6558256, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x64130a", + "base": 6558474, + "width": 2, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6413e4", + "base": 6558692, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x641598", + "base": 6559128, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x64159c", + "base": 6559132, + "width": 8, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6415a4", + "base": 6559140, + "width": 48, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes reads (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6509a0", + "base": 6621600, + "width": 4, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6509b0", + "base": 6621616, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), input (2026-09-18), inside (2026-09-19), leftovers (2026-09-19), map (2026-09-19), numbers (2026-09-18), reads (2026-09-18), recursion (2026-09-19), spike (2026-09-18), strings (2026-09-19), units (2026-09-18), v3 (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x654887", + "base": 6637703, + "width": 15, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x654aa0", + "base": 6638240, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x656eb0", + "base": 6647472, + "width": 2, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x660260", + "base": 6685280, + "width": 2, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes strings (2026-09-19), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6602aa", + "base": 6685354, + "width": 2, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x661002", + "base": 6688770, + "width": 1, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x66158c", + "base": 6690188, + "width": 2, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x66171a", + "base": 6690586, + "width": 1, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x661998", + "base": 6691224, + "width": 8, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6620d2", + "base": 6693074, + "width": 1, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x662350", + "base": 6693712, + "width": 4, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x662948", + "base": 6695240, + "width": 4, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x662df2", + "base": 6696434, + "width": 1, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x66318a", + "base": 6697354, + "width": 1, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x663272", + "base": 6697586, + "width": 1, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6636f2", + "base": 6698738, + "width": 1, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6637a0", + "base": 6698912, + "width": 2, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6637c0", + "base": 6698944, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6637c3", + "base": 6698947, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6637c5", + "base": 6698949, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6637da", + "base": 6698970, + "width": 1, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x66380a", + "base": 6699018, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x663823", + "base": 6699043, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x663888", + "base": 6699144, + "width": 2, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x664081", + "base": 6701185, + "width": 1, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x664101", + "base": 6701313, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x664168", + "base": 6701416, + "width": 4, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6644f8", + "base": 6702328, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x664532", + "base": 6702386, + "width": 1, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6c98a6", + "base": 7116966, + "width": 1, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6c9a68", + "base": 7117416, + "width": 4, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6c9d14", + "base": 7118100, + "width": 2, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6ca030", + "base": 7118896, + "width": 4, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6cddc0", + "base": 7134656, + "width": 1, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "0x6cddc4", + "base": 7134660, + "width": 8, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0x8", + "base": 5885104, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), leftovers (2026-09-19), numbers (2026-09-18), recursion (2026-09-19), units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0xc", + "base": 5885108, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), leftovers (2026-09-19), numbers (2026-09-18), recursion (2026-09-19), units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0x10", + "base": 5885112, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0x28", + "base": 5885136, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes classes (2026-09-19), input (2026-09-18), leftovers (2026-09-19), units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0x34", + "base": 5885148, + "width": 4, + "stride": 336, + "count": 1700, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0x4c", + "base": 5885172, + "width": 1, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), leftovers (2026-09-19), numbers (2026-09-18), recursion (2026-09-19), units (2026-09-18), input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0x4d", + "base": 5885173, + "width": 1, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), leftovers (2026-09-19), numbers (2026-09-18), recursion (2026-09-19), units (2026-09-18), input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0x4e", + "base": 5885174, + "width": 2, + "stride": 336, + "count": 1700, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0x64", + "base": 5885196, + "width": 2, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "unknown", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), leftovers (2026-09-19), numbers (2026-09-18), recursion (2026-09-19), units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0x68", + "base": 5885200, + "width": 8, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0x8f", + "base": 5885239, + "width": 1, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0xa2", + "base": 5885258, + "width": 2, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0xa5", + "base": 5885261, + "width": 1, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "touched by the probes arrays (2026-09-19), callbacks (2026-09-19), classes (2026-09-19), functions (2026-09-19), leftovers (2026-09-19), numbers (2026-09-18), recursion (2026-09-19), units (2026-09-18), input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0xdc", + "base": 5885316, + "width": 3, + "stride": 336, + "count": 1700, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0xdf", + "base": 5885319, + "width": 1, + "stride": 336, + "count": 1700, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes input (2026-09-18), units (2026-09-18), each played in Remastered with every line as expected" + ] + }, + { + "name": "unit table: +0x117", + "base": 5885375, + "width": 1, + "stride": 336, + "count": 1700, + "read": "unknown", + "write": "allowed", + "evidence": [ + "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" + ] + } + ] +} diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 3ed9215..8d5a46f 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -204,6 +204,50 @@ coverage. A1 therefore also shows coverage: lines that ran in the game, lines th Ships as 4.0.0, the first version with *Run in the game*, because it adds a whole mode. +**As built (2026-10-15, under Node; the editor's half comes with A2).** One command does it: +`npx tsx scripts/game-check.mts probes/units.ts`. It builds the script, plays it in the lifted +game with nobody at it, prints the probe's own checks, then lists each access Remastered was +seen to refuse or ignore, or has never been seen to do, by line. + +- **Back to the line** works as the candidate said, without touching eudplib. + `python/trigscript.py`, when the build sets `triggerMap`, wraps `RawTrigger.__init__`, + `Lowering.statement` and `EUDFuncN._create_func_body`, so every trigger is tagged with + the statement being lowered (or the eudplib function whose body it is). It also wraps + eudplib's module-level `_construct_payload` to log where each tagged trigger landed: + offset, conditions, actions, origin, 400 a line. The runner turns those offsets into the + address of every condition and action record (the payload sits at the end of `STRx`), and + the hooks already see each record's address. Triggers eudplib made at its own import, and + its variables (trigger-shaped, but not `RawTrigger`s), are put down as "eudplib". Only + eudplib's start code in the scenario's own TRIG stays unnamed. +- **The trace** (`scm-sim scripts/exact-eud.ts --trace`) is one row per origin, dword and + direction, with the union of the masks, so a check is byte by byte. A run of the units + probe has 42,815 rows. +- **The allowlist** (`data/allowlist.json`, made by `scripts/allowlist.mts`, 191 entries) + has three sources: + - Magenta's catalogue: 73 entries, a verified yes counts as allowed. + - Three failures seen in the game: unit position write ends the game; cloak bits and game + speed writes are ignored. + - The traces of the 15 played probes: 115 runs of bytes, the unit table folded into one + entry per field. Each probe passed in Remastered, so what its build touches was + allowed there. The traces come from today's builds, not the ones played, so this is + evidence by family of address, and a probe played again settles a new one. + + The map's own memory (strings and payload) is always allowed. eudplib's walk of the + game's heap (its trigger lists) is allowed; a line of the script in the heap is not seen. +- **What it showed with Magenta's catalogue alone** (an allowlist built without the + probes): nearly every line of every probe touches something Magenta never verified. + Examples are 0x6509B0 (eudplib's current-player trick, written around every action that + takes a player), the trigger lists at 0x51A284, and unit-table fields read by unit + loops. So the played probes are the larger part of the evidence. The check names each + such line, and an unseen field of the unit table is one finding however many units the + loop read. +- **Not yet:** the check sees only what the run reached (no coverage view yet), and runs only + under Node. The editor needs scm-sim's `run` (A2) before it can show a finding at its line. +- **Found on the way:** `spawnSync` keeps 1 MB of a child's output by default, and + plugin-eudplib's `build-map.mts` exits before a long log has gone down its pipe. It now + writes the log to `EUDPLIB_LOG_FILE` when asked, and `build-fixture.mts` reads it from + there. + ### A2. Tests in the game (4.0.0, with A1) `test()` blocks run in the lifted game as well as the simulator. diff --git a/python/trigscript.py b/python/trigscript.py index b30cbd6..2dcd061 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -3096,6 +3096,83 @@ def match_line(self): EUDEndIf() +# Where each trigger came from, for a run in the lifted game to name the line a trigger belongs to (TrigScript 4, +# A1). Only when the build asks for it (the `triggerMap` setting): every RawTrigger made is tagged with the +# statement being lowered, or with the eudplib function whose body it is, and once the payload is laid out one +# header line and lines of rows say where each trigger sits in it: +# TRIGSCRIPT-MAP {"payload": , "count": } +# TRIGSCRIPT-ROWS [[offset, conditions, actions, origin], ...] (400 a line, as many lines as it takes) +# An origin is "line:column" (or "file:line:column"), "fn:" for a function of eudplib's, "" for what no +# 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 +# finds it at the section's address plus its length less the payload's. +if settings.get("triggerMap"): + import eudplib.core.allocator.payload as _payload + from eudplib.core.eudfunc.eudfuncn import EUDFuncN as _EUDFuncN + from eudplib.core.rawtrigger.rawtriggerdef import RawTrigger as _RawTrigger + + _ORIGINS = [] + _MADE = {} + + def _origin_of(at): + if not at: + return "" + if at.get("file"): + return "%s:%s:%s" % (at.get("file"), at.get("line"), at.get("column")) + return "%s:%s" % (at.get("line"), at.get("column")) + + _trigger_init = _RawTrigger.__init__ + + def _tagged_trigger(self, *args, **kwargs): + _trigger_init(self, *args, **kwargs) + _MADE[self] = _ORIGINS[-1] if _ORIGINS else "" + + _RawTrigger.__init__ = _tagged_trigger + + _func_body = _EUDFuncN._create_func_body + + def _tagged_body(self): + _ORIGINS.append("fn:" + getattr(self._bodyfunc, "__name__", "?")) + try: + return _func_body(self) + finally: + _ORIGINS.pop() + + _EUDFuncN._create_func_body = _tagged_body + + _statement = Lowering.statement + + def _tagged_statement(self, st, ctx): + _ORIGINS.append(_origin_of(st.get("at")) or (_ORIGINS[-1] if _ORIGINS else "")) + try: + return _statement(self, st, ctx) + finally: + _ORIGINS.pop() + + Lowering.statement = _tagged_statement + + _construct = _payload._construct_payload + + def _construct_and_say(): + built = _construct() + found = _payload._found_objects_dict + rows = [] + # (Every trigger in the payload, also those eudplib made before this file was read, at its own import: + # those say "eudplib".) + for trg, i in found.items(): + if not isinstance(trg, _RawTrigger): + continue + origin = _MADE.get(trg, "eudplib") + rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin]) + rows.sort() + # (In parts, so that no one line of the log runs to megabytes.) + print("TRIGSCRIPT-MAP " + json.dumps({"payload": len(built.data), "count": len(rows)}, separators=(",", ":"))) + for k in range(0, len(rows), 400): + print("TRIGSCRIPT-ROWS " + json.dumps(rows[k:k + 400], separators=(",", ":"))) + return built + + _payload._construct_payload = _construct_and_say + + INPUT = Input(IR.get("input")) PROGRAMS = [Lowering(p) for p in IR.get("programs", [])] diff --git a/scripts/allowlist.mts b/scripts/allowlist.mts new file mode 100644 index 0000000..1be67f7 --- /dev/null +++ b/scripts/allowlist.mts @@ -0,0 +1,112 @@ +/** + * Writes data/allowlist.json: what Remastered is known to allow, each entry with what it rests on (see + * compiler/allowlist.ts). Three sources, in this order: + * + * 1. Magenta's catalogue (plugin-magenta's src/catalogue/eud.json): every entry with its address, its Remastered + * read/write and whether a probe verified it. A verified yes is allowed; anything else is not known. + * 2. Failures seen in the game, written out below with the probe that showed each. + * 3. The traces of TrigScript's probes run in the lifted game (scm-sim's `scripts/exact-eud.ts --trace`): every + * probe here was played in Remastered with every line as expected, so whatever its build reads or writes was + * allowed there. A trace is made from today's build of the probe, not the build that was played; the families of + * addresses are the same, and a probe played again is what makes a new one certain. + * + * npx tsx scripts/allowlist.mts .trace.json> [--magenta=] [--out=] + */ +import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { basename, join, resolve } from "node:path"; +import type { AllowEntry, Allowlist, Trace, Verdict } from "../compiler/allowlist"; + +const root = resolve(import.meta.dirname, ".."); +const args = process.argv.slice(2); +const traceDir = args.find((a) => !a.startsWith("--")); +const magentaPath = args.find((a) => a.startsWith("--magenta="))?.slice(10) ?? join(root, "..", "plugin-magenta", "src", "catalogue", "eud.json"); +if (!traceDir || !existsSync(traceDir)) { console.error("usage: allowlist.mts .trace.json> [--magenta=]"); process.exit(2); } + +/** When each probe was played in Remastered with every line as expected (docs/eud-plan.md's slices). */ +const PLAYED: Record = { + spike: "2026-09-18", v3: "2026-09-18", reads: "2026-09-18", units: "2026-09-18", input: "2026-09-18", numbers: "2026-09-18", + arrays: "2026-09-19", functions: "2026-09-19", recursion: "2026-09-19", callbacks: "2026-09-19", inside: "2026-09-19", + strings: "2026-09-19", classes: "2026-09-19", map: "2026-09-19", leftovers: "2026-09-19", +}; +/** 1.16.1's unit table: 1700 rows of 336 bytes. A probe's reads of it become one entry per field, not one per unit. */ +const UNITS = { base: 0x59cca8, stride: 336, count: 1700 }; +const ROWS = { stride: UNITS.stride, count: UNITS.count }; + +const entries: AllowEntry[] = []; + +// 1. Magenta's catalogue. +interface MagentaEntry { + id: string; name: string; width: number | "bit"; verified?: boolean; local?: boolean; + address?: { base: string; terms?: { arg: string; stride: number }[] }; + args?: { name: string; max?: number }[]; + remastered?: { read?: boolean; write?: boolean }; +} +const magenta = JSON.parse(readFileSync(magentaPath, "utf8")) as { entries: MagentaEntry[] }; +for (const e of magenta.entries) { + if (!e.address?.base) continue; + const base = Number(e.address.base), terms = e.address.terms ?? []; + const width = e.width === "bit" ? 4 : e.width; + const max = (arg: string) => e.args?.find((a) => a.name === arg)?.max ?? 1; + const verdict = (yes: boolean | undefined): Verdict => (yes && e.verified ? "allowed" : "unknown"); + const evidence = [`Magenta ${e.id}${e.verified ? ", verified by its probes" : ", not verified"}`]; + const shape = terms.length === 1 + ? { stride: terms[0].stride, count: max(terms[0].arg) } + : terms.length > 1 ? { width: terms.reduce((n, t) => n + t.stride * (max(t.arg) - 1), 0) + width } : {}; + entries.push({ name: e.name.toLowerCase(), base, width, ...shape, read: verdict(e.remastered?.read), write: verdict(e.remastered?.write), evidence, ...(e.local ? { local: true } : {}) }); +} + +// 2. What the game was seen to refuse or ignore (Magenta's docs/candidates.md). +entries.push( + { name: "unit table: position", base: 0x59ccd0, ...ROWS, width: 4, read: "allowed", write: "ends", evidence: ["Magenta probe 6, key I (2026-09-14): writing a unit's position ends the game, \"EUD not supported\""] }, + { name: "unit table: cloak flags", base: 0x59cd85, ...ROWS, width: 1, read: "allowed", write: "ignored", evidence: ["Magenta probe 6, keys Y and H (2026-09-14): status flags 0x100 and 0x200 written, no visible cloak"] }, + { name: "game speed", base: 0x6cdfd4, width: 4, read: "allowed", write: "ignored", evidence: ["Magenta probe 5, key 9 (2026-09-14): written, the game kept its speed"] }, +); + +// 3. The probes' traces. +/** byte address (or unit-table field) → what was seen there */ +const seen = new Map; write: Set }>(); +let probes = 0; +for (const file of readdirSync(traceDir).filter((f) => f.endsWith(".trace.json")).sort()) { + const probe = basename(file, ".trace.json"); + if (!PLAYED[probe]) { console.warn(`${file}: no play recorded for a probe called ${probe}; left out`); continue; } + probes++; + const trace = JSON.parse(readFileSync(join(traceDir, file), "utf8")) as Trace; + for (const [, address, write, mask] of trace.rows) { + if (address >= trace.strings.from && address < trace.strings.to) continue; + if (trace.heap && address >= trace.heap.from && address < trace.heap.to) continue; + for (let k = 0; k < 4; k++) { + if (!((mask >>> (8 * k)) & 0xff)) continue; + const byte = address + k, off = byte - UNITS.base; + const units = off >= 0 && off < UNITS.stride * UNITS.count; + const at = units ? UNITS.base + (off % UNITS.stride) : byte; + const key = `${units ? "u" : "b"}${at}`; + const s = seen.get(key) ?? { base: at, width: 1, units, read: new Set(), write: new Set() }; + (write ? s.write : s.read).add(probe); + seen.set(key, s); + } + } +} +// Runs of neighbouring bytes seen by the same probes the same way become one entry. +const bytes = [...seen.values()].sort((a, b) => Number(a.units) - Number(b.units) || a.base - b.base); +const same = (a: Set, b: Set) => a.size === b.size && [...a].every((x) => b.has(x)); +const runs: typeof bytes = []; +for (const b of bytes) { + const last = runs[runs.length - 1]; + if (last && last.units === b.units && last.base + last.width === b.base && same(last.read, b.read) && same(last.write, b.write)) last.width++; + else runs.push({ ...b }); +} +const named = (set: Set) => [...set].sort().map((p) => `${p} (${PLAYED[p]})`); +for (const r of runs) { + const evidence = [...new Set([...named(r.read), ...named(r.write)])]; + entries.push({ + name: r.units ? `unit table: +0x${(r.base - UNITS.base).toString(16)}` : `0x${r.base.toString(16)}`, + base: r.base, width: r.width, ...(r.units ? { stride: UNITS.stride, count: UNITS.count } : {}), + read: r.read.size ? "allowed" : "unknown", write: r.write.size ? "allowed" : "unknown", + evidence: [`touched by the probes ${evidence.join(", ")}, each played in Remastered with every line as expected`], + }); +} + +const list: Allowlist = { version: 1, entries }; +const out = args.find((a) => a.startsWith("--out="))?.slice(6) ?? join(root, "data", "allowlist.json"); +writeFileSync(out, JSON.stringify(list, null, 1) + "\n"); +console.log(`${out}: ${entries.length} entries — ${magenta.entries.length} from Magenta's catalogue, 3 failures, ${runs.length} runs from ${probes} probes' traces`); diff --git a/scripts/build-fixture.mts b/scripts/build-fixture.mts index e6e1524..34ec6fd 100644 --- a/scripts/build-fixture.mts +++ b/scripts/build-fixture.mts @@ -4,7 +4,7 @@ * plugin-eudplib checkout's `scripts/build-map.mts`, write the built map. For probe maps * that get played. * - * npx tsx scripts/build-fixture.mts probes/spike.ts fixtures/eud/spike-eud.scx [map.scx] [../plugin-eudplib] + * [TRIGGER_MAP=1] npx tsx scripts/build-fixture.mts probes/spike.ts fixtures/eud/spike-eud.scx [map.scx] [../plugin-eudplib] * * The output lands in the ignored fixtures/ folder: it is built on a Blizzard map, which is never committed. * @@ -95,10 +95,24 @@ writeFileSync(mapPath, withStrings); const extra = process.env.EXTRA_PLUGIN; const extraName = extra?.slice(0, extra.indexOf("=")); const sections = buildPlugins(r.input, "/work/files/trigscript.json"); +// TRIGGER_MAP=1: the Python says where each trigger it made sits in the payload (a TRIGSCRIPT-MAP line in the +// log), and that line is written beside the map as .triggers.json, for scm-sim's run in the lifted game. +if (process.env.TRIGGER_MAP) (sections.trigscript as Record).triggerMap = "1"; const { eudTurbo, ...before } = sections; writeFileSync(pluginsPath, JSON.stringify(extraName ? { ...before, [extraName]: {}, eudTurbo } : sections)); const extraArgs = extra && extraName ? [`${extraName}=${resolve(extra.slice(extraName.length + 1))}`] : []; -const res = spawnSync("npx", ["tsx", join(eudplib, "scripts", "build-map.mts"), mapPath, resolve(outPath), pluginsPath, `trigscript=${join(root, "python", "trigscript.py")}`, `file=trigscript.json=${irPath}`, ...extraArgs], { cwd: eudplib, encoding: "utf8", env: { ...process.env, EUDPLIB_LOG: "1" } }); +const logPath = join(dir, "eudplib.log"); +const res = spawnSync("npx", ["tsx", join(eudplib, "scripts", "build-map.mts"), mapPath, resolve(outPath), pluginsPath, `trigscript=${join(root, "python", "trigscript.py")}`, `file=trigscript.json=${irPath}`, ...extraArgs], { cwd: eudplib, encoding: "utf8", maxBuffer: 256 << 20, env: { ...process.env, EUDPLIB_LOG: "1", EUDPLIB_LOG_FILE: logPath } }); +// (The log whole, from the file: the copy on stdout can be cut short when it runs to megabytes.) +const fullLog = existsSync(logPath) ? readFileSync(logPath, "utf8") : res.stdout; rmSync(dir, { recursive: true, force: true }); -process.stdout.write(res.stdout); +const logLines = fullLog.split("\n"); +const mapLine = logLines.find((l) => l.startsWith("TRIGSCRIPT-MAP ")); +if (mapLine) { + const head = JSON.parse(mapLine.slice("TRIGSCRIPT-MAP ".length)) as { payload: number; count: number }; + const triggers = logLines.filter((l) => l.startsWith("TRIGSCRIPT-ROWS ")).flatMap((l) => JSON.parse(l.slice("TRIGSCRIPT-ROWS ".length)) as unknown[]); + if (triggers.length !== head.count) { console.error(`the map of triggers came out with ${triggers.length} of ${head.count} rows`); process.exit(1); } + writeFileSync(resolve(outPath).replace(/\.scx$/, ".triggers.json"), JSON.stringify({ payload: head.payload, triggers })); +} +process.stdout.write(res.stdout.split("\n").filter((l) => !l.startsWith("TRIGSCRIPT-MAP ") && !l.startsWith("TRIGSCRIPT-ROWS ")).join("\n")); if (res.status !== 0) { console.error(res.stderr); process.exit(1); } diff --git a/scripts/game-check.mts b/scripts/game-check.mts new file mode 100644 index 0000000..14be10e --- /dev/null +++ b/scripts/game-check.mts @@ -0,0 +1,47 @@ +/** + * A script played in the lifted game and held against what Remastered is known to allow (TrigScript 4, A1): + * built as Save builds it with the map of triggers on (scripts/build-fixture.mts, TRIGGER_MAP=1), played with nobody + * at it by a scm-sim checkout's scripts/exact-eud.ts (its lifted game, EUD let through, nothing drawn), and every + * dword the run read or wrote checked against data/allowlist.json, line by line. + * + * npx tsx scripts/game-check.mts [--frames=N] [--allowlist=path] [map.scx] + * + * A built map is taken as it is, with its .triggers.json beside it when there is one. Needs a scm-sim + * checkout beside this repository (or SCM_SIM_DIR) with its lifted game generated; says so when it is not there. + * Exits 1 when the run touched something Remastered ends the game on, or a probe line came out not as expected. + */ +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { basename, join, resolve } from "node:path"; +import { checkTrace, describeFinding, type Allowlist, type Trace } from "../compiler/allowlist"; + +const root = resolve(import.meta.dirname, ".."); +const args = process.argv.slice(2); +const flag = (n: string) => args.find((a) => a.startsWith(`--${n}=`))?.slice(n.length + 3); +const [input, baseMap] = args.filter((a) => !a.startsWith("--")); +if (!input || !existsSync(input)) { console.error("usage: game-check.mts [--frames=N] [--allowlist=path] [map.scx]"); process.exit(2); } +const sim = resolve(process.env.SCM_SIM_DIR ?? join(root, "..", "scm-sim")); +if (!existsSync(join(sim, "scripts", "exact-eud.ts"))) { console.error(`no scm-sim checkout with scripts/exact-eud.ts at ${sim} (set SCM_SIM_DIR)`); process.exit(2); } +const list = JSON.parse(readFileSync(flag("allowlist") ?? join(root, "data", "allowlist.json"), "utf8")) as Allowlist; + +const dir = mkdtempSync(join(tmpdir(), "trigscript-game-")); +try { + let map = resolve(input); + if (/\.ts$/.test(input)) { + map = join(dir, basename(input, ".ts") + ".scx"); + const built = spawnSync("npx", ["tsx", join(root, "scripts", "build-fixture.mts"), resolve(input), map, ...(baseMap ? [resolve(baseMap)] : [])], { cwd: root, encoding: "utf8", maxBuffer: 256 << 20, env: { ...process.env, TRIGGER_MAP: "1" } }); + if (built.status !== 0) { console.error(built.stdout.split("\n").slice(-20).join("\n"), built.stderr); process.exit(1); } + } + const tracePath = join(dir, "trace.json"); + const run = spawnSync("node", ["--experimental-strip-types", join(sim, "scripts", "exact-eud.ts"), map, `--frames=${flag("frames") ?? 3000}`, "--quiet", `--trace=${tracePath}`], { cwd: sim, encoding: "utf8", maxBuffer: 256 << 20 }); + process.stdout.write(run.stdout.split("\n").filter((l) => !/^pages (read|written):/.test(l)).join("\n")); + if (!existsSync(tracePath)) { console.error(run.stderr || "the run left no trace"); process.exit(1); } + const trace = JSON.parse(readFileSync(tracePath, "utf8")) as Trace; + const { findings, counted } = checkTrace(trace, list); + console.log(`\nRemastered: ${trace.rows.length} distinct accesses — ${counted.own} the map's own memory, ${counted.heap} eudplib in the game's heap, ${counted.allowed} known allowed, ${counted.ends} ending the game, ${counted.ignored} ignored, ${counted.unknown} never seen`); + for (const f of findings) console.log(` ${f.verdict === "ends" ? "ENDS THE GAME" : f.verdict === "ignored" ? "IGNORED" : "not seen"} ${describeFinding(f)}`); + if (findings.some((f) => f.verdict === "ends") || run.status !== 0) process.exitCode = 1; +} finally { + rmSync(dir, { recursive: true, force: true }); +} diff --git a/tests/allowlist.test.ts b/tests/allowlist.test.ts new file mode 100644 index 0000000..60fe1a0 --- /dev/null +++ b/tests/allowlist.test.ts @@ -0,0 +1,65 @@ +// The allowlist check (compiler/allowlist.ts) on traces made up for it: the map's own memory, the game's heap, +// what Magenta verified, what the game was seen to refuse or ignore, and what nobody has tried — byte by byte. +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { checkTrace, describeFinding, verdictOf, type Allowlist, type Trace, type TraceRow } from "../compiler/allowlist"; + +const list = JSON.parse(readFileSync(join(import.meta.dirname, "..", "data", "allowlist.json"), "utf8")) as Allowlist; +const UNIT = (i: number, off: number) => 0x59cca8 + 336 * i + off; +const trace = (rows: TraceRow[]): Trace => ({ frames: 100, strings: { from: 0x19190000, to: 0x191b0000 }, heap: { from: 0x30000000, to: 0x40000000 }, rows }); + +describe("the allowlist", () => { + it("has Magenta's verified fields allowed and the failures seen in the game", () => { + expect(verdictOf(list, UNIT(5, 8), true).verdict).toBe("allowed"); // a unit's hit points + expect(verdictOf(list, UNIT(5, 0x28), false).verdict).toBe("allowed"); // its position, read + expect(verdictOf(list, UNIT(5, 0x28), true).verdict).toBe("ends"); // … and written + expect(verdictOf(list, 0x6cdfd4, true).verdict).toBe("ignored"); // the game speed + expect(verdictOf(list, 0x7fff0000, false).verdict).toBe("unknown"); + }); + it("names what each verdict rests on", () => { + const e = verdictOf(list, UNIT(5, 0x28), true).entry!; + expect(e.evidence.join(" ")).toMatch(/Magenta probe 6/); + }); +}); + +describe("checkTrace", () => { + it("lets the map's own memory and eudplib's walk of the game's heap through", () => { + const r = checkTrace(trace([["12:3", 0x19191000, 1, 0xffffffff, 4], ["eudplib", 0x30a30010, 0, 0xffffffff, 2], ["", 0x30a30014, 1, 0xffffffff, 1]]), list); + expect(r.findings).toEqual([]); + expect(r.counted).toMatchObject({ own: 1, heap: 2 }); + }); + it("says at the line when a line of the script reaches into the game's heap", () => { + const r = checkTrace(trace([["40:7", 0x30a30010, 1, 0xffffffff, 1]]), list); + expect(r.findings).toMatchObject([{ line: 40, verdict: "unknown", write: true }]); + }); + it("finds a write Remastered ends the game on, and says why", () => { + const r = checkTrace(trace([["7:1", UNIT(3, 0x28), 1, 0xffffffff, 1], ["7:1", UNIT(9, 0x28), 1, 0x0000ffff, 1]]), list); + expect(r.findings).toHaveLength(1); + expect(r.findings[0]).toMatchObject({ line: 7, verdict: "ends", entry: "unit table: position", addresses: 2, times: 2 }); + expect(describeFinding(r.findings[0])).toMatch(/^line 7: writes unit table: position .* Remastered ends the game on this \(Magenta probe 6/); + }); + it("holds a masked write byte by byte: the cloak bits and the invincible bit share a dword", () => { + const flags = UNIT(2, 0xdc); + expect(checkTrace(trace([["3:1", flags, 1, 0x00000300, 1]]), list).findings).toMatchObject([{ verdict: "ignored", entry: "unit table: cloak flags" }]); + expect(checkTrace(trace([["3:1", flags, 1, 0x04000000, 1]]), list).findings).toEqual([]); + }); + it("says what nobody has tried, and puts the gravest first", () => { + const r = checkTrace(trace([["20:1", 0x7fff0000, 0, 0xffffffff, 1], ["5:1", 0x6cdfd4, 1, 0xffffffff, 1], ["9:1", UNIT(0, 0x28), 1, 0xffffffff, 1]]), list); + expect(r.findings.map((f) => f.verdict)).toEqual(["ends", "ignored", "unknown"]); + expect(describeFinding(r.findings[2])).toBe("line 20: reads 0x7fff0000 — not seen in Remastered: needs a probe"); + }); + it("calls eudplib's own code by its name", () => { + const r = checkTrace(trace([["fn:f_dwread_epd", 0x7fff0000, 0, 0xffffffff, 1]]), list); + expect(describeFinding(r.findings[0])).toMatch(/^eudplib's f_dwread_epd: reads/); + }); +}); + +describe("what no entry covers", () => { + it("is said by the table of the game it lies in, one finding a field however many units", () => { + const off = 0x7c; // a field no probe and no catalogue entry has touched + const r = checkTrace(trace([0, 1, 2, 500].map((i): TraceRow => ["38:3", UNIT(i, off), 0, 0xffffffff, 1])), list); + expect(r.findings).toHaveLength(1); + expect(describeFinding(r.findings[0])).toBe("line 38: reads unit table +0x7c (4 dwords from 0x59cd24) — not seen in Remastered: needs a probe"); + }); +}); From cadc31788f8086831f3f6e002a027adfcf38e5d2 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 00:22:07 -0600 Subject: [PATCH 003/238] Simulator: a unit a trigger or a program kills is not a death Kill Unit, Kill Unit At Location, a unit's kill() and hit points brought to 0 by damage() or a write all counted a death in the simulator. The lifted game counts none of them: only a unit that dies in the game, shot or killed by a spell, is a death (the editor's trigger reference, checked in the game, says the same of Kill Unit). Found by the first tests run in the lifted game. sim.kill() in a test stays a fight, and still counts. Co-Authored-By: Claude Opus 5.5 --- compiler/simulateIr.ts | 6 +++--- compiler/world.ts | 15 ++++++++++----- tests/testing.test.ts | 2 +- tests/world.test.ts | 9 ++++++--- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/compiler/simulateIr.ts b/compiler/simulateIr.ts index 7903d6c..ca68b57 100644 --- a/compiler/simulateIr.ts +++ b/compiler/simulateIr.ts @@ -405,7 +405,7 @@ class ProgramRun { const amount = verb.do === "damage" || verb.do === "heal" ? yield* this.amount(verb.amount) : 0; if (!u) return; switch (verb.do) { - case "kill": case "remove": this.sim.game.gone(u, verb.do === "kill"); this.sim.unitEvent(this, verb.do === "kill" ? ActionType.KillUnit : ActionType.RemoveUnit, u, at); break; + case "kill": case "remove": this.sim.game.gone(u, verb.do === "kill", undefined, false); this.sim.unitEvent(this, verb.do === "kill" ? ActionType.KillUnit : ActionType.RemoveUnit, u, at); break; case "give": { const to = this.sim.slotOf(verb.to); if (to < 12) u.owner = to; break; } case "order": this.sim.unitEvent(this, ActionType.Order, u, at, { location: verb.target, text: verb.order }); break; case "locate": this.sim.centre(verb.location, u.x, u.y); break; @@ -414,7 +414,7 @@ class ProgramRun { const step = verb.percent ? Math.floor((u.maxHp * 256 * amount) / 100) : amount * 256; const raw = verb.do === "damage" ? Math.max(0, u.hp * 256 - step) : Math.min(u.maxHp * 256, u.hp * 256 + step); u.hp = Math.ceil(raw / 256); - if (u.hp === 0) { this.sim.game.gone(u, true); this.sim.unitEvent(this, ActionType.KillUnit, u, at); } + if (u.hp === 0) { this.sim.game.gone(u, true, undefined, false); this.sim.unitEvent(this, ActionType.KillUnit, u, at); } break; } } @@ -849,7 +849,7 @@ class ProgramRun { const value = yield* this.amount(s.value as NumExpr); if (!u) return "next"; (u as unknown as Record)[field] = Math.min(value, UNIT_FIELD_MAX[field as UnitNumField] ?? 0xffff_ffff); - if (field === "hp" && u.hp === 0) { this.sim.game.gone(u, true); this.sim.unitEvent(this, ActionType.KillUnit, u, s.at); } + if (field === "hp" && u.hp === 0) { this.sim.game.gone(u, true, undefined, false); this.sim.unitEvent(this, ActionType.KillUnit, u, s.at); } return "next"; } case "unitDo": yield* this.unitDo(s.unit, s.verb, s.at); return "next"; diff --git a/compiler/world.ts b/compiler/world.ts index 84e0304..1cfecb6 100644 --- a/compiler/world.ts +++ b/compiler/world.ts @@ -181,12 +181,17 @@ export class World { }); } - /** Off the map, its place free for the next unit made. `killed`: it died — counted as a death, and as a kill of `by` when someone is named. */ - gone(unit: SimUnit, killed: boolean, by?: number): void { + /** + * Off the map, its place free for the next unit made. `killed`: it died. `fought`: it died in the game — shot, or + * killed by a spell — and so counts as a death, and as a kill of `by` when someone is named. A unit a trigger or a + * program kills (Kill Unit, a unit's kill(), hit points written down to 0) dies without counting: the lifted game + * says so for all three (TrigScript 4, A2, 2026-10-16), as the editor's trigger reference does for Kill Unit. + */ + gone(unit: SimUnit, killed: boolean, by?: number, fought = true): void { if (!unit.alive) return; unit.alive = false; if (this.table[unit.slot] === unit) this.table[unit.slot] = null; - if (!killed) return; + if (!killed || !fought) return; this.onDeath(unit); if (by !== undefined) this.killed.set(unit.type * 4096 + by, (this.killed.get(unit.type * 4096 + by) ?? 0) + 1); } @@ -288,9 +293,9 @@ export class World { for (const owner of owners) if (owner < 12) for (let i = 0; i < Math.max(1, a.modifier); i++) { const u = this.create(a.unitId, owner, x, y, properties); if (u) made?.push(u); } return true; } - case ActionType.KillUnit: for (const u of this.matching(owners, a.unitId)) this.gone(u, true); return true; + case ActionType.KillUnit: for (const u of this.matching(owners, a.unitId)) this.gone(u, true, undefined, false); return true; case ActionType.RemoveUnit: for (const u of this.matching(owners, a.unitId)) this.gone(u, false); return true; - case ActionType.KillUnitAt: for (const u of this.some(this.matching(owners, a.unitId, a.location), a.modifier)) this.gone(u, true); return true; + case ActionType.KillUnitAt: for (const u of this.some(this.matching(owners, a.unitId, a.location), a.modifier)) this.gone(u, true, undefined, false); return true; case ActionType.RemoveUnitAt: for (const u of this.some(this.matching(owners, a.unitId, a.location), a.modifier)) this.gone(u, false); return true; case ActionType.GiveUnits: { const to = this.players.of(a.target, current)[0]; diff --git a/tests/testing.test.ts b/tests/testing.test.ts index 381eb7e..1e24255 100644 --- a/tests/testing.test.ts +++ b/tests/testing.test.ts @@ -42,7 +42,7 @@ test("the second wave is bigger", (sim) => { expect(sim.count(P2, units.ZergZergling, SPAWN)).toBe(12); expect(sim).toHavePrinted("Wave 2"); expect(sim).not.toHavePrinted(/Wave 3/); - expect(sim.deaths(P1, units.TerranMarine)).toBe(2); + expect(sim.deaths(P1, units.TerranMarine)).toBe(0); // killed by the program's trigger: not deaths }); test("nothing happens with nobody on the beacon", (sim) => { sim.seconds(2); diff --git a/tests/world.test.ts b/tests/world.test.ts index a640f84..5ab33d7 100644 --- a/tests/world.test.ts +++ b/tests/world.test.ts @@ -60,7 +60,9 @@ describe("units are made", () => { }); describe("what is done to units", () => { - it("gives, moves, kills some and removes the rest; deaths are counted, removals are not", () => { + // (Neither a trigger's kill nor a removal is a death: only a unit that dies in the game counts. The lifted game says + // so — TrigScript 4, A2 — and the editor's trigger reference, checked in the game, says so of Kill Unit.) + it("gives, moves, kills some and removes the rest; neither the kills nor the removals are deaths", () => { const sim = run(` createUnit(P1, units.TerranMarine, 5, ${HOME}); giveUnits(P1, P2, units.TerranMarine, 2, ${HOME}); @@ -69,7 +71,7 @@ describe("what is done to units", () => { killUnitAt(P1, units.TerranMarine, 2, ${HOME}); removeUnit(P2, units.TerranMarine); print(\`\${a} \${b} \${countUnits(P1, units.AnyUnit)} \${countUnits(P2, units.AnyUnit)} \${deaths(P1, units.TerranMarine)} \${deaths(P2, units.TerranMarine)}\`);`); - expect(shown(sim)).toContain("3 2 1 0 2 0"); + expect(shown(sim)).toContain("3 2 1 0 0 0"); expect(sim.units.filter((u) => u.owner === 1).map((u) => [u.x, u.y])).toEqual([[1100, 1050], [1100, 1050]]); }); @@ -109,7 +111,8 @@ describe("what is done to units", () => { const programs = new ProgramSimulation(r.ir, { world, strings: r.strings, table: TABLE }); for (let i = 0; i < 4; i++) { world.step(); programs.step(); } expect(world.events.map((e) => e.text).filter(Boolean)).toEqual(["three left"]); - expect(world.death(0, ZERGLING)).toBe(1); + // (Killed by the program: gone, and not a death.) + expect(world.death(0, ZERGLING)).toBe(0); }); }); From 75f74cd885ede71a23216dce88e10a43cf085dd0 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 00:22:07 -0600 Subject: [PATCH 004/238] TrigScript 4, A2: a script's tests run in the lifted game - compiler/gameSim.ts: GameSim, the world a test is handed in the game, over a GameHandle the host supplies (scm-sim's testGame); place, kill (counted as a fight by the game's own routine), remove, give and move through the game's trigger actions and a scratch location; keys and the mouse as the local machine's memory; a typed line as the chat command; every read from memory. readLocations: the map's locations once eudplib has put them right. - compiler/testing.ts: TestWorldLike and a brand both worlds carry; TestRunOptions.makeWorld. - compiler/gameTests.ts + dist/gametest.js: the compile's JavaScript (CompileResult.modules) run again beside the game, without TypeScript. - scripts/test-game.mts: each test in the game and in the simulator, said twice; probes/game-tests.ts passes in both. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- compiler/compiler.ts | 7 +- compiler/gameSim.ts | 322 ++++++++++++++++++++++++++++++++++++++ compiler/gameTestEntry.ts | 6 + compiler/gameTests.ts | 42 +++++ compiler/testing.ts | 68 ++++++-- package.json | 2 +- probes/game-tests.ts | 62 ++++++++ scripts/test-game.mts | 93 +++++++++++ 9 files changed, 589 insertions(+), 15 deletions(-) create mode 100644 compiler/gameSim.ts create mode 100644 compiler/gameTestEntry.ts create mode 100644 compiler/gameTests.ts create mode 100644 probes/game-tests.ts create mode 100644 scripts/test-game.mts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a09e90c..ef008c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,5 +29,5 @@ jobs: # `dist/testing.js` and its declarations are what another repository's tests import at # a tag to run triggers, so they are held to the same rule. with: - artifacts: dist/compiler.js dist/testing.js dist/testing.d.ts + artifacts: dist/compiler.js dist/testing.js dist/testing.d.ts dist/gametest.js secrets: inherit diff --git a/compiler/compiler.ts b/compiler/compiler.ts index 6b881c1..aaff165 100644 --- a/compiler/compiler.ts +++ b/compiler/compiler.ts @@ -150,6 +150,11 @@ export interface CompileResult { input: InputPlan | null; /** The script's `test()`s, and what running them found; null when it has none, or did not compile that far. */ tests: TestReport | null; + /** + * The script as JavaScript, file by file, when it has tests and compiled: what runs them somewhere else — in the + * lifted game, beside the engine (`gameTests.ts`) — without TypeScript there. + */ + modules?: [string, LinkedFile][]; /** No errors: `triggers` and `ir` are the complete output. */ ok: boolean; } @@ -401,7 +406,7 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa ? runTests(registry, { triggers, sources, ir, strings: collector.strings, locate: where }, options.tests) : { list: listTests(registry).list, results: [], only: [], ms: 0 }; } - return result({ ir, input, triggers, sources, strings: collector.strings, variables, programs, buildTime, hints, tests }); + return result({ ir, input, triggers, sources, strings: collector.strings, variables, programs, buildTime, hints, tests, ...(registry.empty ? {} : { modules: [...linked] }) }); } /** diff --git a/compiler/gameSim.ts b/compiler/gameSim.ts new file mode 100644 index 0000000..7c08113 --- /dev/null +++ b/compiler/gameSim.ts @@ -0,0 +1,322 @@ +/** + * The world a test is handed when it runs in the lifted game (TrigScript 4, A2): `sim` as the simulator's `TestSim` + * has it, but every frame is the game's own and every read is the game's memory. It drives a `GameHandle`, which + * whatever hosts the game supplies (scm-sim's `src/exact/testGame.ts`); nothing here imports the game. + * + * What a test does to the world goes through the game's own trigger actions, run between frames as the player's + * trigger would run them: Create Unit for `place`, and for one unit Kill, Remove, Give or Move with a location + * centred on it (the last of the map's 255, put back afterwards). A typed line is the game's chat command; a key and + * the mouse are the local machine's memory, where MSQC reads them. + * + * Not in the game yet, and said so when asked: a program's variables (`program()`, which needs the build to say + * where each lives) and what another player was shown. A + * program's fault is the red line it prints ("TrigScript: …"), its line not known. + */ +import { ActionType } from "../vendor/triggers"; +import type { At } from "./ir"; +import { msqcKey } from "./input"; +import { ExpectError, WORLD, type TestEvent, type TestWorldLike } from "./testing"; +import type { SimBounds, SimUnit } from "./world"; + +/** What a host of the lifted game hands a test (scm-sim's `TestGameHandle`). */ +export interface GameHandle { + readonly m: DataView; + readonly u8: Uint8Array; + readonly frame: number; + readonly localPlayer: number; + readonly over: boolean; + step(): void; + lines(): { frame: number; text: string }[]; + action(record: Uint8Array, player: number): number; + chat(text: string): void; + call(address: number, registers: Record, args?: number[]): number; + reset(): void; +} + +// 1.16.1's addresses, which Remastered's emulation keeps. +const UNITS = 0x59cca8, UNIT_BYTES = 336, UNIT_COUNT = 1700, FIRST_UNIT = 0x628430; +const ORE = 0x57f0f0, GAS = 0x57f120, DEATHS = 0x58a364, KILLS = 0x5878a4, SWITCHES = 0x58dc40, LOCATIONS = 0x58dc60; +const MAX_HP = 0x662350, MAX_SHIELDS = 0x660e00, KEYS = 0x596a18, BUTTONS = 0x6cddc0, MOUSE = 0x6cddc4; +const SCRATCH = 255, SCRATCH_TO = 254; +/** The routine a unit that dies in the game goes through to be counted: its owner's death, the killer's kill (0x488AF0). */ +const COUNT_DEATH = 0x488af0; +const BUTTON_BITS = { left: 2, right: 8, middle: 32 } as const; +const FRAMES_PER_SECOND = 24, UNTIL_FRAMES = 2400; + +/** The frame from which the game's own locations are the map's (eudplib has put them right by then). */ +export const LOCATIONS_READY = 2; + +function readLocation(game: GameHandle, n: number): SimBounds | undefined { + const m = game.m, at = LOCATIONS + 20 * (n - 1); + const b = { left: m.getUint32(at, true), top: m.getUint32(at + 4, true), right: m.getUint32(at + 8, true), bottom: m.getUint32(at + 12, true) }; + return b.right > b.left || b.bottom > b.top ? b : undefined; +} + +/** + * The map's locations as the game holds them once eudplib has put them right: played to `LOCATIONS_READY` and put + * back to frame 0. For `GameSim`'s `locations`, and for the simulator's world of the same map. + */ +export function readLocations(game: GameHandle): Record { + game.reset(); + while (game.frame < LOCATIONS_READY && !game.over) game.step(); + const out: Record = {}; + for (let n = 1; n <= 255; n++) { const b = readLocation(game, n); if (b) out[n] = b; } + game.reset(); + return out; +} + +/** Each unit type's hit points and shields as the game's tables have them: the simulator's `unitStats` for the same map. */ +export function readUnitStats(game: GameHandle): Record { + const out: Record = {}; + for (let t = 0; t < 228; t++) out[t] = { hp: Math.floor(game.m.getInt32(MAX_HP + 4 * t, true) / 256), shields: game.m.getUint16(MAX_SHIELDS + 2 * t, true) }; + return out; +} + +/** The virtual key MSQC's name for a key stands for. */ +function virtualKey(key: string): number { + const k = msqcKey(key); + if (/^[A-Z0-9]$/.test(k)) return k.charCodeAt(0); + if (/^F([1-9]|1[0-2])$/.test(k)) return 0x6f + Number(k.slice(1)); + if (/^NUMPAD[0-9]$/.test(k)) return 0x60 + Number(k.slice(6)); + const named: Record = { SPACE: 0x20, ENTER: 0x0d, ESC: 0x1b, TAB: 0x09, SHIFT: 0x10, LCTRL: 0x11, LALT: 0x12, LEFT: 0x25, UP: 0x26, RIGHT: 0x27, DOWN: 0x28, BACK: 0x08, DELETE: 0x2e, INSERT: 0x2d, HOME: 0x24, END: 0x23, PGUP: 0x21, PGDN: 0x22 }; + if (k in named) return named[k]; + throw new ExpectError(`press: "${key}" is not a key the game reports`); +} + +/** A line as a player reads it: the game's colour codes and eudplib's padding (carriage returns) taken out. */ +const clean = (text: string) => text.replace(/[\x01-\x1f]/g, ""); + +/** A unit as the game holds it: every property read from its slot while it is the same unit, its last value after. */ +class GameUnit implements SimUnit { + readonly slot: number; + readonly uid: number; + private readonly at: number; + private last: Partial = {}; + private readonly game: GameHandle; + constructor(game: GameHandle, slot: number) { + this.game = game; + this.slot = slot; + this.at = UNITS + UNIT_BYTES * slot; + this.uid = game.u8[this.at + 0xa5]; + } + get alive(): boolean { + const { m, u8 } = this.game; + return u8[this.at + 0xa5] === this.uid && m.getUint32(this.at + 0x0c, true) !== 0 && u8[this.at + 0x4d] !== 0; + } + private read(key: K, f: () => SimUnit[K]): SimUnit[K] { + if (this.alive) (this.last as Record)[key] = f(); + return (this.last[key] ?? 0) as SimUnit[K]; + } + private get m() { return this.game.m; } + private flag(bit: number): boolean { return (this.m.getUint32(this.at + 0xdc, true) & bit) !== 0; } + get type() { return this.read("type", () => this.m.getUint16(this.at + 0x64, true)); } + get owner() { return this.read("owner", () => this.game.u8[this.at + 0x4c]); } + get x() { return this.read("x", () => this.m.getInt16(this.at + 0x28, true)); } + get y() { return this.read("y", () => this.m.getInt16(this.at + 0x2a, true)); } + get hp() { return this.read("hp", () => Math.ceil(this.m.getInt32(this.at + 0x08, true) / 256)); } + set hp(v: number) { if (this.alive) this.m.setInt32(this.at + 0x08, Math.max(0, v) * 256, true); } + get maxHp() { return Math.floor(this.m.getInt32(MAX_HP + 4 * this.type, true) / 256); } + get shields() { return this.read("shields", () => Math.floor(this.m.getInt32(this.at + 0x60, true) / 256)); } + get maxShields() { return this.m.getUint16(MAX_SHIELDS + 2 * this.type, true); } + get energy() { return this.read("energy", () => Math.floor(this.m.getUint16(this.at + 0xa2, true) / 256)); } + get kills() { return this.read("kills", () => this.game.u8[this.at + 0x8f]); } + get orderId() { return this.read("orderId", () => this.game.u8[this.at + 0x4d]); } + get cooldown() { return this.read("cooldown", () => this.game.u8[this.at + 0x55]); } + get resources() { return this.read("resources", () => this.m.getUint16(this.at + 0xd0, true)); } + get invincible() { return this.read("invincible", () => this.flag(0x04000000)); } + get cloaked() { return this.read("cloaked", () => this.flag(0x00000300)); } + get burrowed() { return this.read("burrowed", () => this.flag(0x00000010)); } + get hallucinated() { return this.read("hallucinated", () => this.flag(0x40000000)); } + // Not read from the game yet: the spell timers and whether the unit is under attack. + stim = 0; ensnare = 0; plague = 0; lockdown = 0; maelstrom = 0; irradiate = 0; stasis = 0; underAttack = false; +} + +export class GameSim implements TestWorldLike { + readonly [WORLD] = true as const; + faultsExpected = false; + private shown: { frame: number; text: string }[] = []; + private readonly units_ = new Map(); + private readonly game: GameHandle; + private readonly known: Record; + + /** + * `locations`: the map's locations as the script means them. eudplib keeps them scrambled in the game until its + * first frames put them right (frame 2 on a probe's map), so a test that acts at frame 0 needs them from elsewhere + * (`readLocations`); from then on the game's own are read. + */ + constructor(game: GameHandle, options: { locations?: Record } = {}) { + this.game = game; + this.known = options.locations ?? {}; + game.reset(); + this.take(); + } + + private take(): void { for (const l of this.game.lines()) this.shown.push(l); } + private record(type: number, fields: { location?: number; player?: number; number?: number; unit?: number; count?: number }): Uint8Array { + const r = new Uint8Array(32), v = new DataView(r.buffer); + v.setUint32(0x00, fields.location ?? 0, true); + v.setUint32(0x10, fields.player ?? 0, true); + v.setUint32(0x14, fields.number ?? 0, true); + v.setUint16(0x18, fields.unit ?? 0, true); + r[0x1a] = type; + r[0x1b] = fields.count ?? 0; + r[0x1c] = 0x04; // (always display: the flag every action the editor writes carries) + return r; + } + private act(record: Uint8Array, player: number): void { this.game.action(record, player); this.take(); } + + get frame(): number { return this.game.frame; } + seed(_n: number): this { return this; } + + /* ── time ── */ + + frames(n = 1): this { + for (let i = 0; i < n && !this.game.over; i++) { this.game.step(); this.take(); this.releaseKeys(); } + return this; + } + seconds(n: number): this { return this.frames(Math.round(n * FRAMES_PER_SECOND)); } + until(done: () => unknown, most = UNTIL_FRAMES): this { + for (let i = 0; ; i++) { + if (done()) return this; + if (i >= most) throw new ExpectError(`until: still not true after ${most} frames`); + this.frames(1); + } + } + + /* ── what the test does to the world ── */ + + private slots(): Set { + const out = new Set(), m = this.game.m; + for (let p = m.getUint32(FIRST_UNIT, true), guard = 0; p && guard < UNIT_COUNT; p = m.getUint32(p + 0x04, true), guard++) out.add((p - UNITS) / UNIT_BYTES); + return out; + } + private unit(slot: number): GameUnit { + const u8 = this.game.u8, uid = u8[UNITS + UNIT_BYTES * slot + 0xa5]; + const was = this.units_.get(slot); + if (was && was.uid === uid) return was; + const u = new GameUnit(this.game, slot); + this.units_.set(slot, u); + return u; + } + + /** + * `run(location)` with a scratch location set to `b` — its four sides, and its elevation flags cleared (a flag + * left set keeps the units of that elevation out of it, and at frame 0 they are eudplib's scrambled bits) — then + * put back as it was, all twenty bytes. + */ + private scratch(n: number, b: SimBounds, run: (location: number) => T): T { + const u8 = this.game.u8, m = this.game.m, at = LOCATIONS + 20 * (n - 1), was = u8.slice(at, at + 20); + [b.left, b.top, b.right, b.bottom].forEach((v, i) => m.setUint32(at + 4 * i, Math.max(0, v), true)); + m.setUint16(at + 0x12, 0, true); + try { return run(n); } finally { u8.set(was, at); } + } + private within255(b: SimBounds, run: (location: number) => T): T { return this.scratch(SCRATCH, b, run); } + place(player: number, type: number, at: number, n = 1): SimUnit[] { + const where = this.location(at); + if (!where) throw new ExpectError(`place: there is no location ${at}`); + const before = this.slots(); + this.within255(where, (loc) => this.act(this.record(ActionType.CreateUnit, { location: loc, player, unit: type, count: Math.min(255, n) }), player)); + return [...this.slots()].filter((s) => !before.has(s)).map((s) => this.unit(s)).filter((u) => u.type === type && u.owner === player); + } + /** One unit, by the scratch location centred on it for one action. */ + private onOne(unit: SimUnit, run: (location: number) => void): this { + if (unit.alive) this.within255({ left: unit.x - 1, top: unit.y - 1, right: unit.x + 1, bottom: unit.y + 1 }, run); + return this; + } + /** + * What a fight is in a test, as `TestSim` has it: the unit dies and counts as its owner's death, and as a kill of + * `by`'s. The game's Kill Unit counts nothing (only a death in a fight does), so the game's own routine that counts + * one is called first, the unit and the killer handed to it as the game hands them, then the unit is killed. + */ + kill(unit: SimUnit, by?: number): this { + if (!unit.alive) return this; + this.game.call(COUNT_DEATH, { edi: UNITS + UNIT_BYTES * unit.slot, edx: by ?? unit.owner, ecx: 0 }); + return this.onOne(unit, (loc) => this.act(this.record(ActionType.KillUnitAt, { location: loc, player: unit.owner, unit: unit.type, count: 1 }), unit.owner)); + } + remove(unit: SimUnit): this { + return this.onOne(unit, (loc) => this.act(this.record(ActionType.RemoveUnitAt, { location: loc, player: unit.owner, unit: unit.type, count: 1 }), unit.owner)); + } + give(unit: SimUnit, to: number): this { + return this.onOne(unit, (loc) => this.act(this.record(ActionType.GiveUnits, { location: loc, player: unit.owner, number: to, unit: unit.type, count: 1 }), unit.owner)); + } + move(unit: SimUnit, to: number | { x: number; y: number }): this { + // (The destination through a second scratch location, so that a location still scrambled at frame 0 is not used.) + const b = typeof to === "number" ? this.location(to) : { left: to.x, top: to.y, right: to.x, bottom: to.y }; + if (!b) throw new ExpectError(`move: there is no location ${String(to)}`); + return this.scratch(SCRATCH_TO, b, (dest) => this.onOne(unit, (loc) => this.act(this.record(ActionType.MoveUnit, { location: loc, player: unit.owner, number: dest, unit: unit.type, count: 1 }), unit.owner))); + } + + /* ── what the players do: the local player only ── */ + + private held: number[] = []; + private local(player: number | undefined, what: string): void { + if (player !== undefined && player !== this.game.localPlayer) throw new ExpectError(`${what}: in the game only the player at this computer (P${this.game.localPlayer + 1}) can do it`); + } + /** A key down for one frame, as a press is (MSQC sees it down, then up). */ + press(key: string, player?: number): this { this.local(player, "press"); const k = virtualKey(key); this.game.u8[KEYS + k] = 1; this.held.push(k); return this; } + click(button: "left" | "right" | "middle" = "left", player?: number): this { + this.local(player, "click"); + const m = this.game.m; + m.setUint32(BUTTONS, m.getUint32(BUTTONS, true) | BUTTON_BITS[button], true); + this.held.push(-BUTTON_BITS[button]); + return this; + } + private releaseKeys(): void { + if (!this.held.length) return; + const m = this.game.m; + for (const k of this.held.splice(0)) { if (k >= 0) this.game.u8[KEYS + k] = 0; else m.setUint32(BUTTONS, m.getUint32(BUTTONS, true) & ~-k, true); } + } + type(line: string, player?: number): this { this.local(player, "type"); this.game.chat(line); return this; } + moveMouse(x: number, y: number, player?: number): this { + this.local(player, "moveMouse"); + this.game.m.setUint16(MOUSE, x, true); + this.game.m.setUint16(MOUSE + 2, y, true); + return this; + } + + /* ── the world read back ── */ + + private owners(player: number): number[] { + if (player < 12) return [player]; + if (player === 13) return [this.game.localPlayer]; + if (player === 17) return Array.from({ length: 12 }, (_, i) => i); + throw new ExpectError(`player ${player}: only one player, Current Player or All Players are read in the game yet`); + } + private within(u: SimUnit, at: number): boolean { + const b = this.location(at); + return !!b && u.x >= b.left && u.x <= b.right && u.y >= b.top && u.y <= b.bottom; + } + units(filter: { owner?: number; type?: number; at?: number } = {}): SimUnit[] { + const owners = filter.owner === undefined ? null : this.owners(filter.owner); + return [...this.slots()].sort((a, b) => a - b).map((s) => this.unit(s)) + .filter((u) => (owners === null || owners.includes(u.owner)) && (filter.type === undefined || u.type === filter.type) && (filter.at === undefined || this.within(u, filter.at))); + } + count(player: number, type: number, at?: number): number { return this.units({ owner: player, type, at }).length; } + resources(player: number): { ore: number; gas: number } { + const m = this.game.m; + return { ore: m.getUint32(ORE + 4 * player, true), gas: m.getUint32(GAS + 4 * player, true) }; + } + deaths(player: number, type: number): number { return this.game.m.getUint32(DEATHS + 4 * (player + 12 * type), true); } + kills(player: number, type: number): number { return this.owners(player).reduce((n, p) => n + this.game.m.getUint32(KILLS + 4 * (p + 12 * type), true), 0); } + switch(n: number): boolean { return ((this.game.u8[SWITCHES + (n >> 3)] >> (n & 7)) & 1) === 1; } + location(n: number): SimBounds | undefined { + if (n < 1 || n > 255) return undefined; + if (this.game.frame < LOCATIONS_READY) return this.known[n] ? { ...this.known[n] } : undefined; + return readLocation(this.game, n); + } + program(): Record { + throw new ExpectError("sim.program(…) reads a program's variables in the simulator only, for now: in the game, read what the program did (printed, resources, units)"); + } + + /** What was shown, in order, with the game's colour codes taken out. `player`: only the player at this computer is seen. */ + printed(player?: number): string[] { + if (player !== undefined && player !== this.game.localPlayer) throw new ExpectError(`printed(P${player + 1}): in the game only what the player at this computer (P${this.game.localPlayer + 1}) was shown is seen`); + return this.shown.map((l) => clean(l.text)); + } + log(): TestEvent[] { return this.shown.map((l) => ({ frame: l.frame, text: `Display Text Message — ${clean(l.text)}`, player: this.game.localPlayer })); } + get events(): TestEvent[] { return this.log(); } + /** A program's faults, as it says them in the game: a red line that begins "TrigScript:". */ + get faults(): { at: At; message: string; cycle: number; frame: number; program: number }[] { + return this.shown.filter((l) => /^\W*TrigScript: /.test(l.text)).map((l) => ({ at: { file: "", line: 0, column: 0 } as At, message: clean(l.text).replace(/^TrigScript: /, ""), cycle: 0, frame: l.frame, program: 0 })); + } +} diff --git a/compiler/gameTestEntry.ts b/compiler/gameTestEntry.ts new file mode 100644 index 0000000..b5551bb --- /dev/null +++ b/compiler/gameTestEntry.ts @@ -0,0 +1,6 @@ +/** + * `dist/gametest.js`: what a host of the lifted game imports to run a script's tests beside it (see `gameTests.ts`). + * The plugin never loads it itself; it hands its text to the host. + */ +export { runGameTests, type GameTestPayload } from "./gameTests"; +export { GameSim, readLocations, readUnitStats, type GameHandle } from "./gameSim"; diff --git a/compiler/gameTests.ts b/compiler/gameTests.ts new file mode 100644 index 0000000..1c0b970 --- /dev/null +++ b/compiler/gameTests.ts @@ -0,0 +1,42 @@ +/** + * A script's tests run in the lifted game (TrigScript 4, A2), where the game is: beside the engine, in whatever + * worker hosts it, with no TypeScript there. The compile hands over the script as JavaScript (`CompileResult.modules`) + * and the names it was compiled with; this runs the modules again to register the tests, and runs each with a + * `GameSim` over the host's `GameHandle`, the game put back to frame 0 before each. What it returns is plain data, + * the `TestReport` the editor already shows for the simulator. + * + * Bundled on its own as `dist/gametest.js`, which the plugin hands to the host as text. + */ +import { ENTRY_FILE } from "./compiler"; +import { MODULE_NAME } from "./api"; +import { GameSim, readLocations, type GameHandle } from "./gameSim"; +import { locate, runModules, type LinkedFile } from "./link"; +import type { ScriptNames } from "./names"; +import { Collector, createRuntime } from "./runtime"; +import { isTestFile, runTests, TestRegistry, TESTING_NAMES, type TestReport } from "./testing"; +import type { SimBounds } from "./world"; + +export interface GameTestPayload { + modules: [string, LinkedFile][]; + names: ScriptNames; + /** Run only these: tests of these files, or with these ids. Absent: all. */ + files?: string[]; + ids?: string[]; + /** The map's locations as the script means them; read from the game (frame 2) when absent. */ + locations?: Record; +} + +export function runGameTests(game: GameHandle, payload: GameTestPayload): TestReport & { error?: string } { + const linked = new Map(payload.modules); + const collector = new Collector(); + const registry = new TestRegistry(); + const where = (err: unknown) => { const at = locate(err, linked); return at.file ? { file: at.file, line: at.line ?? 1, ...(at.column ? { column: at.column } : {}) } : null; }; + registry.where = () => where(new Error()) ?? { file: ENTRY_FILE, line: 1 }; + const runtime = { ...createRuntime(payload.names, collector), ...registry.library() }; + collector.running = true; + const failure = runModules(linked, ENTRY_FILE, runtime, MODULE_NAME, { imported: TESTING_NAMES, then: [...linked.keys()].filter(isTestFile).sort() }); + collector.running = false; + if (failure) return { list: [], results: [], only: [], ms: 0, error: `${failure.file ?? ENTRY_FILE}:${failure.line ?? 1}: ${failure.message}` }; + const locations = payload.locations ?? readLocations(game); + return runTests(registry, { triggers: [], sources: [], ir: [], strings: [], locate: where }, { files: payload.files, ids: payload.ids, makeWorld: () => new GameSim(game, { locations }) }); +} diff --git a/compiler/testing.ts b/compiler/testing.ts index 014ee5a..4085063 100644 --- a/compiler/testing.ts +++ b/compiler/testing.ts @@ -25,8 +25,8 @@ import type { SimBounds, SimUnit, SimUnitInit, SimUnitProperties } from "./world /* ── What is registered ── */ type Mode = "run" | "skip" | "only"; -type TestFn = (sim: TestSim, ...args: unknown[]) => unknown; -type Hook = (sim: TestSim) => unknown; +type TestFn = (sim: TestWorldLike, ...args: unknown[]) => unknown; +type Hook = (sim: TestWorldLike) => unknown; interface Where { file: string; line: number; column?: number } interface Suite { @@ -112,6 +112,11 @@ export interface TestWorld { export interface TestRunOptions { world?: TestWorld; + /** + * A world other than the simulator's for each test (the lifted game's, `GameSim`): made fresh for every test, + * given the player the test asked to be (`{ as: P2 }`). Absent: the simulator. + */ + makeWorld?: (as?: number) => TestWorldLike; /** Run only these: tests of these files, or with these ids (a suite's id runs what is under it). Absent: all. */ files?: string[]; ids?: string[]; @@ -212,13 +217,51 @@ export class TestRegistry { /** The names `library()` gives: a script imports them, they are not globals (a script may well have a `test` of its own). */ export const TESTING_NAMES = ["test", "it", "describe", "beforeEach", "afterEach", "expect"] as const; +/* ── The world a test is handed ── */ + +/** What every world a test may be handed carries: the simulator's (`TestSim`) and the lifted game's (`GameSim`). */ +export const WORLD = Symbol.for("trigscript.world"); +export const isWorld = (v: unknown): v is TestWorldLike => typeof v === "object" && v !== null && (v as { [WORLD]?: unknown })[WORLD] === true; + +/** What a test can ask of its world, whichever it was handed. */ +export interface TestWorldLike { + readonly [WORLD]: true; + faultsExpected: boolean; + readonly frame: number; + seed(n: number): this; + frames(n?: number): this; + seconds(n: number): this; + until(done: () => unknown, most?: number): this; + place(player: number, type: number, at: number, n?: number): SimUnit[]; + kill(unit: SimUnit, by?: number): this; + remove(unit: SimUnit): this; + give(unit: SimUnit, to: number): this; + move(unit: SimUnit, to: number | { x: number; y: number }): this; + press(key: string, player?: number): this; + click(button?: "left" | "right" | "middle", player?: number): this; + type(line: string, player?: number): this; + moveMouse(x: number, y: number, player?: number): this; + count(player: number, type: number, at?: number): number; + units(filter?: { owner?: number; type?: number; at?: number }): SimUnit[]; + resources(player: number): { ore: number; gas: number }; + deaths(player: number, type: number): number; + kills(player: number, type: number): number; + switch(n: number): boolean; + location(n: number): SimBounds | undefined; + program(name?: string | number, player?: number): Record; + printed(player?: number): string[]; + log(sourceOf?: (trigger: number) => Where | null): TestEvent[]; + readonly events: TestEvent[]; + readonly faults: { at: At; message: string; cycle: number; frame: number; program: number }[]; +} + /* ── expect ── */ const show = (v: unknown): string => { if (typeof v === "string") return JSON.stringify(v); if (typeof v === "function") return "a function"; if (v instanceof RegExp) return String(v); - if (v instanceof TestSim) return "sim"; + if (isWorld(v)) return "sim"; try { return JSON.stringify(v, (_, x) => (typeof x === "bigint" ? String(x) : x === undefined ? "undefined" : x)) ?? String(v); } catch { return String(v); } }; const brief = (v: unknown): string => { const s = show(v); return s.length > 60 ? `${s.slice(0, 57)}…` : s; }; @@ -237,11 +280,11 @@ function expect(actual: unknown) { /** `holds` is the matcher's answer; `what` finishes "expected …". */ const check = (holds: boolean, what: string, expected?: unknown) => { if (holds !== not) return; - const got = actual instanceof TestSim ? "" : `, got ${brief(actual)}`; - throw new ExpectError(`expected ${not ? "not " : ""}${what}${got}`, expected === undefined ? undefined : show(expected), actual instanceof TestSim ? undefined : show(actual)); + const got = isWorld(actual) ? "" : `, got ${brief(actual)}`; + throw new ExpectError(`expected ${not ? "not " : ""}${what}${got}`, expected === undefined ? undefined : show(expected), isWorld(actual) ? undefined : show(actual)); }; const number = (name: string): number => { if (typeof actual !== "number") throw new ExpectError(`${name}: ${brief(actual)} is not a number`); return actual; }; - const sim = (name: string): TestSim => { if (!(actual instanceof TestSim)) throw new ExpectError(`${name} is asked of the world: expect(sim).${name}(…)`); return actual; }; + const sim = (name: string): TestWorldLike => { if (!isWorld(actual)) throw new ExpectError(`${name} is asked of the world: expect(sim).${name}(…)`); return actual; }; return { toBe: (e: unknown) => check(Object.is(actual, e), brief(e), e), toEqual: (e: unknown) => check(same(actual, e), brief(e), e), @@ -293,8 +336,9 @@ const mulberry = (seed: number) => () => { return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; -/** The world a test is handed. */ -export class TestSim { +/** The world a test is handed: the simulator's. */ +export class TestSim implements TestWorldLike { + readonly [WORLD] = true as const; /** Set by `expect(sim).toHaveFaulted`: the faults are the test's business, and do not fail it by themselves. */ faultsExpected = false; private rng = mulberry(DEFAULT_SEED); @@ -476,7 +520,7 @@ export function runTests(registry: TestRegistry, ctx: TestContext, options: Test for (let s: Suite | null = test.parent; s; s = s.parent) chain.unshift(s); const skipped = test.mode === "skip" || chain.some((s) => s.mode === "skip") || (narrowed.has(info.file) && test.mode !== "only" && !chain.some((s) => s.mode === "only")); if (skipped) { results.push({ id: info.id, status: "skipped", printed: [], events: [], frames: 0, ms: 0 }); continue; } - results.push(runOne(info.id, test, chain, ctx, options.world ?? {})); + results.push(runOne(info.id, test, chain, ctx, options.world ?? {}, options.makeWorld)); } } finally { registry.running = false; @@ -484,14 +528,14 @@ export function runTests(registry: TestRegistry, ctx: TestContext, options: Test return { list, results, only, ms: Date.now() - started }; } -function runOne(id: string, test: Case, chain: Suite[], ctx: TestContext, world: TestWorld): TestResult { +function runOne(id: string, test: Case, chain: Suite[], ctx: TestContext, world: TestWorld, makeWorld?: (as?: number) => TestWorldLike): TestResult { const began = Date.now(); - let sim: TestSim | null = null; + let sim: TestWorldLike | null = null; const done = (extra: Partial): TestResult => ({ id, status: "passed", printed: sim?.printed() ?? [], events: (sim?.log((t) => ctx.sources[t] ?? null) ?? []).slice(0, 500), frames: sim?.frame ?? 0, ms: Date.now() - began, ...extra, }); try { - sim = new TestSim(ctx.triggers, ctx.ir, ctx.strings, world, test.as); + sim = makeWorld ? makeWorld(test.as) : new TestSim(ctx.triggers, ctx.ir, ctx.strings, world, test.as); for (const s of chain) for (const h of s.before) h(sim); const returned = test.fn(sim, ...test.args); if (returned && typeof (returned as { then?: unknown }).then === "function") throw new Error("A test is not async: what it returned is a promise. Nothing in sim waits."); diff --git a/package.json b/package.json index 573b321..02e6c3e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "./package.json": "./package.json" }, "scripts": { - "build": "esbuild plugin.ts --bundle --format=esm --target=es2022 --platform=browser --outfile=dist/plugin.js --banner:js=\"// Generated by npm run build - do not edit; the source is plugin.ts.\" && esbuild compiler/entry.ts --bundle --format=esm --target=es2022 --platform=browser --outfile=dist/compiler.js --banner:js=\"// Generated by npm run build - do not edit; the source is compiler/entry.ts. The compile worker imports this when the plugin is compiled into the editor.\" && esbuild compiler/testingEntry.ts --bundle --format=esm --target=es2022 --platform=neutral --external:node:* --outfile=dist/testing.js --banner:js=\"// Generated by npm run build - do not edit; the source is compiler/testingEntry.ts. The plugin never loads this: it is what another repository's tests import to run triggers.\" && dts-bundle-generator --silent --no-banner --project tsconfig.json -o dist/testing.d.ts compiler/testingEntry.ts", + "build": "esbuild plugin.ts --bundle --format=esm --target=es2022 --platform=browser --outfile=dist/plugin.js --banner:js=\"// Generated by npm run build - do not edit; the source is plugin.ts.\" && esbuild compiler/entry.ts --bundle --format=esm --target=es2022 --platform=browser --outfile=dist/compiler.js --banner:js=\"// Generated by npm run build - do not edit; the source is compiler/entry.ts. The compile worker imports this when the plugin is compiled into the editor.\" && esbuild compiler/testingEntry.ts --bundle --format=esm --target=es2022 --platform=neutral --external:node:* --outfile=dist/testing.js --banner:js=\"// Generated by npm run build - do not edit; the source is compiler/testingEntry.ts. The plugin never loads this: it is what another repository's tests import to run triggers.\" && dts-bundle-generator --silent --no-banner --project tsconfig.json -o dist/testing.d.ts compiler/testingEntry.ts && esbuild compiler/gameTestEntry.ts --bundle --format=esm --target=es2022 --platform=neutral --outfile=dist/gametest.js --banner:js=\"// Generated by npm run build - do not edit; the source is compiler/gameTestEntry.ts. The plugin hands its text to a host of the lifted game, which runs a script's tests beside the game.\"", "dev": "npm run build -- --watch", "typecheck": "tsc -p tsconfig.json", "test": "vitest run", diff --git a/probes/game-tests.ts b/probes/game-tests.ts new file mode 100644 index 0000000..562c1dd --- /dev/null +++ b/probes/game-tests.ts @@ -0,0 +1,62 @@ +// TrigScript 4, A2: a script whose tests run in the lifted game as well as in the simulator. +// npx tsx scripts/test-game.mts probes/game-tests.ts +// Each kind of thing a test does to the world or reads back from it, once: time, a typed line, a key, units placed, +// killed, given and moved, and what was printed. Built like a probe (Player 1 Terran, Player 2 a Zerg computer, +// location 1 "Home" by Player 1's base, location 2 "Away" beside it). +import { test, expect, program, displayText, setResources, chatted, keyPressed, createUnit, sleep, frames, seconds, units, P1, P2, type Location } from "trigscript"; + +const HOME = 1 as Location, AWAY = 2 as Location; + +program(() => { + let ticks = 0; + while (true) { + ticks += 1; + setResources(P1, "set", ticks, "gas"); + if (ticks == 3) displayText("three ticks"); + sleep(seconds(1)); + } +}, { owner: P1 }); + +program(() => { + while (true) { + const m = chatted(P1, "-give {n}"); + if (m) { setResources(P1, "add", m.n, "ore"); displayText(`gave ${m.n}`); } + if (keyPressed(P1, "Q")) { createUnit(P1, units.TerranFirebat, 1, HOME); displayText("Q: a Firebat"); } + sleep(frames(1)); + } +}, { owner: P1 }); + +test("gas counts the seconds", (sim) => { + sim.seconds(5); + expect(sim.resources(P1).gas).toBe(5); + expect(sim).toHavePrinted("three ticks"); +}); + +test("a typed line gives ore", (sim) => { + sim.frames(10); + const before = sim.resources(P1).ore; + sim.type("-give 250").seconds(1); + expect(sim.resources(P1).ore).toBe(before + 250); + expect(sim).toHavePrinted("gave 250"); +}); + +test("a key makes a Firebat", (sim) => { + sim.frames(10).press("Q").frames(10); + expect(sim.count(P1, units.TerranFirebat, HOME)).toBe(1); + expect(sim).toHavePrinted("Q: a Firebat"); +}); + +test("units placed, killed, given and moved", (sim) => { + const made = sim.place(P1, units.TerranMarine, HOME, 3); + expect(made).toHaveLength(3); + expect(made[0].hp).toBe(40); + sim.kill(made[0]).frames(30); + expect(made[0].alive).toBe(false); + expect(sim.deaths(P1, units.TerranMarine)).toBe(1); + sim.give(made[1], P2).frames(1); + expect(made[1].owner).toBe(P2); + sim.move(made[2], AWAY).frames(1); + const away = sim.location(AWAY)!; + expect(made[2].x).toBeGreaterThanOrEqual(away.left); + expect(made[2].x).toBeLessThanOrEqual(away.right); +}); diff --git a/scripts/test-game.mts b/scripts/test-game.mts new file mode 100644 index 0000000..a31afe9 --- /dev/null +++ b/scripts/test-game.mts @@ -0,0 +1,93 @@ +/** + * A script's tests run in the lifted game (TrigScript 4, A2), each beside its run in the simulator: the script built + * as Save builds it (scripts/build-fixture.mts), started in a scm-sim checkout's lifted game with EUD let through + * and nothing drawn, and every `test()` handed a `GameSim` over it (compiler/gameSim.ts) — the game put back to + * frame 0 before each. The same tests then run in the simulator over the map's own world, and each is said twice. + * + * npx tsx scripts/test-game.mts [map.scx] + * + * Needs a scm-sim checkout beside this repository (or SCM_SIM_DIR) with its lifted game generated. + */ +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { basename, join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import ts from "typescript"; +import { compileScript } from "../compiler/compiler"; +import { readLocations, readUnitStats, type GameHandle } from "../compiler/gameSim"; +import { defaultScriptNames } from "../compiler/names"; +import type { TestReport, TestWorld } from "../compiler/testing"; +import { defaultLib } from "../bundle/lib.mjs"; + +const root = resolve(import.meta.dirname, ".."); +const [script, baseMap] = process.argv.slice(2); +if (!script || !existsSync(script)) { console.error("usage: test-game.mts [map.scx]"); process.exit(2); } +const sim = resolve(process.env.SCM_SIM_DIR ?? join(root, "..", "scm-sim")); +if (!existsSync(join(sim, "src", "exact", "testGame.ts"))) { console.error(`no scm-sim checkout with src/exact/testGame.ts at ${sim} (set SCM_SIM_DIR)`); process.exit(2); } +const from = (p: string) => import(pathToFileURL(join(sim, p)).href); + +const dir = mkdtempSync(join(tmpdir(), "trigscript-tests-")); +try { + // The map, built as Save builds it. + const built = join(dir, basename(script, ".ts") + ".scx"); + const b = spawnSync("npx", ["tsx", join(root, "scripts", "build-fixture.mts"), resolve(script), built, ...(baseMap ? [resolve(baseMap)] : [])], { cwd: root, encoding: "utf8", maxBuffer: 256 << 20 }); + if (b.status !== 0) { console.error(b.stdout.split("\n").slice(-20).join("\n"), b.stderr); process.exit(1); } + + // The game, at frame 0, EUD let through. + const { Archive } = await import(pathToFileURL(join(sim, "node_modules", "mopaq", "dist", "index.mjs")).href); + const { exactMissing, startExactGame } = await from("src/exact/node.ts"); + const { classicScenario, installEud, placeStrings } = await from("src/exact/eud.ts"); + const { testGame } = await from("src/exact/testGame.ts"); + const missing = exactMissing() as string[]; + if (missing.length) { console.error("the lifted game cannot run here; missing:\n " + missing.join("\n ")); process.exit(1); } + const chk = Archive.open(new Uint8Array(readFileSync(built))).readFile("staredit\\scenario.chk") as Uint8Array; + const classic = classicScenario(chk); + const chkPath = join(dir, "scenario.chk"); + writeFileSync(chkPath, classic); + const run = await startExactGame(chkPath, { game: "ums" }); + const strings = placeStrings(run.lifted, chk); + installEud(run.lifted, strings); + // (eudplib's payload lies outside what the game's snapshot covers: the handle keeps it itself.) + const handle = testGame(run.engine, run.lifted, [{ from: strings.gameTable, to: 0x191943c8 + strings.bytes }]) as GameHandle; + + // The tests in the simulator over the map's own world, as the editor runs them; then in the game, the way a host + // of the game runs them: the compile's JavaScript handed to dist/gametest.js, loaded from its text. + const source = { "main.ts": readFileSync(script, "utf8") }; + const names = defaultScriptNames(); + // (The locations from the game once eudplib has put them right: in the built map they are scrambled.) + const locations = readLocations(handle); + const world = await mapWorld(classic, locations, readUnitStats(handle)); + const compiled = compileScript(ts, source, names, { lib: defaultLib(), tests: { world } }); + const simulated = compiled.tests; + if (!compiled.modules) { console.error(compiled.ok ? "the script has no tests" : compiled.diagnostics.map((d) => `${d.file}:${d.line}: ${d.message}`).join("\n")); process.exit(1); } + const bundle = join(dir, "gametest.js"); + spawnSync("npx", ["esbuild", join(root, "compiler", "gameTestEntry.ts"), "--bundle", "--format=esm", "--target=es2022", "--platform=neutral", `--outfile=${bundle}`], { cwd: root }); + const host = await import(`data:text/javascript;base64,${Buffer.from(readFileSync(bundle)).toString("base64")}`) as typeof import("../compiler/gameTestEntry"); + const inGame = host.runGameTests(handle, { modules: compiled.modules, names, locations }) as TestReport & { error?: string }; + if (inGame.error) { console.error(`the script did not run beside the game: ${inGame.error}`); process.exit(1); } + const bySim = new Map((simulated?.results ?? []).map((r) => [r.id, r])); + let failed = 0; + for (const r of inGame.results) { + const s = bySim.get(r.id); + const mark = (st: string | undefined) => (st === "passed" ? "✓" : st === "failed" ? "✗" : st === "skipped" ? "–" : "?"); + if (r.status === "failed") failed++; + console.log(`${mark(r.status)} game ${mark(s?.status)} simulator ${r.id.replace(/^.*?::/, "")} (${r.frames} frames, ${r.ms} ms)`); + if (r.status === "failed") console.log(` in the game: ${r.message}${r.at ? ` — line ${r.at.line}` : ""}${r.actual !== undefined ? `\n got ${r.actual}` : ""}`); + if (s?.status === "failed") console.log(` in the simulator: ${s.message}${s.at ? ` — line ${s.at.line}` : ""}${s.actual !== undefined ? `\n got ${s.actual}` : ""}`); + if (s && s.status !== r.status) console.log(" the two disagree: one of them is wrong"); + } + console.log(`\n${inGame.results.length} tests in the game: ${inGame.results.length - failed} passed, ${failed} failed`); + if (failed) process.exitCode = 1; +} finally { + rmSync(dir, { recursive: true, force: true }); +} + +/** The world the editor would hand the simulator for this map: its units, the locations and stats read from the game, P1 a person, P2 a computer. */ +async function mapWorld(chk: Uint8Array, locations: TestWorld["locations"], unitStats: TestWorld["unitStats"]): Promise { + const EDITOR = resolve(process.env.SCMJS_DIR ?? join(root, "..", "scm-js")); + const { parseScenario } = await import(join(EDITOR, "src", "formats", "chk", "scenario.ts")); + const scn = parseScenario(chk); + const units = (scn.units ?? []).filter((u: { unitId: number }) => u.unitId !== 214).map((u: { unitId: number; owner: number; x: number; y: number }) => ({ type: u.unitId, owner: u.owner, x: u.x, y: u.y })); + return { units, locations, unitStats, players: [0, 1], forces: { 0: 0, 1: 1 }, computers: [1] }; +} From df6d27510cbb149b3ad4ab6e1b81928bb1e16827 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 00:31:23 -0600 Subject: [PATCH 005/238] TrigScript 4, A2: tests in the game from the editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run All Tests in the Game (Testing view and palette): the map built as Save builds it, exported whole, handed with dist/gametest.js's text and the compile's JavaScript to the scm-sim plugin's engine through its new test(); each test gets a second mark, "game", red where the two disagree, and the Test Results panel says what the game found. vendor/scmSim.ts is the part of the scm-sim contract TrigScript uses; it never imports the engine. docs/v4-plan.md: A2 as built — a test is not a program, the traps met, the simulator bug the first run found. Co-Authored-By: Claude Opus 5.5 --- compile.ts | 7 ++++ docs/v4-plan.md | 58 +++++++++++++++++++++++++++++++++ editor.ts | 85 ++++++++++++++++++++++++++++++++++++++++++++++-- ko.ts | 17 ++++++++++ service.ts | 11 +++++++ vendor/scmSim.ts | 28 ++++++++++++++++ 6 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 vendor/scmSim.ts diff --git a/compile.ts b/compile.ts index 416d16f..7aff1c5 100644 --- a/compile.ts +++ b/compile.ts @@ -41,6 +41,13 @@ export const libUrl = (dist: string = DEFAULT_DIST) => `${dist.replace(/\/+$/, " export const compilerUrl = (dist: string = DEFAULT_DIST) => dist === DEFAULT_DIST ? `https://cdn.jsdelivr.net/gh/scm-js/plugin-trigscript@v${VERSION}/dist/compiler.js` : `${dist.replace(/\/+$/, "")}/compiler.js`; +/** + * What runs a script's tests beside the lifted game (`dist/gametest.js`, from `compiler/gameTestEntry.ts`), at this + * release's own tag like the compiler: its text is handed to the engine's page (TrigScript 4, A2). + */ +export const gametestUrl = (dist: string = DEFAULT_DIST) => + dist === DEFAULT_DIST ? `https://cdn.jsdelivr.net/gh/scm-js/plugin-trigscript@v${VERSION}/dist/gametest.js` : `${dist.replace(/\/+$/, "")}/gametest.js`; + /** * What the worker is told to import. The editor's loader turns a fetched plugin into * `blob:` modules, and the compiler's own blob URL is the compiler the main thread would diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 8d5a46f..02a6809 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -270,6 +270,64 @@ seen to refuse or ignore, or has never been seen to do, by line. A headless run draws nothing, and is allowed from any copy that unlocks the editor (decided 2026-10-15, see *Open decisions*). +**As built (2026-10-16).** One part of the design above was wrong and was not built: a test does +*not* become a program. A test is synchronous JavaScript that steps frames and reads between them +(`until(() => …)` among it), which cannot be compiled into triggers ahead of time. Instead the test +stays JavaScript and is handed a world backed by the game, and it runs where the game is. + +- **`GameSim`** (`compiler/gameSim.ts`) has `TestSim`'s surface over a small synchronous + `GameHandle`. scm-sim supplies the handle (`src/exact/testGame.ts`), and TrigScript never imports + the engine. + - Every read is the game's memory. + - `place`, `kill`, `remove`, `give` and `move` are the game's own trigger actions, run between + frames as a player's trigger would run them, through a scratch location (255; 254 for a + destination) whose twenty bytes are put back afterwards. + - `kill` is a fight, as `TestSim` has it: the game's death-counting routine (0x488AF0) is called + first, so `by` gets the kill. + - A typed line is the game's own chat command (0x5C). chatEvent reads it from the text lines as + in play, and MSQC sends it on. + - A key and a mouse button are the local machine's memory (0x596A18, 0x6CDDC0), held for one + frame. + - Not yet: `program()` (a program's variables; the build would have to say where each lives), + and what another player was shown. +- **The same tests, twice.** `TestRunOptions.makeWorld` hands each test a world other than the + simulator's. A compile also returns the script as JavaScript (`CompileResult.modules`). + `compiler/gameTests.ts`, bundled as `dist/gametest.js` (198 KB, no TypeScript in it), runs those + modules again beside the game, registers the tests and runs each with a `GameSim`. The game is + put back to frame 0 before each test. +- **Where it runs.** + - Under Node: `npx tsx scripts/test-game.mts ` prints each test's game and simulator + marks side by side. + - In the editor: **Run All Tests in the Game** in the Testing view and the palette. It builds + the map as Save does, exports it whole (a bare `.chk` has no room for what the build steps + make), and hands it, with `gametest.js`'s text and the modules, to scm-sim's analysis page + through the plugin's new `test()` (contract unchanged in version; `status().tests`). The page + reads the scenario out of the map, starts it in a worker of its own with EUD let through, + loads the module from its text and answers with the report. Each test then shows a second + mark, "game", which turns red when the two disagree. Nothing is drawn, so under the decision + of 2026-10-15 any copy of the game that unlocks the editor is enough. +- **Traps met.** + - eudplib keeps the map's locations scrambled until its first frames put them right (by frame 2 + on a probe's map). So an action at frame 0 goes through a scratch location given the map's + real bounds, read from the game once (`readLocations`), with the elevation flags cleared, + since scrambled flags keep units out of a location. + - The game's snapshot covers only the pages it had mapped at the start. eudplib's payload lies + outside them, so a reset left eudplib's variables and its self-rewriting triggers as the last + test left them, and typed lines and keys went unanswered from the second test on. The handle + now keeps that range itself. +- **What the first run found:** a simulator bug. Kill Unit, Kill Unit At Location, a unit's + `kill()` and hit points written down to 0 all counted a death in the simulator. The game counts + none of them, and the editor's trigger reference, checked in the real game, says the same of + Kill Unit. Fixed in the simulator (`World.gone(…, fought)`), with three tests corrected to the + game's rule. +- **Checked:** + - `probes/game-tests.ts` (time, a typed line, a key; units placed, killed, given and moved) + passes in the game and in the simulator under Node, and through scm-sim's analysis page in + headless Chromium (3.3 s, game start included). + - plugin-scm-sim's bridge has two new tests. + - The editor's button has not been driven end to end in the built editor yet. Its parts have: + compile, export and the page, each on its own. + ### A3. The desync detector (4.1.0) Run two to eight copies of the game seated as different local players diff --git a/editor.ts b/editor.ts index 2579b71..0cf3b05 100644 --- a/editor.ts +++ b/editor.ts @@ -27,7 +27,7 @@ * Monaco and TypeScript come from the CDN on first open. */ import type { PluginApi } from "@scm-js/plugin-api"; -import { CompileSuperseded, retainCompileWorker } from "./compile"; +import { CompileSuperseded, gametestUrl, retainCompileWorker } from "./compile"; import { ENTRY_FILE, normalizePath, type CompileResult, type ScriptDiagnostic, type ScriptFiles, type TriggerSource } from "./compiler/compiler"; import { HEAP_CELLS, HEAP_CELLS_MAX, HEAP_CELLS_MIN, STACK_CELLS_MAX, STACK_DEPTH, STACK_DEPTH_MAX, STACK_DEPTH_MIN, heapCells, stackDepth } from "./compiler/ir"; import { largestFrame } from "./compiler/recursion"; @@ -42,7 +42,7 @@ import { } from "./monaco"; import { renamedKeys, renamesInUse, replaceReferences, type Renamed } from "./refs"; import { FILE_NAME } from "./script"; -import { isTestFile, type TestInfo, type TestRunOptions } from "./compiler/testing"; +import { isTestFile, type TestInfo, type TestReport, type TestResult, type TestRunOptions } from "./compiler/testing"; import { countTests, foldPlayers, idsUnder, marksOf, mergeReport, NO_TESTS, notesOf, playersLabel, stateOf, stateOfCounts, summary, testTree, warningsOf, type TestNode, type TestState, type TestStateName } from "./testState"; import { testName } from "./service"; import { applyMoves, buildTree, filesUnder, folderOf, movesOf, refuseMoves, tabLabels, validFolder, type Moves, type TreeNode } from "./tree"; @@ -326,7 +326,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa null, ...["import", "newFile", "newFolder"].map(menuItem), null, - ...["runTests", "runFailedTests"].map(menuItem), + ...["runTests", "runFailedTests", "runTestsInGame"].map(menuItem), null, ...["problems", "output", "panel", "explorer", "testing"].map(menuItem), null, @@ -355,6 +355,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa const testingView = shell.sidebarView({ id: "testing", title: t("Testing"), icon: "beaker", actions: [ { icon: "run-all", title: t("Run all tests"), run: () => { void runTests(); } }, { icon: "run-errors", title: t("Run the tests that failed"), run: () => { void runFailedTests(); } }, + { icon: "target", title: t("Run all tests in the game (the lifted game of the scm-sim plugin): each gets a second mark"), run: () => { void runTestsInGame(); } }, { icon: "filter", title: t("Show only the tests that fail"), run: () => { onlyFailing = !onlyFailing; renderTesting(); } }, ] }); testingView.body.append(testingList); @@ -432,6 +433,13 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa /** Running every test took more than `TESTS_SLOW_MS` the last time. */ let testsSlow = false; let testsRunning = false; + /** + * The tests' last results in the lifted game (TrigScript 4, A2), by id: a mark of their own beside the simulator's, + * kept until the next run in the game. The game is the judge: where the two differ, the simulator is wrong. + */ + let gameResults = new Map(); + /** The text of `dist/gametest.js`, fetched once. */ + let gametest: Promise | null = null; /** The files with a tab, in the order they were opened. */ let openTabs: string[] = [normalizePath(options.file ?? ENTRY_FILE)]; /** What Apply, Play and the builds of the programs reported, oldest first. */ @@ -694,6 +702,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa stateIcon(state), el("span", { className: state === "failed" ? "tsd-name tsd-problem" : "tsd-name" }, name), result && result.status !== "skipped" ? el("span", { className: "tsd-about" }, t("{ms} ms", { ms: result.ms })) : undefined, + node.kind === "test" && gameResults.has(node.info.id) ? gameMark(gameResults.get(node.info.id)!, result) : undefined, el("span", { className: "tsd-row-actions" }, shell.iconButton({ icon: "play", title: node.kind === "test" ? t("Run this test") : t("Run these tests"), run: () => { void runTests(node.kind === "file" ? { files: [node.path] } : { ids }); } }).element, ), @@ -708,6 +717,66 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa testingList.replaceChildren(...rows); }; + /** A test's mark from the game, beside the simulator's: its state, and a word when the two disagree. */ + const gameMark = (game: TestResult, simulated: TestResult | undefined) => { + const state: TestStateName = game.status === "failed" ? "failed" : game.status === "passed" ? "passed" : "skipped"; + const differ = simulated && simulated.status !== "skipped" && game.status !== "skipped" && simulated.status !== game.status; + return el("span", { className: differ ? "tsd-about tsd-problem" : "tsd-about", title: differ + ? t("In the game: {status}. The simulator and the game disagree: the game is right, so the simulator is wrong here (or the test leans on something it does differently).", { status: game.status === "failed" ? game.message ?? t("failed") : t("passed") }) + : game.status === "failed" ? t("In the game: {message}", { message: game.message ?? t("failed") }) : t("In the game: passed") }, + stateIcon(state), t("game")); + }; + + /** + * The tests run in the lifted game (TrigScript 4, A2): the map built as Save builds it, handed with the script's + * JavaScript and `dist/gametest.js` to the scm-sim plugin's engine page, which runs each test beside the game. The + * simulator runs them too, so that each test has both marks. + */ + const runTestsInGame = async (filter: Pick = {}) => { + if (!editor || testsRunning || building || testing || !ready) return; + const engine = svc.engine(); + if (!engine?.present || !engine.test) { setStatus("error", t("Running tests in the game needs the scm-sim plugin (0.8 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….")); return; } + testsRunning = true; + renderTesting(); + try { + const status = await engine.status(); + if (!status.available) { setStatus("error", t("Not run in the game: {message}", { message: status.reason ?? t("the engine cannot run a game now") })); return; } + if (!status.tests) { setStatus("error", t("Not run in the game: the engine's page is from before tests in the game. Reload the editor and try again.")); return; } + if (!(await build()) || cancelled) return; + const a = await compileNow({ world: svc.world(), ...filter }); + if (!a || cancelled) return; + if (!a.compiled.ok) { setStatus("error", t("Tests not run: {n, plural, one {# problem} other {# problems}} in the script.", { n: a.compiled.diagnostics.length }), NOTICE_MS); shell.showPanel("problems"); return; } + if (!a.compiled.modules) { setStatus("info", t("The script has no tests.")); return; } + if (!a.compiled.ir.length) { setStatus("info", t("Tests run in the game only for a script with a program: without one, nothing of the script is built into the map.")); return; } + const names = svc.names()?.names; + if (!names) { setStatus("error", t("No map is open.")); return; } + setStatus("busy", t("Building the map…")); + // (Whole: a bare .chk has no room for what the build steps make, so the engine's page reads the scenario out of it.) + const file = await api.document.export({ format: "scx" }); + if (!file || cancelled) { if (!file) setStatus("error", t("No map is open.")); return; } + gametest ??= fetch(gametestUrl(svc.dist())).then((r) => { if (!r.ok) throw new Error(`gametest.js: ${r.status}`); return r.text(); }); + const module = await gametest.catch((err) => { gametest = null; throw err; }); + setStatus("busy", t("Running the tests in the game…")); + const report = await engine.test(new Uint8Array(await file.arrayBuffer()), module, { modules: a.compiled.modules, names, files: filter.files, ids: filter.ids }) as TestReport & { error?: string }; + if (cancelled) return; + if (report.error) { setStatus("error", t("Not run in the game: {message}", { message: report.error })); return; } + const ran = report.results.filter((x) => x.status !== "skipped"); + const asked = new Set(ran.map((x) => x.id)); + gameResults = new Map([...gameResults].filter(([id]) => !asked.has(id) && tests.list.some((x) => x.id === id))); + for (const r of ran) gameResults.set(r.id, r); + const failed = ran.filter((x) => x.status === "failed"); + const differ = ran.filter((x) => { const s = tests.results.get(x.id); return s && s.status !== "skipped" && s.status !== x.status; }); + if (failed.length) { chosenTest = failed[0].id; shell.showPanel("tests"); } + const said = t("In the game: {passed} of {n, plural, one {# test} other {# tests}} passed.", { passed: ran.length - failed.length, n: ran.length }); + setStatus(failed.length || differ.length ? "error" : "ok", differ.length ? `${said} ${t("The simulator disagrees on {n, plural, one {# test} other {# tests}}.", { n: differ.length })}` : said, NOTICE_MS); + } catch (err) { + setStatus("error", t("Not run in the game: {message}", { message: (err as Error).message })); + } finally { + testsRunning = false; + render(); + } + }; + /** Test Results: the chosen test's message, what it printed, and what happened by frame, each a link to its line. */ const renderResults = () => { const failed = tests.list.filter((x) => x.kind === "test" && tests.results.get(x.id)?.status === "failed"); @@ -728,6 +797,15 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa if (r.expected !== undefined) list.append(el("li", { className: "tsd-plain" }, el("span", { className: "frame" }, t("expected")), el("span", { className: "msg" }, r.expected))); if (r.actual !== undefined) list.append(el("li", { className: "tsd-plain" }, el("span", { className: "frame" }, t("got")), el("span", { className: "msg" }, r.actual))); } + const game = gameResults.get(info.id); + if (game && game.status !== "skipped") { + const gat = game.at ?? { file: info.file, line: info.line }; + list.append(el("li", { className: game.status === "failed" ? "tsd-fault" : "tsd-plain", title: t("Go to where it failed in the game"), onClick: () => goTo(gat.file, gat.line, gat.column ?? 1) }, + el("span", { className: "frame" }, t("in the game")), + el("span", { className: "msg" }, game.status === "failed" ? game.message ?? t("failed") : t("passed")), + el("span", { className: "where" }, game.status === "failed" ? where({ file: gat.file, line: gat.line }) : t("{frames, plural, one {# frame} other {# frames}} · {ms} ms", { frames: game.frames, ms: game.ms })))); + if (game.status === "failed" && game.actual !== undefined) list.append(el("li", { className: "tsd-plain" }, el("span", { className: "frame" }, t("got")), el("span", { className: "msg" }, game.actual))); + } // The same line several times over — to each of eight players, say — is one row that says how often. const printed: { line: string; times: number }[] = []; for (const line of r.printed) { const last = printed[printed.length - 1]; if (last?.line === line) last.times++; else printed.push({ line, times: 1 }); } @@ -1441,6 +1519,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa { id: "save", label: t("Save the Map"), key: { code: "KeyS", mod: true }, run: () => { void api.document.save(); } }, { id: "test", label: t("Play the Map"), key: { code: "F5" }, run: () => { void test(); } }, { id: "runTests", label: t("Run All Tests"), run: () => { void runTests(); } }, + { id: "runTestsInGame", label: t("Run All Tests in the Game"), run: () => { void runTestsInGame(); } }, { id: "runTestAtCursor", label: t("Run Test at Cursor"), context: true, run: () => { void runTestAtCursor(); } }, { id: "runFailedTests", label: t("Run Failed Tests"), run: () => { void runFailedTests(); } }, { id: "testing", label: t("Show Testing"), run: () => testingView.show() }, diff --git a/ko.ts b/ko.ts index 3cefbd0..2b9b9a0 100644 --- a/ko.ts +++ b/ko.ts @@ -270,4 +270,21 @@ export const KO: Record = { "{n} skipped": "{n}개 건너뜀", "{what}. Click for its log": "{what}. 클릭하면 로그를 봅니다", "{what}; {n, plural, one {# import} other {# imports}} rewritten.": "{what}. import {n}개를 고쳐 썼습니다.", + "Run all tests in the game (the lifted game of the scm-sim plugin): each gets a second mark": "모든 테스트를 게임에서 실행합니다(scm-sim 플러그인의 옮겨 온 게임). 테스트마다 두 번째 표시가 붙습니다", + "In the game: {status}. The simulator and the game disagree: the game is right, so the simulator is wrong here (or the test leans on something it does differently).": "게임에서: {status}. 시뮬레이터와 게임의 결과가 다릅니다. 게임이 옳으므로 여기서는 시뮬레이터가 틀렸습니다(또는 테스트가 둘이 다르게 하는 일에 기대고 있습니다).", + "passed": "통과", + "In the game: {message}": "게임에서: {message}", + "In the game: passed": "게임에서: 통과", + "game": "게임", + "Running tests in the game needs the scm-sim plugin (0.8 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….": "테스트를 게임에서 실행하려면 scmjs.dev에서 scm-sim 플러그인(0.8 이상)이 필요합니다. 플러그인 ▸ 플러그인 관리…에서 설치하거나 켜세요.", + "Not run in the game: {message}": "게임에서 실행하지 못함: {message}", + "the engine cannot run a game now": "엔진이 지금 게임을 실행할 수 없습니다", + "Not run in the game: the engine's page is from before tests in the game. Reload the editor and try again.": "게임에서 실행하지 못함: 엔진 페이지가 게임 속 테스트보다 오래된 판입니다. 편집기를 새로 고친 뒤 다시 시도하세요.", + "Tests run in the game only for a script with a program: without one, nothing of the script is built into the map.": "테스트는 프로그램이 있는 스크립트만 게임에서 실행됩니다. 프로그램이 없으면 스크립트의 어떤 것도 맵에 빌드되지 않습니다.", + "Running the tests in the game…": "게임에서 테스트를 실행하는 중…", + "In the game: {passed} of {n, plural, one {# test} other {# tests}} passed.": "게임에서: 테스트 {n}개 중 {passed}개 통과.", + "The simulator disagrees on {n, plural, one {# test} other {# tests}}.": "테스트 {n}개에서 시뮬레이터의 결과가 다릅니다.", + "Go to where it failed in the game": "게임에서 실패한 곳으로 이동", + "in the game": "게임에서", + "Run All Tests in the Game": "게임에서 모든 테스트 실행", }; diff --git a/service.ts b/service.ts index a0c8adf..a237c3c 100644 --- a/service.ts +++ b/service.ts @@ -32,6 +32,7 @@ import { } from "./script"; import { english, msg, type Translate } from "./i18n"; import { EUDPLIB_SERVICE, type EudplibBuildEvent, type EudplibInput, type EudplibService } from "./vendor/eudplib"; +import { SCM_SIM_SERVICE, type ScmSimEngine } from "./vendor/scmSim"; /** * A compile, with what it was compiled *for*: the map it read its names from and the @@ -188,6 +189,16 @@ export class ScriptService { return this.api.services.get(EUDPLIB_SERVICE); } + /** The scm-sim plugin's engine, or null when it is not installed or is off: where a script's tests run in the game. */ + engine(): ScmSimEngine | null { + return this.api.services.get(SCM_SIM_SERVICE); + } + + /** Called at once and whenever the scm-sim plugin arrives or goes. */ + watchEngine(listener: (service: ScmSimEngine | null) => void): { dispose(): void } { + return this.api.services.watch(SCM_SIM_SERVICE, (s) => listener(s)); + } + /** Called at once and whenever the eudplib plugin arrives or goes. */ watchLibrary(listener: (service: EudplibService | null) => void): { dispose(): void } { return this.api.services.watch(EUDPLIB_SERVICE, (s) => listener(s)); diff --git a/vendor/scmSim.ts b/vendor/scmSim.ts new file mode 100644 index 0000000..a9568f1 --- /dev/null +++ b/vendor/scmSim.ts @@ -0,0 +1,28 @@ +/** + * The part of the scm-sim plugin's `scm-sim.engine` service that TrigScript uses — a copy of + * what `contract.d.ts` in github.com/scm-js/plugin-scm-sim says of it, taken by `import type` + * only. TrigScript never imports the engine: it hands the engine's page its tests (TrigScript + * 4, A2), and the page runs them beside the game. + */ +export const SCM_SIM_SERVICE = "scm-sim.engine"; + +export interface ScmSimStatus { + available: boolean; + /** When not available: why, in words for the person. */ + reason: string | null; + /** The version of the test request the page answers; null or absent: it answers none. */ + tests?: number | null; +} + +export interface ScmSimEngine { + /** Whether this editor has the engine beside it at all (scmjs.dev, or a stand-in for the site). */ + readonly present: boolean; + status(): Promise; + /** + * A script's tests beside the game: the map as Save builds it (a whole .scx; the page reads + * its scenario) or that scenario as eudplib built it, the text of a module + * whose `runGameTests(game, payload)` runs them, and its payload; resolves with what the + * module returned. Absent from the plugin before 0.8.0. + */ + test?(scenario: Uint8Array, module: string, payload: unknown, options?: { signal?: AbortSignal; onProgress?: (p: { stage: "starting" }) => void }): Promise; +} From 66fbfaa4201a297eac98aea33c35d0ea3306666b Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 00:52:32 -0600 Subject: [PATCH 006/238] TrigScript 4, A2: the game's mark beside the simulator's, checked in the editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run All Tests in the Game was driven end to end in a built editor (scm-js main under /editor/, scm-sim's analysis page beside it, the local plugin builds): 4 of 5 passed in the game, the fifth — sim.program(), which the game cannot answer yet — shown as a disagreement. The game's mark now sits next to the simulator's icon, where a narrow sidebar still shows it, ringed when the two differ; the scm-sim plugin it needs is 0.9 (0.8.0 is out without test()). Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 15 +++++++++++++-- editor.ts | 11 ++++++----- ko.ts | 3 +-- shell.ts | 2 ++ vendor/scmSim.ts | 2 +- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 02a6809..424ac46 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -325,8 +325,19 @@ stays JavaScript and is handed a world backed by the game, and it runs where the passes in the game and in the simulator under Node, and through scm-sim's analysis page in headless Chromium (3.3 s, game start included). - plugin-scm-sim's bridge has two new tests. - - The editor's button has not been driven end to end in the built editor yet. Its parts have: - compile, export and the page, each on its own. + - **End to end in a built editor (2026-10-16):** scm-js `main` built under `/editor/` and + served by scm-sim's dev server beside its analysis page, with the local TrigScript and + scm-sim plugin builds in place of the released ones, driven in headless Chromium: + 1. The probe map is opened and the script written into it. + 2. **Run all tests in the game** is clicked. + 3. eudplib asks for its one-time runtime and gets it, then builds the 721 KB map in 3.7 s. + 4. The status bar says "In the game: 4 of 5 tests passed. The simulator disagrees on 1 + test." That came about 9 seconds after the click. + + The fifth test reads `sim.program()`, which the game cannot answer yet, and it shows as + meant: a ringed red mark beside the simulator's green one, and the game's reason on its own + line in Test Results. The game's mark sits beside the simulator's icon rather than at the + row's end, where a narrow sidebar cut it off in the first run. ### A3. The desync detector (4.1.0) diff --git a/editor.ts b/editor.ts index 0cf3b05..6840b73 100644 --- a/editor.ts +++ b/editor.ts @@ -700,9 +700,10 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa }; const row = el("li", { className: node.kind === "test" && node.info.id === chosenTest ? "tsd-row tsd-active" : "tsd-row", title: result?.message ?? name, onClick: open }, stateIcon(state), + // (The game's mark next to the simulator's, where a narrow sidebar still shows it.) + node.kind === "test" && gameResults.has(node.info.id) ? gameMark(gameResults.get(node.info.id)!, result) : undefined, el("span", { className: state === "failed" ? "tsd-name tsd-problem" : "tsd-name" }, name), result && result.status !== "skipped" ? el("span", { className: "tsd-about" }, t("{ms} ms", { ms: result.ms })) : undefined, - node.kind === "test" && gameResults.has(node.info.id) ? gameMark(gameResults.get(node.info.id)!, result) : undefined, el("span", { className: "tsd-row-actions" }, shell.iconButton({ icon: "play", title: node.kind === "test" ? t("Run this test") : t("Run these tests"), run: () => { void runTests(node.kind === "file" ? { files: [node.path] } : { ids }); } }).element, ), @@ -717,14 +718,14 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa testingList.replaceChildren(...rows); }; - /** A test's mark from the game, beside the simulator's: its state, and a word when the two disagree. */ + /** A test's mark from the game, the second beside the simulator's: its state, ringed when the two disagree, and in words on hover. */ const gameMark = (game: TestResult, simulated: TestResult | undefined) => { const state: TestStateName = game.status === "failed" ? "failed" : game.status === "passed" ? "passed" : "skipped"; const differ = simulated && simulated.status !== "skipped" && game.status !== "skipped" && simulated.status !== game.status; - return el("span", { className: differ ? "tsd-about tsd-problem" : "tsd-about", title: differ + return el("span", { className: differ ? "tsd-game tsd-game-differs" : "tsd-game", title: differ ? t("In the game: {status}. The simulator and the game disagree: the game is right, so the simulator is wrong here (or the test leans on something it does differently).", { status: game.status === "failed" ? game.message ?? t("failed") : t("passed") }) : game.status === "failed" ? t("In the game: {message}", { message: game.message ?? t("failed") }) : t("In the game: passed") }, - stateIcon(state), t("game")); + stateIcon(state)); }; /** @@ -735,7 +736,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa const runTestsInGame = async (filter: Pick = {}) => { if (!editor || testsRunning || building || testing || !ready) return; const engine = svc.engine(); - if (!engine?.present || !engine.test) { setStatus("error", t("Running tests in the game needs the scm-sim plugin (0.8 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….")); return; } + if (!engine?.present || !engine.test) { setStatus("error", t("Running tests in the game needs the scm-sim plugin (0.9 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….")); return; } testsRunning = true; renderTesting(); try { diff --git a/ko.ts b/ko.ts index 2b9b9a0..f0c149e 100644 --- a/ko.ts +++ b/ko.ts @@ -275,8 +275,7 @@ export const KO: Record = { "passed": "통과", "In the game: {message}": "게임에서: {message}", "In the game: passed": "게임에서: 통과", - "game": "게임", - "Running tests in the game needs the scm-sim plugin (0.8 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….": "테스트를 게임에서 실행하려면 scmjs.dev에서 scm-sim 플러그인(0.8 이상)이 필요합니다. 플러그인 ▸ 플러그인 관리…에서 설치하거나 켜세요.", + "Running tests in the game needs the scm-sim plugin (0.9 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….": "테스트를 게임에서 실행하려면 scmjs.dev에서 scm-sim 플러그인(0.9 이상)이 필요합니다. 플러그인 ▸ 플러그인 관리…에서 설치하거나 켜세요.", "Not run in the game: {message}": "게임에서 실행하지 못함: {message}", "the engine cannot run a game now": "엔진이 지금 게임을 실행할 수 없습니다", "Not run in the game: the engine's page is from before tests in the game. Reload the editor and try again.": "게임에서 실행하지 못함: 엔진 페이지가 게임 속 테스트보다 오래된 판입니다. 편집기를 새로 고친 뒤 다시 시도하세요.", diff --git a/shell.ts b/shell.ts index 67c2be4..e690997 100644 --- a/shell.ts +++ b/shell.ts @@ -230,6 +230,8 @@ ${Object.entries(ICONS).map(([name, code]) => `.tsd-i-${name}::before { content: .tsd-row.tsd-active { background: var(--bg-4); color: var(--text); } .tsd-row .tsd-name { flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; } .tsd-row .tsd-about { flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--text-faint); font-size: var(--fs-sm); } +.tsd-row .tsd-game { display: inline-flex; flex: none; margin-left: -2px; border-radius: 50%; } +.tsd-row .tsd-game-differs { box-shadow: 0 0 0 1px var(--danger); } .tsd-row .tsd-problem, .tsd-tab .tsd-problem { color: var(--danger); } .tsd-row .tsd-count { flex: none; margin-left: auto; color: var(--danger); font-size: var(--fs-sm); } .tsd-row .tsd-icon-button { display: none; margin-left: 0; } diff --git a/vendor/scmSim.ts b/vendor/scmSim.ts index a9568f1..fbcc2ff 100644 --- a/vendor/scmSim.ts +++ b/vendor/scmSim.ts @@ -22,7 +22,7 @@ export interface ScmSimEngine { * A script's tests beside the game: the map as Save builds it (a whole .scx; the page reads * its scenario) or that scenario as eudplib built it, the text of a module * whose `runGameTests(game, payload)` runs them, and its payload; resolves with what the - * module returned. Absent from the plugin before 0.8.0. + * module returned. Absent from the plugin before 0.9.0. */ test?(scenario: Uint8Array, module: string, payload: unknown, options?: { signal?: AbortSignal; onProgress?: (p: { stage: "starting" }) => void }): Promise; } From 00855182fe12c2ffef8af236c54119a83b909ad3 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 09:49:28 -0600 Subject: [PATCH 007/238] TrigScript 4, A2: sim.program() in the game The lowering says where each program's variables are when the build asks it to (the variableMap setting): the address of every cell once eudplib has placed the payload, in TRIGSCRIPT-VARS / TRIGSCRIPT-PROGRAM lines of the build's log. compiler/gameVariables.ts reads those lines and reads a program's variables out of the game's memory as the simulator's lookup does, so GameSim answers sim.program(). Run All Tests in the Game and scripts/test-game.mts ask for the map; Output leaves those lines out. probes/game-program.ts passes in the game and the simulator; in a built editor all five tests of the end-to-end run pass in the game. The scm-sim plugin's test() ships in 0.10 (0.9.0 went out with the Replay view's zoom), and the message says so. Co-Authored-By: Claude Opus 5.5 --- compiler/gameSim.ts | 26 ++++-- compiler/gameTestEntry.ts | 1 + compiler/gameTests.ts | 5 +- compiler/gameVariables.ts | 140 +++++++++++++++++++++++++++++ compiler/generated/trigscriptPy.ts | 2 +- docs/v4-plan.md | 30 ++++++- editor.ts | 22 +++-- ko.ts | 2 +- probes/game-program.ts | 77 ++++++++++++++++ python/trigscript.py | 114 +++++++++++++++++++++++ scripts/build-fixture.mts | 9 +- scripts/test-game.mts | 6 +- service.ts | 15 +++- tests/game-variables.test.ts | 108 ++++++++++++++++++++++ vendor/scmSim.ts | 2 +- 15 files changed, 534 insertions(+), 25 deletions(-) create mode 100644 compiler/gameVariables.ts create mode 100644 probes/game-program.ts create mode 100644 tests/game-variables.test.ts diff --git a/compiler/gameSim.ts b/compiler/gameSim.ts index 7c08113..1997d00 100644 --- a/compiler/gameSim.ts +++ b/compiler/gameSim.ts @@ -8,13 +8,14 @@ * centred on it (the last of the map's 255, put back afterwards). A typed line is the game's chat command; a key and * the mouse are the local machine's memory, where MSQC reads them. * - * Not in the game yet, and said so when asked: a program's variables (`program()`, which needs the build to say - * where each lives) and what another player was shown. A - * program's fault is the red line it prints ("TrigScript: …"), its line not known. + * A program's variables (`program()`) are read where the build said they are (`gameVariables.ts`), when it was asked + * to say. Not in the game yet, and said so when asked: what another player was shown. A program's fault is the red + * line it prints ("TrigScript: …"), its line not known. */ import { ActionType } from "../vendor/triggers"; import type { At } from "./ir"; import { msqcKey } from "./input"; +import { programVariables, type VariableMap } from "./gameVariables"; import { ExpectError, WORLD, type TestEvent, type TestWorldLike } from "./testing"; import type { SimBounds, SimUnit } from "./world"; @@ -138,15 +139,18 @@ export class GameSim implements TestWorldLike { private readonly units_ = new Map(); private readonly game: GameHandle; private readonly known: Record; + private readonly variables: VariableMap | null; /** * `locations`: the map's locations as the script means them. eudplib keeps them scrambled in the game until its * first frames put them right (frame 2 on a probe's map), so a test that acts at frame 0 needs them from elsewhere - * (`readLocations`); from then on the game's own are read. + * (`readLocations`); from then on the game's own are read. `variables`: where the build put each program's + * variables, for `program()`. */ - constructor(game: GameHandle, options: { locations?: Record } = {}) { + constructor(game: GameHandle, options: { locations?: Record; variables?: VariableMap | null } = {}) { this.game = game; this.known = options.locations ?? {}; + this.variables = options.variables ?? null; game.reset(); this.take(); } @@ -304,8 +308,16 @@ export class GameSim implements TestWorldLike { if (this.game.frame < LOCATIONS_READY) return this.known[n] ? { ...this.known[n] } : undefined; return readLocation(this.game, n); } - program(): Record { - throw new ExpectError("sim.program(…) reads a program's variables in the simulator only, for now: in the game, read what the program did (printed, resources, units)"); + program(name: string | number = 0, player?: number): Record { + if (!this.variables) throw new ExpectError("sim.program(…): the build did not say where the programs' variables are, so the game cannot be read for them"); + return programVariables({ m: this.game.m, u8: this.game.u8, unitAt: (ptr, uid) => this.unitAt(ptr, uid) }, this.variables, name, player); + } + /** The unit a variable holds: by its pointer, while its slot still has the uniqueness byte the variable kept with it. */ + private unitAt(ptr: number, uid: number): SimUnit | null { + const slot = (ptr - UNITS) / UNIT_BYTES; + if (!Number.isInteger(slot) || slot < 0 || slot >= UNIT_COUNT || this.game.u8[ptr + 0xa5] !== ((uid >>> 8) & 0xff)) return null; + const u = this.unit(slot); + return u.alive ? u : null; } /** What was shown, in order, with the game's colour codes taken out. `player`: only the player at this computer is seen. */ diff --git a/compiler/gameTestEntry.ts b/compiler/gameTestEntry.ts index b5551bb..4a6f4a2 100644 --- a/compiler/gameTestEntry.ts +++ b/compiler/gameTestEntry.ts @@ -4,3 +4,4 @@ */ export { runGameTests, type GameTestPayload } from "./gameTests"; export { GameSim, readLocations, readUnitStats, type GameHandle } from "./gameSim"; +export { programVariables, readVariableMap, type VariableMap } from "./gameVariables"; diff --git a/compiler/gameTests.ts b/compiler/gameTests.ts index 1c0b970..3c4ce79 100644 --- a/compiler/gameTests.ts +++ b/compiler/gameTests.ts @@ -10,6 +10,7 @@ import { ENTRY_FILE } from "./compiler"; import { MODULE_NAME } from "./api"; import { GameSim, readLocations, type GameHandle } from "./gameSim"; +import type { VariableMap } from "./gameVariables"; import { locate, runModules, type LinkedFile } from "./link"; import type { ScriptNames } from "./names"; import { Collector, createRuntime } from "./runtime"; @@ -24,6 +25,8 @@ export interface GameTestPayload { ids?: string[]; /** The map's locations as the script means them; read from the game (frame 2) when absent. */ locations?: Record; + /** Where the build put each program's variables (`readVariableMap` of its log), for `sim.program()`. */ + variables?: VariableMap | null; } export function runGameTests(game: GameHandle, payload: GameTestPayload): TestReport & { error?: string } { @@ -38,5 +41,5 @@ export function runGameTests(game: GameHandle, payload: GameTestPayload): TestRe collector.running = false; if (failure) return { list: [], results: [], only: [], ms: 0, error: `${failure.file ?? ENTRY_FILE}:${failure.line ?? 1}: ${failure.message}` }; const locations = payload.locations ?? readLocations(game); - return runTests(registry, { triggers: [], sources: [], ir: [], strings: [], locate: where }, { files: payload.files, ids: payload.ids, makeWorld: () => new GameSim(game, { locations }) }); + return runTests(registry, { triggers: [], sources: [], ir: [], strings: [], locate: where }, { files: payload.files, ids: payload.ids, makeWorld: () => new GameSim(game, { locations, variables: payload.variables }) }); } diff --git a/compiler/gameVariables.ts b/compiler/gameVariables.ts new file mode 100644 index 0000000..0680218 --- /dev/null +++ b/compiler/gameVariables.ts @@ -0,0 +1,140 @@ +/** + * Where a built map keeps each program's variables, and reading them back out of the game's memory (TrigScript 4, + * A2): `sim.program()` for a test that runs in the lifted game. The lowering says where every cell is when the build + * asks it to (the `variableMap` setting of `python/trigscript.py`), in lines of the build's log; this reads those + * lines, and reads a program's variables by the names in the source as the simulator's `ProgramRun.lookup` does. + * + * Two things differ from the simulator, both because the game has every cell from the start: a variable not yet + * declared reads as its zero rather than as missing, and of two declarations with one name the later in the source + * wins rather than the later to run. A list inside a list and a row of a grid are not read (a record of them is). + */ +import type { SimUnit } from "./world"; + +export interface GameVariable { + id: string; + kind: "number" | "boolean" | "unit" | "text"; + /** How a text is kept: `id` a string of the table, `made` characters somewhere in memory. */ + text?: "id" | "made" | null; + bits?: 8 | 16 | null; + unsigned: boolean; + /** A per-player program's own: each cell is a row of twelve, one a player. */ + rowed: boolean; + /** The address of each of its cells (`value`; `addr` + `len`; `ptr` + `uid`), null for one nothing in the map uses. */ + cells: Record; +} + +export interface GameArray { + id: string; + kind: "number" | "boolean"; + bits?: 8 | 16 | null; + unsigned: boolean; + length: number; + rowed: boolean; + /** Where the cells are; or, of one that grows, its handle's `ptr` (its block among the heap's cells) and `len`. */ + at?: number | null; + dynamic?: boolean; + ptr?: number | null; + len?: number | null; +} + +export interface GameProgramPlaces { + index: number; + name?: string | null; + perPlayer: boolean; + owners: number[]; + vars: GameVariable[]; + arrays: GameArray[]; +} + +export interface VariableMap { + /** The heap's first cell, where growing arrays and made texts keep theirs; null when nothing grows. */ + heap: number | null; + /** Where the map's strings are: a text of the table is read through it. */ + strings: number; + programs: GameProgramPlaces[]; +} + +const HEAD = "TRIGSCRIPT-VARS ", PROGRAM = "TRIGSCRIPT-PROGRAM "; + +/** A line of the build's log that is for a run in the lifted game and not for a person (the trigger map's too). */ +export const isMachineLine = (line: string): boolean => /^TRIGSCRIPT-[A-Z]+ /.test(line); + +/** The variable map in a build's log; null when the build did not say one (or said it cut short). */ +export function readVariableMap(lines: Iterable): VariableMap | null { + let head: { programs: number; heap: number | null; strings: number } | null = null; + const programs: GameProgramPlaces[] = []; + for (const line of lines) { + if (line.startsWith(HEAD)) head = JSON.parse(line.slice(HEAD.length)); + else if (line.startsWith(PROGRAM)) programs.push(JSON.parse(line.slice(PROGRAM.length))); + } + if (!head || programs.length !== head.programs) return null; + return { heap: head.heap, strings: head.strings, programs: programs.sort((a, b) => a.index - b.index) }; +} + +/** What reading needs of the game: its memory, and a unit by its pointer and the uniqueness byte kept with it. */ +export interface VariableReader { + m: DataView; + u8: Uint8Array; + unitAt(ptr: number, uid: number): SimUnit | null; +} + +const MOST_TEXT = 1 << 16; +const utf8 = new TextDecoder(); + +/** The program's variables by their names in the source, as `TestSim.program()` gives them. Throws for no such program. */ +export function programVariables(game: VariableReader, map: VariableMap, name: string | number = 0, player?: number): Record { + const p = map.programs.find((x) => (typeof name === "number" ? x.index === name : x.name === name) && (player === undefined || x.owners.includes(player))); + if (!p) { + const known = [...new Set(map.programs.map((x) => x.name).filter(Boolean))]; + throw new Error(typeof name === "number" + ? `There is no program ${name}: the script has ${map.programs.length}.` + : `There is no program named "${name}"${player !== undefined ? ` that runs for P${player + 1}` : ""}. ${known.length ? `Named: ${known.join(", ")}.` : "A program is named by its option: program(() => { … }, { name: \"waves\" })."}`); + } + const who = player ?? p.owners[0] ?? 0; + const { m, u8 } = game; + const raw = (at: number | null | undefined, rowed: boolean): number => (at == null ? 0 : m.getUint32(rowed ? at + 4 * who : at, true)); + const num = (v: number, t: { kind: string; bits?: number | null; unsigned: boolean }): number | boolean => (t.kind === "boolean" ? v !== 0 : t.bits || t.unsigned ? v >>> 0 : v | 0); + const cString = (at: number): string => { let end = at; while (end < u8.length && end - at < MOST_TEXT && u8[end] !== 0) end++; return utf8.decode(u8.subarray(at, end)); }; + const text = (v: GameVariable): string => { + if (v.text === "made") { const at = raw(v.cells.addr, v.rowed); return at ? cString(at) : ""; } + const id = raw(v.cells.value, v.rowed); + return id ? cString(map.strings + m.getUint32(map.strings + 4 * id, true)) : ""; + }; + const value = (v: GameVariable): unknown => { + if (v.kind === "unit") { const ptr = raw(v.cells.ptr, v.rowed); return ptr ? game.unitAt(ptr, raw(v.cells.uid, v.rowed)) : null; } + if (v.kind === "text") return text(v); + return num(raw(v.cells.value, v.rowed), v); + }; + const cells = (a: GameArray): (number | boolean)[] => { + if (a.dynamic) { + if (map.heap == null) return []; + const ptr = raw(a.ptr, a.rowed), n = Math.min(raw(a.len, a.rowed), MOST_TEXT); + return Array.from({ length: n }, (_, i) => num(m.getUint32(map.heap! + 4 * (ptr + i), true), a)); + } + if (a.at == null) return new Array(a.length).fill(num(0, a)); + const row = a.rowed ? who * a.length : 0; + return Array.from({ length: a.length }, (_, i) => num(m.getUint32(a.at! + 4 * (row + i), true), a)); + }; + + // As the simulator looks a name up: a number or a boolean, a text, an array, a unit, then a record of fields. + const named = (id: string, n: string) => id === n || id.startsWith(`${n}#`); + const last = (xs: T[], n: string): T | undefined => { let found: T | undefined; for (const x of xs) if (named(x.id, n)) found = x; return found; }; + const lookup = (n: string): unknown => { + const plain = last(p.vars.filter((v) => v.kind === "number" || v.kind === "boolean"), n); + if (plain) return value(plain); + const t = last(p.vars.filter((v) => v.kind === "text"), n); + if (t) return text(t); + const a = last(p.arrays, n); + if (a) return cells(a); + const u = last(p.vars.filter((v) => v.kind === "unit"), n); + if (u) return value(u); + const field = (id: string) => (id.startsWith(`${n}.`) ? id.slice(n.length + 1).split("#")[0] : null); + const record: Record = {}; + for (const v of p.vars) { const f = field(v.id); if (f && v.kind !== "unit" && !(v.kind !== "text" && f.includes(" ("))) record[f] = value(v); } + if (Object.keys(record).length) return record; + const rows: Record[] = []; + for (const arr of p.arrays) { const f = field(arr.id); if (f) cells(arr).forEach((cell, i) => { (rows[i] ??= {})[f] = cell; }); } + return rows.length ? rows : undefined; + }; + return new Proxy({}, { get: (_, key) => (typeof key === "string" ? lookup(key) : undefined), has: (_, key) => typeof key === "string" && lookup(key) !== undefined }); +} diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index 9b306e6..ee99c2a 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 14\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# 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 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# 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\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\nclass Storage:\n \"\"\"A variable's cell: plain, or a 12-slot row of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n self.store = EUDArray([0] * 12) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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, per_player, player_of):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.bits = decl.get(\"bits\")\n values = decl.get(\"values\")\n self.rowed = per_player and not decl.get(\"shared\") and values is None\n self.player_of = player_of\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 self.store = EUDArray([int(v) & U32 for v in values] if values is not None else [0] * (self.length * (12 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 player's row first.\"\"\"\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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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\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\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.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 12-slot rows of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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\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 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 self.arrays = {a[\"id\"]: SliceStorage(a, self) if a.get(\"slice\") else InnerListStorage(a, self) if a.get(\"through\") else (ListStorage if a.get(\"dynamic\") else ArrayStorage)(a, self.per_player, self.player_of) for a in program.get(\"arrays\", [])}\n self.state = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.wait = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.resumes = [] # (index, Forward) for every sleep\n self.frame_end = None\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 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 declare(self, decl):\n if decl.get(\"kind\") == \"unit\":\n s = UnitStorage(decl, self.per_player, self.player_of)\n elif decl.get(\"kind\") == \"text\" and decl.get(\"text\") != \"id\":\n s = TextStorage(decl, self.per_player, self.player_of)\n else:\n s = Storage(decl, self.per_player, self.player_of)\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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n def get_state(self):\n return self.state[self.player] if self.per_player else self.state\n\n def set_state(self, value):\n if self.per_player:\n self.state[self.player] = value\n else:\n self.state << value\n\n def get_wait(self):\n return self.wait[self.player] if self.per_player else self.wait\n\n def set_wait(self, value):\n if self.per_player:\n self.wait[self.player] = value\n else:\n self.wait << value\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 == \"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 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\"] == \"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] = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: the latch\n latch = self.latches[key]\n\n def get():\n return latch[self.player] if self.per_player else latch\n\n def put(v):\n if self.per_player:\n latch[self.player] = v\n else:\n latch << v\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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"action\":\n self.action(st)\n elif k == \"print\":\n self.print_(st)\n elif k == \"centerLocation\":\n self.center_location(st)\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 index = len(self.resumes) + 1\n resume = Forward()\n self.resumes.append((index, resume))\n self.set_state(index)\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 self.set_wait(frames - 1)\n EUDJump(self.frame_end)\n resume << 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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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 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 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 = [], []\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\":\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 else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n group, room, first = [], 0, [True]\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 <= T[\"scratch\"] + TEXT_BYTES):\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 << T[\"append\"](pos, value.addr)\n else:\n if EUDIf()(pos <= T[\"scratch\"] + TEXT_BYTES):\n pos << T[\"append\"](pos, value.addr)\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 return pos, temps\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 == \"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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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.rowed))\n elif isinstance(s, TextStorage):\n for cell in (s.addr, s.block, s.len):\n pairs.append(self.cell_pair(cell, s.rowed))\n elif isinstance(s, Storage):\n pairs.append(self.cell_pair(s.store, s.rowed))\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.rowed))\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, rowed):\n if rowed:\n return (lambda: cell[self.player_of()]), (lambda v: cell.__setitem__(self.player_of(), 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 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 self.set_state(DONE)\n EUDJump(self.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 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 self.frame_end = Forward()\n start = Forward()\n skip = Forward()\n # Still waiting: count down and leave.\n EUDJumpIfNot(as_var(self.get_wait()) >= 1, skip)\n self.set_wait(as_var(self.get_wait()) - 1)\n EUDJump(self.frame_end)\n skip << NextTrigger()\n # The body once, resumes recorded as sleeps are met; the jump table is filled in after.\n table = Forward()\n EUDJump(table)\n start << NextTrigger()\n try:\n self.block(self.p[\"body\"], {})\n except Leave:\n pass\n # The body ended: the program stops for good, as the classic backend's does.\n self.set_state(DONE)\n EUDJump(self.frame_end)\n table << NextTrigger()\n EUDJumpIf(as_var(self.get_state()) == DONE, self.frame_end)\n for index, resume in self.resumes:\n EUDJumpIf(as_var(self.get_state()) == index, resume)\n EUDJump(start)\n self.frame_end << NextTrigger()\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 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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 14\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# 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 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# 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\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\nclass Storage:\n \"\"\"A variable's cell: plain, or a 12-slot row of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n self.store = EUDArray([0] * 12) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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, per_player, player_of):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.bits = decl.get(\"bits\")\n values = decl.get(\"values\")\n self.rowed = per_player and not decl.get(\"shared\") and values is None\n self.player_of = player_of\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 self.store = EUDArray([int(v) & U32 for v in values] if values is not None else [0] * (self.length * (12 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 player's row first.\"\"\"\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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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\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\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.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 12-slot rows of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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\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 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 self.arrays = {a[\"id\"]: SliceStorage(a, self) if a.get(\"slice\") else InnerListStorage(a, self) if a.get(\"through\") else (ListStorage if a.get(\"dynamic\") else ArrayStorage)(a, self.per_player, self.player_of) for a in program.get(\"arrays\", [])}\n self.state = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.wait = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.resumes = [] # (index, Forward) for every sleep\n self.frame_end = None\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 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 declare(self, decl):\n if decl.get(\"kind\") == \"unit\":\n s = UnitStorage(decl, self.per_player, self.player_of)\n elif decl.get(\"kind\") == \"text\" and decl.get(\"text\") != \"id\":\n s = TextStorage(decl, self.per_player, self.player_of)\n else:\n s = Storage(decl, self.per_player, self.player_of)\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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n def get_state(self):\n return self.state[self.player] if self.per_player else self.state\n\n def set_state(self, value):\n if self.per_player:\n self.state[self.player] = value\n else:\n self.state << value\n\n def get_wait(self):\n return self.wait[self.player] if self.per_player else self.wait\n\n def set_wait(self, value):\n if self.per_player:\n self.wait[self.player] = value\n else:\n self.wait << value\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 == \"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 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\"] == \"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] = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: the latch\n latch = self.latches[key]\n\n def get():\n return latch[self.player] if self.per_player else latch\n\n def put(v):\n if self.per_player:\n latch[self.player] = v\n else:\n latch << v\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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"action\":\n self.action(st)\n elif k == \"print\":\n self.print_(st)\n elif k == \"centerLocation\":\n self.center_location(st)\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 index = len(self.resumes) + 1\n resume = Forward()\n self.resumes.append((index, resume))\n self.set_state(index)\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 self.set_wait(frames - 1)\n EUDJump(self.frame_end)\n resume << 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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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 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 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 = [], []\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\":\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 else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n group, room, first = [], 0, [True]\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 <= T[\"scratch\"] + TEXT_BYTES):\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 << T[\"append\"](pos, value.addr)\n else:\n if EUDIf()(pos <= T[\"scratch\"] + TEXT_BYTES):\n pos << T[\"append\"](pos, value.addr)\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 return pos, temps\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 == \"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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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.rowed))\n elif isinstance(s, TextStorage):\n for cell in (s.addr, s.block, s.len):\n pairs.append(self.cell_pair(cell, s.rowed))\n elif isinstance(s, Storage):\n pairs.append(self.cell_pair(s.store, s.rowed))\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.rowed))\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, rowed):\n if rowed:\n return (lambda: cell[self.player_of()]), (lambda v: cell.__setitem__(self.player_of(), 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 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 self.set_state(DONE)\n EUDJump(self.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 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 self.frame_end = Forward()\n start = Forward()\n skip = Forward()\n # Still waiting: count down and leave.\n EUDJumpIfNot(as_var(self.get_wait()) >= 1, skip)\n self.set_wait(as_var(self.get_wait()) - 1)\n EUDJump(self.frame_end)\n skip << NextTrigger()\n # The body once, resumes recorded as sleeps are met; the jump table is filled in after.\n table = Forward()\n EUDJump(table)\n start << NextTrigger()\n try:\n self.block(self.p[\"body\"], {})\n except Leave:\n pass\n # The body ended: the program stops for good, as the classic backend's does.\n self.set_state(DONE)\n EUDJump(self.frame_end)\n table << NextTrigger()\n EUDJumpIf(as_var(self.get_state()) == DONE, self.frame_end)\n for index, resume in self.resumes:\n EUDJumpIf(as_var(self.get_state()) == index, resume)\n EUDJump(start)\n self.frame_end << NextTrigger()\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 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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 d = s.decl\n if d.get(\"temp\"):\n continue\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_of(s).items()}})\n for id_, a in low.arrays.items():\n if isinstance(a, (InnerListStorage, SliceStorage)):\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\n"; diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 424ac46..b752ce5 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -288,8 +288,8 @@ stays JavaScript and is handed a world backed by the game, and it runs where the in play, and MSQC sends it on. - A key and a mouse button are the local machine's memory (0x596A18, 0x6CDDC0), held for one frame. - - Not yet: `program()` (a program's variables; the build would have to say where each lives), - and what another player was shown. + - `program()` reads a program's variables where the build put them (below). Not yet: what + another player was shown. - **The same tests, twice.** `TestRunOptions.makeWorld` hands each test a world other than the simulator's. A compile also returns the script as JavaScript (`CompileResult.modules`). `compiler/gameTests.ts`, bundled as `dist/gametest.js` (198 KB, no TypeScript in it), runs those @@ -334,10 +334,34 @@ stays JavaScript and is handed a world backed by the game, and it runs where the 4. The status bar says "In the game: 4 of 5 tests passed. The simulator disagrees on 1 test." That came about 9 seconds after the click. - The fifth test reads `sim.program()`, which the game cannot answer yet, and it shows as + The fifth test reads `sim.program()`, which the game could not answer yet, and it showed as meant: a ringed red mark beside the simulator's green one, and the game's reason on its own line in Test Results. The game's mark sits beside the simulator's icon rather than at the row's end, where a narrow sidebar cut it off in the first run. +- **`sim.program()` in the game (2026-10-16).** + - The build says where every variable is when asked (the `variableMap` setting, which + **Run All Tests in the Game** and `scripts/test-game.mts` set). Once eudplib has laid the + payload out, the lowering works out each cell's address, and it prints them once + `initialize_payload` has placed the payload at the end of STRx (0x191943C8 plus the + section's length): a `TRIGSCRIPT-VARS` line, then a `TRIGSCRIPT-PROGRAM` line a program. + The editor keeps these lines out of Output. + - `compiler/gameVariables.ts` reads them, and reads a program's variables out of the game's + memory by the same rules as the simulator's lookup. That covers numbers (signed, `u8`/`u16`, + `u32`), booleans, texts of the table and texts that are made, units (null once the slot's + uniqueness byte has moved on), fixed and growing arrays, records and lists of records, and a + per-player program's row by player. + - Two differences, both because the game has every cell from the start. A variable not yet + declared reads as its zero. Of two declarations with one name, the later in the source wins. + A list inside a list and a row of a grid are not read. + - Traps: an `EUDArray` is a proxy of its data's EPD, not of its address (relocation mode 1, so + `4·offset + 0x58A364`). eudplib works addresses out only while it writes the payload, so the + lowering sets that phase again for as long as it reads them. + - Checked: `probes/game-program.ts` (every kind above, a unit, a per-player program by player) + passes in the game and in the simulator under Node, and so does `probes/game-tests.ts`. In a + built editor the five tests of the end-to-end run now pass in the game: "In the game: 5 of 5 + tests passed.", about 10 seconds after the click. + - plugin-scm-sim released 0.9.0 with the Replay view's zoom in the meantime, so `test()` + ships in 0.10, and TrigScript's message now asks for that. ### A3. The desync detector (4.1.0) diff --git a/editor.ts b/editor.ts index 6840b73..ad124cc 100644 --- a/editor.ts +++ b/editor.ts @@ -42,6 +42,7 @@ import { } from "./monaco"; import { renamedKeys, renamesInUse, replaceReferences, type Renamed } from "./refs"; import { FILE_NAME } from "./script"; +import { isMachineLine, readVariableMap } from "./compiler/gameVariables"; import { isTestFile, type TestInfo, type TestReport, type TestResult, type TestRunOptions } from "./compiler/testing"; import { countTests, foldPlayers, idsUnder, marksOf, mergeReport, NO_TESTS, notesOf, playersLabel, stateOf, stateOfCounts, summary, testTree, warningsOf, type TestNode, type TestState, type TestStateName } from "./testState"; import { testName } from "./service"; @@ -108,6 +109,8 @@ const OWNER_COMPUTER = 5; const OWNER_HUMAN = 6; /** How many of a simulation's events the list shows before it says how many more there were. */ const SIMULATE_ROWS = 200; +/** A build's log as a person reads it: without the lines the lowering says for a run in the lifted game. */ +const withoutMachineLines = (text: string) => text.split("\n").filter((l) => !isMachineLine(l)).join("\n"); /** The most lines with a fault the Simulate view lists. */ const SIMULATE_FAULTS = 20; @@ -736,7 +739,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa const runTestsInGame = async (filter: Pick = {}) => { if (!editor || testsRunning || building || testing || !ready) return; const engine = svc.engine(); - if (!engine?.present || !engine.test) { setStatus("error", t("Running tests in the game needs the scm-sim plugin (0.9 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….")); return; } + if (!engine?.present || !engine.test) { setStatus("error", t("Running tests in the game needs the scm-sim plugin (0.10 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….")); return; } testsRunning = true; renderTesting(); try { @@ -752,13 +755,17 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa const names = svc.names()?.names; if (!names) { setStatus("error", t("No map is open.")); return; } setStatus("busy", t("Building the map…")); - // (Whole: a bare .chk has no room for what the build steps make, so the engine's page reads the scenario out of it.) - const file = await api.document.export({ format: "scx" }); + // (Whole: a bare .chk has no room for what the build steps make, so the engine's page reads the scenario out of + // it. The build says in its log where it put the programs' variables, for sim.program().) + const logged: string[] = []; + const heard = svc.onBuild((e) => { if (e.kind === "log") logged.push(e.line); else if (e.kind === "done" && !logged.length) logged.push(...e.log.split("\n")); }); + const file = await svc.withVariableMap(() => api.document.export({ format: "scx" })).finally(() => heard.dispose()); if (!file || cancelled) { if (!file) setStatus("error", t("No map is open.")); return; } + const variables = readVariableMap(logged); gametest ??= fetch(gametestUrl(svc.dist())).then((r) => { if (!r.ok) throw new Error(`gametest.js: ${r.status}`); return r.text(); }); const module = await gametest.catch((err) => { gametest = null; throw err; }); setStatus("busy", t("Running the tests in the game…")); - const report = await engine.test(new Uint8Array(await file.arrayBuffer()), module, { modules: a.compiled.modules, names, files: filter.files, ids: filter.ids }) as TestReport & { error?: string }; + const report = await engine.test(new Uint8Array(await file.arrayBuffer()), module, { modules: a.compiled.modules, names, files: filter.files, ids: filter.ids, variables }) as TestReport & { error?: string }; if (cancelled) return; if (report.error) { setStatus("error", t("Not run in the game: {message}", { message: report.error })); return; } const ran = report.results.filter((x) => x.status !== "skipped"); @@ -1184,14 +1191,15 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa if (failing) log(t("{n, plural, one {A test fails} other {# tests fail}}: see the Testing view.", { n: failing }), false); } else if (e.kind === "log") { streamed++; - log(e.line, false); + // (What the lowering says for a run in the lifted game is not for a person to read.) + if (!isMachineLine(e.line)) log(e.line, false); } else if (e.kind === "done") { - if (!streamed && e.log) log(e.log, false); + if (!streamed && e.log) log(withoutMachineLines(e.log), false); const kb = Math.round(e.chkBytes / 1024), seconds = (e.ms / 1000).toFixed(1); buildState = { kind: "ok", text: t("Built {kb} KB · {seconds} s", { kb, seconds }), of: buildState?.of ?? files }; log(t("Built: {kb} KB of scenario in {seconds} s", { kb, seconds })); } else { - if (!streamed && e.log) log(e.log, false); + if (!streamed && e.log) log(withoutMachineLines(e.log), false); // Save goes on without the step: the file is the map and its script, with no programs in it. const saved = e.purpose === "save"; buildState = { diff --git a/ko.ts b/ko.ts index f0c149e..b5bd2d7 100644 --- a/ko.ts +++ b/ko.ts @@ -275,7 +275,7 @@ export const KO: Record = { "passed": "통과", "In the game: {message}": "게임에서: {message}", "In the game: passed": "게임에서: 통과", - "Running tests in the game needs the scm-sim plugin (0.9 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….": "테스트를 게임에서 실행하려면 scmjs.dev에서 scm-sim 플러그인(0.9 이상)이 필요합니다. 플러그인 ▸ 플러그인 관리…에서 설치하거나 켜세요.", + "Running tests in the game needs the scm-sim plugin (0.10 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….": "테스트를 게임에서 실행하려면 scmjs.dev에서 scm-sim 플러그인(0.10 이상)이 필요합니다. 플러그인 ▸ 플러그인 관리…에서 설치하거나 켜세요.", "Not run in the game: {message}": "게임에서 실행하지 못함: {message}", "the engine cannot run a game now": "엔진이 지금 게임을 실행할 수 없습니다", "Not run in the game: the engine's page is from before tests in the game. Reload the editor and try again.": "게임에서 실행하지 못함: 엔진 페이지가 게임 속 테스트보다 오래된 판입니다. 편집기를 새로 고친 뒤 다시 시도하세요.", diff --git a/probes/game-program.ts b/probes/game-program.ts new file mode 100644 index 0000000..dd31016 --- /dev/null +++ b/probes/game-program.ts @@ -0,0 +1,77 @@ +// TrigScript 4, A2: sim.program() in the lifted game — every kind of variable a program keeps, read back out of the +// game's memory where the build said it put them, and the same test in the simulator beside it. +// npx tsx scripts/test-game.mts probes/game-program.ts +// Built like a probe (Player 1 Terran, Player 2 a Zerg computer, location 1 "Home" by Player 1's base). +import { test, expect, program, sleep, frames, shared, first, createUnit, units, P1, P2, CurrentPlayer, type Location, type Unit, type u8, type u32 } from "trigscript"; + +const HOME = 1 as Location; + +program(() => { + let ticks = 0; + let small: u8 = 0; + let big: u32 = 0; + let minus = 0; + let even = false; + let title = "start"; + let said = ""; + let pos = { x: 1, y: 2 }; + const seen = [0, 0, 0]; + const queue: number[] = []; + let marine: Unit | null = null; + createUnit(P1, units.TerranMarine, 1, HOME); + while (true) { + ticks += 1; + small = ticks * 100; + big = 4000000000; + minus = -ticks; + even = ticks % 2 == 0; + if (ticks == 3) title = "three"; + said = `tick ${ticks}`; + pos.x = ticks * 10; + seen[ticks % 3] = ticks; + if (queue.length < 4) queue.push(ticks); + if (!marine) marine = first({ owner: P1, type: units.TerranMarine }); + sleep(frames(1)); + } +}, { owner: P1, name: "kinds" }); + +program(() => { + let mine = 0; + let all = shared(0); + while (true) { mine += 1; all += 1; sleep(frames(1)); } +}, { owner: [P1, P2], name: "each" }); + +test("numbers, booleans, texts, a record and arrays", (sim) => { + sim.frames(5); + const k = sim.program("kinds"); + const ticks = k.ticks as number; + expect(ticks).toBeGreaterThan(2); + expect(k.small).toBe(255); + expect(k.big).toBe(4000000000); + expect(k.minus).toBe(-ticks); + expect(k.even).toBe(ticks % 2 == 0); + expect(k.title).toBe("three"); + expect(k.said).toBe(`tick ${ticks}`); + expect(k.pos).toEqual({ x: ticks * 10, y: 2 }); + expect(k.queue).toEqual([1, 2, 3, 4]); + expect((k.seen as number[])[ticks % 3]).toBe(ticks); + expect(k.nothing).toBeUndefined(); +}); + +test("a unit a program holds", (sim) => { + sim.frames(5); + const marine = sim.program("kinds").marine as { alive: boolean; type: number; owner: number } | null; + expect(marine).not.toBeNull(); + expect(marine!.type).toBe(units.TerranMarine); + expect(marine!.owner).toBe(P1); +}); + +test("a per-player program, by player", (sim) => { + sim.frames(4); + const one = sim.program("each", P1).mine as number, two = sim.program("each", P2).mine as number; + expect(one).toBeGreaterThan(0); + expect(two).toBe(one); + // (Each player's run declares the shared cell again when it starts, so it counts from the later start.) + expect(sim.program("each").all).toBeGreaterThanOrEqual(one); + expect(() => sim.program("each", CurrentPlayer + 5)).toThrow(/runs for P/); +}); diff --git a/python/trigscript.py b/python/trigscript.py index 2dcd061..7888737 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -3173,6 +3173,120 @@ def _construct_and_say(): _payload._construct_payload = _construct_and_say +# Where each program's variables are, for a test in the lifted game to read them as `sim.program()` reads the +# simulator's (TrigScript 4, A2). Only when the build asks for it (the `variableMap` setting): once the payload is +# placed, a header line and a line a program give the address of every cell: +# TRIGSCRIPT-VARS {"programs": , "heap":
, "strings":
} +# TRIGSCRIPT-PROGRAM {"index", "name", "perPlayer", "owners", "vars": [...], "arrays": [...]} +# A variable is its id (what the simulator keys it by), its kind, and its cells: `value` (a number, a boolean, a text +# of the table by its id), `addr` + `len` (a text that is made), `ptr` + `uid` (a unit). An array is its cells' `at`, +# or, growing, its handle's `ptr` + `len` among the heap's cells. A cell of a per-player program's own (`rowed`) is a +# row of twelve, one a player; an address is null for what nothing in the map uses, which eudplib leaves out. +if settings.get("variableMap"): + import eudplib.core.allocator.payload as _vpayload + import eudplib.maprw.injector.apply_injector as _injector + from eudplib.core.allocator.constexpr import Evaluate as _evaluate + from eudplib.core.mapdata.stringmap import get_string_section_name as _strings_name + from eudplib.core.variable.vbuf import get_current_varbuffer as _varbuffer + from eudplib.utils import unProxy as _unproxy + + _PLACES = {} + + def _offset(expr): + """A payload-relative address as a byte offset into the payload: eudplib's own (relocated whole) or an EPD + (relocated a quarter at a time, as EPD(x) = (x - 0x58A364) / 4 is).""" + r = _evaluate(expr) + if r.rlocmode == 4: + return r.offset + if r.rlocmode == 1: + return r.offset * 4 + 0x58A364 + return None + + def _place(obj): + """Where a variable's value or an array's first cell is in the payload; None for what it left out.""" + if isinstance(obj, EUDVariable): + vb = _varbuffer() + if vb is None or obj.GetVTable() not in vb._vdict: + return None + return _offset(obj.getValueAddr()) + # An EUDArray is a proxy of its data's EPD (or of the data itself), the data an object of its own. + data = _unproxy(obj) + while not isinstance(data, EUDObject) and hasattr(data, "_value"): + data = _unproxy(data) + try: + return _offset(data) + except Exception: + return None # (nothing in the map uses it, so eudplib did not place it) + + def _cells_of(s): + if isinstance(s, UnitStorage): + return {"ptr": s.ptr, "uid": s.uid} + if isinstance(s, TextStorage): + return {"addr": s.addr, "len": s.len} + return {"value": s.store} + + def _program_places(k, low): + p = low.p + out = {"index": k, "name": p.get("name"), "perPlayer": low.per_player, "owners": p.get("owners", [low.owner]), "vars": [], "arrays": []} + for id_, s in low.vars.items(): + d = s.decl + if d.get("temp"): + continue + out["vars"].append({"id": id_, "kind": d.get("kind"), "text": d.get("text"), "bits": d.get("bits"), "unsigned": bool(d.get("unsigned")), + "rowed": bool(s.rowed), "cells": {name: _place(cell) for name, cell in _cells_of(s).items()}}) + for id_, a in low.arrays.items(): + if isinstance(a, (InnerListStorage, SliceStorage)): + continue # (a row of another's cells, or a list inside one: read through what it is in, not here) + d = a.decl + 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)} + if isinstance(a, ListStorage): + row.update(dynamic=True, ptr=_place(a.handle["ptr"]), len=_place(a.handle["len"])) + else: + row.update(at=_place(a.store)) + out["arrays"].append(row) + return out + + _vconstruct = _vpayload._construct_payload + + def _construct_and_place(): + built = _vconstruct() + # (eudplib works addresses out only while it writes the payload: for as long as this takes, it is again.) + was = _vpayload.phase + _vpayload.phase = _vpayload.PHASE_WRITING + try: + _PLACES["programs"] = [_program_places(k, low) for k, low in enumerate(PROGRAMS)] + _PLACES["heap"] = _place(_HEAP["cells"]) if _HEAP else None + finally: + _vpayload.phase = was + _PLACES["bytes"] = len(built.data) + return built + + _vpayload._construct_payload = _construct_and_place + + _initialize = _injector.initialize_payload + + def _initialize_and_say(chkt, payload, *args, **kwargs): + out = _initialize(chkt, payload, *args, **kwargs) + # The payload is the end of the strings section, which Remastered keeps at 0x191943C8. + base = 0x191943C8 + len(chkt.getsection(_strings_name())) - _PLACES["bytes"] + + def at(o): + return None if o is None else base + o + + print("TRIGSCRIPT-VARS " + json.dumps({"programs": len(_PLACES["programs"]), "heap": at(_PLACES["heap"]), "strings": 0x191943C8}, separators=(",", ":"))) + for prog in _PLACES["programs"]: + for v in prog["vars"]: + v["cells"] = {name: at(o) for name, o in v["cells"].items()} + for a in prog["arrays"]: + for key in ("at", "ptr", "len"): + if key in a: + a[key] = at(a[key]) + print("TRIGSCRIPT-PROGRAM " + json.dumps(prog, separators=(",", ":"))) + return out + + _injector.initialize_payload = _initialize_and_say + + INPUT = Input(IR.get("input")) PROGRAMS = [Lowering(p) for p in IR.get("programs", [])] diff --git a/scripts/build-fixture.mts b/scripts/build-fixture.mts index 34ec6fd..6a455cf 100644 --- a/scripts/build-fixture.mts +++ b/scripts/build-fixture.mts @@ -20,6 +20,7 @@ import ts from "typescript"; import { compileScript } from "../compiler/compiler"; import { serializeIr } from "../compiler/eud"; import { buildPlugins } from "../compiler/input"; +import { readVariableMap } from "../compiler/gameVariables"; import { resolveStrings } from "../script"; import { defaultScriptNames } from "../compiler/names"; import { defaultLib } from "../bundle/lib.mjs"; @@ -98,6 +99,9 @@ const sections = buildPlugins(r.input, "/work/files/trigscript.json"); // TRIGGER_MAP=1: the Python says where each trigger it made sits in the payload (a TRIGSCRIPT-MAP line in the // log), and that line is written beside the map as .triggers.json, for scm-sim's run in the lifted game. if (process.env.TRIGGER_MAP) (sections.trigscript as Record).triggerMap = "1"; +// VARIABLE_MAP=1: the same for where each program's variables are (TRIGSCRIPT-VARS), as .variables.json, for a +// test in the lifted game to read them (`sim.program()`). +if (process.env.VARIABLE_MAP) (sections.trigscript as Record).variableMap = "1"; const { eudTurbo, ...before } = sections; writeFileSync(pluginsPath, JSON.stringify(extraName ? { ...before, [extraName]: {}, eudTurbo } : sections)); const extraArgs = extra && extraName ? [`${extraName}=${resolve(extra.slice(extraName.length + 1))}`] : []; @@ -114,5 +118,8 @@ if (mapLine) { if (triggers.length !== head.count) { console.error(`the map of triggers came out with ${triggers.length} of ${head.count} rows`); process.exit(1); } writeFileSync(resolve(outPath).replace(/\.scx$/, ".triggers.json"), JSON.stringify({ payload: head.payload, triggers })); } -process.stdout.write(res.stdout.split("\n").filter((l) => !l.startsWith("TRIGSCRIPT-MAP ") && !l.startsWith("TRIGSCRIPT-ROWS ")).join("\n")); +const variables = readVariableMap(logLines); +if (variables) writeFileSync(resolve(outPath).replace(/\.scx$/, ".variables.json"), JSON.stringify(variables)); +else if (process.env.VARIABLE_MAP && res.status === 0) { console.error("the build said nothing of where the variables are"); process.exit(1); } +process.stdout.write(res.stdout.split("\n").filter((l) => !/^TRIGSCRIPT-[A-Z]+ /.test(l)).join("\n")); if (res.status !== 0) { console.error(res.stderr); process.exit(1); } diff --git a/scripts/test-game.mts b/scripts/test-game.mts index a31afe9..36d6d46 100644 --- a/scripts/test-game.mts +++ b/scripts/test-game.mts @@ -31,7 +31,7 @@ const dir = mkdtempSync(join(tmpdir(), "trigscript-tests-")); try { // The map, built as Save builds it. const built = join(dir, basename(script, ".ts") + ".scx"); - const b = spawnSync("npx", ["tsx", join(root, "scripts", "build-fixture.mts"), resolve(script), built, ...(baseMap ? [resolve(baseMap)] : [])], { cwd: root, encoding: "utf8", maxBuffer: 256 << 20 }); + const b = spawnSync("npx", ["tsx", join(root, "scripts", "build-fixture.mts"), resolve(script), built, ...(baseMap ? [resolve(baseMap)] : [])], { cwd: root, encoding: "utf8", maxBuffer: 256 << 20, env: { ...process.env, VARIABLE_MAP: "1" } }); if (b.status !== 0) { console.error(b.stdout.split("\n").slice(-20).join("\n"), b.stderr); process.exit(1); } // The game, at frame 0, EUD let through. @@ -64,7 +64,9 @@ try { const bundle = join(dir, "gametest.js"); spawnSync("npx", ["esbuild", join(root, "compiler", "gameTestEntry.ts"), "--bundle", "--format=esm", "--target=es2022", "--platform=neutral", `--outfile=${bundle}`], { cwd: root }); const host = await import(`data:text/javascript;base64,${Buffer.from(readFileSync(bundle)).toString("base64")}`) as typeof import("../compiler/gameTestEntry"); - const inGame = host.runGameTests(handle, { modules: compiled.modules, names, locations }) as TestReport & { error?: string }; + // (Where the build put the programs' variables, for sim.program().) + const variables = JSON.parse(readFileSync(built.replace(/\.scx$/, ".variables.json"), "utf8")); + const inGame = host.runGameTests(handle, { modules: compiled.modules, names, locations, variables }) as TestReport & { error?: string }; if (inGame.error) { console.error(`the script did not run beside the game: ${inGame.error}`); process.exit(1); } const bySim = new Map((simulated?.results ?? []).map((r) => [r.id, r])); let failed = 0; diff --git a/service.ts b/service.ts index a237c3c..c8ee160 100644 --- a/service.ts +++ b/service.ts @@ -141,6 +141,8 @@ export class ScriptService { private lastManifest: Uint8Array | null | undefined; /** The last compile, for a save that comes right after an apply: the same files against the same names need no second compile. */ private lastArtifact: ScriptArtifact | null = null; + /** Builds under way that want the lowering to say where the programs' variables are (`withVariableMap`). */ + private variableMaps = 0; /** The editor's language, for what a person reads in the editor's own trigger editors and notices; what the commands return stays English. */ private readonly t: Translate; @@ -304,14 +306,25 @@ export class ScriptService { const artifact = fresh ? cached : await this.prepare(state.files, map, guard ? { world: this.world() } : undefined); if (!artifact.compiled.ok) throw new Error(firstFault(artifact.compiled.diagnostics)); if (guard) this.guardTests(artifact.compiled); + const plugins = buildPlugins(artifact.compiled.input, "/work/files/trigscript.json"); + if (this.variableMaps) (plugins.trigscript as Record).variableMap = "1"; return { // chatEvent and MSQC join in, around the lowering, when a program reads what the players do. - plugins: buildPlugins(artifact.compiled.input, "/work/files/trigscript.json"), + plugins, sources: { trigscript: TRIGSCRIPT_PY }, files: { "trigscript.json": serializeIr(artifact.compiled.ir, artifact.compiled.strings, artifact.compiled.input, this.settings()) }, }; } + /** + * `run` with every build it starts asked to say where the programs' variables are — lines of the build's log + * (`compiler/gameVariables.ts`) — for tests in the lifted game to read them. + */ + async withVariableMap(run: () => Promise): Promise { + this.variableMaps++; + try { return await run(); } finally { this.variableMaps--; } + } + /** Hear about the library's builds (it runs them on Save, Test Map and export): the log, and where a failure points. */ onBuild(listener: (event: EudplibBuildEvent) => void): { dispose(): void } { let mine: { dispose(): void } | null = null; diff --git a/tests/game-variables.test.ts b/tests/game-variables.test.ts new file mode 100644 index 0000000..e910e0b --- /dev/null +++ b/tests/game-variables.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "vitest"; +import { isMachineLine, programVariables, readVariableMap, type VariableMap } from "../compiler/gameVariables"; + +// A memory of 64 KB standing in for the game's, with the cells where a map says they are. +const memory = () => { const u8 = new Uint8Array(1 << 16); return { u8, m: new DataView(u8.buffer) }; }; +const STRINGS = 0x8000, HEAP = 0xa000; + +const MAP: VariableMap = { + heap: HEAP, + strings: STRINGS, + programs: [ + { + index: 0, name: "kinds", perPlayer: false, owners: [0], + vars: [ + { id: "n#0", kind: "number", unsigned: false, rowed: false, cells: { value: 0x100 } }, + { id: "big#1", kind: "number", unsigned: true, rowed: false, cells: { value: 0x104 } }, + { id: "ok#2", kind: "boolean", unsigned: false, rowed: false, cells: { value: 0x108 } }, + { id: "title#3", kind: "text", text: "id", unsigned: false, rowed: false, cells: { value: 0x10c } }, + { id: "said#4", kind: "text", text: "made", unsigned: false, rowed: false, cells: { addr: 0x110, len: 0x114 } }, + { id: "pos.x#5", kind: "number", unsigned: false, rowed: false, cells: { value: 0x118 } }, + { id: "pos.y#6", kind: "number", unsigned: false, rowed: false, cells: { value: null } }, + { id: "who#7", kind: "unit", unsigned: false, rowed: false, cells: { ptr: 0x120, uid: 0x124 } }, + { id: "n#8", kind: "number", unsigned: false, rowed: false, cells: { value: 0x128 } }, + ], + arrays: [ + { id: "seen#0", kind: "number", unsigned: false, length: 3, rowed: false, at: 0x200 }, + { id: "queue#1", kind: "number", unsigned: false, length: 0, rowed: false, dynamic: true, ptr: 0x300, len: 0x304 }, + ], + }, + { + index: 1, name: "each", perPlayer: true, owners: [0, 2], + vars: [{ id: "mine#0", kind: "number", unsigned: false, rowed: true, cells: { value: 0x400 } }], + arrays: [{ id: "row#0", kind: "boolean", unsigned: false, length: 2, rowed: true, at: 0x500 }], + }, + ], +}; + +function filled() { + const g = memory(), { m, u8 } = g; + m.setInt32(0x100, -7, true); + m.setUint32(0x104, 4000000000, true); + m.setUint32(0x108, 1, true); + // String 2 of the table: "three". + m.setUint32(STRINGS + 8, 0x40, true); + u8.set(new TextEncoder().encode("three\0"), STRINGS + 0x40); + m.setUint32(0x10c, 2, true); + u8.set(new TextEncoder().encode("tick 4 ✓\0"), 0x600); + m.setUint32(0x110, 0x600, true); + m.setUint32(0x118, 40, true); + m.setUint32(0x120, 0x7000, true); + m.setUint32(0x124, 0x0500, true); + m.setInt32(0x128, 99, true); + [1, 2, 3].forEach((v, i) => m.setUint32(0x200 + 4 * i, v, true)); + m.setUint32(0x300, 8, true); m.setUint32(0x304, 2, true); + m.setUint32(HEAP + 4 * 8, 5, true); m.setUint32(HEAP + 4 * 9, 6, true); + m.setUint32(0x400 + 4 * 0, 3, true); m.setUint32(0x400 + 4 * 2, 4, true); + m.setUint32(0x500 + 4 * (2 * 2 + 1), 1, true); + const unitAt = (ptr: number, uid: number) => (ptr === 0x7000 && uid === 0x0500 ? ({ alive: true, type: 0 } as never) : null); + return { ...g, unitAt }; +} + +describe("a program's variables read out of the game", () => { + it("each kind, as the simulator gives it", () => { + const k = programVariables(filled(), MAP, "kinds"); + // (Of two with one name, the later.) + expect(k.n).toBe(99); + expect(k.big).toBe(4000000000); + expect(k.ok).toBe(true); + expect(k.title).toBe("three"); + expect(k.said).toBe("tick 4 ✓"); + expect(k.pos).toEqual({ x: 40, y: 0 }); + expect(k.who).toEqual({ alive: true, type: 0 }); + expect(k.seen).toEqual([1, 2, 3]); + expect(k.queue).toEqual([5, 6]); + expect(k.nothing).toBeUndefined(); + expect("seen" in k).toBe(true); + }); + + it("a per-player program's row, by player; the first owner's by default", () => { + const g = filled(); + expect(programVariables(g, MAP, "each").mine).toBe(3); + expect(programVariables(g, MAP, "each", 2).mine).toBe(4); + expect(programVariables(g, MAP, 1, 2).row).toEqual([false, true]); + expect(() => programVariables(g, MAP, "each", 1)).toThrow(/runs for P2/); + expect(() => programVariables(g, MAP, "waves")).toThrow(/Named: kinds, each/); + expect(() => programVariables(g, MAP, 5)).toThrow(/the script has 2/); + }); + + it("a unit that is gone is null; nothing held is null", () => { + const g = filled(); + g.m.setUint32(0x124, 0x0600, true); + expect(programVariables(g, MAP, "kinds").who).toBeNull(); + g.m.setUint32(0x120, 0, true); + expect(programVariables(g, MAP, "kinds").who).toBeNull(); + }); +}); + +describe("the build's lines", () => { + const lines = [ + "[Stage 3/3] ConstructPayload", + `TRIGSCRIPT-VARS {"programs":2,"heap":${HEAP},"strings":${STRINGS}}`, + `TRIGSCRIPT-PROGRAM ${JSON.stringify(MAP.programs[1])}`, + `TRIGSCRIPT-PROGRAM ${JSON.stringify(MAP.programs[0])}`, + ]; + it("make the map, in the programs' order", () => { expect(readVariableMap(lines)).toEqual(MAP); }); + it("cut short, make none", () => { expect(readVariableMap(lines.slice(0, 3))).toBeNull(); expect(readVariableMap(["hello"])).toBeNull(); }); + it("are not for a person", () => { expect(lines.map(isMachineLine)).toEqual([false, true, true, true]); expect(isMachineLine("TRIGSCRIPT-ROWS [[0,1,2,\"\"]]")).toBe(true); }); +}); diff --git a/vendor/scmSim.ts b/vendor/scmSim.ts index fbcc2ff..04b10c6 100644 --- a/vendor/scmSim.ts +++ b/vendor/scmSim.ts @@ -22,7 +22,7 @@ export interface ScmSimEngine { * A script's tests beside the game: the map as Save builds it (a whole .scx; the page reads * its scenario) or that scenario as eudplib built it, the text of a module * whose `runGameTests(game, payload)` runs them, and its payload; resolves with what the - * module returned. Absent from the plugin before 0.9.0. + * module returned. Absent from the plugin before 0.10.0. */ test?(scenario: Uint8Array, module: string, payload: unknown, options?: { signal?: AbortSignal; onProgress?: (p: { stage: "starting" }) => void }): Promise; } From e8518c0ae9606b3cc376ac725716d5ea83c34a8e Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 10:30:59 -0600 Subject: [PATCH 008/238] Tests in the game need scm-sim 0.11, not 0.10 The pointer readout shipped as plugin-scm-sim 0.10.0 first. Co-Authored-By: Claude Opus 5.5 --- editor.ts | 2 +- ko.ts | 2 +- vendor/scmSim.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/editor.ts b/editor.ts index ad124cc..77105f6 100644 --- a/editor.ts +++ b/editor.ts @@ -739,7 +739,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa const runTestsInGame = async (filter: Pick = {}) => { if (!editor || testsRunning || building || testing || !ready) return; const engine = svc.engine(); - if (!engine?.present || !engine.test) { setStatus("error", t("Running tests in the game needs the scm-sim plugin (0.10 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….")); return; } + if (!engine?.present || !engine.test) { setStatus("error", t("Running tests in the game needs the scm-sim plugin (0.11 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….")); return; } testsRunning = true; renderTesting(); try { diff --git a/ko.ts b/ko.ts index b5bd2d7..b49556b 100644 --- a/ko.ts +++ b/ko.ts @@ -275,7 +275,7 @@ export const KO: Record = { "passed": "통과", "In the game: {message}": "게임에서: {message}", "In the game: passed": "게임에서: 통과", - "Running tests in the game needs the scm-sim plugin (0.10 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….": "테스트를 게임에서 실행하려면 scmjs.dev에서 scm-sim 플러그인(0.10 이상)이 필요합니다. 플러그인 ▸ 플러그인 관리…에서 설치하거나 켜세요.", + "Running tests in the game needs the scm-sim plugin (0.11 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….": "테스트를 게임에서 실행하려면 scmjs.dev에서 scm-sim 플러그인(0.11 이상)이 필요합니다. 플러그인 ▸ 플러그인 관리…에서 설치하거나 켜세요.", "Not run in the game: {message}": "게임에서 실행하지 못함: {message}", "the engine cannot run a game now": "엔진이 지금 게임을 실행할 수 없습니다", "Not run in the game: the engine's page is from before tests in the game. Reload the editor and try again.": "게임에서 실행하지 못함: 엔진 페이지가 게임 속 테스트보다 오래된 판입니다. 편집기를 새로 고친 뒤 다시 시도하세요.", diff --git a/vendor/scmSim.ts b/vendor/scmSim.ts index 04b10c6..6a9a41c 100644 --- a/vendor/scmSim.ts +++ b/vendor/scmSim.ts @@ -22,7 +22,7 @@ export interface ScmSimEngine { * A script's tests beside the game: the map as Save builds it (a whole .scx; the page reads * its scenario) or that scenario as eudplib built it, the text of a module * whose `runGameTests(game, payload)` runs them, and its payload; resolves with what the - * module returned. Absent from the plugin before 0.10.0. + * module returned. Absent from the plugin before 0.11.0. */ test?(scenario: Uint8Array, module: string, payload: unknown, options?: { signal?: AbortSignal; onProgress?: (p: { stage: "starting" }) => void }): Promise; } From 2a513c64b348d7962f4bb7d4de6033bde7e6dc02 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 10:59:01 -0600 Subject: [PATCH 009/238] TrigScript 4, A3: tests of several players, each at a copy of the game MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test(…, { players: [P1, P2] }, …) runs in the lifted game as a copy for each player, kept in step by scm-sim's runCopies. What a player does is done at their copy (a key, a click, a line, the mouse), every player's screen is read at every copy, and the test fails at the frame the copies stop agreeing, the lines that wrote what differs and that read the local machine's memory named, and the script's variables that had come apart before. A one-game run lists such a test as skipped. probes/game-players.ts: a key and a line from P2's computer reach P1's, a text made for P2 is P2's only, and nothing splits; 3/3 in two copies. Co-Authored-By: Claude Opus 5.5 --- compiler/declarations.ts | 7 ++ compiler/gameSim.ts | 135 ++++++++++++++++++++++++++++++++++---- compiler/gameTestEntry.ts | 4 +- compiler/gameTests.ts | 17 ++++- compiler/testing.ts | 18 +++-- docs/v4-plan.md | 67 +++++++++++++++++++ probes/game-players.ts | 51 ++++++++++++++ scripts/build-fixture.mts | 4 +- scripts/test-game.mts | 33 +++++++++- tests/game-copies.test.ts | 86 ++++++++++++++++++++++++ 10 files changed, 398 insertions(+), 24 deletions(-) create mode 100644 probes/game-players.ts create mode 100644 tests/game-copies.test.ts diff --git a/compiler/declarations.ts b/compiler/declarations.ts index 361019b..c4ca739 100644 --- a/compiler/declarations.ts +++ b/compiler/declarations.ts @@ -622,6 +622,13 @@ export function expect(actual: T): Expectation & { readonly not: Expectati export interface TestOptions { /** Without the map's player settings the world has one player: this one. */ as?: Player; + /** + * In the game: these players, each at a computer of their own — a copy of the game for each, kept in step as a + * game between people is. What a player does (press, click, type, moveMouse) is done at their computer, and the + * test fails at the frame the copies stop agreeing, the lines that made them differ named. The simulator runs the + * test once, as without it. + */ + players?: readonly Player[]; } type TestBody = (sim: Sim, ...args: A) => void; interface TestEach { (cases: readonly T[]): (name: string, fn: F extends "suite" ? (...args: T extends readonly unknown[] ? T : [T]) => void : TestBody) => void } diff --git a/compiler/gameSim.ts b/compiler/gameSim.ts index 1997d00..e0cbdb2 100644 --- a/compiler/gameSim.ts +++ b/compiler/gameSim.ts @@ -32,6 +32,75 @@ export interface GameHandle { chat(text: string): void; call(address: number, registers: Record, args?: number[]): number; reset(): void; + /** When the game is one of several copies kept in step, a copy for each of a test's players (scm-sim's `CopyHandle`). */ + readonly copy?: GameCopy; +} + +/** One copy of several (TrigScript 4, A3): which, the slot each copy plays, and where they stopped agreeing. */ +export interface GameCopy { + readonly index: number; + readonly copies: number; + readonly players: readonly number[]; + readonly desync: Desync | null; + label: string | null; + linesOf(slot: number): { frame: number; text: string }[] | null; +} +export interface Desync { + frame: number; + /** Each copy's frame, when they were not even at the same one. */ + apart: number[] | null; + differences: { what: string; values: (number | null)[] }[]; + /** By copy: the lines that wrote what differs in the turn before, and those that read the local machine's own memory. */ + wrote: string[][]; + readLocal: string[][]; + players: number[]; + /** eudplib's payload, watched: dwords that had come to differ before the split, from which frame, by copy. */ + watched?: { address: number; since: number; values: (number | null)[] }[]; +} + +/** The script's variables by the address of their cells (`program.name` or `program #k.name`), from the build's map. */ +export function variableNames(map: VariableMap | null): Map { + const out = new Map(); + for (const p of map?.programs ?? []) { + const program = p.name ? p.name : `program #${p.index}`; + for (const v of p.vars) for (const [cell, at] of Object.entries(v.cells)) if (at !== null && at !== undefined) { + const name = `${v.id.replace(/#\d+$/, "")}${cell === "value" ? "" : ` (${cell})`}`; + if (v.rowed) for (let k = 0; k < 12; k++) out.set(at + 4 * k, `${name} of P${k + 1}, in ${program}`); + else out.set(at, `${name}, in ${program}`); + } + for (const a of p.arrays) if (a.at !== null && a.at !== undefined && !a.dynamic) { + const cells = a.length * (a.rowed ? 12 : 1); + const id = a.id.replace(/#\d+$/, ""); + for (let i = 0; i < cells && i < 4096; i++) out.set(a.at + 4 * i, `${id}[${a.rowed ? `${Math.floor(i / 12)}] of P${(i % 12) + 1}` : `${i}]`}, in ${program}`); + } + } + return out; +} + +/** "12:5" → "line 12"; "lib.ts:3:1" → "lib.ts line 3"; anything else as it is. */ +const lineWords = (origin: string) => origin.replace(/(?:^|(?<= ))(?:([^\s:]+):)?(\d+):\d+(?= |$)/g, (_m, file: string | undefined, line: string) => `${file ? `${file} ` : ""}line ${line}`); + +/** + * A desync in words: what differs, where, what each computer's triggers did in the turn before, and which of the + * script's variables had already come apart (`names`: by the address of their cells). + */ +export function desyncMessage(d: Desync, names: Map = new Map()): string { + const who = (c: number) => `P${d.players[c] + 1}'s computer`; + if (d.apart) return `the players' games stopped agreeing: they were at different frames (${d.apart.map((f, c) => `${f} at ${who(c)}`).join(", ")})`; + const first = d.differences[0]; + const what = first ? `${first.what} is ${first.values.map((v, c) => `${v ?? "?"} at ${who(c)}`).join(", ")}` : "their state differs"; + const more = d.differences.length > 1 ? ` (and ${d.differences.length - 1} more)` : ""; + const notes = d.players.map((_, c) => { + const said = [...d.wrote[c] ?? [], ...d.readLocal[c] ?? []].map(lineWords); + return said.length ? ` At ${who(c)}: ${said.slice(0, 4).join("; ")}.` : ""; + }).join(""); + const known = (d.watched ?? []).filter((w) => names.has(w.address)); + const theirs = (d.watched ?? []).length - known.length; + const before = known.length + ? ` Before that, the script's ${known.slice(0, 3).map((w) => `${names.get(w.address)} from frame ${w.since} (${w.values.map((v, c) => `${v ?? "?"} at ${who(c)}`).join(", ")})`).join("; ")}${known.length > 3 ? ` and ${known.length - 3} more` : ""} had come apart.` + : ""; + const own = theirs ? ` (${theirs} of eudplib's own cells differed too; some are each computer's own on purpose.)` : ""; + return `the players' games stopped agreeing at frame ${d.frame}: ${what}${more}.${notes}${before}${own}`; } // 1.16.1's addresses, which Remastered's emulation keeps. @@ -147,15 +216,30 @@ export class GameSim implements TestWorldLike { * (`readLocations`); from then on the game's own are read. `variables`: where the build put each program's * variables, for `program()`. */ - constructor(game: GameHandle, options: { locations?: Record; variables?: VariableMap | null } = {}) { + constructor(game: GameHandle, options: { locations?: Record; variables?: VariableMap | null; test?: { id: string; players?: number[] } } = {}) { this.game = game; this.known = options.locations ?? {}; this.variables = options.variables ?? null; + if (game.copy) { + game.copy.label = options.test?.id ?? null; + for (const p of game.copy.players) if (p !== game.localPlayer) this.shownBy.set(p, []); + } game.reset(); this.take(); } - private take(): void { for (const l of this.game.lines()) this.shown.push(l); } + /** In a test of several players: what each other player's computer showed, by slot. */ + private readonly shownBy = new Map(); + private take(): void { + for (const l of this.game.lines()) this.shown.push(l); + for (const [p, list] of this.shownBy) for (const l of this.game.copy!.linesOf(p) ?? []) list.push(l); + } + /** The copies stopped agreeing: the test fails here, at every computer alike. */ + private checkStep(): void { + const d = this.game.copy?.desync; + if (d) throw new ExpectError(desyncMessage(d, this.names ??= variableNames(this.variables))); + } + private names: Map | null = null; private record(type: number, fields: { location?: number; player?: number; number?: number; unit?: number; count?: number }): Uint8Array { const r = new Uint8Array(32), v = new DataView(r.buffer); v.setUint32(0x00, fields.location ?? 0, true); @@ -175,7 +259,7 @@ export class GameSim implements TestWorldLike { /* ── time ── */ frames(n = 1): this { - for (let i = 0; i < n && !this.game.over; i++) { this.game.step(); this.take(); this.releaseKeys(); } + for (let i = 0; i < n && !this.game.over; i++) { this.game.step(); this.take(); this.releaseKeys(); this.checkStep(); } return this; } seconds(n: number): this { return this.frames(Math.round(n * FRAMES_PER_SECOND)); } @@ -250,16 +334,27 @@ export class GameSim implements TestWorldLike { return this.scratch(SCRATCH_TO, b, (dest) => this.onOne(unit, (loc) => this.act(this.record(ActionType.MoveUnit, { location: loc, player: unit.owner, number: dest, unit: unit.type, count: 1 }), unit.owner))); } - /* ── what the players do: the local player only ── */ + /* ── what the players do: at their own computer ── */ private held: number[] = []; - private local(player: number | undefined, what: string): void { - if (player !== undefined && player !== this.game.localPlayer) throw new ExpectError(`${what}: in the game only the player at this computer (P${this.game.localPlayer + 1}) can do it`); + /** + * Whether what `player` does is done here. One game: only the player at this computer can act. Copies of a game + * (a test's `players`): each player acts at their own copy, and no player named is the first of them. + */ + private local(player: number | undefined, what: string): boolean { + const copy = this.game.copy; + if (!copy) { + if (player !== undefined && player !== this.game.localPlayer) throw new ExpectError(`${what}: in the game only the player at this computer (P${this.game.localPlayer + 1}) can do it; a test of several players says so: test(…, { players: [P1, P${player + 1}] }, …)`); + return true; + } + const who = player ?? copy.players[0]; + if (!copy.players.includes(who)) throw new ExpectError(`${what}: P${who + 1} is not one of this test's players (${copy.players.map((p) => `P${p + 1}`).join(", ")})`); + return who === this.game.localPlayer; } /** A key down for one frame, as a press is (MSQC sees it down, then up). */ - press(key: string, player?: number): this { this.local(player, "press"); const k = virtualKey(key); this.game.u8[KEYS + k] = 1; this.held.push(k); return this; } + press(key: string, player?: number): this { const k = virtualKey(key); if (!this.local(player, "press")) return this; this.game.u8[KEYS + k] = 1; this.held.push(k); return this; } click(button: "left" | "right" | "middle" = "left", player?: number): this { - this.local(player, "click"); + if (!this.local(player, "click")) return this; const m = this.game.m; m.setUint32(BUTTONS, m.getUint32(BUTTONS, true) | BUTTON_BITS[button], true); this.held.push(-BUTTON_BITS[button]); @@ -270,9 +365,9 @@ export class GameSim implements TestWorldLike { const m = this.game.m; for (const k of this.held.splice(0)) { if (k >= 0) this.game.u8[KEYS + k] = 0; else m.setUint32(BUTTONS, m.getUint32(BUTTONS, true) & ~-k, true); } } - type(line: string, player?: number): this { this.local(player, "type"); this.game.chat(line); return this; } + type(line: string, player?: number): this { if (this.local(player, "type")) this.game.chat(line); return this; } moveMouse(x: number, y: number, player?: number): this { - this.local(player, "moveMouse"); + if (!this.local(player, "moveMouse")) return this; this.game.m.setUint16(MOUSE, x, true); this.game.m.setUint16(MOUSE + 2, y, true); return this; @@ -322,10 +417,24 @@ export class GameSim implements TestWorldLike { /** What was shown, in order, with the game's colour codes taken out. `player`: only the player at this computer is seen. */ printed(player?: number): string[] { - if (player !== undefined && player !== this.game.localPlayer) throw new ExpectError(`printed(P${player + 1}): in the game only what the player at this computer (P${this.game.localPlayer + 1}) was shown is seen`); - return this.shown.map((l) => clean(l.text)); + if (player === undefined) { + // (A line every computer showed at once is one line, as the simulator has it.) + const seen = new Set(); + return this.everyone().filter((l) => { const k = `${l.frame}\u0000${l.text}`; if (seen.has(k)) return false; seen.add(k); return true; }).map((l) => clean(l.text)); + } + if (player === this.game.localPlayer) return this.shown.map((l) => clean(l.text)); + const theirs = this.shownBy.get(player); + if (theirs) return theirs.map((l) => clean(l.text)); + throw new ExpectError(this.game.copy + ? `printed(P${player + 1}): P${player + 1} is not one of this test's players (${this.game.copy.players.map((p) => `P${p + 1}`).join(", ")})` + : `printed(P${player + 1}): in the game only what the player at this computer (P${this.game.localPlayer + 1}) was shown is seen; a test of several players sees each one's: test(…, { players: [P1, P${player + 1}] }, …)`); + } + /** Every player's lines, by frame (one game: the local player's). */ + private everyone(): { frame: number; text: string; player?: number }[] { + const all = [...this.shown.map((l) => ({ ...l, player: this.game.localPlayer })), ...[...this.shownBy].flatMap(([player, list]) => list.map((l) => ({ ...l, player })))]; + return all.sort((a, b) => a.frame - b.frame || a.player - b.player); } - log(): TestEvent[] { return this.shown.map((l) => ({ frame: l.frame, text: `Display Text Message — ${clean(l.text)}`, player: this.game.localPlayer })); } + log(): TestEvent[] { return this.everyone().map((l) => ({ frame: l.frame, text: `Display Text Message — ${clean(l.text)}`, player: l.player ?? this.game.localPlayer })); } get events(): TestEvent[] { return this.log(); } /** A program's faults, as it says them in the game: a red line that begins "TrigScript:". */ get faults(): { at: At; message: string; cycle: number; frame: number; program: number }[] { diff --git a/compiler/gameTestEntry.ts b/compiler/gameTestEntry.ts index 4a6f4a2..fad2101 100644 --- a/compiler/gameTestEntry.ts +++ b/compiler/gameTestEntry.ts @@ -2,6 +2,6 @@ * `dist/gametest.js`: what a host of the lifted game imports to run a script's tests beside it (see `gameTests.ts`). * The plugin never loads it itself; it hands its text to the host. */ -export { runGameTests, type GameTestPayload } from "./gameTests"; -export { GameSim, readLocations, readUnitStats, type GameHandle } from "./gameSim"; +export { drive, runGameTests, type GameTestPayload } from "./gameTests"; +export { desyncMessage, GameSim, readLocations, readUnitStats, type Desync, type GameCopy, type GameHandle } from "./gameSim"; export { programVariables, readVariableMap, type VariableMap } from "./gameVariables"; diff --git a/compiler/gameTests.ts b/compiler/gameTests.ts index 3c4ce79..58c6fe3 100644 --- a/compiler/gameTests.ts +++ b/compiler/gameTests.ts @@ -14,7 +14,7 @@ import type { VariableMap } from "./gameVariables"; import { locate, runModules, type LinkedFile } from "./link"; import type { ScriptNames } from "./names"; import { Collector, createRuntime } from "./runtime"; -import { isTestFile, runTests, TestRegistry, TESTING_NAMES, type TestReport } from "./testing"; +import { isTestFile, listTests, runTests, TestRegistry, TESTING_NAMES, type TestReport, type TestResult } from "./testing"; import type { SimBounds } from "./world"; export interface GameTestPayload { @@ -41,5 +41,18 @@ export function runGameTests(game: GameHandle, payload: GameTestPayload): TestRe collector.running = false; if (failure) return { list: [], results: [], only: [], ms: 0, error: `${failure.file ?? ENTRY_FILE}:${failure.line ?? 1}: ${failure.message}` }; const locations = payload.locations ?? readLocations(game); - return runTests(registry, { triggers: [], sources: [], ir: [], strings: [], locate: where }, { files: payload.files, ids: payload.ids, makeWorld: () => new GameSim(game, { locations, variables: payload.variables }) }); + // A test of several players runs only where the game is one of several copies, one a player (A3); one game says so. + const several = listTests(registry).list.filter((t) => t.kind === "test" && t.players); + const ids = game.copy ? payload.ids : payload.ids ?? listTests(registry).list.filter((t) => t.kind === "test" && !t.players).map((t) => t.id); + const report = runTests(registry, { triggers: [], sources: [], ir: [], strings: [], locate: where }, { files: payload.files, ids, makeWorld: (_as, test) => new GameSim(game, { locations, variables: payload.variables, test }) }); + if (!game.copy && !payload.ids) { + const held: TestResult[] = several.filter((t) => !payload.files || payload.files.includes(t.file)).map((t) => ({ id: t.id, status: "skipped", message: `a test of ${t.players!.length} players runs as a copy of the game for each`, printed: [], events: [], frames: 0, ms: 0 })); + report.results.push(...held); + } + return report; +} + +/** What a copy of the game runs (scm-sim's `runCopies` driver): the same tests, at this copy's computer. */ +export function drive(game: GameHandle, payload: GameTestPayload): TestReport & { error?: string } { + return runGameTests(game, payload); } diff --git a/compiler/testing.ts b/compiler/testing.ts index 4085063..80ce283 100644 --- a/compiler/testing.ts +++ b/compiler/testing.ts @@ -49,6 +49,8 @@ interface Case { args: unknown[]; /** `{ as: P2 }`: the player the world is simulated as, when the map's player settings are not given. */ as?: number; + /** `{ players: [P1, P2] }`: in the game, a copy of it for each, kept in step. */ + players?: number[]; } /** A test or a `describe`, as the editor lists it. `id` is the file and the names down to it: the same from one compile to the next. */ @@ -61,6 +63,8 @@ export interface TestInfo { file: string; line: number; mode: Mode; + /** A test's `{ players }`: run in the game as a copy for each. */ + players?: number[]; } export interface TestEvent { frame: number; text: string; file?: string; line?: number; player: number } @@ -116,7 +120,7 @@ export interface TestRunOptions { * A world other than the simulator's for each test (the lifted game's, `GameSim`): made fresh for every test, * given the player the test asked to be (`{ as: P2 }`). Absent: the simulator. */ - makeWorld?: (as?: number) => TestWorldLike; + makeWorld?: (as?: number, test?: { id: string; players?: number[] }) => TestWorldLike; /** Run only these: tests of these files, or with these ids (a suite's id runs what is under it). Absent: all. */ files?: string[]; ids?: string[]; @@ -160,7 +164,11 @@ export class TestRegistry { if (typeof fn !== "function") throw new Error(`test("${name}"): the second argument is the test's function, (sim) => { … }.`); if (fn.constructor?.name === "AsyncFunction") throw new Error(`test("${name}"): a test is not async. Nothing in sim waits: sim.frames(n) and sim.until(…) have run by the time they return.`); const as = options && typeof options === "object" && typeof (options as { as?: unknown }).as === "number" ? (options as { as: number }).as : undefined; - this.current.children.push({ kind: "test", name, at: this.where(), mode, parent: this.current, fn: fn as TestFn, args, ...(as !== undefined ? { as } : {}) }); + const given = options && typeof options === "object" ? (options as { players?: unknown }).players : undefined; + if (given !== undefined && !(Array.isArray(given) && given.length >= 2 && given.length <= 8 && given.every((p) => Number.isInteger(p) && p >= 0 && p < 8) && new Set(given).size === given.length)) + throw new Error(`test("${name}", { players }): two to eight different players of P1 to P8, as [P1, P2].`); + const players = given as number[] | undefined; + this.current.children.push({ kind: "test", name, at: this.where(), mode, parent: this.current, fn: fn as TestFn, args, ...(as !== undefined ? { as } : {}), ...(players ? { players: [...players] } : {}) }); } private addSuite(mode: Mode, name: unknown, body: unknown): void { @@ -493,7 +501,7 @@ export function listTests(registry: TestRegistry): { list: TestInfo[]; cases: Ma const n = (seen.get(id) ?? 0) + 1; seen.set(id, n); if (n > 1) id = `${id} #${n}`; - list.push({ id, kind: node.kind, name: node.name, path, file: node.at.file, line: node.at.line, mode: node.mode }); + list.push({ id, kind: node.kind, name: node.name, path, file: node.at.file, line: node.at.line, mode: node.mode, ...(node.kind === "test" && node.players ? { players: node.players } : {}) }); if (node.kind === "suite") walk(node, [...path, node.name]); else cases.set(id, node); } @@ -528,14 +536,14 @@ export function runTests(registry: TestRegistry, ctx: TestContext, options: Test return { list, results, only, ms: Date.now() - started }; } -function runOne(id: string, test: Case, chain: Suite[], ctx: TestContext, world: TestWorld, makeWorld?: (as?: number) => TestWorldLike): TestResult { +function runOne(id: string, test: Case, chain: Suite[], ctx: TestContext, world: TestWorld, makeWorld?: TestRunOptions["makeWorld"]): TestResult { const began = Date.now(); let sim: TestWorldLike | null = null; const done = (extra: Partial): TestResult => ({ id, status: "passed", printed: sim?.printed() ?? [], events: (sim?.log((t) => ctx.sources[t] ?? null) ?? []).slice(0, 500), frames: sim?.frame ?? 0, ms: Date.now() - began, ...extra, }); try { - sim = makeWorld ? makeWorld(test.as) : new TestSim(ctx.triggers, ctx.ir, ctx.strings, world, test.as); + sim = makeWorld ? makeWorld(test.as, { id, players: test.players }) : new TestSim(ctx.triggers, ctx.ir, ctx.strings, world, test.as); for (const s of chain) for (const h of s.before) h(sim); const returned = test.fn(sim, ...test.args); if (returned && typeof (returned as { then?: unknown }).then === "function") throw new Error("A test is not async: what it returned is a promise. Nothing in sim waits."); diff --git a/docs/v4-plan.md b/docs/v4-plan.md index b752ce5..2b68948 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -375,6 +375,73 @@ A test can ask for it: `test("shop", { players: 4 }, …)`. The Testing view the To check first: UMS with several people on one instance (melee is measured, UMS is not). +**As built (2026-10-16, under Node; the editor's half is not done).** The check was already done: +scm-sim's `tests/exact-net-ums` plays two people on one instance in UMS. Two parts of the design +above changed. The option is a list (`{ players: [P1, P2] }`), because a test's inputs name the +player anyway. And the hash is not the game's own. + +- **Copies.** scm-sim `src/exact/copies.ts`, `copyWorker.ts` and `copiesNode.ts`: one worker + thread a copy (one game a process), each seated as a different person, 0.9 GB each. + - Each turn, inside the game's own request for every player's turn (`TurnFeed.exchange`), the + copies trade what their games queued, over a SharedArrayBuffer barrier. Every machine's + commands reach every game in the same turn, as on a real network. Before this, `TurnFeed` sent + only the checksum command, so a typed line or MSQC's commands from the map's own code went + nowhere. + - After every frame the copies also trade what each was shown, so `printed(P2)` reads the same + at every copy. +- **The digest.** One FNV word per 256-byte block of the memory every machine must hold alike: + units, deaths, kills, resources, supply, switches, locations, the seed. + - The game's 0x37 checksum command does not do: it is built at 0x47CC50 from a ring at + 0x65EB30, filled at 0x47CFC0, and hashed at 0x47CF10. Depending on a mode byte (0x65FBF0) it + hashes the units on some turns and the resources, seed and frame on others. It always folds + in a rolling chunk of the map's tile flags. It never hashes death counts. And it mixes in the + local player (0x47CF93), so machines that agree send different bytes. Measured: deaths, ore, + hit points, position, the seed and a switch changed at frame 40 left 80 turns of it as they + were. + - Two unit fields are each machine's own, found by copies in step that disagreed, and left + out: + - +0x87, the under-attack alert's quiet time (0x48EB30 when the owner is 0x512684, then + 0x48EAE0); + - +0x8A and +0x8B, the minimap blink (0x48F230, 0x45F08B). + - eudplib's payload is **watched, not compared**. chatEvent's line and the unit MSQC selects + for this computer's player differ on purpose, from frame 2 of every map that reads input. So + a payload difference is noted with its frame, and is reported only once what the game holds + splits. + - The strings are never digested: TrigScript's made text for one player is written on that + player's computer only (`IsUserCP`). +- **The report.** At the first turn the digests differ, every copy runs the same detail rounds and + reports the same thing: + - the dwords, named (unit slot and field, deaths of a unit for a player); + - each copy's lines that wrote them that turn; + - each copy's lines that read the local machine's memory (0x512684, the keys, the mouse, the + screen); + - the script's variables that had come apart before, named from the build's variable map. + + In the test it fails the test at that frame, in every copy alike: "the players' games stopped + agreeing at frame 5: deaths of unit 7 for P1 is 1 at P1's computer, 0 at P2's computer. At P1's + computer: line 12 wrote …". +- **TrigScript.** + - `test(…, { players: [P1, P2] }, …)` runs in one copy for each player. `press`, `click`, + `type` and `moveMouse` with a player act at that player's copy; with none, at the first + player's. + - A one-game run lists such a test as skipped ("runs as a copy of the game for each"). + - `scripts/test-game.mts` builds the map again with those slots open to people + (build-fixture's `PEOPLE=0,1`), then runs them through `runCopies` with + `dist/gametest.js`'s `drive`. +- **Measured.** + - `probes/game-players.ts`: a key and a line from P2's computer reach P1's, and a text made for + P2 is seen at P2's only. 3/3 pass in two copies with no desync, beside the one-computer test. + - scm-sim `tests/exact-copies`: a classic EUD trigger on 0x512684 splits two copies at its + first pass, and both name the trigger. + - A 3000-frame fight with a computer player stays in step with 2 and with 4 copies (8 s, + 3.5 GB for four). +- **Not done.** + - The editor: the play page needs cross-origin isolation for SharedArrayBuffer in its workers, + so "Run All Tests in the Game" lists these tests as skipped. + - Dwords that come apart later in a payload block already watched are not reported. + - `sim.place` and the other actions between frames run at every copy, as a trigger for that + player would. + ### A4. What a frame costs (4.1.0) From A1's hooks: conditions and actions run per frame, by IR node and by line. Lifted diff --git a/probes/game-players.ts b/probes/game-players.ts new file mode 100644 index 0000000..1b37c05 --- /dev/null +++ b/probes/game-players.ts @@ -0,0 +1,51 @@ +// TrigScript 4, A3: tests of several players, each at a computer of their own — a copy of the lifted game for each, +// kept in step, every machine's commands handed to all. +// npx tsx scripts/test-game.mts probes/game-players.ts +// A key pressed and a line typed at Player 2's computer reach Player 1's; a text made for one player is seen by +// that player only; and none of it splits the two games. Built like a probe (location 1 "Home" by Player 1's base). +import { test, expect, program, displayText, setResources, chatted, keyPressed, createUnit, sleep, frames, seconds, units, P1, P2, type Location } from "trigscript"; + +const HOME = 1 as Location; + +program(() => { + while (true) { + if (keyPressed(P2, "Q")) { createUnit(P2, units.TerranFirebat, 1, HOME); displayText("P2 pressed Q"); } + const m = chatted(P2, "-give {n}"); + if (m) { setResources(P2, "add", m.n, "ore"); displayText(`P2 was given ${m.n}`); } + sleep(frames(1)); + } +}, { owner: P1 }); + +program(() => { + let n = 0; + while (true) { + n += 1; + displayText(`second ${n}, for two only`); + sleep(seconds(1)); + } +}, { owner: P2 }); + +test("a key at P2's computer reaches every computer", { players: [P1, P2] }, (sim) => { + sim.frames(10).press("Q", P2).frames(10); + expect(sim.count(P2, units.TerranFirebat, HOME)).toBe(1); + expect(sim.printed(P1)).toContain("P2 pressed Q"); +}); + +test("a line typed at P2's computer", { players: [P1, P2] }, (sim) => { + sim.frames(10); + const before = sim.resources(P2).ore; + sim.type("-give 75", P2).seconds(1); + expect(sim.resources(P2).ore).toBe(before + 75); + expect(sim.printed(P1)).toContain("P2 was given 75"); +}); + +test("a text made for one player is that player's", { players: [P1, P2] }, (sim) => { + sim.seconds(3); + expect(sim.printed(P2)).toContain("second 2, for two only"); + expect(sim.printed(P1)).not.toContain("second 2, for two only"); +}); + +test("one computer, as before", (sim) => { + sim.frames(10).press("Q").frames(10); + expect(sim.count(P2, units.TerranFirebat, HOME)).toBe(0); +}); diff --git a/scripts/build-fixture.mts b/scripts/build-fixture.mts index 6a455cf..4fe39d4 100644 --- a/scripts/build-fixture.mts +++ b/scripts/build-fixture.mts @@ -4,7 +4,7 @@ * plugin-eudplib checkout's `scripts/build-map.mts`, write the built map. For probe maps * that get played. * - * [TRIGGER_MAP=1] npx tsx scripts/build-fixture.mts probes/spike.ts fixtures/eud/spike-eud.scx [map.scx] [../plugin-eudplib] + * [TRIGGER_MAP=1] [PEOPLE=0,1] npx tsx scripts/build-fixture.mts probes/spike.ts fixtures/eud/spike-eud.scx [map.scx] [../plugin-eudplib] * * The output lands in the ignored fixtures/ folder: it is built on a Blizzard map, which is never committed. * @@ -46,6 +46,8 @@ const scn = parseScenario(loaded.chk); // with User Selectable the game hands out melee units and drops the placed ones. patchPlayer(scn, 0, { type: 6, race: 1, force: 0 }); patchPlayer(scn, 1, { type: 5, race: 0, force: 1 }); +// PEOPLE=0,1: those slots open to a person each (Terran), for a test of several players (scripts/test-game.mts). +for (const slot of (process.env.PEOPLE ?? "").split(",").filter(Boolean).map(Number)) patchPlayer(scn, slot, { type: 6, race: 1, force: slot === 0 ? 0 : 1 }); // The base map is a melee map: its stock triggers defeat whoever commands no buildings, and in // Use Map Settings nobody is handed melee units — both players lose at once and the game // ends in a draw a second in. The probe's own script is the only source of triggers it should have, diff --git a/scripts/test-game.mts b/scripts/test-game.mts index 36d6d46..fcb99af 100644 --- a/scripts/test-game.mts +++ b/scripts/test-game.mts @@ -71,6 +71,7 @@ try { const bySim = new Map((simulated?.results ?? []).map((r) => [r.id, r])); let failed = 0; for (const r of inGame.results) { + if (r.status === "skipped" && r.message?.startsWith("a test of ")) continue; const s = bySim.get(r.id); const mark = (st: string | undefined) => (st === "passed" ? "✓" : st === "failed" ? "✗" : st === "skipped" ? "–" : "?"); if (r.status === "failed") failed++; @@ -79,7 +80,37 @@ try { if (s?.status === "failed") console.log(` in the simulator: ${s.message}${s.at ? ` — line ${s.at.line}` : ""}${s.actual !== undefined ? `\n got ${s.actual}` : ""}`); if (s && s.status !== r.status) console.log(" the two disagree: one of them is wrong"); } - console.log(`\n${inGame.results.length} tests in the game: ${inGame.results.length - failed} passed, ${failed} failed`); + // A test of several players: the map built again with their slots open to people, then a copy of the game for + // each player, kept in step (scm-sim's runCopies), all running the same tests. + const groups = new Map(); + for (const t of simulated?.list ?? []) if (t.kind === "test" && t.players) { const k = t.players.join(","); const g = groups.get(k) ?? { players: t.players, ids: [] }; g.ids.push(t.id); groups.set(k, g); } + if (groups.size) { + const people = [...new Set([...groups.values()].flatMap((g) => g.players))].sort((a, b) => a - b); + const several = join(dir, basename(script, ".ts") + "-people.scx"); + const b2 = spawnSync("npx", ["tsx", join(root, "scripts", "build-fixture.mts"), resolve(script), several, ...(baseMap ? [resolve(baseMap)] : [])], { cwd: root, encoding: "utf8", maxBuffer: 256 << 20, env: { ...process.env, VARIABLE_MAP: "1", TRIGGER_MAP: "1", PEOPLE: people.join(",") } }); + if (b2.status !== 0) { console.error(b2.stdout.split("\n").slice(-20).join("\n"), b2.stderr); process.exit(1); } + const { runCopies } = await from("src/exact/copiesNode.ts"); + const triggers = JSON.parse(readFileSync(several.replace(/\.scx$/, ".triggers.json"), "utf8")); + const severalVariables = JSON.parse(readFileSync(several.replace(/\.scx$/, ".variables.json"), "utf8")); + for (const g of groups.values()) { + const out = await runCopies({ map: new Uint8Array(readFileSync(several)), seats: g.players.map((slot) => ({ slot, race: 1, name: `P${slot + 1}` })), driver: readFileSync(bundle, "utf8"), payload: { modules: compiled.modules, names, locations, variables: severalVariables, ids: g.ids }, triggers }) as { result: (TestReport & { error?: string }) | null; error: string | null; player: number }[]; + for (const c of out) if (c.error || c.result?.error) { console.error(`P${c.player + 1}'s copy: ${c.error ?? c.result?.error}`); process.exit(1); } + // A test passes when it passes at every computer; the first that failed says why. + for (const id of g.ids) { + const each = out.map((c) => ({ player: c.player, r: c.result!.results.find((x) => x.id === id)! })); + const bad = each.find((e) => e.r.status === "failed"); + const s = bySim.get(id); + const mark = (st: string | undefined) => (st === "passed" ? "✓" : st === "failed" ? "✗" : st === "skipped" ? "–" : "?"); + if (bad) failed++; + console.log(`${mark(bad ? "failed" : "passed")} game ×${g.players.length} ${mark(s?.status)} simulator ${id.replace(/^.*?::/, "")} (${each[0].r.frames} frames, ${Math.max(...each.map((e) => e.r.ms))} ms)`); + if (bad) console.log(` at P${bad.player + 1}'s computer: ${bad.r.message}${bad.r.at ? ` — line ${bad.r.at.line}` : ""}`); + if (s?.status === "failed") console.log(` in the simulator: ${s.message}${s.at ? ` — line ${s.at.line}` : ""}`); + if (s && s.status !== (bad ? "failed" : "passed")) console.log(" the two disagree: one of them is wrong"); + } + } + } + const counted = inGame.results.filter((r) => r.status !== "skipped" || !r.message?.startsWith("a test of ")).length + [...groups.values()].reduce((n, g) => n + g.ids.length, 0); + console.log(`\n${counted} tests in the game: ${counted - failed} passed, ${failed} failed`); if (failed) process.exitCode = 1; } finally { rmSync(dir, { recursive: true, force: true }); diff --git a/tests/game-copies.test.ts b/tests/game-copies.test.ts new file mode 100644 index 0000000..c2cc23d --- /dev/null +++ b/tests/game-copies.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; +import { desyncMessage, GameSim, variableNames, type Desync, type GameCopy, type GameHandle } from "../compiler/gameSim"; +import { listTests, TestRegistry } from "../compiler/testing"; +import type { VariableMap } from "../compiler/gameVariables"; + +// TrigScript 4, A3: a test of several players, each at a copy of the game of their own (scm-sim's CopyHandle). +const KEYS = 0x596a18; + +/** A game handle over memory of its own, one copy of `players`, seated as `local`. */ +function copyOf(players: number[], local: number, heard: Record = {}) { + const u8 = new Uint8Array(0x700000), m = new DataView(u8.buffer); + let frame = 0, desync: Desync | null = null; + const chats: string[] = []; + const copy: GameCopy = { + index: players.indexOf(local), copies: players.length, players, label: null, + get desync() { return desync; }, + linesOf: (slot) => (heard[slot] ?? []).splice(0).map((text) => ({ frame, text })), + }; + const game: GameHandle = { + m, u8, get frame() { return frame; }, localPlayer: local, over: false, copy, + step() { frame++; }, lines: () => [], action: () => 1, chat: (t) => { chats.push(t); }, call: () => 0, reset() { frame = 0; }, + }; + return { game, copy, chats, split: (d: Desync) => { desync = d; } }; +} + +describe("a test's players", () => { + it("are two to eight different players", () => { + const r = new TestRegistry(); + const { test } = r.library() as { test: (name: string, o: unknown, fn: unknown) => void }; + test("two", { players: [0, 1] }, () => {}); + expect(() => test("one", { players: [0] }, () => {})).toThrow(/two to eight/); + expect(() => test("twice", { players: [1, 1] }, () => {})).toThrow(/two to eight/); + expect(() => test("P9", { players: [0, 8] }, () => {})).toThrow(/two to eight/); + expect(listTests(r).list.map((t) => [t.name, t.players])).toEqual([["two", [0, 1]]]); + }); +}); + +describe("a world that is one copy of several", () => { + it("a player's key and line are done at that player's copy only", () => { + const one = copyOf([0, 1], 0), two = copyOf([0, 1], 1); + for (const c of [one, two]) new GameSim(c.game).press("Q", 1).type("-go", 1); + expect([one.game.u8[KEYS + 0x51], two.game.u8[KEYS + 0x51]]).toEqual([0, 1]); + expect([one.chats, two.chats]).toEqual([[], ["-go"]]); + // No player named: the first of the test's. + new GameSim(one.game).press("W"); + expect(one.game.u8[KEYS + 0x57]).toBe(1); + }); + + it("a player not in the test is said to be so", () => { + expect(() => new GameSim(copyOf([0, 1], 0).game).press("Q", 2)).toThrow("press: P3 is not one of this test's players (P1, P2)"); + expect(() => new GameSim(copyOf([0, 1], 0).game).printed(3)).toThrow("not one of this test's players"); + }); + + it("every player's screen is read at every copy, a line shown to all once", () => { + const c = copyOf([0, 1], 0, { 1: ["for two"] }); + const sim = new GameSim(c.game).frames(1); + expect(sim.printed(1)).toEqual(["for two"]); + expect(sim.printed(0)).toEqual([]); + expect(sim.printed()).toEqual(["for two"]); + }); + + it("the copies splitting fails the test at that frame, saying where", () => { + const c = copyOf([0, 1], 0); + const sim = new GameSim(c.game).frames(5); + c.split({ frame: 5, apart: null, players: [0, 1], differences: [{ what: "deaths of unit 7 for P1", values: [1, 0] }], wrote: [["12:5 wrote deaths of unit 7 for P1"], []], readLocal: [["12:5 read the local player"], ["12:5 read the local player"]] }); + expect(() => sim.frames(1)).toThrow("the players' games stopped agreeing at frame 5: deaths of unit 7 for P1 is 1 at P1's computer, 0 at P2's computer. At P1's computer: line 12 wrote deaths of unit 7 for P1; line 12 read the local player. At P2's computer: line 12 read the local player."); + }); +}); + +describe("a desync's words", () => { + const map: VariableMap = { heap: null, strings: 0, programs: [{ index: 0, name: "shop", perPlayer: false, owners: [0], vars: [{ id: "gold#0", kind: "number", unsigned: false, rowed: false, cells: { value: 0x100 } }, { id: "turn#1", kind: "number", unsigned: false, rowed: true, cells: { value: 0x200 } }], arrays: [{ id: "slots#0", kind: "number", unsigned: false, length: 3, rowed: false, at: 0x300 }] }] }; + + it("name the script's variables by their cells", () => { + const names = variableNames(map); + expect([names.get(0x100), names.get(0x204), names.get(0x308)]).toEqual(["gold, in shop", "turn of P2, in shop", "slots[2], in shop"]); + }); + + it("say which variables had come apart before, and how many of eudplib's own", () => { + const d: Desync = { frame: 40, apart: null, players: [0, 1], differences: [{ what: "unit slot 3, hit points", values: [10240, 5120] }], wrote: [[], []], readLocal: [[], []], watched: [{ address: 0x100, since: 31, values: [5, 0] }, { address: 0x9000, since: 2, values: [1, 2] }] }; + expect(desyncMessage(d, variableNames(map))).toBe("the players' games stopped agreeing at frame 40: unit slot 3, hit points is 10240 at P1's computer, 5120 at P2's computer. Before that, the script's gold, in shop from frame 31 (5 at P1's computer, 0 at P2's computer) had come apart. (1 of eudplib's own cells differed too; some are each computer's own on purpose.)"); + }); + + it("say when the copies were not even at the same frame", () => { + expect(desyncMessage({ frame: 9, apart: [9, 12], players: [0, 2], differences: [], wrote: [], readLocal: [] })).toBe("the players' games stopped agreeing: they were at different frames (9 at P1's computer, 12 at P3's computer)"); + }); +}); From 78b661ea2f8d9306fc9b23168657ee700e267093 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 11:26:39 -0600 Subject: [PATCH 010/238] TrigScript 4, A3: tests of several players from the editor Run All Tests in the Game now runs a test of several players through the scm-sim plugin's testCopies: a copy of the game for each player on the analysis page, the reports made into one mark (compiler/gameCopies.ts). The build is asked for its trigger map beside the variable map, so a desync names lines (readTriggerMap, shared with build-fixture). Where the page cannot run copies (no Document-Isolation-Policy, or a browser before Chromium 137), those tests are marked not run, and why. Checked in a built editor in headless Chromium 153: 4 of 4 in the game, three as two copies; without the header, those three marked not run. build-fixture gains PREP_ONLY=1 (the map the editor opens) and test-game.mts TRIGSCRIPT_DUMP= (what the copies are handed, for scm-sim's web/smoke-copies.mjs). Co-Authored-By: Claude Opus 5.5 --- compiler/gameCopies.ts | 29 +++++++++++++++++++++++++++++ compiler/gameTestEntry.ts | 1 + compiler/gameTests.ts | 2 ++ compiler/gameVariables.ts | 19 +++++++++++++++++++ docs/v4-plan.md | 22 +++++++++++++++++++--- editor.ts | 33 ++++++++++++++++++++++++++++----- ko.ts | 4 ++++ scripts/build-fixture.mts | 16 +++++++--------- scripts/test-game.mts | 25 ++++++++++++++++--------- service.ts | 7 ++++--- tests/game-copies.test.ts | 26 +++++++++++++++++++++++++- vendor/scmSim.ts | 17 +++++++++++++++++ 12 files changed, 171 insertions(+), 30 deletions(-) create mode 100644 compiler/gameCopies.ts diff --git a/compiler/gameCopies.ts b/compiler/gameCopies.ts new file mode 100644 index 0000000..43b9eeb --- /dev/null +++ b/compiler/gameCopies.ts @@ -0,0 +1,29 @@ +/** + * A test of several players in the lifted game (TrigScript 4, A3): a copy of the game for each player, each with a + * report of its own, made into one result a test. Apart from the rest of the game's tests so that the editor takes + * this without the compiler. + */ +import type { TestReport, TestResult } from "./testing"; + +/** + * A test of several players, from each copy's report (its answer, in the order of the players): it passed when it + * passed at every computer; the first copy that failed says why, and at whose computer. A copy that could not run + * fails every test with what went wrong. + */ +export function mergeCopies(ids: readonly string[], copies: readonly { player: number; result: unknown; error: string | null }[]): TestResult[] { + return ids.map((id) => { + const each = copies.map((c) => { + const report = c.result as (TestReport & { error?: string }) | null; + const problem = c.error ?? report?.error ?? null; + const r = report?.results.find((x) => x.id === id); + return { player: c.player, r: problem || !r ? ({ id, status: "failed", message: problem ?? "the test did not run at this computer", printed: [], events: [], frames: 0, ms: 0 } as TestResult) : r }; + }); + const bad = each.find((e) => e.r.status === "failed"); + const first = each[0].r; + const ms = Math.max(...each.map((e) => e.r.ms)); + if (!bad) return { ...first, ms }; + // (A desync fails it alike at every computer, and says whose computer is whose itself.) + const same = each.every((e) => e.r.status === "failed" && e.r.message === bad.r.message); + return { ...bad.r, ms, message: same ? bad.r.message : `at P${bad.player + 1}'s computer: ${bad.r.message}` }; + }); +} diff --git a/compiler/gameTestEntry.ts b/compiler/gameTestEntry.ts index fad2101..499859d 100644 --- a/compiler/gameTestEntry.ts +++ b/compiler/gameTestEntry.ts @@ -3,5 +3,6 @@ * The plugin never loads it itself; it hands its text to the host. */ export { drive, runGameTests, type GameTestPayload } from "./gameTests"; +export { mergeCopies } from "./gameCopies"; export { desyncMessage, GameSim, readLocations, readUnitStats, type Desync, type GameCopy, type GameHandle } from "./gameSim"; export { programVariables, readVariableMap, type VariableMap } from "./gameVariables"; diff --git a/compiler/gameTests.ts b/compiler/gameTests.ts index 58c6fe3..e89f675 100644 --- a/compiler/gameTests.ts +++ b/compiler/gameTests.ts @@ -15,6 +15,7 @@ import { locate, runModules, type LinkedFile } from "./link"; import type { ScriptNames } from "./names"; import { Collector, createRuntime } from "./runtime"; import { isTestFile, listTests, runTests, TestRegistry, TESTING_NAMES, type TestReport, type TestResult } from "./testing"; +export { mergeCopies } from "./gameCopies"; import type { SimBounds } from "./world"; export interface GameTestPayload { @@ -56,3 +57,4 @@ export function runGameTests(game: GameHandle, payload: GameTestPayload): TestRe export function drive(game: GameHandle, payload: GameTestPayload): TestReport & { error?: string } { return runGameTests(game, payload); } + diff --git a/compiler/gameVariables.ts b/compiler/gameVariables.ts index 0680218..536b09e 100644 --- a/compiler/gameVariables.ts +++ b/compiler/gameVariables.ts @@ -138,3 +138,22 @@ export function programVariables(game: VariableReader, map: VariableMap, name: s }; return new Proxy({}, { get: (_, key) => (typeof key === "string" ? lookup(key) : undefined), has: (_, key) => typeof key === "string" && lookup(key) !== undefined }); } + +/** Where each trigger the build made sits in eudplib's payload, and the line it came from (the `triggerMap` setting). */ +export interface TriggerMap { + /** The payload's length in bytes: it ends where the strings section does. */ + payload: number; + /** Offset in the payload, conditions, actions, origin ("line:column", "file:line:column", "fn:", "eudplib"). */ + triggers: [number, number, number, string][]; +} + +/** The trigger map in a build's log (TRIGSCRIPT-MAP and its TRIGSCRIPT-ROWS); null when there is none or it is cut short. */ +export function readTriggerMap(lines: Iterable): TriggerMap | null { + let head: { payload: number; count: number } | null = null; + const triggers: TriggerMap["triggers"] = []; + for (const line of lines) { + if (line.startsWith("TRIGSCRIPT-MAP ")) head = JSON.parse(line.slice("TRIGSCRIPT-MAP ".length)); + else if (line.startsWith("TRIGSCRIPT-ROWS ")) triggers.push(...JSON.parse(line.slice("TRIGSCRIPT-ROWS ".length))); + } + return head && triggers.length === head.count ? { payload: head.payload, triggers } : null; +} diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 2b68948..95f31d5 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -375,7 +375,7 @@ A test can ask for it: `test("shop", { players: 4 }, …)`. The Testing view the To check first: UMS with several people on one instance (melee is measured, UMS is not). -**As built (2026-10-16, under Node; the editor's half is not done).** The check was already done: +**As built (2026-10-16, under Node and in the editor).** The check was already done: scm-sim's `tests/exact-net-ums` plays two people on one instance in UMS. Two parts of the design above changed. The option is a list (`{ players: [P1, P2] }`), because a test's inputs name the player anyway. And the hash is not the game's own. @@ -435,9 +435,25 @@ player anyway. And the hash is not the game's own. first pass, and both name the trigger. - A 3000-frame fight with a computer player stays in step with 2 and with 4 copies (8 s, 3.5 GB for four). +- **The editor.** "Run All Tests in the Game" runs a test of several players through the scm-sim + plugin's `testCopies`. The analysis page runs one copy each in workers of its own + (`web/copyWorker.ts`, sharing `src/exact/copy.ts` with Node), and the reports merge into one + mark (`compiler/gameCopies.ts`). The build is asked for its trigger map beside the variable + map, so a desync names lines. + - The copies need SharedArrayBuffer, so the page must be cross-origin isolated. COOP/COEP + cannot do it: the page is a frame inside the editor, isolation needs the top-level page too, + and COOP `same-origin` on the editor cuts scmjs.dev's sign-in popup off from the editor + (it posts back through `window.opener`). + - `Document-Isolation-Policy: isolate-and-credentialless` isolates the one document instead: + `/play/analyse.html`, sent by the site's worker (cloudflare-link-previews + `ISOLATED_PLAY_PAGES`) and by the dev server. Chromium 137 on; elsewhere, or without the + header, the tests are marked not run, and the mark says why. + - Measured in headless Chromium 153 (`web/smoke-copies.mjs`, then a built editor): + - the editor stays as it is; + - `postMessage`, OPFS and the moments BroadcastChannel cross the boundary; + - replay analysis, pathing and measure still pass under the header; + - 4/4 tests in the game from the Testing view, three of them as two copies (11 s). - **Not done.** - - The editor: the play page needs cross-origin isolation for SharedArrayBuffer in its workers, - so "Run All Tests in the Game" lists these tests as skipped. - Dwords that come apart later in a payload block already watched are not reported. - `sim.place` and the other actions between frames run at every copy, as a trigger for that player would. diff --git a/editor.ts b/editor.ts index 77105f6..c00b7f2 100644 --- a/editor.ts +++ b/editor.ts @@ -42,7 +42,8 @@ import { } from "./monaco"; import { renamedKeys, renamesInUse, replaceReferences, type Renamed } from "./refs"; import { FILE_NAME } from "./script"; -import { isMachineLine, readVariableMap } from "./compiler/gameVariables"; +import { isMachineLine, readTriggerMap, readVariableMap } from "./compiler/gameVariables"; +import { mergeCopies } from "./compiler/gameCopies"; import { isTestFile, type TestInfo, type TestReport, type TestResult, type TestRunOptions } from "./compiler/testing"; import { countTests, foldPlayers, idsUnder, marksOf, mergeReport, NO_TESTS, notesOf, playersLabel, stateOf, stateOfCounts, summary, testTree, warningsOf, type TestNode, type TestState, type TestStateName } from "./testState"; import { testName } from "./service"; @@ -727,7 +728,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa const differ = simulated && simulated.status !== "skipped" && game.status !== "skipped" && simulated.status !== game.status; return el("span", { className: differ ? "tsd-game tsd-game-differs" : "tsd-game", title: differ ? t("In the game: {status}. The simulator and the game disagree: the game is right, so the simulator is wrong here (or the test leans on something it does differently).", { status: game.status === "failed" ? game.message ?? t("failed") : t("passed") }) - : game.status === "failed" ? t("In the game: {message}", { message: game.message ?? t("failed") }) : t("In the game: passed") }, + : game.status === "failed" ? t("In the game: {message}", { message: game.message ?? t("failed") }) : game.status === "skipped" ? game.message ?? t("In the game: not run") : t("In the game: passed") }, stateIcon(state)); }; @@ -761,22 +762,44 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa const heard = svc.onBuild((e) => { if (e.kind === "log") logged.push(e.line); else if (e.kind === "done" && !logged.length) logged.push(...e.log.split("\n")); }); const file = await svc.withVariableMap(() => api.document.export({ format: "scx" })).finally(() => heard.dispose()); if (!file || cancelled) { if (!file) setStatus("error", t("No map is open.")); return; } - const variables = readVariableMap(logged); + const variables = readVariableMap(logged), triggers = readTriggerMap(logged); gametest ??= fetch(gametestUrl(svc.dist())).then((r) => { if (!r.ok) throw new Error(`gametest.js: ${r.status}`); return r.text(); }); const module = await gametest.catch((err) => { gametest = null; throw err; }); setStatus("busy", t("Running the tests in the game…")); - const report = await engine.test(new Uint8Array(await file.arrayBuffer()), module, { modules: a.compiled.modules, names, files: filter.files, ids: filter.ids, variables }) as TestReport & { error?: string }; + const bytes = new Uint8Array(await file.arrayBuffer()); + const report = await engine.test(bytes, module, { modules: a.compiled.modules, names, files: filter.files, ids: filter.ids, variables }) as TestReport & { error?: string }; if (cancelled) return; if (report.error) { setStatus("error", t("Not run in the game: {message}", { message: report.error })); return; } + // A test of several players (TrigScript 4, A3): a copy of the game for each player, kept in step, where the + // engine's page can run them; where it cannot, the test is marked as not run, and why. + const several = new Map(); + for (const x of tests.list) { + if (x.kind !== "test" || !x.players || (filter.files && !filter.files.includes(x.file)) || (filter.ids && !filter.ids.some((id) => x.id === id || x.id.startsWith(`${id} > `)))) continue; + const key = x.players.join(","), g = several.get(key) ?? { players: x.players, ids: [] }; + g.ids.push(x.id); + several.set(key, g); + } + const notRun: TestResult[] = []; + if (several.size && (!engine.testCopies || !status.copies)) { + const why = t("Not run: a test of several players runs a copy of the game for each, which needs memory shared between them. This browser does not allow that on the engine's page (Chrome and Edge from version 137 do)."); + for (const g of several.values()) for (const id of g.ids) notRun.push({ id, status: "skipped", message: why, printed: [], events: [], frames: 0, ms: 0 }); + } else for (const g of several.values()) { + setStatus("busy", t("Running a test of {n} players in the game: a copy of the game for each…", { n: g.players.length })); + const out = await engine.testCopies!(bytes, module, { modules: a.compiled.modules, names, ids: g.ids, variables }, { players: g.players, triggers }); + if (cancelled) return; + report.results.push(...mergeCopies(g.ids, out)); + } const ran = report.results.filter((x) => x.status !== "skipped"); const asked = new Set(ran.map((x) => x.id)); gameResults = new Map([...gameResults].filter(([id]) => !asked.has(id) && tests.list.some((x) => x.id === id))); for (const r of ran) gameResults.set(r.id, r); + for (const r of notRun) gameResults.set(r.id, r); const failed = ran.filter((x) => x.status === "failed"); const differ = ran.filter((x) => { const s = tests.results.get(x.id); return s && s.status !== "skipped" && s.status !== x.status; }); if (failed.length) { chosenTest = failed[0].id; shell.showPanel("tests"); } const said = t("In the game: {passed} of {n, plural, one {# test} other {# tests}} passed.", { passed: ran.length - failed.length, n: ran.length }); - setStatus(failed.length || differ.length ? "error" : "ok", differ.length ? `${said} ${t("The simulator disagrees on {n, plural, one {# test} other {# tests}}.", { n: differ.length })}` : said, NOTICE_MS); + const whole = notRun.length ? `${said} ${t("{n, plural, one {# test of several players was} other {# tests of several players were}} not run: this browser cannot run copies of the game.", { n: notRun.length })}` : said; + setStatus(failed.length || differ.length ? "error" : notRun.length ? "info" : "ok", differ.length ? `${whole} ${t("The simulator disagrees on {n, plural, one {# test} other {# tests}}.", { n: differ.length })}` : whole, NOTICE_MS); } catch (err) { setStatus("error", t("Not run in the game: {message}", { message: (err as Error).message })); } finally { diff --git a/ko.ts b/ko.ts index b49556b..a4a1ed1 100644 --- a/ko.ts +++ b/ko.ts @@ -275,6 +275,10 @@ export const KO: Record = { "passed": "통과", "In the game: {message}": "게임에서: {message}", "In the game: passed": "게임에서: 통과", + "Not run: a test of several players runs a copy of the game for each, which needs memory shared between them. This browser does not allow that on the engine's page (Chrome and Edge from version 137 do).": "실행하지 않음: 여러 플레이어의 테스트는 플레이어마다 게임을 하나씩 실행하며, 이 게임들은 메모리를 공유해야 합니다. 이 브라우저는 엔진 페이지에서 이를 허용하지 않습니다(Chrome과 Edge는 137 버전부터 허용합니다).", + "Running a test of {n} players in the game: a copy of the game for each…": "{n}명의 플레이어 테스트를 게임에서 실행하는 중: 플레이어마다 게임을 하나씩…", + "{n, plural, one {# test of several players was} other {# tests of several players were}} not run: this browser cannot run copies of the game.": "여러 플레이어의 테스트 {n}개를 실행하지 않았습니다: 이 브라우저는 게임을 여러 개 실행할 수 없습니다.", + "In the game: not run": "게임에서: 실행하지 않음", "Running tests in the game needs the scm-sim plugin (0.11 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….": "테스트를 게임에서 실행하려면 scmjs.dev에서 scm-sim 플러그인(0.11 이상)이 필요합니다. 플러그인 ▸ 플러그인 관리…에서 설치하거나 켜세요.", "Not run in the game: {message}": "게임에서 실행하지 못함: {message}", "the engine cannot run a game now": "엔진이 지금 게임을 실행할 수 없습니다", diff --git a/scripts/build-fixture.mts b/scripts/build-fixture.mts index 4fe39d4..7ea9498 100644 --- a/scripts/build-fixture.mts +++ b/scripts/build-fixture.mts @@ -4,7 +4,7 @@ * plugin-eudplib checkout's `scripts/build-map.mts`, write the built map. For probe maps * that get played. * - * [TRIGGER_MAP=1] [PEOPLE=0,1] npx tsx scripts/build-fixture.mts probes/spike.ts fixtures/eud/spike-eud.scx [map.scx] [../plugin-eudplib] + * [TRIGGER_MAP=1] [PEOPLE=0,1] [PREP_ONLY=1] npx tsx scripts/build-fixture.mts probes/spike.ts fixtures/eud/spike-eud.scx [map.scx] [../plugin-eudplib] * * The output lands in the ignored fixtures/ folder: it is built on a Blizzard map, which is never committed. * @@ -20,7 +20,7 @@ import ts from "typescript"; import { compileScript } from "../compiler/compiler"; import { serializeIr } from "../compiler/eud"; import { buildPlugins } from "../compiler/input"; -import { readVariableMap } from "../compiler/gameVariables"; +import { readTriggerMap, readVariableMap } from "../compiler/gameVariables"; import { resolveStrings } from "../script"; import { defaultScriptNames } from "../compiler/names"; import { defaultLib } from "../bundle/lib.mjs"; @@ -86,6 +86,8 @@ markDirty(scn, "TRIG", "UNIT"); const ir = serializeIr(r.ir, r.strings, r.input); const extras = loaded.archive ? await readExtras(loaded.archive, loaded.files) : new Map(); const withStrings = await saveMap(serializeScenario(scn), { extras, compress: "pkware", listfile: true }); +// PREP_ONLY=1: the prepared map as it is, not built: what the editor opens in a run end to end, and builds itself. +if (process.env.PREP_ONLY) { writeFileSync(resolve(outPath), withStrings); process.exit(0); } const dir = mkdtempSync(join(tmpdir(), "trigscript-fixture-")); const irPath = join(dir, "trigscript.json"); const pluginsPath = join(dir, "plugins.json"); @@ -113,13 +115,9 @@ const res = spawnSync("npx", ["tsx", join(eudplib, "scripts", "build-map.mts"), const fullLog = existsSync(logPath) ? readFileSync(logPath, "utf8") : res.stdout; rmSync(dir, { recursive: true, force: true }); const logLines = fullLog.split("\n"); -const mapLine = logLines.find((l) => l.startsWith("TRIGSCRIPT-MAP ")); -if (mapLine) { - const head = JSON.parse(mapLine.slice("TRIGSCRIPT-MAP ".length)) as { payload: number; count: number }; - const triggers = logLines.filter((l) => l.startsWith("TRIGSCRIPT-ROWS ")).flatMap((l) => JSON.parse(l.slice("TRIGSCRIPT-ROWS ".length)) as unknown[]); - if (triggers.length !== head.count) { console.error(`the map of triggers came out with ${triggers.length} of ${head.count} rows`); process.exit(1); } - writeFileSync(resolve(outPath).replace(/\.scx$/, ".triggers.json"), JSON.stringify({ payload: head.payload, triggers })); -} +const triggerMap = readTriggerMap(logLines); +if (triggerMap) writeFileSync(resolve(outPath).replace(/\.scx$/, ".triggers.json"), JSON.stringify(triggerMap)); +else if (logLines.some((l) => l.startsWith("TRIGSCRIPT-MAP "))) { console.error("the map of triggers came out cut short"); process.exit(1); } const variables = readVariableMap(logLines); if (variables) writeFileSync(resolve(outPath).replace(/\.scx$/, ".variables.json"), JSON.stringify(variables)); else if (process.env.VARIABLE_MAP && res.status === 0) { console.error("the build said nothing of where the variables are"); process.exit(1); } diff --git a/scripts/test-game.mts b/scripts/test-game.mts index fcb99af..f111ed6 100644 --- a/scripts/test-game.mts +++ b/scripts/test-game.mts @@ -8,7 +8,7 @@ * * Needs a scm-sim checkout beside this repository (or SCM_SIM_DIR) with its lifted game generated. */ -import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { spawnSync } from "node:child_process"; import { tmpdir } from "node:os"; import { basename, join, resolve } from "node:path"; @@ -16,6 +16,7 @@ import { pathToFileURL } from "node:url"; import ts from "typescript"; import { compileScript } from "../compiler/compiler"; import { readLocations, readUnitStats, type GameHandle } from "../compiler/gameSim"; +import { mergeCopies } from "../compiler/gameCopies"; import { defaultScriptNames } from "../compiler/names"; import type { TestReport, TestWorld } from "../compiler/testing"; import { defaultLib } from "../bundle/lib.mjs"; @@ -92,20 +93,26 @@ try { const { runCopies } = await from("src/exact/copiesNode.ts"); const triggers = JSON.parse(readFileSync(several.replace(/\.scx$/, ".triggers.json"), "utf8")); const severalVariables = JSON.parse(readFileSync(several.replace(/\.scx$/, ".variables.json"), "utf8")); + // TRIGSCRIPT_DUMP=: what the copies are handed, kept for a run in a browser (scm-sim's web/smoke-copies.mjs). + if (process.env.TRIGSCRIPT_DUMP) { + const to = resolve(process.env.TRIGSCRIPT_DUMP); + mkdirSync(to, { recursive: true }); + writeFileSync(join(to, "map.scx"), readFileSync(several)); + writeFileSync(join(to, "gametest.js"), readFileSync(bundle)); + writeFileSync(join(to, "request.json"), JSON.stringify({ groups: [...groups.values()], triggers, payload: { modules: compiled.modules, names, locations, variables: severalVariables } })); + } for (const g of groups.values()) { const out = await runCopies({ map: new Uint8Array(readFileSync(several)), seats: g.players.map((slot) => ({ slot, race: 1, name: `P${slot + 1}` })), driver: readFileSync(bundle, "utf8"), payload: { modules: compiled.modules, names, locations, variables: severalVariables, ids: g.ids }, triggers }) as { result: (TestReport & { error?: string }) | null; error: string | null; player: number }[]; for (const c of out) if (c.error || c.result?.error) { console.error(`P${c.player + 1}'s copy: ${c.error ?? c.result?.error}`); process.exit(1); } // A test passes when it passes at every computer; the first that failed says why. - for (const id of g.ids) { - const each = out.map((c) => ({ player: c.player, r: c.result!.results.find((x) => x.id === id)! })); - const bad = each.find((e) => e.r.status === "failed"); - const s = bySim.get(id); + for (const r of mergeCopies(g.ids, out)) { + const s = bySim.get(r.id); const mark = (st: string | undefined) => (st === "passed" ? "✓" : st === "failed" ? "✗" : st === "skipped" ? "–" : "?"); - if (bad) failed++; - console.log(`${mark(bad ? "failed" : "passed")} game ×${g.players.length} ${mark(s?.status)} simulator ${id.replace(/^.*?::/, "")} (${each[0].r.frames} frames, ${Math.max(...each.map((e) => e.r.ms))} ms)`); - if (bad) console.log(` at P${bad.player + 1}'s computer: ${bad.r.message}${bad.r.at ? ` — line ${bad.r.at.line}` : ""}`); + if (r.status === "failed") failed++; + console.log(`${mark(r.status)} game ×${g.players.length} ${mark(s?.status)} simulator ${r.id.replace(/^.*?::/, "")} (${r.frames} frames, ${r.ms} ms)`); + if (r.status === "failed") console.log(` in the game: ${r.message}${r.at ? ` — line ${r.at.line}` : ""}`); if (s?.status === "failed") console.log(` in the simulator: ${s.message}${s.at ? ` — line ${s.at.line}` : ""}`); - if (s && s.status !== (bad ? "failed" : "passed")) console.log(" the two disagree: one of them is wrong"); + if (s && s.status !== r.status) console.log(" the two disagree: one of them is wrong"); } } } diff --git a/service.ts b/service.ts index c8ee160..60a756e 100644 --- a/service.ts +++ b/service.ts @@ -307,7 +307,8 @@ export class ScriptService { if (!artifact.compiled.ok) throw new Error(firstFault(artifact.compiled.diagnostics)); if (guard) this.guardTests(artifact.compiled); const plugins = buildPlugins(artifact.compiled.input, "/work/files/trigscript.json"); - if (this.variableMaps) (plugins.trigscript as Record).variableMap = "1"; + // (And where each trigger is, for the lines of a desync between copies of the game, TrigScript 4's A3.) + if (this.variableMaps) Object.assign(plugins.trigscript as Record, { variableMap: "1", triggerMap: "1" }); return { // chatEvent and MSQC join in, around the lowering, when a program reads what the players do. plugins, @@ -317,8 +318,8 @@ export class ScriptService { } /** - * `run` with every build it starts asked to say where the programs' variables are — lines of the build's log - * (`compiler/gameVariables.ts`) — for tests in the lifted game to read them. + * `run` with every build it starts asked to say where the programs' variables and its triggers are — lines of the + * build's log (`compiler/gameVariables.ts`) — for tests in the lifted game to read them and name lines. */ async withVariableMap(run: () => Promise): Promise { this.variableMaps++; diff --git a/tests/game-copies.test.ts b/tests/game-copies.test.ts index c2cc23d..fb3ba92 100644 --- a/tests/game-copies.test.ts +++ b/tests/game-copies.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from "vitest"; import { desyncMessage, GameSim, variableNames, type Desync, type GameCopy, type GameHandle } from "../compiler/gameSim"; import { listTests, TestRegistry } from "../compiler/testing"; -import type { VariableMap } from "../compiler/gameVariables"; +import { readTriggerMap, type VariableMap } from "../compiler/gameVariables"; +import { mergeCopies } from "../compiler/gameCopies"; // TrigScript 4, A3: a test of several players, each at a copy of the game of their own (scm-sim's CopyHandle). const KEYS = 0x596a18; @@ -84,3 +85,26 @@ describe("a desync's words", () => { expect(desyncMessage({ frame: 9, apart: [9, 12], players: [0, 2], differences: [], wrote: [], readLocal: [] })).toBe("the players' games stopped agreeing: they were at different frames (9 at P1's computer, 12 at P3's computer)"); }); }); + +describe("a test of several players' result, from each copy's report", () => { + const report = (status: "passed" | "failed", message?: string) => ({ list: [], only: [], ms: 0, results: [{ id: "t", status, message, printed: [], events: [], frames: 30, ms: 5 }] }); + it("passes when it passed at every computer", () => { + expect(mergeCopies(["t"], [{ player: 0, result: report("passed"), error: null }, { player: 1, result: report("passed"), error: null }])[0]).toMatchObject({ status: "passed", frames: 30 }); + }); + it("fails with the first computer's reason, said to be that computer's when the others differ", () => { + expect(mergeCopies(["t"], [{ player: 0, result: report("passed"), error: null }, { player: 1, result: report("failed", "expected 1, got 0"), error: null }])[0]).toMatchObject({ status: "failed", message: "at P2's computer: expected 1, got 0" }); + expect(mergeCopies(["t"], [{ player: 0, result: report("failed", "split"), error: null }, { player: 1, result: report("failed", "split"), error: null }])[0].message).toBe("split"); + }); + it("fails every test of a copy that could not run", () => { + expect(mergeCopies(["t"], [{ player: 0, result: null, error: "P2 is not a slot the map opens to a person" }, { player: 1, result: report("passed"), error: null }])[0]).toMatchObject({ status: "failed", message: "at P1's computer: P2 is not a slot the map opens to a person" }); + }); +}); + +describe("the build's map of triggers", () => { + it("is read from its lines, and is none when cut short", () => { + const lines = ["TRIGSCRIPT-MAP {\"payload\":4096,\"count\":2}", "TRIGSCRIPT-ROWS [[0,1,2,\"3:1\"]]", "hello", "TRIGSCRIPT-ROWS [[2408,0,1,\"eudplib\"]]"]; + expect(readTriggerMap(lines)).toEqual({ payload: 4096, triggers: [[0, 1, 2, "3:1"], [2408, 0, 1, "eudplib"]] }); + expect(readTriggerMap(lines.slice(0, 2))).toBeNull(); + expect(readTriggerMap(["hello"])).toBeNull(); + }); +}); diff --git a/vendor/scmSim.ts b/vendor/scmSim.ts index 6a9a41c..aab6e4c 100644 --- a/vendor/scmSim.ts +++ b/vendor/scmSim.ts @@ -12,6 +12,17 @@ export interface ScmSimStatus { reason: string | null; /** The version of the test request the page answers; null or absent: it answers none. */ tests?: number | null; + /** The version of the request for a test of several players; null or absent: the page (or the browser) cannot run copies. */ + copies?: number | null; +} + +/** One copy's answer to `testCopies`. */ +export interface TestCopyResult { + result: unknown; + error: string | null; + desync: unknown; + frame: number; + player: number; } export interface ScmSimEngine { @@ -25,4 +36,10 @@ export interface ScmSimEngine { * module returned. Absent from the plugin before 0.11.0. */ test?(scenario: Uint8Array, module: string, payload: unknown, options?: { signal?: AbortSignal; onProgress?: (p: { stage: "starting" }) => void }): Promise; + /** + * A test of several players (TrigScript 4, A3): a copy of the game for each of `players`, kept in step, each + * running the module's `drive(game, payload)`; `triggers` the build's map of them, for the lines of a desync. + * Resolves with each copy's answer, in the order of `players`. Absent from the plugin before 0.11.0. + */ + testCopies?(scenario: Uint8Array, module: string, payload: unknown, options: { players: number[]; triggers?: { payload: number; triggers: [number, number, number, string][] } | null; signal?: AbortSignal; onProgress?: (p: { stage: "starting" }) => void }): Promise; } From e71cf040bcdb5aa1ebc71e138cc45a0e9c7a5b56 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 12:28:25 -0600 Subject: [PATCH 011/238] TrigScript 4, A4: what a frame costs, by line, from the game Each test run in the game reports the conditions and actions its frames ran, by frame and by line (compiler/gameCost.ts, through the build's map of triggers); eudplib's start and its work at the top of each frame are kept apart. The editor puts the count beside a line that ran 1,000 or more in a frame, and Test Results shows a graph and the costliest lines. probes/frame-cost.ts is the dial for Remastered: a ladder from 1 thousand to 1 million a frame. Co-Authored-By: Claude Opus 5.5 --- compiler/gameCost.ts | 144 ++++++++++++++++++++++++++++++++++++++++ compiler/gameSim.ts | 11 ++- compiler/gameTests.ts | 10 ++- compiler/testing.ts | 12 +++- editor.ts | 73 ++++++++++++++++++-- ko.ts | 8 +++ probes/frame-cost.ts | 47 +++++++++++++ scripts/game-check.mts | 4 +- scripts/test-game.mts | 21 ++++-- tests/game-cost.test.ts | 90 +++++++++++++++++++++++++ 10 files changed, 405 insertions(+), 15 deletions(-) create mode 100644 compiler/gameCost.ts create mode 100644 probes/frame-cost.ts create mode 100644 tests/game-cost.test.ts diff --git a/compiler/gameCost.ts b/compiler/gameCost.ts new file mode 100644 index 0000000..196aa6b --- /dev/null +++ b/compiler/gameCost.ts @@ -0,0 +1,144 @@ +/** + * What a frame costs (TrigScript 4, A4): the conditions and actions the game ran in each frame of a test, and by + * line of the script, read from the lifted game's count (scm-sim's `src/exact/cost.ts`) through the build's map of + * triggers. Lifted wall time is not the game's, so the count is the measure; `probes/frame-cost.ts` is the dial that + * says what count Remastered feels. + * + * A record is put down to a line by the trigger it is in: the build's map says which statement made each trigger. + * eudplib's variables, its functions and the triggers no statement made are counted under the line that ran just + * before them in the frame, since a line's work is its own triggers and the variables they go through (measured on a + * loop: `x += 1` is 7 actions of its own and 2 more in eudplib's variables). What eudplib runs before any line in a + * frame (its start, once, and its work at the top of every frame) and the map's triggers outside the payload are + * counted under no line: `TestCost.other`. + */ +import { ENTRY_FILE } from "./compiler"; +import type { TriggerMap } from "./gameVariables"; + +/** What the host counted (scm-sim's `CostTap`, by shape: TrigScript never imports the engine). */ +export interface GameCostTap { + /** + * A record's address → the group it is counted under: a group's number, -1 for none, or -2 (`CARRY`) for "the + * group counted just before it in the frame, or none when it is the first". + */ + group: ((record: number) => number) | null; + report(from?: number): { + from: number; + conditions: number[]; + actions: number[]; + /** Of those, the records under no group. */ + ungrouped: number[]; + groups: Map; + }; +} + +/** A line's cost over a test's run. */ +export interface LineCost { + file: string; + line: number; + /** Conditions and actions over the run. */ + total: number; + /** The most in one frame, and the first frame it was that many. */ + peak: number; + peakFrame: number; + /** Frames the line ran in. */ + frames: number; +} + +/** What a test's run in the game cost. */ +export interface TestCost { + /** The frame `perFrame` starts at. */ + from: number; + /** Conditions and actions a frame that the script's build ran: its lines, eudplib's variables and functions. */ + perFrame: number[]; + /** + * What the map's other triggers ran a frame: eudplib's start (once, by frame 2: tens of thousands on any map) and + * any trigger the map has besides the build's. Kept apart so that the script's own peak is not that start. + */ + other: number[]; + /** The most of `perFrame`, and the first frame it was that many. */ + peak: number; + peakFrame: number; + /** By line, the costliest first. */ + lines: LineCost[]; +} + +/** What `GameCostTap.group` answers for a record counted under the record before it (scm-sim's `CARRY`). */ +const CARRY = -2; + +/** How records become lines, for one build: set on the host's tap once, read after every test. */ +export class CostLines { + private readonly at: { file: string; line: number }[] = []; + private readonly ids = new Map(); + private readonly byRecord = new Map(); + private readonly payloadStart: number; + private readonly payloadEnd: number; + + /** `payloadEnd`: where eudplib's payload ends in the game's memory (the host says; the end of `STRx`). */ + constructor(map: TriggerMap, payloadEnd: number) { + this.payloadEnd = payloadEnd; + this.payloadStart = payloadEnd - map.payload; + for (const [offset, conditions, actions, origin] of map.triggers) { + const where = lineOf(origin); + const g = where ? this.idOf(where) : CARRY; + const at = this.payloadStart + offset; + for (let i = 0; i < conditions; i++) this.byRecord.set(at + 8 + 20 * i, g); + for (let i = 0; i < actions; i++) this.byRecord.set(at + 8 + 320 + 32 * i, g); + } + } + + private idOf(where: { file: string; line: number }): number { + const key = `${where.file}:${where.line}`; + let id = this.ids.get(key); + if (id === undefined) { id = this.at.length; this.at.push(where); this.ids.set(key, id); } + return id; + } + + /** For the host's tap. */ + readonly group = (record: number): number => { + const g = this.byRecord.get(record); + if (g !== undefined) return g; + return record >= this.payloadStart && record < this.payloadEnd ? CARRY : -1; + }; + + /** The tap's count since the game was put back, as a test's cost. */ + read(tap: GameCostTap): TestCost { + const r = tap.report(0); + const perFrame = r.conditions.map((c, i) => c + r.actions[i] - r.ungrouped[i]); + let peak = 0, peakFrame = r.from; + perFrame.forEach((n, i) => { if (n > peak) { peak = n; peakFrame = r.from + i; } }); + const lines: LineCost[] = []; + for (const [g, k] of r.groups) { + const where = this.at[g]; + if (!where) continue; + lines.push({ file: where.file, line: where.line, total: k.conditions + k.actions, peak: k.peak, peakFrame: k.peakFrame, frames: k.frames }); + } + lines.sort((a, b) => b.peak - a.peak || b.total - a.total); + return { from: r.from, perFrame, other: [...r.ungrouped], peak, peakFrame, lines }; + } +} + +/** "main.ts:12:5" or "12:5" → the file and line; null for eudplib's ("fn:…", "eudplib", ""). */ +function lineOf(origin: string): { file: string; line: number } | null { + const m = /^(?:(.*):)?(\d+):\d+$/.exec(origin); + return m ? { file: m[1] || ENTRY_FILE, line: Number(m[2]) } : null; +} + +/** One number a frame, short: 640, 12.3k, 1.0M. */ +export function shortCount(n: number): string { + if (n < 1000) return String(n); + if (n < 1e6) return `${(n / 1000).toFixed(n < 1e4 ? 1 : 0)}k`; + return `${(n / 1e6).toFixed(1)}M`; +} + +/** + * Every test's cost folded into one per line: the most a frame the line ran in any test, and the test it was in. + * What the editor puts beside each line after a run in the game. + */ +export function costByLine(costs: { id: string; cost: TestCost }[]): (LineCost & { test: string })[] { + const out = new Map(); + for (const { id, cost } of costs) for (const l of cost.lines) { + const key = `${l.file}:${l.line}`, was = out.get(key); + if (!was || l.peak > was.peak) out.set(key, { ...l, test: id }); + } + return [...out.values()]; +} diff --git a/compiler/gameSim.ts b/compiler/gameSim.ts index e0cbdb2..f0680d3 100644 --- a/compiler/gameSim.ts +++ b/compiler/gameSim.ts @@ -16,6 +16,7 @@ import { ActionType } from "../vendor/triggers"; import type { At } from "./ir"; import { msqcKey } from "./input"; import { programVariables, type VariableMap } from "./gameVariables"; +import type { CostLines, GameCostTap, TestCost } from "./gameCost"; import { ExpectError, WORLD, type TestEvent, type TestWorldLike } from "./testing"; import type { SimBounds, SimUnit } from "./world"; @@ -32,6 +33,10 @@ export interface GameHandle { chat(text: string): void; call(address: number, registers: Record, args?: number[]): number; reset(): void; + /** What each frame of the map's triggers ran (TrigScript 4, A4), when the host counts it; `reset()` clears it. */ + readonly cost?: GameCostTap; + /** Where eudplib's payload ends in the game's memory, when the host knows: with the build's map of triggers, each record's line. */ + readonly payloadEnd?: number; /** When the game is one of several copies kept in step, a copy for each of a test's players (scm-sim's `CopyHandle`). */ readonly copy?: GameCopy; } @@ -209,6 +214,7 @@ export class GameSim implements TestWorldLike { private readonly game: GameHandle; private readonly known: Record; private readonly variables: VariableMap | null; + private readonly costLines: CostLines | null; /** * `locations`: the map's locations as the script means them. eudplib keeps them scrambled in the game until its @@ -216,8 +222,9 @@ export class GameSim implements TestWorldLike { * (`readLocations`); from then on the game's own are read. `variables`: where the build put each program's * variables, for `program()`. */ - constructor(game: GameHandle, options: { locations?: Record; variables?: VariableMap | null; test?: { id: string; players?: number[] } } = {}) { + constructor(game: GameHandle, options: { locations?: Record; variables?: VariableMap | null; test?: { id: string; players?: number[] }; costLines?: CostLines | null } = {}) { this.game = game; + this.costLines = options.costLines ?? null; this.known = options.locations ?? {}; this.variables = options.variables ?? null; if (game.copy) { @@ -255,6 +262,8 @@ export class GameSim implements TestWorldLike { get frame(): number { return this.game.frame; } seed(_n: number): this { return this; } + /** What the test's frames cost in the game, by line; null when the host does not count it or the build gave no map of triggers. */ + cost(): TestCost | null { return this.costLines && this.game.cost ? this.costLines.read(this.game.cost) : null; } /* ── time ── */ diff --git a/compiler/gameTests.ts b/compiler/gameTests.ts index e89f675..7da6c3b 100644 --- a/compiler/gameTests.ts +++ b/compiler/gameTests.ts @@ -10,7 +10,8 @@ import { ENTRY_FILE } from "./compiler"; import { MODULE_NAME } from "./api"; import { GameSim, readLocations, type GameHandle } from "./gameSim"; -import type { VariableMap } from "./gameVariables"; +import type { TriggerMap, VariableMap } from "./gameVariables"; +import { CostLines } from "./gameCost"; import { locate, runModules, type LinkedFile } from "./link"; import type { ScriptNames } from "./names"; import { Collector, createRuntime } from "./runtime"; @@ -28,6 +29,8 @@ export interface GameTestPayload { locations?: Record; /** Where the build put each program's variables (`readVariableMap` of its log), for `sim.program()`. */ variables?: VariableMap | null; + /** The build's map of triggers (`readTriggerMap` of its log): with it, what each test's frames cost is read by line. */ + triggers?: TriggerMap | null; } export function runGameTests(game: GameHandle, payload: GameTestPayload): TestReport & { error?: string } { @@ -45,7 +48,10 @@ export function runGameTests(game: GameHandle, payload: GameTestPayload): TestRe // A test of several players runs only where the game is one of several copies, one a player (A3); one game says so. const several = listTests(registry).list.filter((t) => t.kind === "test" && t.players); const ids = game.copy ? payload.ids : payload.ids ?? listTests(registry).list.filter((t) => t.kind === "test" && !t.players).map((t) => t.id); - const report = runTests(registry, { triggers: [], sources: [], ir: [], strings: [], locate: where }, { files: payload.files, ids, makeWorld: (_as, test) => new GameSim(game, { locations, variables: payload.variables, test }) }); + // (What a frame costs, where the host counts it and knows where the payload is.) + const costLines = payload.triggers && game.cost && game.payloadEnd ? new CostLines(payload.triggers, game.payloadEnd) : null; + if (costLines && game.cost) game.cost.group = costLines.group; + const report = runTests(registry, { triggers: [], sources: [], ir: [], strings: [], locate: where }, { files: payload.files, ids, makeWorld: (_as, test) => new GameSim(game, { locations, variables: payload.variables, test, costLines }) }); if (!game.copy && !payload.ids) { const held: TestResult[] = several.filter((t) => !payload.files || payload.files.includes(t.file)).map((t) => ({ id: t.id, status: "skipped", message: `a test of ${t.players!.length} players runs as a copy of the game for each`, printed: [], events: [], frames: 0, ms: 0 })); report.results.push(...held); diff --git a/compiler/testing.ts b/compiler/testing.ts index 80ce283..04d0162 100644 --- a/compiler/testing.ts +++ b/compiler/testing.ts @@ -16,6 +16,7 @@ */ import { ActionType, type ActionRecord, type TriggerRecord } from "../vendor/triggers"; import { actionDef } from "../vendor/triggerDefs"; +import type { TestCost } from "./gameCost"; import type { At, Program } from "./ir"; import type { ScriptString } from "./runtime"; import { Simulation } from "./simulate"; @@ -85,6 +86,8 @@ export interface TestResult { events: TestEvent[]; frames: number; ms: number; + /** In the lifted game: what the test's frames cost, by frame and by line (TrigScript 4, A4). */ + cost?: TestCost; } export interface TestReport { @@ -234,6 +237,8 @@ export const isWorld = (v: unknown): v is TestWorldLike => typeof v === "object" /** What a test can ask of its world, whichever it was handed. */ export interface TestWorldLike { readonly [WORLD]: true; + /** What the test's frames cost, where the world counts it (the lifted game's). */ + cost?(): TestCost | null; faultsExpected: boolean; readonly frame: number; seed(n: number): this; @@ -536,11 +541,16 @@ export function runTests(registry: TestRegistry, ctx: TestContext, options: Test return { list, results, only, ms: Date.now() - started }; } +function costOf(sim: TestWorldLike | null): { cost?: TestCost } { + const cost = sim?.cost?.(); + return cost ? { cost } : {}; +} + function runOne(id: string, test: Case, chain: Suite[], ctx: TestContext, world: TestWorld, makeWorld?: TestRunOptions["makeWorld"]): TestResult { const began = Date.now(); let sim: TestWorldLike | null = null; const done = (extra: Partial): TestResult => ({ - id, status: "passed", printed: sim?.printed() ?? [], events: (sim?.log((t) => ctx.sources[t] ?? null) ?? []).slice(0, 500), frames: sim?.frame ?? 0, ms: Date.now() - began, ...extra, + id, status: "passed", printed: sim?.printed() ?? [], events: (sim?.log((t) => ctx.sources[t] ?? null) ?? []).slice(0, 500), frames: sim?.frame ?? 0, ms: Date.now() - began, ...costOf(sim), ...extra, }); try { sim = makeWorld ? makeWorld(test.as, { id, players: test.players }) : new TestSim(ctx.triggers, ctx.ir, ctx.strings, world, test.as); diff --git a/editor.ts b/editor.ts index c00b7f2..8c95929 100644 --- a/editor.ts +++ b/editor.ts @@ -28,7 +28,7 @@ */ import type { PluginApi } from "@scm-js/plugin-api"; import { CompileSuperseded, gametestUrl, retainCompileWorker } from "./compile"; -import { ENTRY_FILE, normalizePath, type CompileResult, type ScriptDiagnostic, type ScriptFiles, type TriggerSource } from "./compiler/compiler"; +import { ENTRY_FILE, normalizePath, type CompileResult, type LineHint, type ScriptDiagnostic, type ScriptFiles, type TriggerSource } from "./compiler/compiler"; import { HEAP_CELLS, HEAP_CELLS_MAX, HEAP_CELLS_MIN, STACK_CELLS_MAX, STACK_DEPTH, STACK_DEPTH_MAX, STACK_DEPTH_MIN, heapCells, stackDepth } from "./compiler/ir"; import { largestFrame } from "./compiler/recursion"; import { entryFor } from "./compiler/names"; @@ -44,6 +44,7 @@ import { renamedKeys, renamesInUse, replaceReferences, type Renamed } from "./re import { FILE_NAME } from "./script"; import { isMachineLine, readTriggerMap, readVariableMap } from "./compiler/gameVariables"; import { mergeCopies } from "./compiler/gameCopies"; +import { costByLine, shortCount, type TestCost } from "./compiler/gameCost"; import { isTestFile, type TestInfo, type TestReport, type TestResult, type TestRunOptions } from "./compiler/testing"; import { countTests, foldPlayers, idsUnder, marksOf, mergeReport, NO_TESTS, notesOf, playersLabel, stateOf, stateOfCounts, summary, testTree, warningsOf, type TestNode, type TestState, type TestStateName } from "./testState"; import { testName } from "./service"; @@ -123,6 +124,10 @@ const STATUS_MS = 10_000; const NOTICE_MS = 8_000; /** When running every test takes longer than this, only the open file's run by themselves after a compile. */ const TESTS_SLOW_MS = 2_000; +/** A line that ran this many conditions and actions in one frame of a test in the game says so beside it. */ +const COST_HINT = 1_000; +/** The costliest lines a test's results name. */ +const COST_LINES = 3; /** The key the shortcuts are written with. */ const MOD = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "Cmd" : "Ctrl"; @@ -164,6 +169,9 @@ const STYLE = `${SHELL_STYLE} .${TEST_MARK_CLASS}-failed::before { content: "\\ea87"; } .${TEST_MARK_CLASS}-skipped::before { content: "\\eabd"; } .${TEST_MARK_CLASS}-note { color: var(--danger); font-style: italic; opacity: 0.9; } +.tsd .tsd-list li.tsd-cost { align-items: center; } +.tsd .tsd-cost-graph { flex: 1 1 auto; min-width: 0; max-width: 480px; height: 36px; } +.tsd .tsd-cost-graph path { fill: var(--accent); fill-opacity: 0.35; stroke: var(--accent); stroke-width: 1px; vector-effect: non-scaling-stroke; } .${BUILD_TIME_CLASS} { text-decoration: underline dotted rgba(153, 162, 179, 0.55); text-underline-offset: 3px; } `; @@ -442,6 +450,11 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa * kept until the next run in the game. The game is the judge: where the two differ, the simulator is wrong. */ let gameResults = new Map(); + /** + * The files as they were when the game counted what each line costs (TrigScript 4, A4): a line's count is shown + * beside it only while its file is still that text, since an edit moves the lines. + */ + let costFiles: ScriptFiles | null = null; /** The text of `dist/gametest.js`, fetched once. */ let gametest: Promise | null = null; /** The files with a tab, in the order they were opened. */ @@ -767,7 +780,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa const module = await gametest.catch((err) => { gametest = null; throw err; }); setStatus("busy", t("Running the tests in the game…")); const bytes = new Uint8Array(await file.arrayBuffer()); - const report = await engine.test(bytes, module, { modules: a.compiled.modules, names, files: filter.files, ids: filter.ids, variables }) as TestReport & { error?: string }; + const report = await engine.test(bytes, module, { modules: a.compiled.modules, names, files: filter.files, ids: filter.ids, variables, triggers }) as TestReport & { error?: string }; if (cancelled) return; if (report.error) { setStatus("error", t("Not run in the game: {message}", { message: report.error })); return; } // A test of several players (TrigScript 4, A3): a copy of the game for each player, kept in step, where the @@ -785,7 +798,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa for (const g of several.values()) for (const id of g.ids) notRun.push({ id, status: "skipped", message: why, printed: [], events: [], frames: 0, ms: 0 }); } else for (const g of several.values()) { setStatus("busy", t("Running a test of {n} players in the game: a copy of the game for each…", { n: g.players.length })); - const out = await engine.testCopies!(bytes, module, { modules: a.compiled.modules, names, ids: g.ids, variables }, { players: g.players, triggers }); + const out = await engine.testCopies!(bytes, module, { modules: a.compiled.modules, names, ids: g.ids, variables, triggers }, { players: g.players, triggers }); if (cancelled) return; report.results.push(...mergeCopies(g.ids, out)); } @@ -794,6 +807,8 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa gameResults = new Map([...gameResults].filter(([id]) => !asked.has(id) && tests.list.some((x) => x.id === id))); for (const r of ran) gameResults.set(r.id, r); for (const r of notRun) gameResults.set(r.id, r); + costFiles = { ...files }; + refreshLineHints(); const failed = ran.filter((x) => x.status === "failed"); const differ = ran.filter((x) => { const s = tests.results.get(x.id); return s && s.status !== "skipped" && s.status !== x.status; }); if (failed.length) { chosenTest = failed[0].id; shell.showPanel("tests"); } @@ -808,6 +823,41 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa } }; + /** + * What a frame costs, beside the line (TrigScript 4, A4): after a run in the game, each line that ran at least + * COST_HINT conditions and actions in one frame of some test says how many at the most, while its file is as it + * was then. + */ + const costHints = (): LineHint[] => { + if (!costFiles) return []; + const seen = costFiles; + const costs = [...gameResults.values()].flatMap((r) => (r.cost ? [{ id: r.id, cost: r.cost }] : [])); + return costByLine(costs) + .filter((l) => l.peak >= COST_HINT && seen[normalizePath(l.file)] === files[normalizePath(l.file)]) + .map((l) => ({ + file: l.file, line: l.line, + label: t("{n} a frame", { n: shortCount(l.peak) }), + note: t("In the game, this line ran {n} conditions and actions in one frame at the most: frame {frame} of the test “{test}”, which it ran in for {frames, plural, one {# frame} other {# frames}}. A trigger costs the game by its conditions and actions, so the count is the measure.", { n: l.peak.toLocaleString(), frame: l.peakFrame, test: testName({ id: l.test }), frames: l.frames }), + })); + }; + + /** The test's conditions and actions a frame in the game, as a small graph; eudplib's own work is not in it. */ + const costGraph = (cost: TestCost): Element => { + const NS = "http://www.w3.org/2000/svg", n = Math.max(cost.perFrame.length, 2), top = Math.max(cost.peak, 1); + const svg = document.createElementNS(NS, "svg"); + svg.setAttribute("class", "tsd-cost-graph"); + svg.setAttribute("viewBox", `0 0 ${n - 1} 100`); + svg.setAttribute("preserveAspectRatio", "none"); + const points = cost.perFrame.map((v, i) => `${i},${(100 - (v / top) * 96).toFixed(2)}`); + const area = document.createElementNS(NS, "path"); + area.setAttribute("d", `M0,100 L${points.join(" L")} L${cost.perFrame.length - 1},100 Z`); + svg.append(area); + const title = document.createElementNS(NS, "title"); + title.textContent = t("Conditions and actions a frame, frame {from} to {to}: {peak} at the most, at frame {frame}", { from: cost.from, to: cost.from + cost.perFrame.length - 1, peak: cost.peak.toLocaleString(), frame: cost.peakFrame }); + svg.append(title); + return svg; + }; + /** Test Results: the chosen test's message, what it printed, and what happened by frame, each a link to its line. */ const renderResults = () => { const failed = tests.list.filter((x) => x.kind === "test" && tests.results.get(x.id)?.status === "failed"); @@ -836,6 +886,21 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa el("span", { className: "msg" }, game.status === "failed" ? game.message ?? t("failed") : t("passed")), el("span", { className: "where" }, game.status === "failed" ? where({ file: gat.file, line: gat.line }) : t("{frames, plural, one {# frame} other {# frames}} · {ms} ms", { frames: game.frames, ms: game.ms })))); if (game.status === "failed" && game.actual !== undefined) list.append(el("li", { className: "tsd-plain" }, el("span", { className: "frame" }, t("got")), el("span", { className: "msg" }, game.actual))); + // (What its frames cost in the game, and the lines that cost the most.) + const cost = game.cost; + if (cost) { + const ran = cost.perFrame.filter((x) => x > 0), average = ran.length ? Math.round(ran.reduce((a, b) => a + b, 0) / ran.length) : 0; + const other = cost.other.slice(3), runtime = other.length ? Math.round(other.reduce((a, b) => a + b, 0) / other.length) : 0; + list.append(el("li", { className: "tsd-plain", title: t("Counted in the lifted game: the script's own triggers and the variables they go through. eudplib's work at the top of each frame is apart: {n} a frame.", { n: runtime.toLocaleString() }) }, + el("span", { className: "frame" }, t("cost")), + el("span", { className: "msg" }, t("{average} conditions and actions a frame on average, {peak} at the most (frame {frame})", { average: average.toLocaleString(), peak: cost.peak.toLocaleString(), frame: cost.peakFrame })))); + if (cost.perFrame.length > 1) list.append(el("li", { className: "tsd-plain tsd-cost" }, el("span", { className: "frame" }), costGraph(cost))); + for (const l of cost.lines.slice(0, COST_LINES)) { + list.append(el("li", { title: t("Go to the line"), onClick: () => goTo(l.file, l.line) }, + el("span", { className: "frame" }), el("span", { className: "msg note" }, t("{peak} a frame at the most (frame {frame}), in {frames, plural, one {# frame} other {# frames}}", { peak: l.peak.toLocaleString(), frame: l.peakFrame, frames: l.frames })), + el("span", { className: "where" }, where({ file: l.file, line: l.line })))); + } + } } // The same line several times over — to each of eight players, say — is one row that says how often. const printed: { line: string; times: number }[] = []; @@ -1615,7 +1680,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa monaco = m; if (generated) setDeclarations(m, generated.decls); setHoverVariables(m, () => result?.variables ?? []); - setLineHints(m, () => result?.hints ?? []); + setLineHints(m, () => [...(result?.hints ?? []), ...costHints()]); setMapRefs(m, mapRefs); // Uncover first: `done` puts the host back in its own place, and Monaco measures it where it lands. loadingCover.done(); diff --git a/ko.ts b/ko.ts index a4a1ed1..5a4ca72 100644 --- a/ko.ts +++ b/ko.ts @@ -290,4 +290,12 @@ export const KO: Record = { "Go to where it failed in the game": "게임에서 실패한 곳으로 이동", "in the game": "게임에서", "Run All Tests in the Game": "게임에서 모든 테스트 실행", + "{n} a frame": "프레임당 {n}", + "In the game, this line ran {n} conditions and actions in one frame at the most: frame {frame} of the test “{test}”, which it ran in for {frames, plural, one {# frame} other {# frames}}. A trigger costs the game by its conditions and actions, so the count is the measure.": "게임에서 이 줄은 한 프레임에 조건과 액션을 최대 {n}개 실행했습니다: 테스트 “{test}”의 {frame}번째 프레임이며, 그 테스트에서 이 줄이 실행된 프레임은 {frames}개입니다. 트리거가 게임에 드는 비용은 조건과 액션의 수로 정해지므로, 이 수가 기준입니다.", + "Conditions and actions a frame, frame {from} to {to}: {peak} at the most, at frame {frame}": "프레임당 조건과 액션, {from}–{to}번째 프레임: {frame}번째 프레임에서 최대 {peak}개", + "Counted in the lifted game: the script's own triggers and the variables they go through. eudplib's work at the top of each frame is apart: {n} a frame.": "리프트한 게임에서 센 수입니다: 스크립트 자신의 트리거와 그 트리거가 거치는 변수. 매 프레임 처음에 eudplib가 하는 일은 따로입니다: 프레임당 {n}개.", + "cost": "비용", + "{average} conditions and actions a frame on average, {peak} at the most (frame {frame})": "프레임당 조건과 액션 평균 {average}개, 최대 {peak}개 ({frame}번째 프레임)", + "Go to the line": "그 줄로 이동", + "{peak} a frame at the most (frame {frame}), in {frames, plural, one {# frame} other {# frames}}": "프레임당 최대 {peak}개 ({frame}번째 프레임), {frames}개 프레임에서 실행", }; diff --git a/probes/frame-cost.ts b/probes/frame-cost.ts new file mode 100644 index 0000000..aafcfa6 --- /dev/null +++ b/probes/frame-cost.ts @@ -0,0 +1,47 @@ +// TrigScript 4, A4's probe: the dial. How much trigger work a frame Remastered takes before the game stutters. +// You are Player 1 with whatever the base map gives you; nothing here needs a second player. Play it alone, at +// Fastest, and keep something moving (a unit walking back and forth, or scroll the screen) so a stutter shows. +// +// The map climbs a ladder by itself: every STEP seconds of game time it does more work every frame, and says so: +// "step 4: about 10 thousand conditions and actions a frame" +// The count is the lifted game's: each step's loop was measured there (scripts/game-check.mts prints it), at about +// 39 conditions and actions a turn of the loop. Elapsed time in the game slows when the frames do, so a step that +// lags also lasts longer on the wall clock. +// +// W one step up Q one step down E stop climbing / climb again +// +// WHAT TO NOTE: the first step at which the game stutters (units jerk, the screen hitches), the first at which it +// is plainly slow (a game second clearly longer than a real one), and, if it gets there, the step at which it +// freezes or drops. Say which Remastered (patch number) and the computer it ran on. +// +// Built with `npx tsx scripts/build-fixture.mts probes/frame-cost.ts fixtures/eud/frame-cost.scx`. +import { program, displayText, sleep, seconds, frames, keyPressed, P1 } from "trigscript"; + +const STEP = 10; + +program(() => { + // Turns of the loop for each step, and what that comes to a frame in thousands (39 a turn, measured). + const turns = [0, 26, 51, 128, 256, 513, 1282, 2564, 5128, 12820, 25641]; + const thousands = [0, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]; + let step = 0; + let climbing = true; + let next = 0; + let x = 0; + let shown = -1; + let frame = 0; + while (true) { + frame += 1; + if (keyPressed(P1, "W") && step < 10) { step += 1; next = frame + STEP * 24; } + if (keyPressed(P1, "Q") && step > 0) { step -= 1; next = frame + STEP * 24; } + if (keyPressed(P1, "E")) { climbing = !climbing; displayText(climbing ? "climbing again" : "holding at this step"); next = frame + STEP * 24; } + if (climbing && frame >= next && step < 10) { if (next > 0) step += 1; next = frame + STEP * 24; } + if (step != shown) { + shown = step; + if (step == 0) displayText("step 0: nothing a frame — W up, Q down, E holds"); + else displayText(`step ${step}: about ${thousands[step]} thousand conditions and actions a frame`); + } + const n = turns[step]; + for (let i = 0; i < n; i++) { x += 1; } + sleep(frames(1)); + } +}, { owner: P1 }); diff --git a/scripts/game-check.mts b/scripts/game-check.mts index 14be10e..7478b8d 100644 --- a/scripts/game-check.mts +++ b/scripts/game-check.mts @@ -4,7 +4,7 @@ * at it by a scm-sim checkout's scripts/exact-eud.ts (its lifted game, EUD let through, nothing drawn), and every * dword the run read or wrote checked against data/allowlist.json, line by line. * - * npx tsx scripts/game-check.mts [--frames=N] [--allowlist=path] [map.scx] + * npx tsx scripts/game-check.mts [--frames=N] [--allowlist=path] [--cost=out.json] [map.scx] * * A built map is taken as it is, with its .triggers.json beside it when there is one. Needs a scm-sim * checkout beside this repository (or SCM_SIM_DIR) with its lifted game generated; says so when it is not there. @@ -34,7 +34,7 @@ try { if (built.status !== 0) { console.error(built.stdout.split("\n").slice(-20).join("\n"), built.stderr); process.exit(1); } } const tracePath = join(dir, "trace.json"); - const run = spawnSync("node", ["--experimental-strip-types", join(sim, "scripts", "exact-eud.ts"), map, `--frames=${flag("frames") ?? 3000}`, "--quiet", `--trace=${tracePath}`], { cwd: sim, encoding: "utf8", maxBuffer: 256 << 20 }); + const run = spawnSync("node", ["--experimental-strip-types", join(sim, "scripts", "exact-eud.ts"), map, `--frames=${flag("frames") ?? 3000}`, "--quiet", `--trace=${tracePath}`, ...(flag("cost") ? [`--cost=${resolve(flag("cost")!)}`] : [])], { cwd: sim, encoding: "utf8", maxBuffer: 256 << 20 }); process.stdout.write(run.stdout.split("\n").filter((l) => !/^pages (read|written):/.test(l)).join("\n")); if (!existsSync(tracePath)) { console.error(run.stderr || "the run left no trace"); process.exit(1); } const trace = JSON.parse(readFileSync(tracePath, "utf8")) as Trace; diff --git a/scripts/test-game.mts b/scripts/test-game.mts index f111ed6..270ae59 100644 --- a/scripts/test-game.mts +++ b/scripts/test-game.mts @@ -32,7 +32,7 @@ const dir = mkdtempSync(join(tmpdir(), "trigscript-tests-")); try { // The map, built as Save builds it. const built = join(dir, basename(script, ".ts") + ".scx"); - const b = spawnSync("npx", ["tsx", join(root, "scripts", "build-fixture.mts"), resolve(script), built, ...(baseMap ? [resolve(baseMap)] : [])], { cwd: root, encoding: "utf8", maxBuffer: 256 << 20, env: { ...process.env, VARIABLE_MAP: "1" } }); + const b = spawnSync("npx", ["tsx", join(root, "scripts", "build-fixture.mts"), resolve(script), built, ...(baseMap ? [resolve(baseMap)] : [])], { cwd: root, encoding: "utf8", maxBuffer: 256 << 20, env: { ...process.env, VARIABLE_MAP: "1", TRIGGER_MAP: "1" } }); if (b.status !== 0) { console.error(b.stdout.split("\n").slice(-20).join("\n"), b.stderr); process.exit(1); } // The game, at frame 0, EUD let through. @@ -40,6 +40,7 @@ try { const { exactMissing, startExactGame } = await from("src/exact/node.ts"); const { classicScenario, installEud, placeStrings } = await from("src/exact/eud.ts"); const { testGame } = await from("src/exact/testGame.ts"); + const { installCost } = await from("src/exact/cost.ts"); const missing = exactMissing() as string[]; if (missing.length) { console.error("the lifted game cannot run here; missing:\n " + missing.join("\n ")); process.exit(1); } const chk = Archive.open(new Uint8Array(readFileSync(built))).readFile("staredit\\scenario.chk") as Uint8Array; @@ -49,8 +50,10 @@ try { const run = await startExactGame(chkPath, { game: "ums" }); const strings = placeStrings(run.lifted, chk); installEud(run.lifted, strings); - // (eudplib's payload lies outside what the game's snapshot covers: the handle keeps it itself.) - const handle = testGame(run.engine, run.lifted, [{ from: strings.gameTable, to: 0x191943c8 + strings.bytes }]) as GameHandle; + // (eudplib's payload lies outside what the game's snapshot covers: the handle keeps it itself. What each frame of + // the triggers ran is counted, A4.) + const payloadEnd = 0x191943c8 + strings.bytes; + const handle = testGame(run.engine, run.lifted, [{ from: strings.gameTable, to: payloadEnd }], { cost: installCost(run.lifted), payloadEnd }) as GameHandle; // The tests in the simulator over the map's own world, as the editor runs them; then in the game, the way a host // of the game runs them: the compile's JavaScript handed to dist/gametest.js, loaded from its text. @@ -67,7 +70,8 @@ try { const host = await import(`data:text/javascript;base64,${Buffer.from(readFileSync(bundle)).toString("base64")}`) as typeof import("../compiler/gameTestEntry"); // (Where the build put the programs' variables, for sim.program().) const variables = JSON.parse(readFileSync(built.replace(/\.scx$/, ".variables.json"), "utf8")); - const inGame = host.runGameTests(handle, { modules: compiled.modules, names, locations, variables }) as TestReport & { error?: string }; + const triggerMap = JSON.parse(readFileSync(built.replace(/\.scx$/, ".triggers.json"), "utf8")); + const inGame = host.runGameTests(handle, { modules: compiled.modules, names, locations, variables, triggers: triggerMap }) as TestReport & { error?: string }; if (inGame.error) { console.error(`the script did not run beside the game: ${inGame.error}`); process.exit(1); } const bySim = new Map((simulated?.results ?? []).map((r) => [r.id, r])); let failed = 0; @@ -80,6 +84,12 @@ try { if (r.status === "failed") console.log(` in the game: ${r.message}${r.at ? ` — line ${r.at.line}` : ""}${r.actual !== undefined ? `\n got ${r.actual}` : ""}`); if (s?.status === "failed") console.log(` in the simulator: ${s.message}${s.at ? ` — line ${s.at.line}` : ""}${s.actual !== undefined ? `\n got ${s.actual}` : ""}`); if (s && s.status !== r.status) console.log(" the two disagree: one of them is wrong"); + // (A4: what the test's frames cost in the game, and its costliest lines.) + if (r.cost && process.env.COST !== "0") { + const ran = r.cost.perFrame.filter((n) => n > 0), avg = ran.length ? Math.round(ran.reduce((a, b) => a + b, 0) / ran.length) : 0; + console.log(` cost: ${avg} conditions and actions a frame on average, ${r.cost.peak} at the most (frame ${r.cost.peakFrame})`); + for (const l of r.cost.lines.slice(0, 3)) console.log(` line ${l.line}: ${l.peak} at the most (frame ${l.peakFrame}), ran in ${l.frames} frames`); + } } // A test of several players: the map built again with their slots open to people, then a copy of the game for // each player, kept in step (scm-sim's runCopies), all running the same tests. @@ -102,7 +112,7 @@ try { writeFileSync(join(to, "request.json"), JSON.stringify({ groups: [...groups.values()], triggers, payload: { modules: compiled.modules, names, locations, variables: severalVariables } })); } for (const g of groups.values()) { - const out = await runCopies({ map: new Uint8Array(readFileSync(several)), seats: g.players.map((slot) => ({ slot, race: 1, name: `P${slot + 1}` })), driver: readFileSync(bundle, "utf8"), payload: { modules: compiled.modules, names, locations, variables: severalVariables, ids: g.ids }, triggers }) as { result: (TestReport & { error?: string }) | null; error: string | null; player: number }[]; + const out = await runCopies({ map: new Uint8Array(readFileSync(several)), seats: g.players.map((slot) => ({ slot, race: 1, name: `P${slot + 1}` })), driver: readFileSync(bundle, "utf8"), payload: { modules: compiled.modules, names, locations, variables: severalVariables, triggers, ids: g.ids }, triggers }) as { result: (TestReport & { error?: string }) | null; error: string | null; player: number }[]; for (const c of out) if (c.error || c.result?.error) { console.error(`P${c.player + 1}'s copy: ${c.error ?? c.result?.error}`); process.exit(1); } // A test passes when it passes at every computer; the first that failed says why. for (const r of mergeCopies(g.ids, out)) { @@ -110,6 +120,7 @@ try { const mark = (st: string | undefined) => (st === "passed" ? "✓" : st === "failed" ? "✗" : st === "skipped" ? "–" : "?"); if (r.status === "failed") failed++; console.log(`${mark(r.status)} game ×${g.players.length} ${mark(s?.status)} simulator ${r.id.replace(/^.*?::/, "")} (${r.frames} frames, ${r.ms} ms)`); + if (r.cost && process.env.COST !== "0") console.log(` cost: ${r.cost.peak} conditions and actions at the most in a frame (frame ${r.cost.peakFrame})`); if (r.status === "failed") console.log(` in the game: ${r.message}${r.at ? ` — line ${r.at.line}` : ""}`); if (s?.status === "failed") console.log(` in the simulator: ${s.message}${s.at ? ` — line ${s.at.line}` : ""}`); if (s && s.status !== r.status) console.log(" the two disagree: one of them is wrong"); diff --git a/tests/game-cost.test.ts b/tests/game-cost.test.ts new file mode 100644 index 0000000..73e656a --- /dev/null +++ b/tests/game-cost.test.ts @@ -0,0 +1,90 @@ +// What a frame costs (TrigScript 4, A4), without a game: the build's map of triggers turns records into lines, and a +// tap shaped like scm-sim's CostTap is read back as a test's cost. +import { describe, expect, it } from "vitest"; +import { CostLines, costByLine, shortCount, type GameCostTap } from "../compiler/gameCost"; +import type { TriggerMap } from "../compiler/gameVariables"; + +const END = 0x19200000; +// A payload of 4000 bytes: a trigger of line 12 at 0 (one condition, two actions), one of eudplib's at 2400 (no +// line), and one of main.ts line 30 at 1200. +const MAP: TriggerMap = { payload: 4000, triggers: [[0, 1, 2, "main.ts:12:5"], [2400, 0, 1, "eudplib"], [1200, 0, 1, "30:3"]] }; +const START = END - 4000; +const cond = (trigger: number, i: number) => START + trigger + 8 + 20 * i; +const act = (trigger: number, i: number) => START + trigger + 8 + 320 + 32 * i; + +/** The tap's rules (scm-sim's CostTap, in small): a frame's records in order, a carried one under the one before it. */ +function tap(frames: number[][]): GameCostTap { + const t: GameCostTap = { + group: null, + report() { + const groups = new Map(); + const conditions: number[] = [], actions: number[] = [], ungrouped: number[] = []; + frames.forEach((records, f) => { + let last = -1, c = 0, a = 0, u = 0; + const now = new Map(); + for (const r of records) { + const isCondition = (r - START - 8) % 20 === 0 && (r - START) % 1200 < 328; + if (isCondition) c++; else a++; + let g = t.group!(r); + if (g === -2) g = last; else last = g; + if (g < 0) { u++; continue; } + now.set(g, (now.get(g) ?? 0) + 1); + const k = groups.get(g) ?? { conditions: 0, actions: 0, frames: 0, peak: 0, peakFrame: f }; + if (isCondition) k.conditions++; else k.actions++; + groups.set(g, k); + } + for (const [g, n] of now) { const k = groups.get(g)!; k.frames++; if (n > k.peak) { k.peak = n; k.peakFrame = f; } } + conditions.push(c); actions.push(a); ungrouped.push(u); + }); + return { from: 0, conditions, actions, ungrouped, groups }; + }, + }; + return t; +} + +describe("CostLines", () => { + it("puts a record down to its trigger's line, eudplib's under the line before it, and keeps the rest apart", () => { + const lines = new CostLines(MAP, END); + expect(lines.group(cond(0, 0))).toBeGreaterThanOrEqual(0); + expect(lines.group(act(0, 1))).toBe(lines.group(cond(0, 0))); + expect(lines.group(act(2400, 0))).toBe(-2); + expect(lines.group(START + 3000)).toBe(-2); + expect(lines.group(0x51a284)).toBe(-1); + expect(lines.group(END)).toBe(-1); + }); + + it("reads a test's cost: by frame without what eudplib ran before any line, by line with the variables a line went through", () => { + const lines = new CostLines(MAP, END); + const t = tap([ + [0x51a284, act(2400, 0), act(2400, 0)], // eudplib's start: none of it a line's + [act(2400, 0), cond(0, 0), act(0, 0), act(0, 1), act(2400, 0), act(1200, 0)], // eudplib, line 12 + a variable, line 30 + [act(1200, 0), act(2400, 0), act(2400, 0)], // line 30 + two variables + ]); + t.group = lines.group; + const cost = lines.read(t); + expect(cost.perFrame).toEqual([0, 5, 3]); + expect(cost.other).toEqual([3, 1, 0]); + expect(cost.peak).toBe(5); + expect(cost.peakFrame).toBe(1); + expect(cost.lines).toEqual([ + { file: "main.ts", line: 12, total: 4, peak: 4, peakFrame: 1, frames: 1 }, + { file: "main.ts", line: 30, total: 4, peak: 3, peakFrame: 2, frames: 2 }, + ]); + }); +}); + +describe("costByLine", () => { + it("keeps each line's most in a frame over every test, and the test it was in", () => { + const line = (l: number, peak: number) => ({ file: "main.ts", line: l, total: peak, peak, peakFrame: 1, frames: 1 }); + const cost = (lines: ReturnType[]) => ({ from: 0, perFrame: [], other: [], peak: 0, peakFrame: 0, lines }); + const out = costByLine([{ id: "a", cost: cost([line(3, 10), line(4, 900)]) }, { id: "b", cost: cost([line(3, 2000)]) }]); + expect(out.find((l) => l.line === 3)).toMatchObject({ peak: 2000, test: "b" }); + expect(out.find((l) => l.line === 4)).toMatchObject({ peak: 900, test: "a" }); + }); +}); + +describe("shortCount", () => { + it("writes a count short", () => { + expect([640, 1234, 30708, 1004714].map(shortCount)).toEqual(["640", "1.2k", "31k", "1.0M"]); + }); +}); From d7811420ccb051caeb9c0d5a0fba8ed3f182a840 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 12:34:16 -0600 Subject: [PATCH 012/238] TrigScript 4, A4: the cost graph in the shell's teal --accent is not one of the editor's tokens, so the graph's area came out black. Co-Authored-By: Claude Opus 5.5 --- editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor.ts b/editor.ts index 8c95929..a9d7508 100644 --- a/editor.ts +++ b/editor.ts @@ -171,7 +171,7 @@ const STYLE = `${SHELL_STYLE} .${TEST_MARK_CLASS}-note { color: var(--danger); font-style: italic; opacity: 0.9; } .tsd .tsd-list li.tsd-cost { align-items: center; } .tsd .tsd-cost-graph { flex: 1 1 auto; min-width: 0; max-width: 480px; height: 36px; } -.tsd .tsd-cost-graph path { fill: var(--accent); fill-opacity: 0.35; stroke: var(--accent); stroke-width: 1px; vector-effect: non-scaling-stroke; } +.tsd .tsd-cost-graph path { fill: var(--teal); fill-opacity: 0.35; stroke: var(--teal); stroke-width: 1px; vector-effect: non-scaling-stroke; } .${BUILD_TIME_CLASS} { text-decoration: underline dotted rgba(153, 162, 179, 0.55); text-underline-offset: 3px; } `; From 45a65725f185868a5fbd62ca43e7e4992fcda5a2 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 12:34:51 -0600 Subject: [PATCH 013/238] Test Results: a passing test's icon is green, not red; the settings field's focus ring in teal .tsd-list .tsd-i set every icon red and outranked .tsd-test-passed; --accent is not one of the editor's tokens. Co-Authored-By: Claude Opus 5.5 --- editor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/editor.ts b/editor.ts index a9d7508..2007cfc 100644 --- a/editor.ts +++ b/editor.ts @@ -142,7 +142,7 @@ const STYLE = `${SHELL_STYLE} .tsd .tsd-settings p { margin: 0 0 8px; color: var(--text-dim); line-height: 1.45; } .tsd .tsd-settings .row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } .tsd .tsd-settings input { width: 110px; padding: 2px 6px; font: inherit; font-family: var(--font-mono); color: var(--text); background: var(--bg-1); border: 1px solid var(--border); border-radius: 2px; } -.tsd .tsd-settings input:focus { outline: 1px solid var(--accent); outline-offset: -1px; } +.tsd .tsd-settings input:focus { outline: 1px solid var(--teal); outline-offset: -1px; } .tsd .tsd-settings button { padding: 2px 8px; font: inherit; color: var(--text); background: var(--bg-3); border: 1px solid var(--border); border-radius: 2px; cursor: pointer; } .tsd .tsd-settings button:disabled { opacity: 0.5; cursor: default; } .tsd .tsd-settings .now { color: var(--text-faint); } @@ -158,9 +158,9 @@ const STYLE = `${SHELL_STYLE} .tsd .tsd-output { margin: 0; padding: 4px 20px; font-family: var(--font-mono); font-size: var(--fs-sm); line-height: 17px; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--text-dim); } .tsd .tsd-list li.tsd-warning .tsd-i { color: var(--warn); } .tsd .tsd-rows .tsd-empty { padding: 6px 12px 6px 20px; color: var(--text-faint); white-space: normal; line-height: 1.45; cursor: default; } -.tsd .tsd-test-passed { color: var(--ok); } +.tsd .tsd-test-passed, .tsd .tsd-list .tsd-i.tsd-test-passed { color: var(--ok); } .tsd .tsd-test-failed { color: var(--danger); } -.tsd .tsd-test-none, .tsd .tsd-test-skipped { color: var(--text-faint); } +.tsd .tsd-test-none, .tsd .tsd-test-skipped, .tsd .tsd-list .tsd-i.tsd-test-none, .tsd .tsd-list .tsd-i.tsd-test-skipped { color: var(--text-faint); } .${TEST_MARK_CLASS} { cursor: pointer; font: normal normal normal 14px/18px codicon; text-align: center; color: var(--text-faint); } .${TEST_MARK_CLASS}::before { content: "\\eabc"; } .${TEST_MARK_CLASS}-passed { color: var(--ok); } From 2f93fde0772b118cfbd9defea1dd91a39ce05a21 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 12:34:58 -0600 Subject: [PATCH 014/238] TrigScript 4: A4 as built Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 95f31d5..79cc79f 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -467,6 +467,70 @@ like the existing hints ("~1,200 actions a frame"), and as a graph over a test's Open: what count, on what machine, actually lags Remastered. A probe with a dial (run k × 1,000 actions a frame, the user says when it stutters) gives the first number. +**As built (2026-10-16, under Node and in the editor; the dial not yet played).** + +- **The count.** scm-sim `src/exact/cost.ts`. The game runs a trigger's conditions at 0x489200 and its actions at + 0x489130, each through a table of routines by type: conditions at 0x515A98, actions at 0x512800, the record in + ecx. Those routines are reached by pointer. `installCost` therefore wraps every one of them with a count, after + `installEud`, so the two EUD routines are counted too. It also wraps the engine's own stand-ins behind import + cells (Center View and Minimap Ping, from `live.ts`). + - What is not counted: a switched-off condition and an empty slot cost nothing, because the game calls nothing + for them. + - What is counted every time: an action that waits (Wait, Transmission) is counted on every frame it runs, as + the game does the work every frame. + - A check: on the units probe the tap's Deaths and Set Deaths totals agree with `EudTap`'s. +- **Records to lines.** A group function turns a record into its line, through the build's map of triggers (A1). + `CARRY` puts a record under the line that ran just before it in the frame. eudplib's variables, its functions + and the triggers no statement made are carried. A variable is a trigger the line's triggers jump through, so a + line's work includes the variables it touches. Measured on `for (…) { x += 1; }`, one turn of the loop is: + - the compare: 21; + - `i++`: 9; + - `x += 1`: 9 (7 of its own and 2 in the variable). + + That is 39 conditions and actions a turn. +- **What is apart from the lines (`TestCost.other`).** Records carried before any line in a frame, and records + outside the payload, count under no line: + - eudplib's start, once: about 82–89 thousand at frame 2 on any map, mostly payload set-up reached from the + scenario's own TRIG; + - eudplib's work at the top of every frame: about 580 a frame on the game-tests probe (payloadMain, ReceiveQC + for MSQC, RunTrigTrigger), more than that probe's own lines (about 225). + + Without this split, eudplib's start was every test's peak. +- **Where it shows.** + - `scripts/exact-eud.ts` prints the totals and the costliest origins, and writes them with `--cost=`. + `scripts/game-check.mts` passes `--cost` through. + - The test handle carries the tap. It is paused for a test's own actions between frames, and cleared on + `reset()`. + - `GameSim.cost()` reads it after each test, so `TestResult.cost` holds a series a frame and the lines by their + peak. + - `scripts/test-game.mts` prints each test's average, peak and three costliest lines. The copies of A3 count too. + A merged result keeps the first copy's. + - The editor: + - A line that ran 1,000 or more in one frame of any test gets a hint at its end, such as "31k a frame". The + note names the test and frame. + - The hint shows only while the file's text is what was measured. + - Test Results shows a "cost" row (average and peak), a graph of the frames and the three costliest lines. +- **Measured.** + - The units probe peaks at 103,639 in one frame (frame 842): three unit-loop lines of about 30 thousand each. + - The game-tests probe runs about 225 a frame of its own. + - **End to end in a built editor:** a program with `for (let i = 0; i < 200; i++) { x += 1; }` every frame and + two tests. The status bar said "In the game: 2 of 2 tests passed." about 8 s after the click. Test Results + showed: + - the cost row: "1,834 conditions and actions a frame on average, 1,837 at the most (frame 49)"; + - the graph; + - the line at 1,800 a frame. + + The line's hint read "1.8k a frame" after "unrolled ×200", the same numbers as under Node. A loop with bounds + known at build time is unrolled, so it costs 9 a turn (only `x += 1`), not 39. The graph's colour first used a + token the editor does not have (`--accent`); it now uses `--teal`. +- **The dial.** `probes/frame-cost.ts` climbs a ladder by itself, one step every 10 game seconds. Keys: W up, + Q down, E holds. The steps aim at 1, 2, 5, 10, 20, 50, 100, 200, 500 and 1,000 thousand a frame. Measured in the + lifted game (`--cost`), each step lands at its target plus about 630 (eudplib's own and the program's). + 1,004,714 at the top. + - Built to `fixtures/eud/frame-cost.scx` and copied to the Remastered maps folder (`TrigScript/`). + - Nothing yet says what count stutters. The hint's threshold (1,000) and the graph have no line for "lags" + until the dial has been played. + ### A5. The debugger in the game (4.2.0) 9½'s debugger gets a second backend. From d9a5d606779aec17bfab0532021ffc290d3a8e7c Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 12:45:31 -0600 Subject: [PATCH 015/238] =?UTF-8?q?TrigScript=204,=20A4:=20Remastered's=20?= =?UTF-8?q?budget=20from=20the=20dial=20=E2=80=94=20500=20thousand=20a=20f?= =?UTF-8?q?rame?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Played: no effect up to 500 thousand conditions and actions a frame; at a million the screen flickers (distracting) without slowing. The hint now starts at 10,000 a line, the cost row and note give a count as a share of the 500 thousand, and the dial gains steps at 2 and 5 million to find where the game slows. Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 12 +++++++++--- editor.ts | 17 +++++++++++++---- ko.ts | 4 ++-- probes/frame-cost.ts | 11 +++++++---- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 79cc79f..78680b1 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -506,7 +506,7 @@ k × 1,000 actions a frame, the user says when it stutters) gives the first numb - `scripts/test-game.mts` prints each test's average, peak and three costliest lines. The copies of A3 count too. A merged result keeps the first copy's. - The editor: - - A line that ran 1,000 or more in one frame of any test gets a hint at its end, such as "31k a frame". The + - A line that ran 10,000 or more in one frame of any test gets a hint at its end, such as "31k a frame". The note names the test and frame. - The hint shows only while the file's text is what was measured. - Test Results shows a "cost" row (average and peak), a graph of the frames and the three costliest lines. @@ -528,8 +528,14 @@ k × 1,000 actions a frame, the user says when it stutters) gives the first numb lifted game (`--cost`), each step lands at its target plus about 630 (eudplib's own and the program's). 1,004,714 at the top. - Built to `fixtures/eud/frame-cost.scx` and copied to the Remastered maps folder (`TrigScript/`). - - Nothing yet says what count stutters. The hint's threshold (1,000) and the graph have no line for "lags" - until the dial has been played. + - **Played in Remastered (2026-10-16, single player, Fastest):** + - steps 1–9, up to 500 thousand a frame: no visible effect; + - step 10, a million: the screen flickers slightly, enough to distract, and the game does not slow. + + So the budget is 500 thousand a frame, about 12,800 turns of a plain loop. The editor's hint starts at 10,000 a + line (2% of it). The cost row and the hint's note give a count as a share of the 500 thousand. + - Steps 11 and 12 were added (2 and 5 million; measured 2,000,640 and 5,000,643) to find where the game slows. + - Not played yet: those two steps, and the patch number and machine. ### A5. The debugger in the game (4.2.0) diff --git a/editor.ts b/editor.ts index 2007cfc..21c0571 100644 --- a/editor.ts +++ b/editor.ts @@ -124,8 +124,14 @@ const STATUS_MS = 10_000; const NOTICE_MS = 8_000; /** When running every test takes longer than this, only the open file's run by themselves after a compile. */ const TESTS_SLOW_MS = 2_000; -/** A line that ran this many conditions and actions in one frame of a test in the game says so beside it. */ -const COST_HINT = 1_000; +/** + * What Remastered was seen to run in a frame with no effect: the dial (probes/frame-cost.ts, played 2026-10-16) ran + * 500 thousand conditions and actions a frame with none, and a million with a flicker of the screen that the player + * found distracting, though the game did not slow. + */ +const COST_SEEN = 500_000; +/** A line that ran this many conditions and actions in one frame of a test in the game says so beside it: 2% of COST_SEEN. */ +const COST_HINT = 10_000; /** The costliest lines a test's results name. */ const COST_LINES = 3; /** The key the shortcuts are written with. */ @@ -837,10 +843,13 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa .map((l) => ({ file: l.file, line: l.line, label: t("{n} a frame", { n: shortCount(l.peak) }), - note: t("In the game, this line ran {n} conditions and actions in one frame at the most: frame {frame} of the test “{test}”, which it ran in for {frames, plural, one {# frame} other {# frames}}. A trigger costs the game by its conditions and actions, so the count is the measure.", { n: l.peak.toLocaleString(), frame: l.peakFrame, test: testName({ id: l.test }), frames: l.frames }), + note: t("In the game, this line ran {n} conditions and actions in one frame at the most: frame {frame} of the test “{test}”, which it ran in for {frames, plural, one {# frame} other {# frames}}. Remastered has run 500 thousand a frame with no visible effect, and its screen flickers at a million; this is {share} of the 500 thousand.", { n: l.peak.toLocaleString(), frame: l.peakFrame, test: testName({ id: l.test }), frames: l.frames, share: shareOf(l.peak) }), })); }; + /** "0.2%", "31%": a count as a share of what Remastered was seen to run in a frame. */ + const shareOf = (n: number) => `${(n / COST_SEEN * 100).toFixed(n < COST_SEEN / 10 ? 1 : 0)}%`; + /** The test's conditions and actions a frame in the game, as a small graph; eudplib's own work is not in it. */ const costGraph = (cost: TestCost): Element => { const NS = "http://www.w3.org/2000/svg", n = Math.max(cost.perFrame.length, 2), top = Math.max(cost.peak, 1); @@ -893,7 +902,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa const other = cost.other.slice(3), runtime = other.length ? Math.round(other.reduce((a, b) => a + b, 0) / other.length) : 0; list.append(el("li", { className: "tsd-plain", title: t("Counted in the lifted game: the script's own triggers and the variables they go through. eudplib's work at the top of each frame is apart: {n} a frame.", { n: runtime.toLocaleString() }) }, el("span", { className: "frame" }, t("cost")), - el("span", { className: "msg" }, t("{average} conditions and actions a frame on average, {peak} at the most (frame {frame})", { average: average.toLocaleString(), peak: cost.peak.toLocaleString(), frame: cost.peakFrame })))); + el("span", { className: "msg" }, t("{average} conditions and actions a frame on average, {peak} at the most (frame {frame}): {share} of the 500 thousand a frame Remastered has run with no visible effect", { average: average.toLocaleString(), peak: cost.peak.toLocaleString(), frame: cost.peakFrame, share: shareOf(cost.peak) })))); if (cost.perFrame.length > 1) list.append(el("li", { className: "tsd-plain tsd-cost" }, el("span", { className: "frame" }), costGraph(cost))); for (const l of cost.lines.slice(0, COST_LINES)) { list.append(el("li", { title: t("Go to the line"), onClick: () => goTo(l.file, l.line) }, diff --git a/ko.ts b/ko.ts index 5a4ca72..76ef892 100644 --- a/ko.ts +++ b/ko.ts @@ -291,11 +291,11 @@ export const KO: Record = { "in the game": "게임에서", "Run All Tests in the Game": "게임에서 모든 테스트 실행", "{n} a frame": "프레임당 {n}", - "In the game, this line ran {n} conditions and actions in one frame at the most: frame {frame} of the test “{test}”, which it ran in for {frames, plural, one {# frame} other {# frames}}. A trigger costs the game by its conditions and actions, so the count is the measure.": "게임에서 이 줄은 한 프레임에 조건과 액션을 최대 {n}개 실행했습니다: 테스트 “{test}”의 {frame}번째 프레임이며, 그 테스트에서 이 줄이 실행된 프레임은 {frames}개입니다. 트리거가 게임에 드는 비용은 조건과 액션의 수로 정해지므로, 이 수가 기준입니다.", + "In the game, this line ran {n} conditions and actions in one frame at the most: frame {frame} of the test “{test}”, which it ran in for {frames, plural, one {# frame} other {# frames}}. Remastered has run 500 thousand a frame with no visible effect, and its screen flickers at a million; this is {share} of the 500 thousand.": "게임에서 이 줄은 한 프레임에 조건과 액션을 최대 {n}개 실행했습니다: 테스트 “{test}”의 {frame}번째 프레임이며, 그 테스트에서 이 줄이 실행된 프레임은 {frames}개입니다. 리마스터는 프레임당 50만 개에서 눈에 띄는 영향이 없었고, 백만 개에서는 화면이 깜박입니다. 이것은 50만 개 중 {share}입니다.", "Conditions and actions a frame, frame {from} to {to}: {peak} at the most, at frame {frame}": "프레임당 조건과 액션, {from}–{to}번째 프레임: {frame}번째 프레임에서 최대 {peak}개", "Counted in the lifted game: the script's own triggers and the variables they go through. eudplib's work at the top of each frame is apart: {n} a frame.": "리프트한 게임에서 센 수입니다: 스크립트 자신의 트리거와 그 트리거가 거치는 변수. 매 프레임 처음에 eudplib가 하는 일은 따로입니다: 프레임당 {n}개.", "cost": "비용", - "{average} conditions and actions a frame on average, {peak} at the most (frame {frame})": "프레임당 조건과 액션 평균 {average}개, 최대 {peak}개 ({frame}번째 프레임)", + "{average} conditions and actions a frame on average, {peak} at the most (frame {frame}): {share} of the 500 thousand a frame Remastered has run with no visible effect": "프레임당 조건과 액션 평균 {average}개, 최대 {peak}개 ({frame}번째 프레임): 리마스터가 눈에 띄는 영향 없이 실행한 프레임당 50만 개 중 {share}", "Go to the line": "그 줄로 이동", "{peak} a frame at the most (frame {frame}), in {frames, plural, one {# frame} other {# frames}}": "프레임당 최대 {peak}개 ({frame}번째 프레임), {frames}개 프레임에서 실행", }; diff --git a/probes/frame-cost.ts b/probes/frame-cost.ts index aafcfa6..c81da8d 100644 --- a/probes/frame-cost.ts +++ b/probes/frame-cost.ts @@ -10,6 +10,9 @@ // // W one step up Q one step down E stop climbing / climb again // +// Played 2026-10-16 (steps 0–10 then): no effect up to step 9 (500 thousand); at step 10 (a million) the screen +// flickered, enough to distract, and the game did not slow. Steps 11 and 12 (2 and 5 million) were added to find where it does. +// // WHAT TO NOTE: the first step at which the game stutters (units jerk, the screen hitches), the first at which it // is plainly slow (a game second clearly longer than a real one), and, if it gets there, the step at which it // freezes or drops. Say which Remastered (patch number) and the computer it ran on. @@ -21,8 +24,8 @@ const STEP = 10; program(() => { // Turns of the loop for each step, and what that comes to a frame in thousands (39 a turn, measured). - const turns = [0, 26, 51, 128, 256, 513, 1282, 2564, 5128, 12820, 25641]; - const thousands = [0, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]; + const turns = [0, 26, 51, 128, 256, 513, 1282, 2564, 5128, 12820, 25641, 51282, 128205]; + const thousands = [0, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000]; let step = 0; let climbing = true; let next = 0; @@ -31,10 +34,10 @@ program(() => { let frame = 0; while (true) { frame += 1; - if (keyPressed(P1, "W") && step < 10) { step += 1; next = frame + STEP * 24; } + if (keyPressed(P1, "W") && step < 12) { step += 1; next = frame + STEP * 24; } if (keyPressed(P1, "Q") && step > 0) { step -= 1; next = frame + STEP * 24; } if (keyPressed(P1, "E")) { climbing = !climbing; displayText(climbing ? "climbing again" : "holding at this step"); next = frame + STEP * 24; } - if (climbing && frame >= next && step < 10) { if (next > 0) step += 1; next = frame + STEP * 24; } + if (climbing && frame >= next && step < 12) { if (next > 0) step += 1; next = frame + STEP * 24; } if (step != shown) { shown = step; if (step == 0) displayText("step 0: nothing a frame — W up, Q down, E holds"); From f5ddb5e4f2ea0ec1a78f0c7d87dac6fafd650dd5 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 12:48:41 -0600 Subject: [PATCH 016/238] =?UTF-8?q?TrigScript=204,=20A4:=20the=20dial's=20?= =?UTF-8?q?top=20steps=20=E2=80=94=20flicker=20from=20a=20million,=20slowi?= =?UTF-8?q?ng=20at=205=20million?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Played on a Ryzen 9 7900X. The budget stays at 500 thousand a frame, and the note says it was measured on a fast computer. Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 7 +++++-- editor.ts | 6 +++--- ko.ts | 2 +- probes/frame-cost.ts | 3 ++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 78680b1..cde5181 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -534,8 +534,11 @@ k × 1,000 actions a frame, the user says when it stutters) gives the first numb So the budget is 500 thousand a frame, about 12,800 turns of a plain loop. The editor's hint starts at 10,000 a line (2% of it). The cost row and the hint's note give a count as a share of the 500 thousand. - - Steps 11 and 12 were added (2 and 5 million; measured 2,000,640 and 5,000,643) to find where the game slows. - - Not played yet: those two steps, and the patch number and machine. + - Steps 11 and 12 were added (2 and 5 million; measured 2,000,640 and 5,000,643) and played the same day: + - 2 million: the flicker, no slowing; + - 5 million: the game slows. + - The machine: AMD Ryzen 9 7900X (12 cores, 4.7 GHz), a fast one. A slower computer reaches each step sooner, + so the budget stays at 500 thousand and the note says it was a fast computer. Patch number not noted. ### A5. The debugger in the game (4.2.0) diff --git a/editor.ts b/editor.ts index 21c0571..5af4bda 100644 --- a/editor.ts +++ b/editor.ts @@ -126,8 +126,8 @@ const NOTICE_MS = 8_000; const TESTS_SLOW_MS = 2_000; /** * What Remastered was seen to run in a frame with no effect: the dial (probes/frame-cost.ts, played 2026-10-16) ran - * 500 thousand conditions and actions a frame with none, and a million with a flicker of the screen that the player - * found distracting, though the game did not slow. + * 500 thousand conditions and actions a frame with none; from a million the screen flickers, enough to distract, + * and at 5 million the game slows. That was a fast computer (Ryzen 9 7900X): a slower one reaches each sooner. */ const COST_SEEN = 500_000; /** A line that ran this many conditions and actions in one frame of a test in the game says so beside it: 2% of COST_SEEN. */ @@ -843,7 +843,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa .map((l) => ({ file: l.file, line: l.line, label: t("{n} a frame", { n: shortCount(l.peak) }), - note: t("In the game, this line ran {n} conditions and actions in one frame at the most: frame {frame} of the test “{test}”, which it ran in for {frames, plural, one {# frame} other {# frames}}. Remastered has run 500 thousand a frame with no visible effect, and its screen flickers at a million; this is {share} of the 500 thousand.", { n: l.peak.toLocaleString(), frame: l.peakFrame, test: testName({ id: l.test }), frames: l.frames, share: shareOf(l.peak) }), + note: t("In the game, this line ran {n} conditions and actions in one frame at the most: frame {frame} of the test “{test}”, which it ran in for {frames, plural, one {# frame} other {# frames}}. On a fast computer Remastered ran 500 thousand a frame with no visible effect; its screen flickers from a million and the game slows at 5 million. This is {share} of the 500 thousand.", { n: l.peak.toLocaleString(), frame: l.peakFrame, test: testName({ id: l.test }), frames: l.frames, share: shareOf(l.peak) }), })); }; diff --git a/ko.ts b/ko.ts index 76ef892..6e5ce63 100644 --- a/ko.ts +++ b/ko.ts @@ -291,7 +291,7 @@ export const KO: Record = { "in the game": "게임에서", "Run All Tests in the Game": "게임에서 모든 테스트 실행", "{n} a frame": "프레임당 {n}", - "In the game, this line ran {n} conditions and actions in one frame at the most: frame {frame} of the test “{test}”, which it ran in for {frames, plural, one {# frame} other {# frames}}. Remastered has run 500 thousand a frame with no visible effect, and its screen flickers at a million; this is {share} of the 500 thousand.": "게임에서 이 줄은 한 프레임에 조건과 액션을 최대 {n}개 실행했습니다: 테스트 “{test}”의 {frame}번째 프레임이며, 그 테스트에서 이 줄이 실행된 프레임은 {frames}개입니다. 리마스터는 프레임당 50만 개에서 눈에 띄는 영향이 없었고, 백만 개에서는 화면이 깜박입니다. 이것은 50만 개 중 {share}입니다.", + "In the game, this line ran {n} conditions and actions in one frame at the most: frame {frame} of the test “{test}”, which it ran in for {frames, plural, one {# frame} other {# frames}}. On a fast computer Remastered ran 500 thousand a frame with no visible effect; its screen flickers from a million and the game slows at 5 million. This is {share} of the 500 thousand.": "게임에서 이 줄은 한 프레임에 조건과 액션을 최대 {n}개 실행했습니다: 테스트 “{test}”의 {frame}번째 프레임이며, 그 테스트에서 이 줄이 실행된 프레임은 {frames}개입니다. 빠른 컴퓨터에서 리마스터는 프레임당 50만 개에서 눈에 띄는 영향이 없었고, 백만 개부터 화면이 깜박이며 5백만 개에서 게임이 느려졌습니다. 이것은 50만 개 중 {share}입니다.", "Conditions and actions a frame, frame {from} to {to}: {peak} at the most, at frame {frame}": "프레임당 조건과 액션, {from}–{to}번째 프레임: {frame}번째 프레임에서 최대 {peak}개", "Counted in the lifted game: the script's own triggers and the variables they go through. eudplib's work at the top of each frame is apart: {n} a frame.": "리프트한 게임에서 센 수입니다: 스크립트 자신의 트리거와 그 트리거가 거치는 변수. 매 프레임 처음에 eudplib가 하는 일은 따로입니다: 프레임당 {n}개.", "cost": "비용", diff --git a/probes/frame-cost.ts b/probes/frame-cost.ts index c81da8d..0a024d0 100644 --- a/probes/frame-cost.ts +++ b/probes/frame-cost.ts @@ -11,7 +11,8 @@ // W one step up Q one step down E stop climbing / climb again // // Played 2026-10-16 (steps 0–10 then): no effect up to step 9 (500 thousand); at step 10 (a million) the screen -// flickered, enough to distract, and the game did not slow. Steps 11 and 12 (2 and 5 million) were added to find where it does. +// flickered, enough to distract, and the game did not slow. Steps 11 and 12 (2 and 5 million) were added and played +// the same day: still the flicker at 2 million, the game slows at 5 million. Ryzen 9 7900X. // // WHAT TO NOTE: the first step at which the game stutters (units jerk, the screen hitches), the first at which it // is plainly slow (a game second clearly longer than a real one), and, if it gets there, the step at which it From c1d12e5c9c4b15cae837e0c576ca922b43d61a47 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 12:49:09 -0600 Subject: [PATCH 017/238] TrigScript 4, A4: the dial was played on Remastered 1.23.10.13515 Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 2 +- probes/frame-cost.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index cde5181..396a3cb 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -538,7 +538,7 @@ k × 1,000 actions a frame, the user says when it stutters) gives the first numb - 2 million: the flicker, no slowing; - 5 million: the game slows. - The machine: AMD Ryzen 9 7900X (12 cores, 4.7 GHz), a fast one. A slower computer reaches each step sooner, - so the budget stays at 500 thousand and the note says it was a fast computer. Patch number not noted. + so the budget stays at 500 thousand and the note says it was a fast computer. Remastered 1.23.10.13515. ### A5. The debugger in the game (4.2.0) diff --git a/probes/frame-cost.ts b/probes/frame-cost.ts index 0a024d0..d126816 100644 --- a/probes/frame-cost.ts +++ b/probes/frame-cost.ts @@ -12,7 +12,7 @@ // // Played 2026-10-16 (steps 0–10 then): no effect up to step 9 (500 thousand); at step 10 (a million) the screen // flickered, enough to distract, and the game did not slow. Steps 11 and 12 (2 and 5 million) were added and played -// the same day: still the flicker at 2 million, the game slows at 5 million. Ryzen 9 7900X. +// the same day: still the flicker at 2 million, the game slows at 5 million. Ryzen 9 7900X, Remastered 1.23.10.13515. // // WHAT TO NOTE: the first step at which the game stutters (units jerk, the screen hitches), the first at which it // is plainly slow (a game second clearly longer than a real one), and, if it gets there, the step at which it From cdcf4646ef1edaf800dbb642c759afdbffa0462e Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 13:13:58 -0600 Subject: [PATCH 018/238] TrigScript 4, A5: the debugger's record of a test run in the game One test run once in the lifted game with everything written down (compiler/gameDebug.ts): each frame's steps in the order the game ran them, by line; every write to the script's memory with what it held before and after; the test's line that asked for the frame; what was shown. A picture of any frame is the nearest kept game, the test's own actions between frames done again, and the frames played. - GameSim tells the handle of its own writes to memory (wrote()). - link.ts here(): the test's line from call sites, not stack text (Node formatted a source-mapped stack each frame: 10x the game's cost). - trigscript.py: the variable map skipped a loop's unit (LoopUnit has no decl) and crashed the build of any program with a unit loop. - scripts/debug-game.mts checks pictures against the record under Node; probes/game-debug.ts, tests/game-debug.test.ts. Co-Authored-By: Claude Opus 5.5 --- compiler/gameCost.ts | 3 + compiler/gameDebug.ts | 379 +++++++++++++++++++++++++++++ compiler/gameSim.ts | 19 +- compiler/gameTestEntry.ts | 3 +- compiler/gameTests.ts | 40 ++- compiler/generated/trigscriptPy.ts | 2 +- compiler/link.ts | 44 ++++ probes/game-debug.ts | 43 ++++ python/trigscript.py | 5 +- scripts/debug-game.mts | 144 +++++++++++ tests/game-debug.test.ts | 121 +++++++++ 11 files changed, 792 insertions(+), 11 deletions(-) create mode 100644 compiler/gameDebug.ts create mode 100644 probes/game-debug.ts create mode 100644 scripts/debug-game.mts create mode 100644 tests/game-debug.test.ts diff --git a/compiler/gameCost.ts b/compiler/gameCost.ts index 196aa6b..e7cb111 100644 --- a/compiler/gameCost.ts +++ b/compiler/gameCost.ts @@ -93,6 +93,9 @@ export class CostLines { return id; } + /** The lines groups stand for, by group: what the tap's groups are numbers into. */ + get places(): readonly { file: string; line: number }[] { return this.at; } + /** For the host's tap. */ readonly group = (record: number): number => { const g = this.byRecord.get(record); diff --git a/compiler/gameDebug.ts b/compiler/gameDebug.ts new file mode 100644 index 0000000..9099c06 --- /dev/null +++ b/compiler/gameDebug.ts @@ -0,0 +1,379 @@ +/** + * The debugger in the game (TrigScript 4, A5): one test run once in the lifted game with everything it did written + * down, so that stepping forward and back, by frame or by line, is a walk over the record and never the game again. + * + * What is recorded, frame by frame: + * + * - **Steps.** The game runs a frame's triggers one record after another; the host's cost tap says each record, in + * order, under the line the build's map of triggers puts it (A1, A4). Records of one line in a row are one step: + * "line 12 ran 9 conditions and actions". A loop's turns come out as the lines of its body, again and again. + * - **Writes.** Every dword of the script's memory a step changed (eudplib's variables and arrays, the heap, the made + * texts: everything between the start of `STRx` and the end of the payload), with what it held before and after. + * With both, a step back is the writes put back in reverse, and the variables at any step are read the way + * `sim.program()` reads them (`gameVariables.ts`), over a copy of that memory the debugger keeps. What the game wrote + * there without a record (a routine of its own) is found at the frame's end, compared, and written down as the + * frame's last step, so the copy never drifts. + * - **The test's line.** Which line of the test asked for the frame (a `sim.frames`, `until`, `seconds`). + * - **What was shown** on the text lines, by frame. + * + * And, for a picture of any frame, kept on the host's side and never sent: the game kept every so many frames + * (`TestGameHandle.keep`), and everything the test did to it between frames (its actions, typed lines, routines + * called and its own writes to memory: keys, the mouse, a scratch location). A picture of frame F is the nearest + * kept game before it, those things done again at their frames, and the frames in between played. + * + * A test is synchronous JavaScript, so a run cannot stop in the middle; a frame cannot either (the game's trigger + * loop runs to its end), which is why a step inside a frame comes from the record and not from a pause. + */ +import type { GameHandle } from "./gameSim"; +import type { TestResult } from "./testing"; + +/** Where the script's memory starts: Remastered's strings, where eudplib puts `STRx` and its payload after it. */ +export const SCRIPT_MEMORY = 0x191943c8; + +/** A step's line when it is not a line of the script. */ +export const STEP_EUDPLIB = -1; +/** What the test did between frames (its own actions), recorded as the step before the frame's first. */ +export const STEP_TEST = -3; +/** What the game changed in the script's memory with no record to say so, found at the frame's end. */ +export const STEP_UNSEEN = -4; + +/** What is recorded at most; past it the record says where it stopped, and the run goes on without it. */ +export interface DebugLimits { + steps?: number; + writes?: number; + /** What the kept games may hold together, in bytes; past it every other one goes and they are kept half as often. */ + keptBytes?: number; + /** A game kept every this many frames at first. */ + every?: number; +} +const LIMITS: Required = { steps: 4_000_000, writes: 6_000_000, keptBytes: 256 * 2 ** 20, every: 48 }; + +/** + * A test's run in the game, as plain data (structured-clone safe; the typed arrays transfer). + * + * Frame `k` of the record is game frame `first + k`. Its steps are `frameSteps[k]` to `frameSteps[k + 1]`; a step's + * writes are `stepWrites[s]` to `stepWrites[s + 1]`, three numbers each in `writes` (address, before, after). + */ +export interface DebugRecording { + version: 1; + test: { id: string; name: string; file: string; line: number }; + result: TestResult; + /** The lines a step can be, by number (a step's `stepLine`). */ + lines: { file: string; line: number }[]; + /** The script's memory as the test found it (frame `first` - 1, before its first frame): `memory.at` on. */ + memory: { at: number; bytes: Uint8Array }; + /** The frame the test started at (the game put back: 0), and the first recorded. */ + start: number; + first: number; + frames: number; + frameSteps: Int32Array; + /** By frame: the test's line that asked for it (0: none known). */ + testLine: Int32Array; + testFile: string[]; + /** By frame: an index into `testFile`. */ + testFileOf: Int32Array; + stepLine: Int32Array; + stepRecords: Int32Array; + stepWrites: Int32Array; + writes: Uint32Array; + shown: { frame: number; text: string }[]; + /** Where the record stopped short, in words, and from which frame; null when it is whole. */ + cut: { frame: number; why: string } | null; + /** The frames a picture can be had of quickly: the kept games'. */ + kept: number[]; + ms: number; +} + +/** A host's game, as the debugger needs it: a test's handle that can keep and restore, and its two taps. */ +export interface DebugHost extends GameHandle { + keep(): K; + restore(kept: K): void; + portable(kept: K): { kept: P; transfer: ArrayBuffer[] }; + readonly eud?: { listen: ((record: number, address: number, write: boolean, mask: number, was: number) => void) | null }; + readonly cost?: GameHandle["cost"] & { trace: ((record: number, group: number, action: boolean) => void) | null; paused: boolean }; +} + +/** What the test did to the game between frames, to do again on the way to a picture. */ +type Op = + | { kind: "action"; record: Uint8Array; player: number } + | { kind: "call"; address: number; registers: Record; args?: number[] } + | { kind: "chat"; text: string } + | { kind: "poke"; address: number; bytes: Uint8Array }; + +class Ints { + a: Int32Array; + n = 0; + constructor(size = 1024) { this.a = new Int32Array(size); } + push(v: number) { if (this.n === this.a.length) { const b = new Int32Array(this.a.length * 2); b.set(this.a); this.a = b; } this.a[this.n++] = v; } + done(): Int32Array { return this.a.slice(0, this.n); } +} +class Words { + a: Uint32Array; + n = 0; + constructor(size = 3072) { this.a = new Uint32Array(size); } + push3(x: number, y: number, z: number) { + if (this.n + 3 > this.a.length) { const b = new Uint32Array(this.a.length * 2); b.set(this.a); this.a = b; } + this.a[this.n++] = x; this.a[this.n++] = y; this.a[this.n++] = z; + } + done(): Uint32Array { return this.a.slice(0, this.n); } +} + +/** + * Wraps a host's handle for one test: everything `GameSim` does goes through it and is written down. `where` says the + * test's line from inside a step (the stack's innermost line of a linked file). + */ +export class RecordingHandle implements GameHandle { + private readonly host: DebugHost; + private readonly where: () => { file: string; line: number } | null; + private readonly limits: Required; + private readonly lo: number; + private readonly hi: number; + + private recording = false; + private stepping = false; + private group = Number.NaN; + private image: Uint8Array = new Uint8Array(0); + private mirror: Uint32Array = new Uint32Array(0); + private start = 0; + private first = -1; + private frameSteps = new Ints(); + private testLine = new Ints(); + private testFileOf = new Ints(); + private testFile: string[] = []; + private stepLine = new Ints(); + private stepRecords = new Ints(); + private stepWrites = new Ints(); + private writes = new Words(); + private shownLines: { frame: number; text: string }[] = []; + private cut: { frame: number; why: string } | null = null; + private keptGames: { frame: number; kept: K; bytes: number }[] = []; + private keptBytes = 0; + private every: number; + private ops = new Map(); + private lastFrame = 0; + private began = 0; + + /** `from`: where the script's memory starts (Remastered's strings; another for a host of its own, a test's). */ + constructor(host: DebugHost, where: () => { file: string; line: number } | null, limits: DebugLimits = {}, from = SCRIPT_MEMORY) { + this.host = host; + this.where = where; + this.limits = { ...LIMITS, ...limits }; + this.every = this.limits.every; + const end = host.payloadEnd ?? from; + this.lo = from; + this.hi = Math.max(from, Math.min(end, host.u8.length)) & ~3; + } + + get m() { return this.host.m; } + get u8() { return this.host.u8; } + get frame() { return this.host.frame; } + get localPlayer() { return this.host.localPlayer; } + get over() { return this.host.over; } + get cost() { return this.host.cost; } + get payloadEnd() { return this.host.payloadEnd; } + + /** The test's world is made: the game back at its start, and the record begins. */ + reset(): void { + this.host.reset(); + this.begin(); + } + + private begin() { + this.recording = true; + this.began = Date.now(); + this.start = this.host.frame; + this.first = -1; + this.image = this.host.u8.slice(this.lo, this.hi); + this.mirror = new Uint32Array(this.image.slice().buffer); + for (const k of ["frameSteps", "testLine", "testFileOf", "stepLine", "stepRecords", "stepWrites"] as const) this[k] = new Ints(); + this.writes = new Words(); + this.testFile = []; + this.shownLines = []; + this.cut = null; + this.keptGames = []; + this.keptBytes = 0; + this.every = this.limits.every; + this.ops.clear(); + this.group = Number.NaN; + this.frameSteps.push(0); + this.keepNow(); + const cost = this.host.cost, eud = this.host.eud; + if (cost) cost.trace = (_record, group) => this.ran(group); + if (eud) eud.listen = (_record, address, write, _mask, was) => { if (write) this.wrote32(address, was); }; + } + + /** Stops listening (the run is over, or a picture is being made). */ + stop(): void { + const cost = this.host.cost, eud = this.host.eud; + if (cost) cost.trace = null; + if (eud) eud.listen = null; + this.recording = false; + } + + private full(): boolean { + if (this.cut) return true; + if (this.stepLine.n >= this.limits.steps || this.writes.n / 3 >= this.limits.writes) { + this.cut = { frame: this.host.frame, why: this.stepLine.n >= this.limits.steps ? `more than ${this.limits.steps.toLocaleString("en")} steps` : `more than ${this.limits.writes.toLocaleString("en")} writes` }; + return true; + } + return false; + } + + private newStep(line: number) { + this.stepLine.push(line); + this.stepRecords.push(0); + this.stepWrites.push(this.writes.n / 3); + } + + /** A record ran under `group`. */ + private ran(group: number) { + if (!this.recording || !this.stepping || this.full()) return; + const line = group < 0 ? STEP_EUDPLIB : group; + if (line !== this.group) { this.newStep(line); this.group = line; } + this.stepRecords.a[this.stepRecords.n - 1]++; + } + + /** A dword was written by an EUD action; `was` what it held before. */ + private wrote32(address: number, was: number) { + if (!this.recording || address < this.lo || address + 4 > this.hi || (address & 3) !== 0 || this.full()) return; + const now = this.host.m.getUint32(address, true); + const k = (address - this.lo) >>> 2; + if (now === this.mirror[k]) return; + // Between frames (the test's own action did it): the frame-to-be's first step is the test's. In a frame with no + // record said yet (a host that does not trace), eudplib's. + if (!this.stepping && this.group !== STEP_TEST) { this.newStep(STEP_TEST); this.group = STEP_TEST; } + else if (this.stepping && Number.isNaN(this.group)) { this.newStep(STEP_EUDPLIB); this.group = STEP_EUDPLIB; } + void was; + this.writes.push3(address, this.mirror[k], now); + this.mirror[k] = now; + } + + step(): void { + if (!this.recording) { this.host.step(); return; } + const at = this.where(); + // (A frame's steps start where the last frame's ended: a step the test made between frames is the first of them.) + if (this.first < 0) this.first = this.host.frame + 1; + this.group = Number.NaN; + this.stepping = true; + try { this.host.step(); } finally { this.stepping = false; } + this.unseen(); + this.group = Number.NaN; + this.frameSteps.push(this.stepLine.n); + let file = -1; + if (at) { file = this.testFile.indexOf(at.file); if (file < 0) { file = this.testFile.length; this.testFile.push(at.file); } } + this.testLine.push(at?.line ?? 0); + this.testFileOf.push(file); + this.lastFrame = this.host.frame; + if (this.host.frame - (this.keptGames.at(-1)?.frame ?? -Infinity) >= this.every) this.keepNow(); + } + + /** The script's memory as the game left it, against the copy: what no record said is the frame's last step. */ + private unseen() { + if (this.cut) return; + const u8 = this.host.u8; + const words = u8.byteOffset % 4 === 0 && (u8.byteOffset + this.lo) % 4 === 0 ? new Uint32Array(u8.buffer, u8.byteOffset + this.lo, (this.hi - this.lo) >>> 2) : null; + const read = words ? (k: number) => words[k] : (k: number) => this.host.m.getUint32(this.lo + 4 * k, true); + const mirror = this.mirror; + let opened = false; + for (let k = 0; k < mirror.length; k++) { + const now = read(k); + if (now === mirror[k]) continue; + if (!opened) { this.newStep(STEP_UNSEEN); opened = true; } + this.writes.push3(this.lo + 4 * k, mirror[k], now); + mirror[k] = now; + } + } + + private keepNow() { + const kept = this.host.keep(); + const bytes = (kept as { bytes?: number }).bytes ?? 0; + this.keptGames.push({ frame: this.host.frame, kept, bytes }); + this.keptBytes += bytes; + if (this.keptBytes > this.limits.keptBytes && this.keptGames.length > 2) { + // Every other one goes (never the first), and from now on they are kept half as often. + this.keptGames = this.keptGames.filter((_, i) => i === 0 || i % 2 === 0); + this.keptBytes = this.keptGames.reduce((n, k) => n + k.bytes, 0); + this.every *= 2; + } + } + + private op(o: Op) { + if (!this.recording) return; + const f = this.host.frame; + const list = this.ops.get(f); + if (list) list.push(o); else this.ops.set(f, [o]); + } + + lines(): { frame: number; text: string }[] { + const l = this.host.lines(); + if (this.recording) this.shownLines.push(...l); + return l; + } + action(record: Uint8Array, player: number): number { + this.op({ kind: "action", record: record.slice(0, 32), player }); + return this.host.action(record, player); + } + chat(text: string): void { this.op({ kind: "chat", text }); this.host.chat(text); } + call(address: number, registers: Record, args?: number[]): number { + this.op({ kind: "call", address, registers: { ...registers }, ...(args ? { args: [...args] } : {}) }); + return this.host.call(address, registers, args); + } + wrote(address: number, length: number): void { + this.op({ kind: "poke", address, bytes: this.host.u8.slice(address, address + length) }); + } + + /** The record, once the test has run. */ + finish(test: DebugRecording["test"], result: TestResult, lines: DebugRecording["lines"]): DebugRecording { + this.stop(); + const frames = this.testLine.n; + // (Steps the test made after its last frame belong to no frame: the last frame's list ends before them.) + const frameSteps = this.frameSteps.done(); + this.stepWrites.push(this.writes.n / 3); + return { + version: 1, test, result, lines, + memory: { at: this.lo, bytes: this.image }, + start: this.start, first: this.first < 0 ? this.start + 1 : this.first, frames, + frameSteps, testLine: this.testLine.done(), testFile: this.testFile, testFileOf: this.testFileOf.done(), + stepLine: this.stepLine.done(), stepRecords: this.stepRecords.done(), stepWrites: this.stepWrites.done(), writes: this.writes.done(), + shown: this.shownLines, cut: this.cut, kept: this.keptGames.map((k) => k.frame), ms: Date.now() - this.began, + }; + } + + /** Where the game stands after a picture (so the next, a frame or two on, plays on from there). */ + private standing: number | null = null; + + /** + * The game at frame `frame` (clamped to the run), for another engine to draw: the nearest kept game before it, + * what the test did between frames done again, and the frames in between played. Only once the record is finished. + */ + picture(frame: number): { frame: number; kept: P; transfer: ArrayBuffer[] } { + const host = this.host; + const target = Math.max(this.start, Math.min(frame, this.lastFrame)); + const cost = host.cost; + const paused = cost?.paused ?? false; + if (cost) cost.paused = true; + try { + const from = this.keptGames.filter((k) => k.frame <= target).at(-1)!; + const here = this.standing; + // Playing on from where the last picture left the game is quicker when it is behind the target and nearer. + if (here === null || here > target || here < from.frame) host.restore(from.kept); + while (host.frame < target && !host.over) { + for (const o of this.ops.get(host.frame) ?? []) this.redo(o); + host.step(); + } + host.lines(); + this.standing = host.frame; + const kept = host.keep(); + const p = host.portable(kept); + return { frame: host.frame, kept: p.kept, transfer: p.transfer }; + } finally { if (cost) cost.paused = paused; } + } + + private redo(o: Op) { + const host = this.host; + if (o.kind === "action") host.action(o.record, o.player); + else if (o.kind === "call") host.call(o.address, o.registers, o.args); + else if (o.kind === "chat") host.chat(o.text); + else host.u8.set(o.bytes, o.address); + } +} diff --git a/compiler/gameSim.ts b/compiler/gameSim.ts index f0680d3..87939e3 100644 --- a/compiler/gameSim.ts +++ b/compiler/gameSim.ts @@ -39,6 +39,11 @@ export interface GameHandle { readonly payloadEnd?: number; /** When the game is one of several copies kept in step, a copy for each of a test's players (scm-sim's `CopyHandle`). */ readonly copy?: GameCopy; + /** + * Told after the test wrote the game's memory itself between frames (a key, the mouse, a scratch location, hit + * points), when the handle listens: the debugger (A5) keeps every such write, to play the run again to a frame. + */ + wrote?(address: number, length: number): void; } /** One copy of several (TrigScript 4, A3): which, the slot each copy plays, and where they stopped agreeing. */ @@ -189,7 +194,7 @@ class GameUnit implements SimUnit { get x() { return this.read("x", () => this.m.getInt16(this.at + 0x28, true)); } get y() { return this.read("y", () => this.m.getInt16(this.at + 0x2a, true)); } get hp() { return this.read("hp", () => Math.ceil(this.m.getInt32(this.at + 0x08, true) / 256)); } - set hp(v: number) { if (this.alive) this.m.setInt32(this.at + 0x08, Math.max(0, v) * 256, true); } + set hp(v: number) { if (this.alive) { this.m.setInt32(this.at + 0x08, Math.max(0, v) * 256, true); this.game.wrote?.(this.at + 0x08, 4); } } get maxHp() { return Math.floor(this.m.getInt32(MAX_HP + 4 * this.type, true) / 256); } get shields() { return this.read("shields", () => Math.floor(this.m.getInt32(this.at + 0x60, true) / 256)); } get maxShields() { return this.m.getUint16(MAX_SHIELDS + 2 * this.type, true); } @@ -305,7 +310,8 @@ export class GameSim implements TestWorldLike { const u8 = this.game.u8, m = this.game.m, at = LOCATIONS + 20 * (n - 1), was = u8.slice(at, at + 20); [b.left, b.top, b.right, b.bottom].forEach((v, i) => m.setUint32(at + 4 * i, Math.max(0, v), true)); m.setUint16(at + 0x12, 0, true); - try { return run(n); } finally { u8.set(was, at); } + this.game.wrote?.(at, 20); + try { return run(n); } finally { u8.set(was, at); this.game.wrote?.(at, 20); } } private within255(b: SimBounds, run: (location: number) => T): T { return this.scratch(SCRATCH, b, run); } place(player: number, type: number, at: number, n = 1): SimUnit[] { @@ -361,24 +367,29 @@ export class GameSim implements TestWorldLike { return who === this.game.localPlayer; } /** A key down for one frame, as a press is (MSQC sees it down, then up). */ - press(key: string, player?: number): this { const k = virtualKey(key); if (!this.local(player, "press")) return this; this.game.u8[KEYS + k] = 1; this.held.push(k); return this; } + press(key: string, player?: number): this { const k = virtualKey(key); if (!this.local(player, "press")) return this; this.game.u8[KEYS + k] = 1; this.game.wrote?.(KEYS + k, 1); this.held.push(k); return this; } click(button: "left" | "right" | "middle" = "left", player?: number): this { if (!this.local(player, "click")) return this; const m = this.game.m; m.setUint32(BUTTONS, m.getUint32(BUTTONS, true) | BUTTON_BITS[button], true); + this.game.wrote?.(BUTTONS, 4); this.held.push(-BUTTON_BITS[button]); return this; } private releaseKeys(): void { if (!this.held.length) return; const m = this.game.m; - for (const k of this.held.splice(0)) { if (k >= 0) this.game.u8[KEYS + k] = 0; else m.setUint32(BUTTONS, m.getUint32(BUTTONS, true) & ~-k, true); } + for (const k of this.held.splice(0)) { + if (k >= 0) { this.game.u8[KEYS + k] = 0; this.game.wrote?.(KEYS + k, 1); } + else { m.setUint32(BUTTONS, m.getUint32(BUTTONS, true) & ~-k, true); this.game.wrote?.(BUTTONS, 4); } + } } type(line: string, player?: number): this { if (this.local(player, "type")) this.game.chat(line); return this; } moveMouse(x: number, y: number, player?: number): this { if (!this.local(player, "moveMouse")) return this; this.game.m.setUint16(MOUSE, x, true); this.game.m.setUint16(MOUSE + 2, y, true); + this.game.wrote?.(MOUSE, 4); return this; } diff --git a/compiler/gameTestEntry.ts b/compiler/gameTestEntry.ts index 499859d..3914c0a 100644 --- a/compiler/gameTestEntry.ts +++ b/compiler/gameTestEntry.ts @@ -2,7 +2,8 @@ * `dist/gametest.js`: what a host of the lifted game imports to run a script's tests beside it (see `gameTests.ts`). * The plugin never loads it itself; it hands its text to the host. */ -export { drive, runGameTests, type GameTestPayload } from "./gameTests"; +export { debugGameTest, drive, runGameTests, type GameDebugPayload, type GameTestPayload } from "./gameTests"; +export type { DebugRecording } from "./gameDebug"; export { mergeCopies } from "./gameCopies"; export { desyncMessage, GameSim, readLocations, readUnitStats, type Desync, type GameCopy, type GameHandle } from "./gameSim"; export { programVariables, readVariableMap, type VariableMap } from "./gameVariables"; diff --git a/compiler/gameTests.ts b/compiler/gameTests.ts index 7da6c3b..327481f 100644 --- a/compiler/gameTests.ts +++ b/compiler/gameTests.ts @@ -12,7 +12,8 @@ import { MODULE_NAME } from "./api"; import { GameSim, readLocations, type GameHandle } from "./gameSim"; import type { TriggerMap, VariableMap } from "./gameVariables"; import { CostLines } from "./gameCost"; -import { locate, runModules, type LinkedFile } from "./link"; +import { RecordingHandle, type DebugHost, type DebugLimits, type DebugRecording } from "./gameDebug"; +import { here, locate, runModules, type LinkedFile } from "./link"; import type { ScriptNames } from "./names"; import { Collector, createRuntime } from "./runtime"; import { isTestFile, listTests, runTests, TestRegistry, TESTING_NAMES, type TestReport, type TestResult } from "./testing"; @@ -33,7 +34,8 @@ export interface GameTestPayload { triggers?: TriggerMap | null; } -export function runGameTests(game: GameHandle, payload: GameTestPayload): TestReport & { error?: string } { +/** The modules run again to register the tests: the registry, or what stopped them. */ +function register(payload: GameTestPayload) { const linked = new Map(payload.modules); const collector = new Collector(); const registry = new TestRegistry(); @@ -43,7 +45,12 @@ export function runGameTests(game: GameHandle, payload: GameTestPayload): TestRe collector.running = true; const failure = runModules(linked, ENTRY_FILE, runtime, MODULE_NAME, { imported: TESTING_NAMES, then: [...linked.keys()].filter(isTestFile).sort() }); collector.running = false; - if (failure) return { list: [], results: [], only: [], ms: 0, error: `${failure.file ?? ENTRY_FILE}:${failure.line ?? 1}: ${failure.message}` }; + return { linked, registry, where, error: failure ? `${failure.file ?? ENTRY_FILE}:${failure.line ?? 1}: ${failure.message}` : null }; +} + +export function runGameTests(game: GameHandle, payload: GameTestPayload): TestReport & { error?: string } { + const { registry, where, error } = register(payload); + if (error) return { list: [], results: [], only: [], ms: 0, error }; const locations = payload.locations ?? readLocations(game); // A test of several players runs only where the game is one of several copies, one a player (A3); one game says so. const several = listTests(registry).list.filter((t) => t.kind === "test" && t.players); @@ -64,3 +71,30 @@ export function drive(game: GameHandle, payload: GameTestPayload): TestReport & return runGameTests(game, payload); } + +/** What `debugGameTest` is asked: the tests' payload and the one test to run. */ +export interface GameDebugPayload extends GameTestPayload { + test: string; + limits?: DebugLimits; +} + +/** + * One test run in the game with everything written down (A5, `gameDebug.ts`): the record goes to the editor, and the + * session stays with the host for pictures of the run's frames (`picture`). Throws when the test cannot be run so. + */ +export function debugGameTest(game: DebugHost, payload: GameDebugPayload): { recording: DebugRecording; picture(frame: number): { frame: number; kept: P; transfer: ArrayBuffer[] }; close(): void } { + const { linked, registry, where, error } = register(payload); + if (error) throw new Error(error); + const info = listTests(registry).list.find((t) => t.id === payload.test); + if (!info || info.kind !== "test") throw new Error(`There is no test with the id ${payload.test} in the script any more.`); + if (info.players) throw new Error(`"${info.name}" is a test of ${info.players.length} players: the debugger runs one game, and a test of several players runs as a copy for each. Not yet.`); + const locations = payload.locations ?? readLocations(game); + const costLines = payload.triggers && game.cost && game.payloadEnd ? new CostLines(payload.triggers, game.payloadEnd) : null; + if (costLines && game.cost) game.cost.group = costLines.group; + // (The test's line from inside a step: the stack's innermost line of a file of the script.) + const recorder = new RecordingHandle(game, () => here(linked), payload.limits); + const report = runTests(registry, { triggers: [], sources: [], ir: [], strings: [], locate: where }, { ids: [info.id], makeWorld: (_as, test) => new GameSim(recorder, { locations, variables: payload.variables, test, costLines }) }); + const result = report.results.find((r) => r.id === info.id) ?? { id: info.id, status: "skipped", message: "the test did not run", printed: [], events: [], frames: 0, ms: 0 } as TestResult; + const recording = recorder.finish({ id: info.id, name: info.name, file: info.file, line: info.line }, result, costLines ? [...costLines.places] : []); + return { recording, picture: (frame) => recorder.picture(frame), close: () => recorder.stop() }; +} diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index ee99c2a..0fe46e9 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 14\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# 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 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# 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\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\nclass Storage:\n \"\"\"A variable's cell: plain, or a 12-slot row of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n self.store = EUDArray([0] * 12) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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, per_player, player_of):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.bits = decl.get(\"bits\")\n values = decl.get(\"values\")\n self.rowed = per_player and not decl.get(\"shared\") and values is None\n self.player_of = player_of\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 self.store = EUDArray([int(v) & U32 for v in values] if values is not None else [0] * (self.length * (12 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 player's row first.\"\"\"\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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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\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\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.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 12-slot rows of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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\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 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 self.arrays = {a[\"id\"]: SliceStorage(a, self) if a.get(\"slice\") else InnerListStorage(a, self) if a.get(\"through\") else (ListStorage if a.get(\"dynamic\") else ArrayStorage)(a, self.per_player, self.player_of) for a in program.get(\"arrays\", [])}\n self.state = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.wait = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.resumes = [] # (index, Forward) for every sleep\n self.frame_end = None\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 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 declare(self, decl):\n if decl.get(\"kind\") == \"unit\":\n s = UnitStorage(decl, self.per_player, self.player_of)\n elif decl.get(\"kind\") == \"text\" and decl.get(\"text\") != \"id\":\n s = TextStorage(decl, self.per_player, self.player_of)\n else:\n s = Storage(decl, self.per_player, self.player_of)\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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n def get_state(self):\n return self.state[self.player] if self.per_player else self.state\n\n def set_state(self, value):\n if self.per_player:\n self.state[self.player] = value\n else:\n self.state << value\n\n def get_wait(self):\n return self.wait[self.player] if self.per_player else self.wait\n\n def set_wait(self, value):\n if self.per_player:\n self.wait[self.player] = value\n else:\n self.wait << value\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 == \"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 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\"] == \"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] = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: the latch\n latch = self.latches[key]\n\n def get():\n return latch[self.player] if self.per_player else latch\n\n def put(v):\n if self.per_player:\n latch[self.player] = v\n else:\n latch << v\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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"action\":\n self.action(st)\n elif k == \"print\":\n self.print_(st)\n elif k == \"centerLocation\":\n self.center_location(st)\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 index = len(self.resumes) + 1\n resume = Forward()\n self.resumes.append((index, resume))\n self.set_state(index)\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 self.set_wait(frames - 1)\n EUDJump(self.frame_end)\n resume << 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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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 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 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 = [], []\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\":\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 else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n group, room, first = [], 0, [True]\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 <= T[\"scratch\"] + TEXT_BYTES):\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 << T[\"append\"](pos, value.addr)\n else:\n if EUDIf()(pos <= T[\"scratch\"] + TEXT_BYTES):\n pos << T[\"append\"](pos, value.addr)\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 return pos, temps\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 == \"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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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.rowed))\n elif isinstance(s, TextStorage):\n for cell in (s.addr, s.block, s.len):\n pairs.append(self.cell_pair(cell, s.rowed))\n elif isinstance(s, Storage):\n pairs.append(self.cell_pair(s.store, s.rowed))\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.rowed))\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, rowed):\n if rowed:\n return (lambda: cell[self.player_of()]), (lambda v: cell.__setitem__(self.player_of(), 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 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 self.set_state(DONE)\n EUDJump(self.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 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 self.frame_end = Forward()\n start = Forward()\n skip = Forward()\n # Still waiting: count down and leave.\n EUDJumpIfNot(as_var(self.get_wait()) >= 1, skip)\n self.set_wait(as_var(self.get_wait()) - 1)\n EUDJump(self.frame_end)\n skip << NextTrigger()\n # The body once, resumes recorded as sleeps are met; the jump table is filled in after.\n table = Forward()\n EUDJump(table)\n start << NextTrigger()\n try:\n self.block(self.p[\"body\"], {})\n except Leave:\n pass\n # The body ended: the program stops for good, as the classic backend's does.\n self.set_state(DONE)\n EUDJump(self.frame_end)\n table << NextTrigger()\n EUDJumpIf(as_var(self.get_state()) == DONE, self.frame_end)\n for index, resume in self.resumes:\n EUDJumpIf(as_var(self.get_state()) == index, resume)\n EUDJump(start)\n self.frame_end << NextTrigger()\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 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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 d = s.decl\n if d.get(\"temp\"):\n continue\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_of(s).items()}})\n for id_, a in low.arrays.items():\n if isinstance(a, (InnerListStorage, SliceStorage)):\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 14\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# 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 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# 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\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\nclass Storage:\n \"\"\"A variable's cell: plain, or a 12-slot row of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n self.store = EUDArray([0] * 12) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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, per_player, player_of):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.bits = decl.get(\"bits\")\n values = decl.get(\"values\")\n self.rowed = per_player and not decl.get(\"shared\") and values is None\n self.player_of = player_of\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 self.store = EUDArray([int(v) & U32 for v in values] if values is not None else [0] * (self.length * (12 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 player's row first.\"\"\"\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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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\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\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.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 12-slot rows of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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\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 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 self.arrays = {a[\"id\"]: SliceStorage(a, self) if a.get(\"slice\") else InnerListStorage(a, self) if a.get(\"through\") else (ListStorage if a.get(\"dynamic\") else ArrayStorage)(a, self.per_player, self.player_of) for a in program.get(\"arrays\", [])}\n self.state = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.wait = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.resumes = [] # (index, Forward) for every sleep\n self.frame_end = None\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 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 declare(self, decl):\n if decl.get(\"kind\") == \"unit\":\n s = UnitStorage(decl, self.per_player, self.player_of)\n elif decl.get(\"kind\") == \"text\" and decl.get(\"text\") != \"id\":\n s = TextStorage(decl, self.per_player, self.player_of)\n else:\n s = Storage(decl, self.per_player, self.player_of)\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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n def get_state(self):\n return self.state[self.player] if self.per_player else self.state\n\n def set_state(self, value):\n if self.per_player:\n self.state[self.player] = value\n else:\n self.state << value\n\n def get_wait(self):\n return self.wait[self.player] if self.per_player else self.wait\n\n def set_wait(self, value):\n if self.per_player:\n self.wait[self.player] = value\n else:\n self.wait << value\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 == \"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 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\"] == \"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] = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: the latch\n latch = self.latches[key]\n\n def get():\n return latch[self.player] if self.per_player else latch\n\n def put(v):\n if self.per_player:\n latch[self.player] = v\n else:\n latch << v\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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"action\":\n self.action(st)\n elif k == \"print\":\n self.print_(st)\n elif k == \"centerLocation\":\n self.center_location(st)\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 index = len(self.resumes) + 1\n resume = Forward()\n self.resumes.append((index, resume))\n self.set_state(index)\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 self.set_wait(frames - 1)\n EUDJump(self.frame_end)\n resume << 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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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 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 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 = [], []\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\":\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 else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n group, room, first = [], 0, [True]\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 <= T[\"scratch\"] + TEXT_BYTES):\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 << T[\"append\"](pos, value.addr)\n else:\n if EUDIf()(pos <= T[\"scratch\"] + TEXT_BYTES):\n pos << T[\"append\"](pos, value.addr)\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 return pos, temps\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 == \"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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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.rowed))\n elif isinstance(s, TextStorage):\n for cell in (s.addr, s.block, s.len):\n pairs.append(self.cell_pair(cell, s.rowed))\n elif isinstance(s, Storage):\n pairs.append(self.cell_pair(s.store, s.rowed))\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.rowed))\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, rowed):\n if rowed:\n return (lambda: cell[self.player_of()]), (lambda v: cell.__setitem__(self.player_of(), 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 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 self.set_state(DONE)\n EUDJump(self.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 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 self.frame_end = Forward()\n start = Forward()\n skip = Forward()\n # Still waiting: count down and leave.\n EUDJumpIfNot(as_var(self.get_wait()) >= 1, skip)\n self.set_wait(as_var(self.get_wait()) - 1)\n EUDJump(self.frame_end)\n skip << NextTrigger()\n # The body once, resumes recorded as sleeps are met; the jump table is filled in after.\n table = Forward()\n EUDJump(table)\n start << NextTrigger()\n try:\n self.block(self.p[\"body\"], {})\n except Leave:\n pass\n # The body ended: the program stops for good, as the classic backend's does.\n self.set_state(DONE)\n EUDJump(self.frame_end)\n table << NextTrigger()\n EUDJumpIf(as_var(self.get_state()) == DONE, self.frame_end)\n for index, resume in self.resumes:\n EUDJumpIf(as_var(self.get_state()) == index, resume)\n EUDJump(start)\n self.frame_end << NextTrigger()\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 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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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\"):\n continue\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_of(s).items()}})\n for id_, a in low.arrays.items():\n if isinstance(a, (InnerListStorage, SliceStorage)):\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\n"; diff --git a/compiler/link.ts b/compiler/link.ts index 75c0449..2ecf027 100644 --- a/compiler/link.ts +++ b/compiler/link.ts @@ -87,6 +87,50 @@ export function runModules(files: ReadonlyMap, entry: string } } +/** + * Where in the script the code calling this is now: the innermost line of a file of the script on the stack. Called + * every frame of a debugged test (A5), so on V8 it reads the stack as call sites rather than text: Node with source + * maps on formats every frame's path, which cost a debugged run more than the game did. + */ +const placed = new WeakMap>(); + +export function here(files: ReadonlyMap): { file: string; line: number; column: number } | null { + type Site = { getFileName(): string | null; getScriptNameOrSourceURL?(): string | null; getLineNumber(): number | null; getColumnNumber(): number | null }; + const E = Error as unknown as { prepareStackTrace?: unknown; captureStackTrace?: (o: object) => void }; + if (typeof E.captureStackTrace === "function") { + const was = E.prepareStackTrace; + const holder: { stack?: unknown } = {}; + try { + E.prepareStackTrace = (_e: unknown, sites: Site[]) => sites; + E.captureStackTrace(holder); + const sites = holder.stack; + if (Array.isArray(sites)) { + for (const site of sites as Site[]) { + // (A Function's body is named by its sourceURL, which only this says.) + const name = site.getScriptNameOrSourceURL?.() ?? site.getFileName(); + if (!name || !name.startsWith(SOURCE_URL_PREFIX)) continue; + const file = name.slice(SOURCE_URL_PREFIX.length), linked = files.get(file); + if (!linked) continue; + const line = (site.getLineNumber() ?? 1) - 2, column = site.getColumnNumber() ?? 1; + let seen = placed.get(linked); + if (!seen) { seen = new Map(); placed.set(linked, seen); } + const key = line * 65536 + column; + let out = seen.get(key); + if (!out) { + const original = linked.map ? mapPosition(linked.map, line, column) : null; + out = original ? { file, line: original.line, column: original.column } : { file, line: Math.max(1, line), column }; + seen.set(key, out); + } + return out; + } + return null; + } + } catch { /* the text below */ } finally { E.prepareStackTrace = was; } + } + const at = locate(new Error(), files); + return at.file ? { file: at.file, line: at.line ?? 1, column: at.column ?? 1 } : null; +} + /** The error's message and, from its stack, where in the script it happened. */ export function locate(err: unknown, files: ReadonlyMap): RunError { const message = err instanceof Error ? err.message : String(err); diff --git a/probes/game-debug.ts b/probes/game-debug.ts new file mode 100644 index 0000000..6fcec8c --- /dev/null +++ b/probes/game-debug.ts @@ -0,0 +1,43 @@ +// TrigScript 4, A5: the debugger in the game — a program with some work in every frame (a loop over units, an array +// kept sorted, a wave counter) and tests to debug: one that passes over a long run, and one that fails on purpose, +// so a debugging session starts from a failure. +// npx tsx scripts/debug-game.mts probes/game-debug.ts +// npx tsx scripts/test-game.mts probes/game-debug.ts +// Built like a probe (Player 1 Terran, Player 2 a Zerg computer, location 1 "Home" by Player 1's base). +import { test, expect, program, sleep, frames, seconds, createUnit, unitsAt, units, P1, P2, type Location } from "trigscript"; + +const HOME = 1 as Location; + +program(() => { + let wave = 0; + let alive = 0; + let hp = 0; + const best: number[] = []; + while (true) { + sleep(seconds(2)); + wave += 1; + createUnit(P2, units.ZergZergling, 2, HOME); + alive = 0; + hp = 0; + for (const z of unitsAt(HOME, { owner: P2 })) { alive += 1; hp += z.hp; } + // The three highest totals so far, highest first. + best.push(hp); + for (let i = best.length - 1; i > 0; i--) { + if (best[i] > best[i - 1]) { const t = best[i]; best[i] = best[i - 1]; best[i - 1] = t; } + } + if (best.length > 3) best.pop(); + } +}, { owner: P1, name: "waves" }); + +test("waves come every two seconds", (sim) => { + sim.seconds(9); + const w = sim.program("waves"); + expect(w.wave).toBe(4); + expect((w.best as number[]).length).toBe(3); +}); + +test("the Marines hold Home (fails: there are none)", (sim) => { + sim.place(P1, units.TerranMarine, HOME, 2); + sim.until(() => (sim.program("waves").wave as number) >= 3); + expect(sim.program("waves").alive).toBe(0); +}); diff --git a/python/trigscript.py b/python/trigscript.py index 7888737..0da33e9 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -3229,8 +3229,9 @@ def _program_places(k, low): p = low.p out = {"index": k, "name": p.get("name"), "perPlayer": low.per_player, "owners": p.get("owners", [low.owner]), "vars": [], "arrays": []} for id_, s in low.vars.items(): - d = s.decl - if d.get("temp"): + # (A loop's unit is the scan's own pointer for one turn, kept nowhere: nothing to read.) + d = getattr(s, "decl", None) + if d is None or d.get("temp"): continue out["vars"].append({"id": id_, "kind": d.get("kind"), "text": d.get("text"), "bits": d.get("bits"), "unsigned": bool(d.get("unsigned")), "rowed": bool(s.rowed), "cells": {name: _place(cell) for name, cell in _cells_of(s).items()}}) diff --git a/scripts/debug-game.mts b/scripts/debug-game.mts new file mode 100644 index 0000000..33c058f --- /dev/null +++ b/scripts/debug-game.mts @@ -0,0 +1,144 @@ +/** + * The debugger's record of a test run in the lifted game (TrigScript 4, A5), under Node: each test of a script run + * once with everything written down (compiler/gameDebug.ts), what the record holds said in numbers, and the record + * held against the game itself: at frames across the run, a picture of the frame (the nearest kept game, the test's + * own actions done again, the frames played) must hold the script's memory as the record says it was then, and the + * program's variables read from the two must agree. + * + * npx tsx scripts/debug-game.mts [test name] [map.scx] + * + * Needs a scm-sim checkout beside this repository (or SCM_SIM_DIR) with its lifted game generated. + */ +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { basename, join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import ts from "typescript"; +import { compileScript } from "../compiler/compiler"; +import { readLocations } from "../compiler/gameSim"; +import { defaultScriptNames } from "../compiler/names"; +import { STEP_EUDPLIB, STEP_TEST, STEP_UNSEEN, type DebugHost, type DebugRecording } from "../compiler/gameDebug"; +import { programVariables, type VariableMap } from "../compiler/gameVariables"; +import { defaultLib } from "../bundle/lib.mjs"; + +const root = resolve(import.meta.dirname, ".."); +const [script, only, baseMap] = process.argv.slice(2); +if (!script || !existsSync(script)) { console.error("usage: debug-game.mts [test name] [map.scx]"); process.exit(2); } +const sim = resolve(process.env.SCM_SIM_DIR ?? join(root, "..", "scm-sim")); +if (!existsSync(join(sim, "src", "exact", "testGame.ts"))) { console.error(`no scm-sim checkout with src/exact/testGame.ts at ${sim} (set SCM_SIM_DIR)`); process.exit(2); } +const from = (p: string) => import(pathToFileURL(join(sim, p)).href); + +const dir = mkdtempSync(join(tmpdir(), "trigscript-debug-")); +let bad = 0; +try { + const built = join(dir, basename(script, ".ts") + ".scx"); + const b = spawnSync("npx", ["tsx", join(root, "scripts", "build-fixture.mts"), resolve(script), built, ...(baseMap ? [resolve(baseMap)] : [])], { cwd: root, encoding: "utf8", maxBuffer: 256 << 20, env: { ...process.env, VARIABLE_MAP: "1", TRIGGER_MAP: "1" } }); + if (b.status !== 0) { console.error(b.stdout.split("\n").slice(-20).join("\n"), b.stderr); process.exit(1); } + + const { Archive } = await import(pathToFileURL(join(sim, "node_modules", "mopaq", "dist", "index.mjs")).href); + const { exactMissing, startExactGame } = await from("src/exact/node.ts"); + const { classicScenario, installEud, placeStrings } = await from("src/exact/eud.ts"); + const { testGame } = await from("src/exact/testGame.ts"); + const { installCost } = await from("src/exact/cost.ts"); + const missing = exactMissing() as string[]; + if (missing.length) { console.error("the lifted game cannot run here; missing:\n " + missing.join("\n ")); process.exit(1); } + const chk = Archive.open(new Uint8Array(readFileSync(built))).readFile("staredit\\scenario.chk") as Uint8Array; + const chkPath = join(dir, "scenario.chk"); + writeFileSync(chkPath, classicScenario(chk)); + const run = await startExactGame(chkPath, { game: "ums" }); + const strings = placeStrings(run.lifted, chk); + const eud = installEud(run.lifted, strings); + const payloadEnd = 0x191943c8 + strings.bytes; + const handle = testGame(run.engine, run.lifted, [{ from: strings.gameTable, to: payloadEnd }], { cost: installCost(run.lifted), payloadEnd, eud }) as DebugHost; + + const source = { "main.ts": readFileSync(script, "utf8") }; + const names = defaultScriptNames(); + const locations = readLocations(handle); + const compiled = compileScript(ts, source, names, { lib: defaultLib(), tests: {} }); + if (!compiled.modules) { console.error(compiled.ok ? "the script has no tests" : compiled.diagnostics.map((d) => `${d.file}:${d.line}: ${d.message}`).join("\n")); process.exit(1); } + const bundle = join(dir, "gametest.js"); + spawnSync("npx", ["esbuild", join(root, "compiler", "gameTestEntry.ts"), "--bundle", "--format=esm", "--target=es2022", "--platform=neutral", `--outfile=${bundle}`], { cwd: root }); + const host = await import(`data:text/javascript;base64,${Buffer.from(readFileSync(bundle)).toString("base64")}`) as typeof import("../compiler/gameTestEntry"); + const variables = JSON.parse(readFileSync(built.replace(/\.scx$/, ".variables.json"), "utf8")) as VariableMap; + const triggers = JSON.parse(readFileSync(built.replace(/\.scx$/, ".triggers.json"), "utf8")); + const payload = { modules: compiled.modules, names, locations, variables, triggers }; + const listed = host.runGameTests(handle, { ...payload, ids: [] }); + const tests = listed.list.filter((t) => t.kind === "test" && !t.players && (!only || t.name.includes(only))); + if (!tests.length) { console.error(only ? `no test named like "${only}"` : "the script has no test of one player"); process.exit(1); } + + for (const t of tests) { + const t0 = Date.now(); + const session = host.debugGameTest(handle, { ...payload, test: t.id }); + const r: DebugRecording = session.recording; + const steps = r.stepLine.length, writes = r.writes.length / 3; + const kinds = { line: 0, eudplib: 0, test: 0, unseen: 0 }; + for (const l of r.stepLine) { if (l === STEP_EUDPLIB) kinds.eudplib++; else if (l === STEP_TEST) kinds.test++; else if (l === STEP_UNSEEN) kinds.unseen++; else kinds.line++; } + console.log(`${r.result.status === "passed" ? "✓" : r.result.status === "failed" ? "✗" : "–"} ${t.name}: ${r.frames} frames (${r.first}–${r.first + r.frames - 1}), recorded in ${r.ms} ms`); + console.log(` ${steps.toLocaleString("en")} steps (${kinds.line} of lines, ${kinds.eudplib} eudplib's, ${kinds.test} the test's, ${kinds.unseen} unseen), ${writes.toLocaleString("en")} writes, ${r.lines.length} lines, ${r.kept.length} kept games${r.cut ? `; CUT at frame ${r.cut.frame}: ${r.cut.why}` : ""}`); + console.log(` record: ${Math.round((r.memory.bytes.byteLength + r.writes.byteLength + 4 * (r.stepLine.length * 3 + r.frameSteps.length + r.testLine.length * 2)) / 1024)} KB`); + const busiest = [...r.frameSteps.slice(1)].map((e, k) => ({ k, n: e - r.frameSteps[k] })).sort((a, b) => b.n - a.n)[0]; + if (busiest) console.log(` busiest frame ${r.first + busiest.k}: ${busiest.n} steps; test lines asked for frames: ${[...new Set(r.testLine)].filter(Boolean).join(", ") || "none"}`); + if (kinds.unseen) console.log(` note: ${kinds.unseen} frames had writes no record said (the game's own routines)`); + + // The record against the game: pictures at frames across the run. + if (!r.frames) continue; + const at = [...new Set([0, 1, Math.floor(r.frames / 3), Math.floor(r.frames / 2), r.frames - 2, r.frames - 1].filter((k) => k >= 0 && k < r.frames))]; + // Backwards first, then forwards: a picture from a kept game and one played on from the last. + for (const k of [...at].reverse().concat(at)) { + const frame = r.first + k; + const p0 = Date.now(); + const pic = session.picture(frame); + const ms = Date.now() - p0; + const mem = memoryAt(r, k); + const live = handle.u8.subarray(r.memory.at, r.memory.at + mem.byteLength); + let differ = 0, firstAt = -1; + for (let i = 0; i < mem.byteLength; i += 4) if (mem[i] !== live[i] || mem[i + 1] !== live[i + 1] || mem[i + 2] !== live[i + 2] || mem[i + 3] !== live[i + 3]) { differ++; if (firstAt < 0) firstAt = r.memory.at + i; } + let vars = ""; + if (variables.programs.length) { + const a = JSON.stringify(readAll(handle.m, handle.u8, variables)), c = JSON.stringify(readAll(overlay(handle.u8, r.memory.at, mem), overlayBytes(handle.u8, r.memory.at, mem), variables)); + vars = a === c ? ", variables agree" : `, VARIABLES DIFFER: game ${a.slice(0, 160)} / record ${c.slice(0, 160)}`; + if (a !== c) bad++; + } + if (differ || pic.frame !== frame) bad++; + console.log(` picture of frame ${frame}: ${ms} ms, at ${pic.frame}${differ ? `, ${differ} DWORDS DIFFER from the record (first 0x${firstAt.toString(16)})` : ", memory as the record says"}${vars}`); + } + session.close(); + console.log(` ${Date.now() - t0} ms in all`); + } + console.log(bad ? `\n${bad} checks failed` : "\nevery check held"); + if (bad) process.exitCode = 1; +} finally { + rmSync(dir, { recursive: true, force: true }); +} + +/** The script's memory at the end of record frame `k`: the start, and every write up to the end of that frame. */ +function memoryAt(r: DebugRecording, k: number): Uint8Array { + const out = r.memory.bytes.slice(); + const v = new DataView(out.buffer); + const end = r.stepWrites[r.frameSteps[k + 1]]; + for (let w = 0; w < end; w++) v.setUint32(r.writes[3 * w] - r.memory.at, r.writes[3 * w + 2], true); + return out; +} + +/** The game's memory with the record's copy of the script's laid over it (a reader that reads the record). */ +function overlayBytes(u8: Uint8Array, at: number, mem: Uint8Array): Uint8Array { + return new Proxy(u8, { get(t, p) { if (typeof p === "string" && /^\d+$/.test(p)) { const i = Number(p); return i >= at && i < at + mem.length ? mem[i - at] : t[i]; } if (p === "subarray") return (a: number, b: number) => (a >= at && b <= at + mem.length ? mem.subarray(a - at, b - at) : t.subarray(a, b)); const v = Reflect.get(t, p, t); return typeof v === "function" ? v.bind(t) : v; } }); +} +function overlay(u8: Uint8Array, at: number, mem: Uint8Array): DataView { + const real = new DataView(u8.buffer, u8.byteOffset, u8.byteLength), mine = new DataView(mem.buffer); + return new Proxy(real, { get(t, p) { if (p === "getUint32" || p === "getInt32" || p === "getUint16" || p === "getUint8") return (i: number, le?: boolean) => (i >= at && i < at + mem.length ? (mine[p] as (i: number, le?: boolean) => number)(i - at, le) : (t[p] as (i: number, le?: boolean) => number)(i, le)); const v = Reflect.get(t, p, t); return typeof v === "function" ? v.bind(t) : v; } }); +} + +function readAll(m: DataView, u8: Uint8Array, map: VariableMap): unknown { + const out: Record = {}; + for (const p of map.programs) for (const who of p.perPlayer ? p.owners : [undefined]) { + try { + const vars = programVariables({ m, u8, unitAt: (ptr, uid) => (ptr ? ({ slot: ptr, uid } as never) : null) }, map, p.name ?? p.index, who); + const plain: Record = {}; + for (const v of [...p.vars.map((x) => x.id), ...p.arrays.map((x) => x.id)]) { const name = v.replace(/#\d+$/, ""); try { plain[name] = JSON.parse(JSON.stringify(vars[name] ?? null)); } catch { plain[name] = "?"; } } + out[`${p.name ?? p.index}${who !== undefined ? `@P${who + 1}` : ""}`] = plain; + } catch (e) { out[String(p.index)] = String(e); } + } + return out; +} diff --git a/tests/game-debug.test.ts b/tests/game-debug.test.ts new file mode 100644 index 0000000..50d1410 --- /dev/null +++ b/tests/game-debug.test.ts @@ -0,0 +1,121 @@ +// The debugger's record (TrigScript 4, A5: compiler/gameDebug.ts) over a small game of the test's own: what it writes +// down of each frame, and a picture of a frame played again with the test's own actions between frames. The lifted +// game itself is checked by scripts/debug-game.mts, where a scm-sim checkout is. +import { describe, expect, it } from "vitest"; +import { RecordingHandle, STEP_TEST, STEP_UNSEEN, type DebugHost } from "../compiler/gameDebug"; +import type { TestResult } from "../compiler/testing"; + +const FROM = 0x100, END = 0x200, POKED = 0x40; + +/** + * A game of 1 KB. Each frame: line 0 runs (two records), adds the byte at `POKED` (where the test writes) to the + * dword at FROM; line 1 runs (one record) and counts frames at FROM + 4; with `sneak`, the game also writes FROM + 8 + * itself, which no record says. + */ +function fakeGame(sneak = false) { + const u8 = new Uint8Array(1024), m = new DataView(u8.buffer); + let frame = 0; + const host = { + m, u8, + get frame() { return frame; }, + localPlayer: 0, over: false, payloadEnd: END, + eud: { listen: null as ((record: number, address: number, write: boolean, mask: number, was: number) => void) | null }, + cost: { trace: null as ((record: number, group: number, action: boolean) => void) | null, paused: false, group: null, report: () => { throw new Error("not here"); } }, + write(record: number, address: number, value: number) { + const was = m.getUint32(address, true); + m.setUint32(address, value, true); + this.eud.listen?.(record, address, true, 0xffffffff, was); + }, + step() { + frame++; + const t = this.cost.paused ? null : this.cost.trace; + t?.(1000, 0, false); t?.(1032, 0, true); + this.write(1032, FROM, m.getUint32(FROM, true) + u8[POKED]); + t?.(2000, 1, true); + this.write(2000, FROM + 4, frame); + if (sneak && frame === 2) m.setUint32(FROM + 8, 77, true); + }, + lines: () => [], + action: () => 0, chat: () => {}, call: () => 0, + reset() { u8.fill(0); frame = 0; }, + keep: () => ({ frame, u8: u8.slice(), bytes: 1024 }), + restore(k: { frame: number; u8: Uint8Array }) { u8.set(k.u8); frame = k.frame; }, + portable: (k: { frame: number; u8: Uint8Array }) => ({ kept: { frame: k.frame, u8: k.u8.slice() }, transfer: [] }), + }; + return host; +} + +const passed: TestResult = { id: "t", status: "passed", printed: [], events: [], frames: 0, ms: 0 }; +const test = { id: "t", name: "t", file: "main.ts", line: 1 }; + +describe("the debugger's record", () => { + it("writes down each frame's steps, by line, and every write with what it held before", () => { + const game = fakeGame(); + const r = new RecordingHandle(game as unknown as DebugHost, () => ({ file: "main.ts", line: 7 }), {}, FROM); + r.reset(); + r.step(); r.step(); r.step(); + const rec = r.finish(test, passed, [{ file: "main.ts", line: 3 }, { file: "main.ts", line: 4 }]); + expect(rec.frames).toBe(3); + expect(rec.first).toBe(1); + expect([...rec.frameSteps]).toEqual([0, 2, 4, 6]); + expect([...rec.stepLine]).toEqual([0, 1, 0, 1, 0, 1]); + expect([...rec.stepRecords]).toEqual([2, 1, 2, 1, 2, 1]); + expect([...rec.testLine]).toEqual([7, 7, 7]); + // Nothing was poked, so line 0 writes nothing new; line 1 counts 1, 2, 3 at FROM + 4. + expect([...rec.writes]).toEqual([FROM + 4, 0, 1, FROM + 4, 1, 2, FROM + 4, 2, 3]); + expect([...rec.stepWrites]).toEqual([0, 0, 1, 1, 2, 2, 3]); + expect(rec.memory.at).toBe(FROM); + expect(rec.memory.bytes.byteLength).toBe(END - FROM); + }); + + it("puts what no record said down as the frame's last step", () => { + const game = fakeGame(true); + const r = new RecordingHandle(game as unknown as DebugHost, () => null, {}, FROM); + r.reset(); + r.step(); r.step(); r.step(); + const rec = r.finish(test, passed, []); + expect([...rec.stepLine.slice(rec.frameSteps[1], rec.frameSteps[2])]).toEqual([0, 1, STEP_UNSEEN]); + const w = rec.stepWrites[rec.frameSteps[2] - 1]; + expect([...rec.writes.slice(3 * w, 3 * w + 3)]).toEqual([FROM + 8, 0, 77]); + }); + + it("puts a write the test's own action made between frames under the test, first in the next frame", () => { + const game = fakeGame(); + const r = new RecordingHandle(game as unknown as DebugHost, () => null, {}, FROM); + r.reset(); + r.step(); + game.write(5000, FROM + 12, 9); + r.step(); + const rec = r.finish(test, passed, []); + expect([...rec.stepLine]).toEqual([0, 1, STEP_TEST, 0, 1]); + expect([...rec.frameSteps]).toEqual([0, 2, 5]); + }); + + it("makes a picture of any frame with the test's writes between frames done again", () => { + const game = fakeGame(); + const r = new RecordingHandle(game as unknown as DebugHost<{ frame: number; u8: Uint8Array }, { frame: number; u8: Uint8Array }>, () => null, { every: 4 }, FROM); + r.reset(); + // Frame by frame, the test pokes 1, 2, 3 … at POKED, which the game adds up at FROM. + const sums: number[] = []; + for (let f = 1; f <= 10; f++) { game.u8[POKED] = f; r.wrote(POKED, 1); r.step(); sums.push(game.m.getUint32(FROM, true)); } + r.finish(test, passed, []); + for (const f of [7, 3, 4, 5, 10, 1]) { + const pic = r.picture(f); + expect(pic.frame).toBe(f); + expect(new DataView(pic.kept.u8.buffer).getUint32(FROM, true)).toBe(sums[f - 1]); + } + expect(r.picture(99).frame).toBe(10); + }); + + it("stops writing down past its limits, and says where", () => { + const game = fakeGame(); + const r = new RecordingHandle(game as unknown as DebugHost, () => null, { steps: 5 }, FROM); + r.reset(); + for (let i = 0; i < 5; i++) r.step(); + const rec = r.finish(test, passed, []); + expect(rec.frames).toBe(5); + expect(rec.stepLine.length).toBe(5); + expect(rec.cut?.frame).toBe(3); + expect(rec.cut?.why).toMatch(/more than 5 steps/); + }); +}); From 794fd66eb329e0b04c92772d4e741ccc6ca1e6d1 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 13:20:36 -0600 Subject: [PATCH 019/238] TrigScript 4, A5: walking a debugged run (compiler/debugModel.ts) A cursor over the record: steps by line (eudplib's own skipped unless asked), frames, back as quickly as forward (writes put back), continue to a breakpoint or a watched variable's change in either direction, the variables at the cursor read as sim.program() reads them, what each line wrote in the frame, each frame's cost. Co-Authored-By: Claude Opus 5.5 --- compiler/debugModel.ts | 348 ++++++++++++++++++++++++++++++++++++++ tests/debug-model.test.ts | 143 ++++++++++++++++ 2 files changed, 491 insertions(+) create mode 100644 compiler/debugModel.ts create mode 100644 tests/debug-model.test.ts diff --git a/compiler/debugModel.ts b/compiler/debugModel.ts new file mode 100644 index 0000000..5239efe --- /dev/null +++ b/compiler/debugModel.ts @@ -0,0 +1,348 @@ +/** + * Walking a debugged run (TrigScript 4, A5): a cursor over a `DebugRecording`, and the script's memory as it was at + * the cursor. Nothing here touches the game or the page: the record holds everything, so a step back is as quick as + * a step forward (the writes between the two put back or done again). + * + * Where the cursor stands is a frame of the record and a step of it: `{ k, s }`, with `s` from the frame's first step + * to its end (`frameSteps[k]` … `frameSteps[k + 1]`). Standing at step `s` is standing before it, its line about to + * run, the memory as every step before it left it; standing at the end of frame k is the game as the frame left it, + * the same memory as before frame k + 1's first step. + */ +import type { DebugRecording } from "./gameDebug"; +import { programVariables, type GameArray, type GameVariable, type VariableMap } from "./gameVariables"; + +export interface DebugPlace { file: string; line: number } +export interface DebugAt { k: number; s: number } + +/** Why a continue stopped: a breakpoint on a line, a watched variable that changed, or the run's end (or start). */ +export type DebugStop = { kind: "breakpoint"; place: DebugPlace } | { kind: "watch"; name: string; before: string; after: string } | { kind: "end" } | { kind: "start" }; + +/** What the variables view shows of one program: its name, for whom, and each variable's value as the source has it. */ +export interface DebugProgram { label: string; values: { name: string; value: unknown }[] } + +const key = (p: DebugPlace) => `${p.file}:${p.line}`; +const UNITS = 0x59cca8, UNIT_BYTES = 336; + +/** A unit a variable holds, as the debugger says it (the unit table is not in the record). */ +export interface DebugUnitRef { unitSlot: number } + +export class DebugCursor { + readonly rec: DebugRecording; + private readonly map: VariableMap | null; + /** The script's memory at the cursor (`rec.memory.at` on). */ + readonly memory: Uint8Array; + private readonly view: DataView; + private at: DebugAt = { k: 0, s: 0 }; + /** Writes done: everything before this index of `rec.writes` (by write). */ + private done = 0; + /** The step's line, or null for eudplib's own and the rest. */ + readonly placeOf: (s: number) => DebugPlace | null; + + constructor(rec: DebugRecording, map: VariableMap | null) { + this.rec = rec; + this.map = map; + this.memory = rec.memory.bytes.slice(); + this.view = new DataView(this.memory.buffer); + this.placeOf = (s) => { const l = rec.stepLine[s]; return l >= 0 ? rec.lines[l] ?? null : null; }; + if (rec.frames) this.moveTo({ k: 0, s: rec.frameSteps[0] }); + } + + get where(): DebugAt { return { ...this.at }; } + get frames(): number { return this.rec.frames; } + /** The game frame of record frame k. */ + frame(k = this.at.k): number { return this.rec.first + k; } + frameStart(k: number): number { return this.rec.frameSteps[k]; } + frameEnd(k: number): number { return this.rec.frameSteps[k + 1]; } + atEnd(): boolean { return this.at.s >= this.frameEnd(this.at.k); } + /** The memory is what the record says only up to where it was cut. */ + get known(): boolean { return !this.rec.cut || this.frame() < this.rec.cut.frame; } + + /** The line about to run (null at a frame's end, or for a step that is no line of the script's). */ + line(): DebugPlace | null { return this.atEnd() ? null : this.placeOf(this.at.s); } + /** The test's line that asked for the frame the cursor is in. */ + testLine(k = this.at.k): DebugPlace | null { + const f = this.rec.testFileOf[k], line = this.rec.testLine[k]; + return f >= 0 && line > 0 ? { file: this.rec.testFile[f], line } : null; + } + + moveTo(to: DebugAt): void { + const k = Math.max(0, Math.min(to.k, this.rec.frames - 1)); + const s = Math.max(this.frameStart(k), Math.min(to.s, this.frameEnd(k))); + const target = this.rec.stepWrites[s]; + const w = this.rec.writes, base = this.rec.memory.at, v = this.view; + while (this.done < target) { const i = 3 * this.done++; v.setUint32(w[i] - base, w[i + 2], true); } + while (this.done > target) { const i = 3 * --this.done; v.setUint32(w[i] - base, w[i + 1], true); } + this.at = { k, s }; + } + + /** A step the person stops at: a line of the script's, or every step with `all`. */ + private visible(s: number, all: boolean): boolean { return all || this.rec.stepLine[s] >= 0; } + + /** The next line (or the frame's end, then the next frame's first line). */ + stepForward(all = false): void { + const { k, s } = this.at; + for (let i = s + 1; i < this.frameEnd(k); i++) if (this.visible(i, all)) { this.moveTo({ k, s: i }); return; } + if (s < this.frameEnd(k)) { this.moveTo({ k, s: this.frameEnd(k) }); return; } + if (k + 1 >= this.rec.frames) return; + for (let i = this.frameStart(k + 1); i < this.frameEnd(k + 1); i++) if (this.visible(i, all)) { this.moveTo({ k: k + 1, s: i }); return; } + this.moveTo({ k: k + 1, s: this.frameEnd(k + 1) }); + } + + /** The line before (or the end of the frame before). */ + stepBack(all = false): void { + const { k, s } = this.at; + for (let i = s - 1; i >= this.frameStart(k); i--) if (this.visible(i, all)) { this.moveTo({ k, s: i }); return; } + if (k > 0) this.moveTo({ k: k - 1, s: this.frameEnd(k - 1) }); + else this.moveTo({ k: 0, s: this.frameStart(0) }); + } + + /** To the end of this frame; from a frame's end, to the end of the next. */ + nextFrame(): void { + const { k, s } = this.at; + if (s < this.frameEnd(k)) this.moveTo({ k, s: this.frameEnd(k) }); + else if (k + 1 < this.rec.frames) this.moveTo({ k: k + 1, s: this.frameEnd(k + 1) }); + } + + /** To the end of the frame before (the start of this one). */ + prevFrame(): void { + const { k } = this.at; + if (k > 0) this.moveTo({ k: k - 1, s: this.frameEnd(k - 1) }); + else this.moveTo({ k: 0, s: this.frameStart(0) }); + } + + /** Record frame k's end. */ + toFrame(k: number): void { this.moveTo({ k, s: this.frameEnd(Math.max(0, Math.min(k, this.rec.frames - 1))) }); } + + /** The cell addresses of every variable named in `watch` ("waves.wave", or "wave" in any program), at the cursor. */ + private watched(watch: readonly string[]): Map { + const out = new Map(); + if (!this.map) return out; + for (const p of this.map.programs) for (const name of watch) { + const [prog, v] = name.includes(".") ? name.split(".", 2) : [null, name]; + if (prog !== null && prog !== (p.name ?? String(p.index))) continue; + for (const [addr] of this.cellsOf(p, v)) out.set(addr, name); + } + return out; + } + + /** A variable's cells in a program: its own, an array's (a growing one's block as the cursor finds it). */ + private cellsOf(p: VariableMap["programs"][number], name: string): Map { + const out = new Map(); + for (const v of p.vars) if (top(v.id) === name) for (const at of Object.values(v.cells)) if (at != null) for (let r = 0; r < (v.rowed ? 12 : 1); r++) out.set(at + 4 * r, true); + for (const a of p.arrays) if (top(a.id) === name) for (const [addr] of this.arrayCells(a)) out.set(addr, true); + return out; + } + + private arrayCells(a: GameArray): Map { + const out = new Map(); + const rows = a.rowed ? 12 : 1; + if (!a.dynamic && a.at != null) for (let i = 0; i < Math.min(a.length * rows, 4096); i++) out.set(a.at + 4 * i, true); + if (a.dynamic) { + for (const c of [a.ptr, a.len]) if (c != null) for (let r = 0; r < rows; r++) out.set(c + 4 * r, true); + // (The block the handle points at now, as far as its length says.) + for (let r = 0; r < rows; r++) { + const ptr = a.ptr != null ? this.read32(a.ptr + 4 * r) : 0, len = a.len != null ? this.read32(a.len + 4 * r) : 0; + if (ptr) for (let i = 0; i < Math.min(len, 4096); i++) out.set(ptr + 4 * i, true); + } + } + return out; + } + + private read32(addr: number): number { + const i = addr - this.rec.memory.at; + return i >= 0 && i + 4 <= this.memory.length ? this.view.getUint32(i, true) : 0; + } + + /** + * On to the next breakpoint (a line of `breakpoints` about to run, or a test's line that asks for a frame, at that + * frame's end) or the next change of a watched variable (stopped just after the step that made it), else the end. + */ + continueForward(breakpoints: ReadonlySet, watch: readonly string[] = []): DebugStop { + return this.run(1, breakpoints, watch); + } + /** The same, backwards: the last stop before the cursor, else the start. */ + continueBack(breakpoints: ReadonlySet, watch: readonly string[] = []): DebugStop { + return this.run(-1, breakpoints, watch); + } + + private run(dir: 1 | -1, breakpoints: ReadonlySet, watch: readonly string[]): DebugStop { + const rec = this.rec; + const cells = this.watched(watch); + const w = rec.writes; + const touches = (s: number): string | null => { + for (let i = rec.stepWrites[s]; i < rec.stepWrites[s + 1]; i++) { const name = cells.get(w[3 * i]); if (name) return name; } + return null; + }; + let { k, s } = this.at; + // Positions in order: each step of a frame, then its end. A position is checked as it is reached, never where + // the cursor stood; `passed` is the step gone over on the way to it. + const next = (): number | null => { + if (dir > 0) { + if (s < this.frameEnd(k)) { s++; return s - 1; } + if (k + 1 >= rec.frames) return Number.NaN; + k++; s = this.frameStart(k); return null; + } + if (s > this.frameStart(k)) { s--; return s; } + if (k === 0) return Number.NaN; + k--; s = this.frameEnd(k); return null; + }; + for (;;) { + const passed = next(); + if (Number.isNaN(passed)) break; + // A watched variable a passed step wrote, and whose value it changed: stop just after that step. + const name = passed !== null && cells.size ? touches(passed) : null; + if (name && passed !== null) { + this.moveTo({ k, s: passed }); + const before = this.watchText(name); + this.moveTo({ k, s: passed + 1 }); + const after = this.watchText(name); + if (before !== after) return { kind: "watch", name, before, after }; + } + if (s < this.frameEnd(k)) { + const place = this.placeOf(s); + if (place && breakpoints.has(key(place))) { this.moveTo({ k, s }); return { kind: "breakpoint", place }; } + } else { + const t = this.testLine(k); + if (t && breakpoints.has(key(t))) { this.moveTo({ k, s }); return { kind: "breakpoint", place: t }; } + } + } + if (dir > 0) { this.moveTo({ k: rec.frames - 1, s: this.frameEnd(rec.frames - 1) }); return { kind: "end" }; } + this.moveTo({ k: 0, s: this.frameStart(0) }); + return { kind: "start" }; + } + + /** A watched variable's value as text, at the cursor ("?" where it cannot be read). */ + watchText(name: string): string { + const [prog, v] = name.includes(".") ? name.split(".", 2) : [null, name]; + for (const p of this.programs()) { + if (prog !== null && !p.label.startsWith(prog)) continue; + const found = p.values.find((x) => x.name === v); + if (found) return show(found.value); + } + return "?"; + } + + /** The reader `programVariables` reads the record's memory through. */ + private reader() { + const at = this.rec.memory.at, mem = this.memory, view = this.view; + const inside = (i: number, n: number) => i >= at && i + n <= at + mem.length; + const m = { + getUint32: (i: number) => (inside(i, 4) ? view.getUint32(i - at, true) : 0), + getInt32: (i: number) => (inside(i, 4) ? view.getInt32(i - at, true) : 0), + getUint16: (i: number) => (inside(i, 2) ? view.getUint16(i - at, true) : 0), + getInt16: (i: number) => (inside(i, 2) ? view.getInt16(i - at, true) : 0), + getUint8: (i: number) => (inside(i, 1) ? mem[i - at] : 0), + getInt8: (i: number) => (inside(i, 1) ? (mem[i - at] << 24) >> 24 : 0), + } as unknown as DataView; + const u8 = new Proxy(mem, { + get(t, p) { + if (typeof p === "string" && /^\d+$/.test(p)) { const i = Number(p); return i >= at && i < at + t.length ? t[i - at] : 0; } + if (p === "length") return at + t.length; + if (p === "subarray") return (a: number, b: number) => t.subarray(Math.max(0, a - at), Math.max(0, b - at)); + const v = Reflect.get(t, p, t); + return typeof v === "function" ? v.bind(t) : v; + }, + }) as Uint8Array; + return { m, u8, unitAt: (ptr: number) => (ptr ? ({ unitSlot: Math.round((ptr - UNITS) / UNIT_BYTES) } satisfies DebugUnitRef as never) : null) }; + } + + /** Every program's variables at the cursor, as the source names them. */ + programs(): DebugProgram[] { + const map = this.map; + if (!map) return []; + const out: DebugProgram[] = []; + const reader = this.reader(); + for (const p of map.programs) for (const who of p.perPlayer ? p.owners : [undefined]) { + const label = `${p.name ?? `program ${p.index}`}${who !== undefined ? ` (P${who + 1})` : ""}`; + let vars: Record; + try { vars = programVariables(reader, map, p.name ?? p.index, who); } catch { continue; } + const names = [...new Set([...p.vars.filter((v) => !isTemp(v)).map((v) => v.id), ...p.arrays.map((a) => a.id)].map(top))]; + const values: { name: string; value: unknown }[] = []; + for (const name of names) { let value: unknown; try { value = plain(vars[name]); } catch { value = "?"; } values.push({ name, value }); } + out.push({ label, values }); + } + return out; + } + + /** + * What each line run so far in the cursor's frame wrote: the variables, by line, with their values at the cursor; + * and how many steps of the frame each line took. For the values beside the lines (`label`). + */ + frameLines(): { place: DebugPlace; steps: number; names: string[] }[] { + const map = this.map; + const { k, s } = this.at; + const byLine = new Map }>(); + const owners = this.cellOwners(); + const w = this.rec.writes; + for (let i = this.frameStart(k); i < s; i++) { + const place = this.placeOf(i); + if (!place) continue; + let entry = byLine.get(key(place)); + if (!entry) { entry = { place, steps: 0, names: new Set() }; byLine.set(key(place), entry); } + entry.steps++; + if (map) for (let j = this.rec.stepWrites[i]; j < this.rec.stepWrites[i + 1]; j++) { const n = owners.get(w[3 * j]); if (n) entry.names.add(n); } + } + return [...byLine.values()].map((e) => ({ place: e.place, steps: e.steps, names: [...e.names] })); + } + + /** Which variable each cell is (by its name in the source), arrays' cells and growing arrays' blocks included. */ + private cellOwners(): Map { + const out = new Map(); + for (const p of this.map?.programs ?? []) { + for (const v of p.vars) if (!isTemp(v)) for (const at of Object.values(v.cells)) if (at != null) for (let r = 0; r < (v.rowed ? 12 : 1); r++) out.set(at + 4 * r, top(v.id)); + for (const a of p.arrays) for (const [addr] of this.arrayCells(a)) out.set(addr, top(a.id)); + } + return out; + } + + /** Each frame's work: the conditions and actions the script's lines ran (eudplib's own not counted). */ + frameCosts(): Int32Array { + const rec = this.rec, out = new Int32Array(rec.frames); + for (let k = 0; k < rec.frames; k++) for (let s = rec.frameSteps[k]; s < rec.frameSteps[k + 1]; s++) if (rec.stepLine[s] >= 0) out[k] += rec.stepRecords[s]; + return out; + } + + /** The record frames a breakpoint line ran in. */ + hits(place: DebugPlace): number[] { + const rec = this.rec, out: number[] = []; + const id = rec.lines.findIndex((l) => l.file === place.file && l.line === place.line); + for (let k = 0; k < rec.frames; k++) { + let hit = false; + if (id >= 0) for (let s = rec.frameSteps[k]; s < rec.frameSteps[k + 1] && !hit; s++) hit = rec.stepLine[s] === id; + const t = this.testLine(k); + if (!hit && t && t.file === place.file && t.line === place.line) hit = true; + if (hit) out.push(k); + } + return out; + } + + /** What was shown on the text lines up to the cursor's frame: the last `n`. */ + shown(n = 13): { frame: number; text: string }[] { + const f = this.frame(); + return this.rec.shown.filter((x) => x.frame <= f).slice(-n); + } +} + +function isTemp(v: GameVariable): boolean { return /^\$|^_/.test(v.id); } +/** The name a variable is read by: a record's field ("pos.x#5") is its record's ("pos"). */ +const top = (id: string) => id.replace(/#\d+$/, "").split(".")[0]; + +/** A value as plain data (a unit as its slot), for the view and for comparing. */ +function plain(v: unknown): unknown { + if (v === null || v === undefined || typeof v !== "object") return v; + if ("unitSlot" in (v as object)) return v; + return JSON.parse(JSON.stringify(v)); +} + +/** A value as one line: 3, true, "tick 3", [1, 2, 3], { x: 10, y: 2 }, unit 12. */ +export function show(v: unknown, most = 80): string { + const s = ((): string => { + if (v === null || v === undefined) return String(v); + if (typeof v === "string") return JSON.stringify(v); + if (typeof v !== "object") return String(v); + if ("unitSlot" in (v as object)) return `unit ${(v as DebugUnitRef).unitSlot}`; + if (Array.isArray(v)) return `[${v.map((x) => show(x, most)).join(", ")}]`; + return `{ ${Object.entries(v as object).map(([k, x]) => `${k}: ${show(x, most)}`).join(", ")} }`; + })(); + return s.length > most ? `${s.slice(0, most - 1)}…` : s; +} diff --git a/tests/debug-model.test.ts b/tests/debug-model.test.ts new file mode 100644 index 0000000..65a0255 --- /dev/null +++ b/tests/debug-model.test.ts @@ -0,0 +1,143 @@ +// Walking a debugged run (TrigScript 4, A5: compiler/debugModel.ts): steps and frames forward and back, the +// variables at the cursor, breakpoints and watched variables in both directions, over a record made by the +// recorder from a small game of the test's own. +import { describe, expect, it } from "vitest"; +import { RecordingHandle, type DebugHost, type DebugRecording } from "../compiler/gameDebug"; +import { DebugCursor, show } from "../compiler/debugModel"; +import type { VariableMap } from "../compiler/gameVariables"; +import type { TestResult } from "../compiler/testing"; + +const FROM = 0x100, END = 0x200; +const MAP: VariableMap = { + heap: null, strings: FROM, + programs: [{ + index: 0, name: "p", perPlayer: false, owners: [0], + vars: [ + { id: "a#0", kind: "number", unsigned: false, rowed: false, cells: { value: 0x100 } }, + { id: "n#1", kind: "number", unsigned: false, rowed: false, cells: { value: 0x104 } }, + ], + arrays: [{ id: "arr#0", kind: "number", unsigned: false, length: 2, rowed: false, at: 0x110 }], + }], +}; +const LINES = [{ file: "main.ts", line: 3 }, { file: "main.ts", line: 4 }]; + +/** + * Six frames. Line 3 (two records) adds 10 to `a` on even frames; line 4 (one record) sets `n` to the frame; on + * frame 4 line 4 runs twice and the second time sets `arr[1]` to 9. The test's line is 20, then 21 from frame 4. + */ +function record(): DebugRecording { + const u8 = new Uint8Array(1024), m = new DataView(u8.buffer); + let frame = 0; + const eud = { listen: null as ((record: number, address: number, write: boolean, mask: number, was: number) => void) | null }; + const cost = { trace: null as ((record: number, group: number, action: boolean) => void) | null, paused: false, group: null, report: () => { throw new Error("no"); } }; + const write = (address: number, value: number) => { const was = m.getUint32(address, true); m.setUint32(address, value, true); eud.listen?.(0, address, true, 0xffffffff, was); }; + const host = { + m, u8, eud, cost, localPlayer: 0, over: false, payloadEnd: END, + get frame() { return frame; }, + step() { + frame++; + cost.trace?.(1, 0, false); cost.trace?.(2, 0, true); + if (frame % 2 === 0) write(0x100, m.getUint32(0x100, true) + 10); + cost.trace?.(3, 1, true); + write(0x104, frame); + if (frame === 4) { cost.trace?.(4, -1, true); cost.trace?.(3, 1, true); write(0x114, 9); } + }, + lines: () => [], action: () => 0, chat: () => {}, call: () => 0, + reset() { u8.fill(0); frame = 0; }, + keep: () => ({ frame, bytes: 0 }), restore() {}, portable: () => ({ kept: null, transfer: [] }), + }; + let testLine = 20; + const r = new RecordingHandle(host as unknown as DebugHost, () => ({ file: "main.ts", line: testLine }), {}, FROM); + r.reset(); + for (let f = 1; f <= 6; f++) { if (f === 4) testLine = 21; r.step(); } + return r.finish({ id: "t", name: "t", file: "main.ts", line: 19 }, { id: "t", status: "passed", printed: [], events: [], frames: 6, ms: 0 } as TestResult, LINES); +} + +const value = (c: DebugCursor, name: string) => c.programs()[0].values.find((v) => v.name === name)?.value; + +describe("walking a debugged run", () => { + it("steps by line within a frame, then on to the frame's end and the next frame's first line", () => { + const c = new DebugCursor(record(), MAP); + expect(c.where).toEqual({ k: 0, s: 0 }); + expect(c.line()).toEqual({ file: "main.ts", line: 3 }); + c.stepForward(); + expect(c.line()).toEqual({ file: "main.ts", line: 4 }); + expect(value(c, "n")).toBe(0); + c.stepForward(); + expect(c.atEnd()).toBe(true); + expect(value(c, "n")).toBe(1); + c.stepForward(); + expect(c.where.k).toBe(1); + expect(c.line()).toEqual({ file: "main.ts", line: 3 }); + }); + + it("goes back as it went forward, the memory with it", () => { + const c = new DebugCursor(record(), MAP); + c.toFrame(5); + expect(value(c, "a")).toBe(30); + expect(value(c, "n")).toBe(6); + expect(value(c, "arr")).toEqual([0, 9]); + c.toFrame(2); + expect(value(c, "a")).toBe(10); + expect(value(c, "arr")).toEqual([0, 0]); + c.stepBack(); + expect(c.where.k).toBe(2); + expect(c.line()).toEqual({ file: "main.ts", line: 4 }); + expect(value(c, "n")).toBe(2); + c.prevFrame(); + expect(c.where).toEqual({ k: 1, s: c.frameEnd(1) }); + c.toFrame(0); c.prevFrame(); + expect(c.where).toEqual({ k: 0, s: 0 }); + expect(value(c, "a")).toBe(0); + }); + + it("skips eudplib's own steps unless asked for every step", () => { + const c = new DebugCursor(record(), MAP); + c.moveTo({ k: 3, s: c.frameStart(3) }); + const lines: (number | null)[] = []; + while (!c.atEnd()) { lines.push(c.line()?.line ?? null); c.stepForward(); } + expect(lines).toEqual([3, 4, 4]); + c.moveTo({ k: 3, s: c.frameStart(3) }); + lines.length = 0; + while (!c.atEnd()) { lines.push(c.line()?.line ?? null); c.stepForward(true); } + expect(lines).toEqual([3, 4, null, 4]); + }); + + it("continues to a breakpoint, forwards and back, and to the end without one", () => { + const c = new DebugCursor(record(), MAP); + const bp = new Set(["main.ts:21"]); + expect(c.continueForward(bp)).toEqual({ kind: "breakpoint", place: { file: "main.ts", line: 21 } }); + // (A test's line stops at the end of the frame it asked for.) + expect(c.where).toEqual({ k: 3, s: c.frameEnd(3) }); + expect(c.continueForward(bp).kind).toBe("breakpoint"); + expect(c.where.k).toBe(4); + expect(c.continueBack(bp)).toEqual({ kind: "breakpoint", place: { file: "main.ts", line: 21 } }); + expect(c.where.k).toBe(3); + expect(c.continueBack(bp)).toEqual({ kind: "start" }); + expect(c.continueForward(new Set())).toEqual({ kind: "end" }); + expect(c.where).toEqual({ k: 5, s: c.frameEnd(5) }); + }); + + it("stops where a watched variable changes, just after the step that changed it", () => { + const c = new DebugCursor(record(), MAP); + expect(c.continueForward(new Set(), ["a"])).toEqual({ kind: "watch", name: "a", before: "0", after: "10" }); + expect(c.where.k).toBe(1); + expect(c.line()).toEqual({ file: "main.ts", line: 4 }); + expect(c.continueForward(new Set(), ["arr"])).toEqual({ kind: "watch", name: "arr", before: "[0, 0]", after: "[0, 9]" }); + expect(c.where.k).toBe(3); + expect(c.continueBack(new Set(), ["a"])).toEqual({ kind: "watch", name: "a", before: "10", after: "20" }); + expect(c.where.k).toBe(3); + }); + + it("says what each line wrote in the frame so far, and what the frames cost", () => { + const c = new DebugCursor(record(), MAP); + c.toFrame(3); + expect(c.frameLines()).toEqual([ + { place: { file: "main.ts", line: 3 }, steps: 1, names: ["a"] }, + { place: { file: "main.ts", line: 4 }, steps: 2, names: ["n", "arr"] }, + ]); + expect([...c.frameCosts()]).toEqual([3, 3, 3, 4, 3, 3]); + expect(c.hits({ file: "main.ts", line: 21 })).toEqual([3, 4, 5]); + expect(show({ x: 1, s: "hi", u: { unitSlot: 4 }, l: [1, 2] })).toBe('{ x: 1, s: "hi", u: unit 4, l: [1, 2] }'); + }); +}); From c386d8c8d1a58ebe8bfca4d7de04f44791f09ddc Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 13:31:10 -0600 Subject: [PATCH 020/238] TrigScript 4, A5: the debugger in the workspace A Debug view in the panel over a test's recorded run (debugger.ts): continue forwards and back to breakpoints and watched variables, a line or a frame either way, a timeline of the frames' cost with breakpoint hits and shown text (click or drag), the variables at the cursor (what changed in gold, an eye to watch), the frame's steps, the game's text lines, and a picture of the game where the scm-sim plugin's view draws one. In the editor: breakpoints in the margin (F9 or a click), the line about to run and the test's line lit, and beside each line run in the frame the values it wrote (inlay hints). Debug this test from its row in the Testing view or Debug Test at Cursor; a failed test opens at its failure. F5/F6 continue, F10/Shift+F10 lines, F11/Shift+F11 frames, Shift+F5 stop, while a test is debugged. Korean for all of it. Co-Authored-By: Claude Opus 5.5 --- debugger.ts | 470 +++++++++++++++++++++++++++++++++++++++++++++++ editor.ts | 166 +++++++++++++---- ko.ts | 66 +++++++ monaco.ts | 27 ++- shell.ts | 13 ++ vendor/scmSim.ts | 30 +++ 6 files changed, 740 insertions(+), 32 deletions(-) create mode 100644 debugger.ts diff --git a/debugger.ts b/debugger.ts new file mode 100644 index 0000000..ccea714 --- /dev/null +++ b/debugger.ts @@ -0,0 +1,470 @@ +/** + * The debugger in the game (TrigScript 4, A5), as the workspace shows it: a Debug view in the panel over a test's + * recorded run (`compiler/gameDebug.ts`, walked by `compiler/debugModel.ts`), and what it puts in the editor — the + * line about to run, the test's line, breakpoints, and the values each line wrote beside it. + * + * Everything is the record, so stepping back is as quick as stepping forward; the one thing asked of the engine as + * the cursor moves is a picture of the game at the cursor's frame, and only when a view to draw it is there. + * + * Keys, as VS Code has them where it can: F5 continue, F6 continue backwards, F10 the next line, Shift+F10 the line + * before, F11 the next frame, Shift+F11 the frame before, Shift+F5 stop, F9 a breakpoint. + */ +import type { PluginApi } from "@scm-js/plugin-api"; +import type { Translate } from "./i18n"; +import type { IconName, Shell } from "./shell"; +import type { LineHint } from "./compiler/compiler"; +import { STEP_EUDPLIB, STEP_TEST, STEP_UNSEEN, type DebugRecording } from "./compiler/gameDebug"; +import { DebugCursor, show, type DebugPlace, type DebugStop } from "./compiler/debugModel"; +import type { VariableMap } from "./compiler/gameVariables"; +import { shortCount } from "./compiler/gameCost"; + +type El = PluginApi["ui"]["el"]; + +/** The engine's side of a session: pictures of the run's frames (scm-sim's `ScmSimDebugSession`). */ +export interface DebugPictures { + picture(frame: number): Promise<{ frame: number; kept: unknown }>; + close(): void; + readonly closed: boolean; +} + +/** Where a picture is drawn: a view of the game the editor made for the session (scm-sim's view), when there is one. */ +export interface DebugGameView { + readonly element: HTMLElement; + show(picture: { frame: number; kept: unknown }): void; + dispose(): void; +} + +export interface DebuggerDeps { + t: Translate; + el: El; + shell: Shell; + /** Shows the line in the editor (opening its file), without taking the keyboard from the panel. */ + reveal(file: string, line: number): void; + /** What the debugger puts in the editor changed: draw it again. */ + changed(): void; + setStatus(kind: "info" | "ok" | "error" | "busy", text: string, timeout?: number): void; +} + +export interface DebugStart { + recording: DebugRecording; + variables: VariableMap | null; + pictures: DebugPictures | null; + /** A view to draw the pictures in, or why there is none (in words). */ + view: DebugGameView | string | null; + /** The script's files as the run was built from them: lines are only meant while the files are the same. */ + files: Record; +} + +export const DEBUG_STYLE = ` +.tsd .tsd-debug { box-sizing: border-box; height: 100%; display: flex; flex-direction: column; min-height: 0; font-size: var(--fs-md); } +.tsd .tsd-debug-bar { flex: none; display: flex; align-items: center; gap: 2px; padding: 2px 8px; border-bottom: 1px solid var(--border); white-space: nowrap; overflow: hidden; } +.tsd .tsd-debug-bar .tsd-icon-button { display: inline-flex; } +.tsd .tsd-debug-bar .tsd-sep { width: 1px; height: 16px; margin: 0 6px; background: var(--border); } +.tsd .tsd-debug-bar .tsd-where { margin-left: 8px; font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--text); } +.tsd .tsd-debug-bar .tsd-why { margin-left: 10px; overflow: hidden; text-overflow: ellipsis; color: var(--text-dim); font-size: var(--fs-sm); } +.tsd .tsd-debug-bar .tsd-why.tsd-failed { color: var(--danger); } +.tsd .tsd-debug-bar label { display: inline-flex; align-items: center; gap: 4px; margin-left: auto; padding-left: 10px; color: var(--text-dim); font-size: var(--fs-sm); cursor: pointer; } +.tsd .tsd-debug-time { flex: none; height: 40px; margin: 4px 8px 2px; cursor: ew-resize; touch-action: none; } +.tsd .tsd-debug-time svg { display: block; width: 100%; height: 100%; } +.tsd .tsd-debug-time .cost { fill: var(--teal); fill-opacity: 0.3; stroke: var(--teal); stroke-width: 1px; vector-effect: non-scaling-stroke; } +.tsd .tsd-debug-time .hit { fill: var(--danger); } +.tsd .tsd-debug-time .said { fill: var(--gold); } +.tsd .tsd-debug-time .cut { fill: var(--text-faint); fill-opacity: 0.25; } +.tsd .tsd-debug-time .now { fill: var(--text); } +.tsd .tsd-debug-time .end { fill: var(--danger); fill-opacity: 0.6; } +.tsd .tsd-debug-cols { flex: 1; min-height: 0; display: flex; border-top: 1px solid var(--border); } +.tsd .tsd-debug-col { flex: 1 1 0; min-width: 0; overflow: auto; border-right: 1px solid var(--border); } +.tsd .tsd-debug-col:last-child { border-right: none; } +.tsd .tsd-debug-col h5 { position: sticky; top: 0; z-index: 1; margin: 0; padding: 3px 10px; background: var(--bg-1); font-size: 11px; font-weight: 700; text-transform: uppercase; color: var(--text-dim); } +.tsd .tsd-debug-col ul { margin: 0; padding: 2px 0; list-style: none; } +.tsd .tsd-debug-col li { display: flex; align-items: baseline; gap: 6px; padding: 1px 10px; line-height: 18px; white-space: nowrap; } +.tsd .tsd-debug-col li.tsd-click { cursor: pointer; } +.tsd .tsd-debug-col li.tsd-click:hover { background: var(--bg-3); } +.tsd .tsd-debug-col li.tsd-here { background: var(--bg-4); } +.tsd .tsd-debug-col li.tsd-dim { color: var(--text-faint); } +.tsd .tsd-debug-col li.tsd-program { padding-top: 4px; color: var(--text-dim); font-size: var(--fs-sm); } +.tsd .tsd-debug-col .k { flex: none; color: var(--text-dim); font-family: var(--font-mono); font-size: var(--fs-sm); } +.tsd .tsd-debug-col .v { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--text); } +.tsd .tsd-debug-col .v.tsd-changed { color: var(--gold); } +.tsd .tsd-debug-col .n { flex: none; margin-left: auto; color: var(--text-faint); font-size: var(--fs-sm); } +.tsd .tsd-debug-col li .tsd-icon-button { visibility: hidden; flex: none; } +.tsd .tsd-debug-col li:hover .tsd-icon-button, .tsd .tsd-debug-col li .tsd-icon-button.tsd-on { visibility: visible; } +.tsd .tsd-debug-col .tsd-icon-button.tsd-on { color: var(--gold); } +.tsd .tsd-debug-col .tsd-empty { padding: 6px 10px; color: var(--text-faint); white-space: normal; line-height: 1.45; } +.tsd .tsd-debug-game { flex: 1.3 1 0; display: flex; flex-direction: column; } +.tsd .tsd-debug-game .tsd-view { position: relative; flex: 1; min-height: 120px; background: #000; } +.tsd .tsd-debug-game .tsd-view > * { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; } +.tsd .tsd-debug-game .tsd-shown { flex: none; max-height: 40%; overflow: auto; padding: 2px 10px; font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--text-dim); } +.tsd .tsd-debug-game .tsd-shown div { white-space: pre-wrap; } +.trigscript-debug-line { background: rgba(214, 178, 74, 0.18); } +.trigscript-debug-arrow::before { content: "\\eb89"; font: normal normal normal 14px/18px codicon; color: var(--gold); padding-left: 2px; } +.trigscript-debug-test { background: rgba(78, 201, 176, 0.12); } +.trigscript-debug-test-arrow::before { content: "\\eb8a"; font: normal normal normal 14px/18px codicon; color: var(--teal); padding-left: 2px; } +.trigscript-bp { cursor: pointer; } +.trigscript-bp::before { content: "\\ea71"; font: normal normal normal 14px/18px codicon; color: #e51400; } +.trigscript-debug-hint { color: var(--gold) !important; opacity: 0.9; } +`; + +const NS = "http://www.w3.org/2000/svg"; + +/** How long the cursor must stay before a picture of its frame is asked for (scrubbing asks for none on the way). */ +const PICTURE_MS = 60; + +interface Session extends DebugStart { + cursor: DebugCursor; + costs: Int32Array; + /** What the variables read at the last cursor, to colour what changed. */ + last: Map; + /** Why the cursor stands where it does (the last continue), for the bar. */ + stop: DebugStop | null; + picture: { timer: ReturnType | null; asked: number; shown: number }; +} + +export class Debugger { + readonly breakpoints = new Set(); + /** Variables watched: a continue stops where one changes ("wave", or "waves.wave"). */ + readonly watch: string[] = []; + /** eudplib's own steps (and the test's, and the unseen) are stopped at too. */ + everyStep = false; + private session: Session | null = null; + private readonly d: DebuggerDeps; + private readonly body: HTMLElement; + private readonly bar: HTMLElement; + private readonly time: HTMLElement; + private readonly cols: HTMLElement; + /** The three columns, made once a session (the game's view is a frame: moved, it would load again). */ + private readonly varsCol: HTMLElement; + private readonly stepsCol: HTMLElement; + private readonly gameCol: HTMLElement; + private readonly viewBox: HTMLElement; + private readonly shownBox: HTMLElement; + + constructor(deps: DebuggerDeps) { + this.d = deps; + const { el, t, shell } = deps; + const view = shell.view({ id: "debug", title: t("Debug") }); + this.body = view.body; + this.bar = el("div", { className: "tsd-debug-bar" }); + this.time = el("div", { className: "tsd-debug-time" }); + this.cols = el("div", { className: "tsd-debug-cols" }); + this.varsCol = el("div", { className: "tsd-debug-col" }); + this.stepsCol = el("div", { className: "tsd-debug-col" }); + this.viewBox = el("div"); + this.shownBox = el("div", { className: "tsd-shown", title: t("The text lines up to this frame") }); + this.gameCol = el("div", { className: "tsd-debug-col tsd-debug-game" }, el("h5", {}, t("The game")), this.viewBox, this.shownBox); + this.body.append(el("div", { className: "tsd-debug" }, this.bar, this.time, this.cols)); + this.time.addEventListener("pointerdown", (e) => { + if (!this.session) return; + this.time.setPointerCapture(e.pointerId); + this.scrub(e); + const move = (m: PointerEvent) => this.scrub(m); + const up = () => { this.time.removeEventListener("pointermove", move); this.time.removeEventListener("pointerup", up); }; + this.time.addEventListener("pointermove", move); + this.time.addEventListener("pointerup", up); + }); + this.render(); + } + + get active(): boolean { return this.session !== null; } + + /** A recorded run to walk: the cursor at its start, or at its end when the test failed (where debugging begins). */ + start(s: DebugStart): void { + this.end(); + const cursor = new DebugCursor(s.recording, s.variables); + this.session = { ...s, cursor, costs: cursor.frameCosts(), last: new Map(), stop: null, picture: { timer: null, asked: -1, shown: -1 } }; + const { el, t } = this.d; + this.viewBox.replaceChildren(s.view && typeof s.view !== "string" ? s.view.element : el("div", { className: "tsd-empty" }, typeof s.view === "string" ? s.view : t("No picture of the game here."))); + this.viewBox.className = s.view && typeof s.view !== "string" ? "tsd-view" : ""; + this.cols.replaceChildren(this.varsCol, this.stepsCol, this.gameCol); + if (s.recording.result.status === "failed" && s.recording.frames) cursor.toFrame(s.recording.frames - 1); + else this.firstLine(); + this.d.shell.showPanel("debug"); + this.moved(); + } + + /** The session is over: the engine's game let go, the editor's marks taken away. */ + end(): void { + const s = this.session; + if (!s) return; + if (s.picture.timer) clearTimeout(s.picture.timer); + s.pictures?.close(); + if (s.view && typeof s.view !== "string") s.view.dispose(); + this.viewBox.replaceChildren(); + this.session = null; + this.render(); + this.d.changed(); + } + + /* ── what the person does ── */ + + toggleBreakpoint(file: string, line: number): void { + const k = `${file}:${line}`; + if (!this.breakpoints.delete(k)) this.breakpoints.add(k); + this.d.changed(); + if (this.session) this.renderTime(); + } + + toggleWatch(name: string): void { + const at = this.watch.indexOf(name); + if (at >= 0) this.watch.splice(at, 1); else this.watch.push(name); + this.render(); + } + + continueForward(): void { this.go((c) => c.continueForward(this.breakpoints, this.watch)); } + continueBack(): void { this.go((c) => c.continueBack(this.breakpoints, this.watch)); } + stepForward(): void { this.go((c) => { c.stepForward(this.everyStep); return null; }); } + stepBack(): void { this.go((c) => { c.stepBack(this.everyStep); return null; }); } + nextFrame(): void { this.go((c) => { c.nextFrame(); return null; }); } + prevFrame(): void { this.go((c) => { c.prevFrame(); return null; }); } + restart(): void { this.go(() => { this.firstLine(); return null; }); } + + private firstLine() { + const c = this.session!.cursor; + c.moveTo({ k: 0, s: c.frameStart(0) }); + if (!c.line() && !this.everyStep && !c.atEnd()) c.stepForward(false); + } + + private go(move: (c: DebugCursor) => DebugStop | null): void { + const s = this.session; + if (!s) return; + s.stop = move(s.cursor); + this.moved(); + } + + private scrub(e: PointerEvent) { + const s = this.session; + if (!s || !s.recording.frames) return; + const r = this.time.getBoundingClientRect(); + const k = Math.floor(((e.clientX - r.left) / Math.max(1, r.width)) * s.recording.frames); + s.cursor.toFrame(Math.max(0, Math.min(s.recording.frames - 1, k))); + s.stop = null; + this.moved(); + } + + /** The cursor moved: the editor's line, the view, the picture. */ + private moved() { + const s = this.session; + if (!s) return; + const at = s.cursor.line() ?? s.cursor.testLine(); + if (at) this.d.reveal(at.file, at.line); + this.render(); + this.d.changed(); + this.askPicture(); + } + + /* ── what goes in the editor ── */ + + /** The editor's marks: breakpoints, the line about to run, the test's line. */ + marks(): { breakpoints: DebugPlace[]; at: DebugPlace | null; test: DebugPlace | null } { + const breakpoints = [...this.breakpoints].map((k) => { const i = k.lastIndexOf(":"); return { file: k.slice(0, i), line: Number(k.slice(i + 1)) }; }); + const s = this.session; + if (!s || !this.current()) return { breakpoints, at: null, test: null }; + return { breakpoints, at: s.cursor.line(), test: s.cursor.testLine() }; + } + + /** Beside each line run so far in the frame: the variables it wrote, at the cursor, and how often it ran. */ + hints(): LineHint[] { + const s = this.session; + if (!s || !this.current() || !s.cursor.known) return []; + const { t } = this.d; + const values = new Map(); + for (const p of s.cursor.programs()) for (const v of p.values) if (!values.has(v.name)) values.set(v.name, show(v.value, 40)); + return s.cursor.frameLines().map((l) => { + const said = l.names.map((n) => `${n} = ${values.get(n) ?? "?"}`); + if (l.steps > 1) said.push(`×${l.steps}`); + return said.length ? { file: l.place.file, line: l.place.line, label: said.join(" "), note: t("What this line wrote in frame {frame}, as it stands at the debugger's cursor; ×n: how many times it ran in the frame so far.", { frame: s.cursor.frame() }) } : null; + }).filter((h): h is LineHint => h !== null); + } + + /** The script is as the run was built from: lines mean what they meant. */ + private current(): boolean { + const s = this.session; + return !!s && Object.keys(s.files).length > 0 && this.filesNow !== null && Object.entries(s.files).every(([f, text]) => this.filesNow![f] === text); + } + private filesNow: Record | null = null; + /** The editor's files now (the debugger's marks show only while they are the run's). */ + setFiles(files: Record): void { + const was = this.current(); + this.filesNow = files; + if (this.session && was !== this.current()) { this.render(); this.d.changed(); } + } + + /* ── the picture ── */ + + private askPicture() { + const s = this.session; + if (!s || !s.pictures || !s.view || typeof s.view === "string") return; + if (s.picture.timer) clearTimeout(s.picture.timer); + s.picture.timer = setTimeout(() => { + s.picture.timer = null; + const c = s.cursor; + // (Mid-frame, the game as the frame found it: the frame before's end.) + const frame = c.atEnd() ? c.frame() : c.frame() - 1; + if (frame === s.picture.shown || frame === s.picture.asked || s.pictures!.closed) return; + s.picture.asked = frame; + s.pictures!.picture(frame).then( + (p) => { + if (this.session !== s || typeof s.view === "string" || !s.view) return; + s.picture.asked = -1; + s.picture.shown = p.frame; + s.view.show(p); + // The cursor moved on while it was made: the next. + this.askPicture(); + }, + (err: Error) => { s.picture.asked = -1; if (this.session === s) this.d.setStatus("error", this.d.t("No picture of the game: {message}", { message: err.message })); }, + ); + }, PICTURE_MS); + } + + /* ── the view ── */ + + private button(icon: IconName, title: string, run: () => void, disabled = false): HTMLElement { + const b = this.d.shell.iconButton({ icon, title, run }); + b.set({ disabled }); + return b.element; + } + + private render() { + const { el, t } = this.d; + const s = this.session; + if (!s) { + this.bar.replaceChildren(el("span", { className: "tsd-why" }, t("No test is being debugged. Debug one from its row in the Testing view (the bug icon) or with Debug Test at Cursor: it runs once in the game with everything it did written down, and here you walk it, forwards and back."))); + this.time.replaceChildren(); + this.cols.replaceChildren(); + return; + } + if (this.cols.childElementCount !== 3) this.cols.replaceChildren(this.varsCol, this.stepsCol, this.gameCol); + const c = s.cursor, rec = s.recording; + const none = !rec.frames; + const why = (() => { + if (!this.current()) return { text: t("The script changed since this run: its lines may have moved. Debug the test again to walk the script as it is."), failed: true }; + if (s.stop?.kind === "breakpoint") return { text: t("Stopped at the breakpoint on line {line}.", { line: s.stop.place.line }), failed: false }; + if (s.stop?.kind === "watch") return { text: t("{name} changed: {before} → {after}", { name: s.stop.name, before: s.stop.before, after: s.stop.after }), failed: false }; + if (s.stop?.kind === "end") return { text: rec.result.status === "failed" ? t("The end of the run: {message}", { message: rec.result.message ?? t("the test failed") }) : t("The end of the run: the test passed."), failed: rec.result.status === "failed" }; + if (rec.cut && !c.known) return { text: t("The record stops at frame {frame} ({why}): the variables are not known past it.", { frame: rec.cut.frame, why: rec.cut.why }), failed: true }; + return rec.result.status === "failed" ? { text: t("{test} failed: {message}", { test: rec.test.name, message: rec.result.message ?? "" }), failed: true } : { text: t("{test} passed.", { test: rec.test.name }), failed: false }; + })(); + const line = c.line(); + const where = none ? t("no frames") : `${t("frame {frame}", { frame: c.frame() })} ${(c.frame() / 24).toFixed(2)} s ${line ? `${line.file}:${line.line}` : c.atEnd() ? t("end of frame") : t("eudplib")}`; + const every = el("input", { type: "checkbox", checked: this.everyStep }) as HTMLInputElement; + every.addEventListener("change", () => { this.everyStep = every.checked; this.render(); }); + this.bar.replaceChildren( + this.button("debug-reverse-continue", t("Continue backwards (F6): to the breakpoint or the change of a watched variable before this, or the start"), () => this.continueBack(), none), + this.button("debug-step-back", t("Back a line (Shift+F10)"), () => this.stepBack(), none), + this.button("debug-step-over", t("On a line (F10)"), () => this.stepForward(), none), + this.button("debug-continue", t("Continue (F5): to the next breakpoint or change of a watched variable, or the end"), () => this.continueForward(), none), + el("span", { className: "tsd-sep" }), + this.button("arrow-left", t("Back a frame (Shift+F11)"), () => this.prevFrame(), none), + this.button("arrow-right", t("On a frame (F11): to the end of this one"), () => this.nextFrame(), none), + this.button("debug-restart", t("To the start of the run"), () => this.restart(), none), + this.button("debug-stop", t("Stop debugging (Shift+F5)"), () => this.end()), + el("span", { className: "tsd-where" }, where), + el("span", { className: why.failed ? "tsd-why tsd-failed" : "tsd-why", title: why.text }, why.text), + el("label", { title: t("Stop at every step, eudplib's own work and what the game wrote by itself included, not only at the script's lines") }, every, t("Every step")), + ); + this.renderTime(); + this.renderCols(); + } + + private renderTime() { + const s = this.session; + if (!s) return; + const rec = s.recording, n = Math.max(rec.frames, 1), c = s.cursor; + const svg = document.createElementNS(NS, "svg"); + svg.setAttribute("viewBox", `0 0 ${n} 100`); + svg.setAttribute("preserveAspectRatio", "none"); + const rect = (x: number, y: number, w: number, h: number, cls: string, title?: string) => { + const r = document.createElementNS(NS, "rect"); + r.setAttribute("x", String(x)); r.setAttribute("y", String(y)); r.setAttribute("width", String(w)); r.setAttribute("height", String(h)); r.setAttribute("class", cls); + if (title) { const tt = document.createElementNS(NS, "title"); tt.textContent = title; r.append(tt); } + svg.append(r); + }; + const top = Math.max(1, ...s.costs); + const area = document.createElementNS(NS, "path"); + const pts: string[] = []; + s.costs.forEach((v, k) => { const y = (100 - (v / top) * 70).toFixed(2); pts.push(`${k},${y} ${k + 1},${y}`); }); + area.setAttribute("d", `M0,100 L${pts.join(" L")} L${n},100 Z`); + area.setAttribute("class", "cost"); + svg.append(area); + const { t } = this.d; + if (rec.cut) { const k = rec.cut.frame - rec.first; rect(k, 0, n - k, 100, "cut", t("Not recorded past frame {frame}: {why}", { frame: rec.cut.frame, why: rec.cut.why })); } + const w = Math.max(n / 600, 0.6); + for (const b of this.marks().breakpoints) for (const k of c.hits(b)) rect(k, 0, w, 14, "hit", t("Line {line} runs in frame {frame}", { line: b.line, frame: rec.first + k })); + for (const x of rec.shown) rect(x.frame - rec.first, 88, w, 12, "said", x.text); + if (rec.result.status === "failed") rect(n - w, 0, w, 100, "end", rec.result.message ?? ""); + rect(c.where.k, 0, Math.max(w, n / 300), 100, "now"); + const title = document.createElementNS(NS, "title"); + title.textContent = t("Frames {from} to {to}: the script's conditions and actions a frame ({peak} at the most). Red: a breakpoint's line runs; gold: a line of text was shown. Click or drag to go to a frame.", { from: rec.first, to: rec.first + rec.frames - 1, peak: shortCount(top) }); + svg.prepend(title); + this.time.replaceChildren(svg); + } + + private renderCols() { + const { el, t } = this.d; + const s = this.session!; + const c = s.cursor, rec = s.recording; + + // Watched variables and every program's, at the cursor, what changed since the last stop in gold. + const vars = this.varsCol; + vars.replaceChildren(el("h5", {}, t("Variables"))); + const list = el("ul"); + const now = new Map(); + if (!c.known) list.append(el("li", { className: "tsd-empty" }, t("Not known here: the record stops at frame {frame}.", { frame: rec.cut!.frame }))); + else { + const programs = c.programs(); + if (this.watch.length) { + list.append(el("li", { className: "tsd-program" }, t("Watched"))); + for (const name of this.watch) list.append(this.varRow(name, c.watchText(name), s.last.get(`watch:${name}`), true, () => this.toggleWatch(name))); + for (const name of this.watch) now.set(`watch:${name}`, c.watchText(name)); + } + if (!programs.length) list.append(el("li", { className: "tsd-empty" }, t("The build did not say where the programs' variables are."))); + for (const p of programs) { + list.append(el("li", { className: "tsd-program" }, p.label)); + for (const v of p.values) { + const text = show(v.value, 400), id = `${p.label}:${v.name}`; + now.set(id, text); + const watchName = programs.filter((x) => x.values.some((y) => y.name === v.name)).length > 1 ? `${p.label.split(" ")[0]}.${v.name}` : v.name; + list.append(this.varRow(v.name, text, s.last.get(id), this.watch.includes(watchName), () => this.toggleWatch(watchName))); + } + } + } + s.last = now; + vars.append(list); + + // The frame's steps, the cursor's highlighted; a click goes there. + const steps = this.stepsCol; + steps.replaceChildren(el("h5", {}, t("Frame {frame}", { frame: c.frame() }))); + const rows = el("ul"); + const k = c.where.k; + const first = c.frameStart(k), end = c.frameEnd(k); + const most = 400; + for (let i = first; i < Math.min(end, first + most); i++) { + const l = rec.stepLine[i], place = c.placeOf(i); + if (!this.everyStep && l === STEP_EUDPLIB && rec.stepWrites[i + 1] === rec.stepWrites[i]) continue; + const label = place ? `${place.file === "main.ts" ? "" : `${place.file} `}${t("line {line}", { line: place.line })}` : l === STEP_TEST ? t("the test") : l === STEP_UNSEEN ? t("the game itself") : t("eudplib"); + const wrote = rec.stepWrites[i + 1] - rec.stepWrites[i]; + const row = el("li", { className: `tsd-click${i === c.where.s ? " tsd-here" : ""}${place ? "" : " tsd-dim"}`, onClick: () => { c.moveTo({ k, s: i }); s.stop = null; this.moved(); } }, + el("span", { className: "k" }, label), + el("span", { className: "v" }, wrote ? t("{n, plural, one {# write} other {# writes}}", { n: wrote }) : ""), + el("span", { className: "n", title: t("conditions and actions") }, String(rec.stepRecords[i]))); + rows.append(row); + } + if (end - first > most) rows.append(el("li", { className: "tsd-dim" }, t("… and {n} more steps", { n: end - first - most }))); + rows.append(el("li", { className: `tsd-click${c.atEnd() ? " tsd-here" : ""} tsd-dim`, onClick: () => { c.moveTo({ k, s: end }); s.stop = null; this.moved(); } }, el("span", { className: "k" }, t("end of frame")), el("span", { className: "n" }, String(s.costs[k] ?? 0)))); + steps.append(rows); + + // What was shown on the game's text lines up to the cursor's frame (the picture is drawn into its own box). + const shown = c.shown(); + this.shownBox.replaceChildren(...(shown.length ? shown.map((x) => el("div", {}, `${x.frame} ${x.text}`)) : [el("div", {}, t("Nothing shown yet."))])); + } + + private varRow(name: string, text: string, before: string | undefined, watched: boolean, toggle: () => void): HTMLElement { + const { el, t } = this.d; + const eye = this.d.shell.iconButton({ icon: "eye", title: watched ? t("Stop watching {name}", { name }) : t("Watch {name}: Continue stops where it changes", { name }), run: toggle }); + if (watched) eye.element.classList.add("tsd-on"); + return el("li", { title: text }, + el("span", { className: "k" }, name), + el("span", { className: before !== undefined && before !== text ? "v tsd-changed" : "v" }, text), + eye.element); + } +} diff --git a/editor.ts b/editor.ts index 5af4bda..810d296 100644 --- a/editor.ts +++ b/editor.ts @@ -52,6 +52,9 @@ import { applyMoves, buildTree, filesUnder, folderOf, movesOf, refuseMoves, tabL 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 { 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 { Translate } from "./i18n"; @@ -179,7 +182,7 @@ const STYLE = `${SHELL_STYLE} .tsd .tsd-cost-graph { flex: 1 1 auto; min-width: 0; max-width: 480px; height: 36px; } .tsd .tsd-cost-graph path { fill: var(--teal); fill-opacity: 0.35; stroke: var(--teal); stroke-width: 1px; vector-effect: non-scaling-stroke; } .${BUILD_TIME_CLASS} { text-decoration: underline dotted rgba(153, 162, 179, 0.55); text-underline-offset: 3px; } -`; +${DEBUG_STYLE}`; /** "P1", "all players", "Force 2", "P1, P2" — who a program runs for. */ function ownerLabel(p: { owners: number[] }, t: Translate): string { @@ -377,6 +380,13 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa { icon: "filter", title: t("Show only the tests that fail"), run: () => { onlyFailing = !onlyFailing; renderTesting(); } }, ] }); testingView.body.append(testingList); + /** The debugger in the game (TrigScript 4, A5): the Debug view, its marks in the editor, its values beside the lines. */ + const debug = new Debugger({ + t, el, shell, + reveal: (file, line) => { if (!editor) return; openFile(file); editor.editor.revealLineInCenterIfOutsideViewport(line); }, + changed: () => { editor?.setDebug(debug.marks()); refreshLineHints(); }, + setStatus: (kind, text, timeout) => setStatus(kind, text, timeout), + }); /** * The map's script settings (`script.ts#ScriptSettings`): kept in the map beside the script, because the built map @@ -729,6 +739,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa result && result.status !== "skipped" ? el("span", { className: "tsd-about" }, t("{ms} ms", { ms: result.ms })) : undefined, el("span", { className: "tsd-row-actions" }, shell.iconButton({ icon: "play", title: node.kind === "test" ? t("Run this test") : t("Run these tests"), run: () => { void runTests(node.kind === "file" ? { files: [node.path] } : { ids }); } }).element, + node.kind === "test" ? shell.iconButton({ icon: "debug-alt", title: t("Debug this test in the game: run it once with everything it did written down, then walk it forwards and back"), run: () => { void debugTest(node.info.id); } }).element : undefined, ), ); row.style.paddingLeft = `${10 + depth * 12}px`; @@ -756,37 +767,57 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa * JavaScript and `dist/gametest.js` to the scm-sim plugin's engine page, which runs each test beside the game. The * simulator runs them too, so that each test has both marks. */ + /** + * What a run in the game needs, made ready: the engine there and willing, the script applied and compiled, the map + * built as Save builds it and exported whole (a bare .chk has no room for what the build steps make, so the engine's + * page reads the scenario out of it), with the build's maps of variables and triggers from its log, and the text of + * `dist/gametest.js`. Null, the reason said in the status bar, when something is missing. + */ + const prepareInGame = async (filter: Pick, ask: "tests" | "debug") => { + const engine = svc.engine(); + if (!engine?.present || !engine.test || (ask === "debug" && !engine.debug)) { + setStatus("error", ask === "debug" + ? t("Debugging in the game needs the scm-sim plugin (0.11 or newer) on scmjs.dev: install, update or turn it on under Plugins ▸ Manage Plugins….") + : t("Running tests in the game needs the scm-sim plugin (0.11 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….")); + return null; + } + const notRun = (message: string) => (ask === "debug" ? t("Not debugged in the game: {message}", { message }) : t("Not run in the game: {message}", { message })); + const status = await engine.status(); + if (!status.available) { setStatus("error", notRun(status.reason ?? t("the engine cannot run a game now"))); return null; } + if (!status.tests) { setStatus("error", t("Not run in the game: the engine's page is from before tests in the game. Reload the editor and try again.")); return null; } + if (ask === "debug" && !status.debug) { setStatus("error", t("Not debugged: the engine's page is from before the debugger. Reload the editor and try again.")); return null; } + if (!(await build()) || cancelled) return null; + const a = await compileNow({ world: svc.world(), ...filter }); + if (!a || cancelled) return null; + if (!a.compiled.ok) { setStatus("error", t("Tests not run: {n, plural, one {# problem} other {# problems}} in the script.", { n: a.compiled.diagnostics.length }), NOTICE_MS); shell.showPanel("problems"); return null; } + if (!a.compiled.modules) { setStatus("info", t("The script has no tests.")); return null; } + if (!a.compiled.ir.length) { setStatus("info", t("Tests run in the game only for a script with a program: without one, nothing of the script is built into the map.")); return null; } + const names = svc.names()?.names; + if (!names) { setStatus("error", t("No map is open.")); return null; } + setStatus("busy", t("Building the map…")); + const built = { ...files }; + // (The build says in its log where it put the programs' variables, for sim.program().) + const logged: string[] = []; + const heard = svc.onBuild((e) => { if (e.kind === "log") logged.push(e.line); else if (e.kind === "done" && !logged.length) logged.push(...e.log.split("\n")); }); + const file = await svc.withVariableMap(() => api.document.export({ format: "scx" })).finally(() => heard.dispose()); + if (!file || cancelled) { if (!file) setStatus("error", t("No map is open.")); return null; } + const variables = readVariableMap(logged), triggers = readTriggerMap(logged); + gametest ??= fetch(gametestUrl(svc.dist())).then((r) => { if (!r.ok) throw new Error(`gametest.js: ${r.status}`); return r.text(); }); + const module = await gametest.catch((err) => { gametest = null; throw err; }); + const bytes = new Uint8Array(await file.arrayBuffer()); + return { engine, status, a, names, variables, triggers, module, bytes, built }; + }; + const runTestsInGame = async (filter: Pick = {}) => { if (!editor || testsRunning || building || testing || !ready) return; - const engine = svc.engine(); - if (!engine?.present || !engine.test) { setStatus("error", t("Running tests in the game needs the scm-sim plugin (0.11 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….")); return; } testsRunning = true; renderTesting(); try { - const status = await engine.status(); - if (!status.available) { setStatus("error", t("Not run in the game: {message}", { message: status.reason ?? t("the engine cannot run a game now") })); return; } - if (!status.tests) { setStatus("error", t("Not run in the game: the engine's page is from before tests in the game. Reload the editor and try again.")); return; } - if (!(await build()) || cancelled) return; - const a = await compileNow({ world: svc.world(), ...filter }); - if (!a || cancelled) return; - if (!a.compiled.ok) { setStatus("error", t("Tests not run: {n, plural, one {# problem} other {# problems}} in the script.", { n: a.compiled.diagnostics.length }), NOTICE_MS); shell.showPanel("problems"); return; } - if (!a.compiled.modules) { setStatus("info", t("The script has no tests.")); return; } - if (!a.compiled.ir.length) { setStatus("info", t("Tests run in the game only for a script with a program: without one, nothing of the script is built into the map.")); return; } - const names = svc.names()?.names; - if (!names) { setStatus("error", t("No map is open.")); return; } - setStatus("busy", t("Building the map…")); - // (Whole: a bare .chk has no room for what the build steps make, so the engine's page reads the scenario out of - // it. The build says in its log where it put the programs' variables, for sim.program().) - const logged: string[] = []; - const heard = svc.onBuild((e) => { if (e.kind === "log") logged.push(e.line); else if (e.kind === "done" && !logged.length) logged.push(...e.log.split("\n")); }); - const file = await svc.withVariableMap(() => api.document.export({ format: "scx" })).finally(() => heard.dispose()); - if (!file || cancelled) { if (!file) setStatus("error", t("No map is open.")); return; } - const variables = readVariableMap(logged), triggers = readTriggerMap(logged); - gametest ??= fetch(gametestUrl(svc.dist())).then((r) => { if (!r.ok) throw new Error(`gametest.js: ${r.status}`); return r.text(); }); - const module = await gametest.catch((err) => { gametest = null; throw err; }); + const ready = await prepareInGame(filter, "tests"); + if (!ready || cancelled) return; + const { engine, status, a, names, variables, triggers, module, bytes } = ready; setStatus("busy", t("Running the tests in the game…")); - const bytes = new Uint8Array(await file.arrayBuffer()); - const report = await engine.test(bytes, module, { modules: a.compiled.modules, names, files: filter.files, ids: filter.ids, variables, triggers }) as TestReport & { error?: string }; + const report = await engine.test!(bytes, module, { modules: a.compiled.modules, names, files: filter.files, ids: filter.ids, variables, triggers }) as TestReport & { error?: string }; if (cancelled) return; if (report.error) { setStatus("error", t("Not run in the game: {message}", { message: report.error })); return; } // A test of several players (TrigScript 4, A3): a copy of the game for each player, kept in step, where the @@ -829,6 +860,63 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa } }; + /** + * One test debugged in the game (TrigScript 4, A5): built as for a run in the game, then run once by the engine's + * page with everything it did written down; the Debug view walks that record. The game stays with the page for + * pictures of the run's frames, drawn by a view of the scm-sim plugin's where this copy of the game may draw. + */ + const debugTest = async (id: string) => { + if (!editor || testsRunning || building || testing || !ready) return; + const info = tests.list.find((x) => x.id === id); + if (!info || info.kind !== "test") { setStatus("info", t("There is no test at the cursor.")); return; } + if (info.players) { setStatus("info", t("“{test}” is a test of several players: the debugger runs one game, and a test of several players runs as a copy of the game for each. Not yet.", { test: info.name })); return; } + testsRunning = true; + renderTesting(); + try { + const got = await prepareInGame({ ids: [id] }, "debug"); + if (!got || cancelled) return; + const { engine, a, names, variables, triggers, module, bytes, built } = got; + setStatus("busy", t("Running “{test}” in the game, writing down everything it does…", { test: info.name })); + const session = await engine.debug!(bytes, module, { modules: a.compiled.modules, names, variables, triggers, test: id }); + if (cancelled) { session.close(); return; } + const recording = session.recording as DebugRecording; + // A view to draw the pictures in, where the plugin has one for a debugged run. + let view: DebugGameView | string | null = null; + if (engine.view) { + try { + const host = el("div"); + const v = engine.view(host) as ScmSimDebugView; + if (typeof v.debug === "function" && typeof v.show === "function") { + const shown = await v.debug(bytes).then(() => null, (err: Error) => err.message); + if (shown) { v.dispose(); view = shown; } + else { + v.onError?.((message) => setStatus("error", t("No picture of the game: {message}", { message }))); + view = { element: host, show: (p) => v.show!(p), dispose: () => v.dispose() }; + } + } else { v.dispose(); view = t("The scm-sim plugin's view is from before the debugger: update the plugin for a picture of the game."); } + } catch (err) { view = (err as Error).message; } + } + debug.start({ recording, variables, pictures: session, view, files: built }); + debug.setFiles(files); + const r = recording.result; + setStatus(r.status === "failed" ? "error" : "ok", r.status === "failed" + ? t("Debugging “{test}”: it failed in the game at frame {frame}. The debugger stands there; step back to see how it came to that.", { test: info.name, frame: recording.first + recording.frames - 1 }) + : t("Debugging “{test}”: {frames, plural, one {# frame} other {# frames}} in the game, recorded in {ms} ms.", { test: info.name, frames: recording.frames, ms: recording.ms }), NOTICE_MS); + } catch (err) { + setStatus("error", t("Not debugged in the game: {message}", { message: (err as Error).message })); + } finally { + testsRunning = false; + render(); + } + }; + + const debugTestAtCursor = () => { + const at = editor?.cursor(); + const here = at ? tests.list.filter((x) => x.kind === "test" && x.file === normalizePath(at.file) && x.line <= at.line).sort((a, b) => b.line - a.line)[0] : undefined; + if (!here) { setStatus("info", t("There is no test at the cursor.")); return; } + void debugTest(here.id); + }; + /** * What a frame costs, beside the line (TrigScript 4, A4): after a run in the game, each line that ran at least * COST_HINT conditions and actions in one frame of some test says how many at the most, while its file is as it @@ -1619,8 +1707,19 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa /* ── Commands ── */ /** Everything the workspace does, once: the keys, the More menu and Monaco's command palette are all read from here. */ - interface Command { id: string; label: string; key?: { code: string; mod?: boolean; shift?: boolean }; context?: boolean; run(): void } + interface Command { id: string; label: string; key?: { code: string; mod?: boolean; shift?: boolean }; context?: boolean; run(): void; when?: () => boolean } + /** While a test is debugged, its keys come before the others (F5 continues rather than plays). */ + const debugging = () => debug.active; const commands: Command[] = [ + { id: "debugContinue", label: t("Debug: Continue"), key: { code: "F5" }, when: debugging, run: () => debug.continueForward() }, + { id: "debugContinueBack", label: t("Debug: Continue Backwards"), key: { code: "F6" }, when: debugging, run: () => debug.continueBack() }, + { id: "debugStep", label: t("Debug: On a Line"), key: { code: "F10" }, when: debugging, run: () => debug.stepForward() }, + { id: "debugStepBack", label: t("Debug: Back a Line"), key: { code: "F10", shift: true }, when: debugging, run: () => debug.stepBack() }, + { id: "debugFrame", label: t("Debug: On a Frame"), key: { code: "F11" }, when: debugging, run: () => debug.nextFrame() }, + { id: "debugFrameBack", label: t("Debug: Back a Frame"), key: { code: "F11", shift: true }, when: debugging, run: () => debug.prevFrame() }, + { id: "debugStop", label: t("Debug: Stop"), key: { code: "F5", shift: true }, when: debugging, run: () => debug.end() }, + { id: "debugTestAtCursor", label: t("Debug Test at Cursor in the Game"), context: true, run: () => debugTestAtCursor() }, + { id: "toggleBreakpoint", label: t("Toggle Breakpoint"), key: { code: "F9" }, run: () => { const at = editor?.cursor(); if (at) debug.toggleBreakpoint(normalizePath(at.file), at.line); } }, // The editor's own Ctrl+S does not reach a map under a dialog, and a browser would offer to save the page. { id: "save", label: t("Save the Map"), key: { code: "KeyS", mod: true }, run: () => { void api.document.save(); } }, { id: "test", label: t("Play the Map"), key: { code: "F5" }, run: () => { void test(); } }, @@ -1660,7 +1759,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa // F1 is the editor's list of shortcuts outside the workspace; inside it, it is the palette, as it is in VS Code. const c = (mod && e.shiftKey && e.code === "KeyP") || (!mod && !e.shiftKey && e.code === "F1") ? { run: palette } - : commands.find((x) => x.key && x.key.code === e.code && !!x.key.mod === mod && !!x.key.shift === e.shiftKey); + : commands.find((x) => x.key && x.key.code === e.code && !!x.key.mod === mod && !!x.key.shift === e.shiftKey && (!x.when || x.when())); if (!c) return; e.preventDefault(); e.stopPropagation(); @@ -1689,7 +1788,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa monaco = m; if (generated) setDeclarations(m, generated.decls); setHoverVariables(m, () => result?.variables ?? []); - setLineHints(m, () => [...(result?.hints ?? []), ...costHints()]); + setLineHints(m, () => [...(result?.hints ?? []), ...costHints(), ...debug.hints()]); setMapRefs(m, mapRefs); // Uncover first: `done` puts the host back in its own place, and Monaco measures it where it lands. loadingCover.done(); @@ -1698,14 +1797,18 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa editor = createScriptEditor(m, hostEl, files, options.file ?? ENTRY_FILE, (path, text) => { files = { ...files, [path]: text }; svc.writeFiles(files); + debug.setFiles(files); check(); - }, (id) => { void runTests({ ids: [id] }); }); + }, (id) => { void runTests({ ids: [id] }); }, (file, line) => debug.toggleBreakpoint(file, line)); + debug.setFiles(files); + editor.setDebug(debug.marks()); const code = editor.editor; for (const c of commands) { code.addAction({ id: `trigscript.${c.id}`, label: `TrigScript: ${c.label}`, - keybindings: c.key ? [(c.key.mod ? m.KeyMod.CtrlCmd : 0) | (c.key.shift ? m.KeyMod.Shift : 0) | m.KeyCode[c.key.code as keyof typeof m.KeyCode]] : undefined, + // (A key that only means something while debugging is the workspace's to hear, not Monaco's.) + keybindings: c.key && !c.when ? [(c.key.mod ? m.KeyMod.CtrlCmd : 0) | (c.key.shift ? m.KeyMod.Shift : 0) | m.KeyCode[c.key.code as keyof typeof m.KeyCode]] : undefined, ...(c.context ? { contextMenuGroupId: "navigation", contextMenuOrder: 9 } : {}), run: () => c.run(), }); @@ -1741,6 +1844,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa if (timer !== null) clearTimeout(timer); if (statusTimer !== null) clearTimeout(statusTimer); root.removeEventListener("keydown", onKey, true); + debug.end(); shell.dispose(); editor?.dispose(); editor = null; diff --git a/ko.ts b/ko.ts index 6e5ce63..79c4296 100644 --- a/ko.ts +++ b/ko.ts @@ -298,4 +298,70 @@ export const KO: Record = { "{average} conditions and actions a frame on average, {peak} at the most (frame {frame}): {share} of the 500 thousand a frame Remastered has run with no visible effect": "프레임당 조건과 액션 평균 {average}개, 최대 {peak}개 ({frame}번째 프레임): 리마스터가 눈에 띄는 영향 없이 실행한 프레임당 50만 개 중 {share}", "Go to the line": "그 줄로 이동", "{peak} a frame at the most (frame {frame}), in {frames, plural, one {# frame} other {# frames}}": "프레임당 최대 {peak}개 ({frame}번째 프레임), {frames}개 프레임에서 실행", + "Debug": "디버그", + "The text lines up to this frame": "이 프레임까지의 텍스트 줄", + "The game": "게임", + "No picture of the game here.": "여기에는 게임 화면이 없습니다.", + "What this line wrote in frame {frame}, as it stands at the debugger's cursor; ×n: how many times it ran in the frame so far.": "{frame}번째 프레임에서 이 줄이 쓴 값으로, 디버거 커서 위치 기준입니다. ×n: 이 프레임에서 지금까지 실행된 횟수.", + "No picture of the game: {message}": "게임 화면 없음: {message}", + "No test is being debugged. Debug one from its row in the Testing view (the bug icon) or with Debug Test at Cursor: it runs once in the game with everything it did written down, and here you walk it, forwards and back.": "디버그 중인 테스트가 없습니다. 테스트 보기에서 테스트 줄의 벌레 아이콘이나 '커서 위치의 테스트를 게임에서 디버그'로 시작하세요. 테스트가 게임에서 한 번 실행되면서 한 일이 모두 기록되고, 여기서 그 기록을 앞뒤로 따라갈 수 있습니다.", + "The script changed since this run: its lines may have moved. Debug the test again to walk the script as it is.": "이 실행 이후 스크립트가 바뀌어 줄 위치가 달라졌을 수 있습니다. 지금의 스크립트를 따라가려면 테스트를 다시 디버그하세요.", + "Stopped at the breakpoint on line {line}.": "{line}번째 줄의 중단점에서 멈췄습니다.", + "{name} changed: {before} → {after}": "{name} 바뀜: {before} → {after}", + "The end of the run: {message}": "실행의 끝: {message}", + "the test failed": "테스트 실패", + "The end of the run: the test passed.": "실행의 끝: 테스트 통과.", + "The record stops at frame {frame} ({why}): the variables are not known past it.": "기록이 {frame}번째 프레임에서 끝납니다({why}). 그 뒤의 변수 값은 알 수 없습니다.", + "{test} failed: {message}": "{test} 실패: {message}", + "{test} passed.": "{test} 통과.", + "no frames": "프레임 없음", + "frame {frame}": "{frame}번째 프레임", + "end of frame": "프레임 끝", + "eudplib": "eudplib", + "Continue backwards (F6): to the breakpoint or the change of a watched variable before this, or the start": "뒤로 계속 (F6): 이전의 중단점이나 조사식 변수가 바뀐 곳, 없으면 처음으로", + "Back a line (Shift+F10)": "한 줄 뒤로 (Shift+F10)", + "On a line (F10)": "한 줄 앞으로 (F10)", + "Continue (F5): to the next breakpoint or change of a watched variable, or the end": "계속 (F5): 다음 중단점이나 조사식 변수가 바뀌는 곳, 없으면 끝으로", + "Back a frame (Shift+F11)": "한 프레임 뒤로 (Shift+F11)", + "On a frame (F11): to the end of this one": "한 프레임 앞으로 (F11): 이 프레임의 끝으로", + "To the start of the run": "실행의 처음으로", + "Stop debugging (Shift+F5)": "디버깅 중지 (Shift+F5)", + "Stop at every step, eudplib's own work and what the game wrote by itself included, not only at the script's lines": "스크립트의 줄뿐 아니라 eudplib 자체의 작업과 게임이 스스로 쓴 것까지 모든 단계에서 멈춥니다", + "Every step": "모든 단계", + "Not recorded past frame {frame}: {why}": "{frame}번째 프레임 이후는 기록되지 않음: {why}", + "Line {line} runs in frame {frame}": "{line}번째 줄이 {frame}번째 프레임에서 실행됨", + "Frames {from} to {to}: the script's conditions and actions a frame ({peak} at the most). Red: a breakpoint's line runs; gold: a line of text was shown. Click or drag to go to a frame.": "{from}~{to}번째 프레임: 프레임당 스크립트의 조건과 액션 수 (최대 {peak}개). 빨간색: 중단점 줄이 실행됨, 금색: 텍스트 줄이 표시됨. 클릭하거나 끌어서 프레임으로 이동합니다.", + "Variables": "변수", + "Not known here: the record stops at frame {frame}.": "여기서는 알 수 없습니다: 기록이 {frame}번째 프레임에서 끝납니다.", + "Watched": "조사식", + "The build did not say where the programs' variables are.": "빌드가 프로그램 변수의 위치를 알려 주지 않았습니다.", + "Frame {frame}": "{frame}번째 프레임", + "line {line}": "{line}번째 줄", + "the test": "테스트", + "the game itself": "게임 자체", + "{n, plural, one {# write} other {# writes}}": "쓰기 {n}회", + "conditions and actions": "조건과 액션", + "… and {n} more steps": "… 외 {n}단계", + "Nothing shown yet.": "아직 표시된 것이 없습니다.", + "Stop watching {name}": "{name} 조사식 해제", + "Watch {name}: Continue stops where it changes": "{name} 조사: 계속하면 값이 바뀌는 곳에서 멈춥니다", + "Debug this test in the game: run it once with everything it did written down, then walk it forwards and back": "이 테스트를 게임에서 디버그: 한 일을 모두 기록하며 한 번 실행한 뒤, 앞뒤로 따라갑니다", + "Debugging in the game needs the scm-sim plugin (0.11 or newer) on scmjs.dev: install, update or turn it on under Plugins ▸ Manage Plugins….": "게임에서 디버그하려면 scmjs.dev의 scm-sim 플러그인(0.11 이상)이 필요합니다. 플러그인 ▸ 플러그인 관리…에서 설치하거나 업데이트하거나 켜세요.", + "Not debugged in the game: {message}": "게임에서 디버그하지 못함: {message}", + "Not debugged: the engine's page is from before the debugger. Reload the editor and try again.": "디버그하지 못함: 엔진 페이지가 디버거 이전 버전입니다. 편집기를 새로 고친 뒤 다시 시도하세요.", + "“{test}” is a test of several players: the debugger runs one game, and a test of several players runs as a copy of the game for each. Not yet.": "“{test}”는 여러 플레이어의 테스트입니다. 디버거는 게임 하나를 실행하지만, 여러 플레이어의 테스트는 플레이어마다 게임 사본을 하나씩 실행합니다. 아직 지원하지 않습니다.", + "Running “{test}” in the game, writing down everything it does…": "“{test}”를 게임에서 실행하며 하는 일을 모두 기록하는 중…", + "The scm-sim plugin's view is from before the debugger: update the plugin for a picture of the game.": "scm-sim 플러그인의 보기가 디버거 이전 버전입니다. 게임 화면을 보려면 플러그인을 업데이트하세요.", + "Debugging “{test}”: it failed in the game at frame {frame}. The debugger stands there; step back to see how it came to that.": "“{test}” 디버그 중: 게임에서 {frame}번째 프레임에 실패했습니다. 디버거가 그곳에 있습니다. 뒤로 가며 어떻게 그렇게 됐는지 보세요.", + "Debugging “{test}”: {frames, plural, one {# frame} other {# frames}} in the game, recorded in {ms} ms.": "“{test}” 디버그 중: 게임에서 {frames}개 프레임, {ms}ms에 기록했습니다.", + "Debug: Continue": "디버그: 계속", + "Debug: Continue Backwards": "디버그: 뒤로 계속", + "Debug: On a Line": "디버그: 한 줄 앞으로", + "Debug: Back a Line": "디버그: 한 줄 뒤로", + "Debug: On a Frame": "디버그: 한 프레임 앞으로", + "Debug: Back a Frame": "디버그: 한 프레임 뒤로", + "Debug: Stop": "디버그: 중지", + "Debug Test at Cursor in the Game": "커서 위치의 테스트를 게임에서 디버그", + "Toggle Breakpoint": "중단점 설정/해제", + "Breakpoint: the debugger's Continue stops before this line runs (a test's line: at the end of the frame it asks for). Click to take it away.": "중단점: 디버거에서 계속하면 이 줄이 실행되기 전에 멈춥니다(테스트의 줄이면 그 줄이 요청한 프레임의 끝에서). 클릭하면 없어집니다.", }; diff --git a/monaco.ts b/monaco.ts index fd126c4..d79ab80 100644 --- a/monaco.ts +++ b/monaco.ts @@ -390,6 +390,11 @@ export interface ScriptEditor { insert(text: string): void; /** The file and line the cursor is on. */ cursor(): { file: string; line: number }; + /** + * The debugger's marks (TrigScript 4, A5): breakpoints in the margin, the line about to run and the test's line + * lit; `onBreakpoint` (of `createScriptEditor`) hears a click in the margin where no test's mark is. + */ + setDebug(marks: { breakpoints: { file: string; line: number }[]; at: { file: string; line: number } | null; test: { file: string; line: number } | null }): void; dispose(): void; } @@ -411,7 +416,7 @@ export function releaseScriptEditor(monaco: MonacoApi): void { defaults.setCompilerOptions(defaults.getCompilerOptions()); } -export function createScriptEditor(monaco: MonacoApi, host: HTMLElement, files: ScriptFiles, active: string, onChange: (path: string, text: string) => void, onMark?: (id: string) => void): ScriptEditor { +export function createScriptEditor(monaco: MonacoApi, host: HTMLElement, files: ScriptFiles, active: string, onChange: (path: string, text: string) => void, onMark?: (id: string) => void, onBreakpoint?: (file: string, line: number) => void): ScriptEditor { disposeModels(monaco); const models = new Map(); const subs = new Map(); @@ -419,6 +424,8 @@ export function createScriptEditor(monaco: MonacoApi, host: HTMLElement, files: const decorations = new Map(); /** Per file, the ids of the tests' decorations. */ const testDecorations = new Map(); + /** Per file, the ids of the debugger's. */ + const debugDecorations = new Map(); /** The marks as last set, to find the one a click in the margin is on. */ let testMarks: TestMark[] = []; /** Per file, the view state (cursor, scroll) to restore when it is shown again. */ @@ -442,6 +449,8 @@ export function createScriptEditor(monaco: MonacoApi, host: HTMLElement, files: minimap: { enabled: false }, // Where a test's mark goes; it stays narrow for a script without any. glyphMargin: true, + // (Room for the debugger's arrow beside the line numbers.) + lineDecorationsWidth: 16, scrollBeyondLastLine: false, renderLineHighlight: "line", tabSize: 2, @@ -458,6 +467,7 @@ export function createScriptEditor(monaco: MonacoApi, host: HTMLElement, files: const line = e.target.position?.lineNumber; const mark = testMarks.find((m) => normalizePath(m.file) === current && m.line === line); if (mark) onMark?.(mark.id); + else if (line) onBreakpoint?.(current, line); }); const show = (path: string) => { const p = normalizePath(path); @@ -492,6 +502,7 @@ export function createScriptEditor(monaco: MonacoApi, host: HTMLElement, files: views.delete(p); decorations.delete(p); testDecorations.delete(p); + debugDecorations.delete(p); model.dispose(); }, rename(from, to) { @@ -508,6 +519,7 @@ export function createScriptEditor(monaco: MonacoApi, host: HTMLElement, files: views.delete(a); decorations.delete(a); testDecorations.delete(a); + debugDecorations.delete(a); if (wasCurrent) editor.setModel(null); model.dispose(); const next = make(b, text); @@ -553,6 +565,19 @@ export function createScriptEditor(monaco: MonacoApi, host: HTMLElement, files: decorations.set(p, model.deltaDecorations(decorations.get(p) ?? [], next)); } }, + setDebug(d) { + const here = (p: { file: string; line: number } | null, path: string, lines: number) => p && normalizePath(p.file) === path && p.line >= 1 && p.line <= lines; + for (const [p, model] of models) { + const lines = model.getLineCount(); + const next: Monaco.editor.IModelDeltaDecoration[] = d.breakpoints.filter((b) => here(b, p, lines)).map((b) => ({ + range: new monaco.Range(b.line, 1, b.line, 1), + options: { glyphMarginClassName: "trigscript-bp", glyphMarginHoverMessage: { value: tr("Breakpoint: the debugger's Continue stops before this line runs (a test's line: at the end of the frame it asks for). Click to take it away.") }, stickiness: monaco.editor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }, + })); + if (d.test && here(d.test, p, d.test.line) && d.test.line <= lines && !(d.at && normalizePath(d.at.file) === p && d.at.line === d.test.line)) next.push({ range: new monaco.Range(d.test.line, 1, d.test.line, 1), options: { isWholeLine: true, className: "trigscript-debug-test", linesDecorationsClassName: "trigscript-debug-test-arrow" } }); + if (d.at && here(d.at, p, lines)) next.push({ range: new monaco.Range(d.at.line, 1, d.at.line, 1), options: { isWholeLine: true, className: "trigscript-debug-line", linesDecorationsClassName: "trigscript-debug-arrow" } }); + debugDecorations.set(p, model.deltaDecorations(debugDecorations.get(p) ?? [], next)); + } + }, dispose() { for (const s of subs.values()) s.dispose(); editor.dispose(); diff --git a/shell.ts b/shell.ts index e690997..3df9135 100644 --- a/shell.ts +++ b/shell.ts @@ -54,6 +54,19 @@ const ICONS = { "circle-outline": "eabc", "circle-slash": "eabd", "pass-filled": "ebb3", + // The debugger's (TrigScript 4, A5). + "debug-alt": "eb91", + "debug-continue": "eacf", + "debug-reverse-continue": "eb8e", + "debug-step-over": "ead6", + "debug-step-back": "eb8f", + "debug-stop": "ead7", + "debug-restart": "ead2", + "debug-line-by-line": "ebd0", + "arrow-left": "ea9b", + "arrow-right": "ea9c", + eye: "ea70", + game: "ec17", } as const; export type IconName = keyof typeof ICONS; diff --git a/vendor/scmSim.ts b/vendor/scmSim.ts index aab6e4c..37fed8d 100644 --- a/vendor/scmSim.ts +++ b/vendor/scmSim.ts @@ -14,6 +14,29 @@ export interface ScmSimStatus { tests?: number | null; /** The version of the request for a test of several players; null or absent: the page (or the browser) cannot run copies. */ copies?: number | null; + /** The version of the debugger's requests (TrigScript 4, A5); null or absent: the page has no debugger. */ + debug?: number | null; +} + +/** A test recorded for the debugger: the module's record, and the run's game held for pictures until `close()`. */ +export interface ScmSimDebugSession { + readonly recording: unknown; + picture(frame: number, options?: { signal?: AbortSignal }): Promise<{ frame: number; kept: unknown }>; + close(): void; + readonly closed: boolean; +} + +/** + * The part of the plugin's view (`ScmSimView`) the debugger draws with: `debug(map)` starts the built map the way + * the engine's page started it for the record, `show(picture)` draws a picture of the run. Absent from a view from + * before the debugger. + */ +export interface ScmSimDebugView { + debug?(map: Uint8Array): Promise; + show?(picture: { frame: number; kept: unknown }): void; + /** What went wrong with no request waiting on it: a picture that could not be drawn. */ + onError?(listener: (message: string) => void): { dispose(): void }; + dispose(): void; } /** One copy's answer to `testCopies`. */ @@ -42,4 +65,11 @@ export interface ScmSimEngine { * Resolves with each copy's answer, in the order of `players`. Absent from the plugin before 0.11.0. */ testCopies?(scenario: Uint8Array, module: string, payload: unknown, options: { players: number[]; triggers?: { payload: number; triggers: [number, number, number, string][] } | null; signal?: AbortSignal; onProgress?: (p: { stage: "starting" }) => void }): Promise; + /** + * One test recorded for the debugger (TrigScript 4, A5): the module's `debugGameTest(game, payload)` runs it once + * with everything written down, and the run's game stays with the page for pictures. Absent before 0.11.0. + */ + debug?(scenario: Uint8Array, module: string, payload: unknown, options?: { signal?: AbortSignal; onProgress?: (p: { stage: "starting" }) => void }): Promise; + /** A view of the game in `element` (the plugin's replay view, which draws a debugged run's pictures from 0.11.0). */ + view?(element: HTMLElement): ScmSimDebugView; } From 39822dc14375b4c25c22d378ec2a5c13117bcb8a Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 13:32:05 -0600 Subject: [PATCH 021/238] TrigScript 4: A5 as built Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 396a3cb..d6b3813 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -551,6 +551,74 @@ k × 1,000 actions a frame, the user says when it stutters) gives the first numb - **Watchpoints.** "Stop when this changes" compares a dword once a frame, which is cheap. - **The game beside the code.** scm-sim renders the frame the debugger stands at. +**As built (2026-10-16, under Node; the editor end to end below).** There was no first backend: 9½ was never +built, and 3.11.0 became the Korean release. So this is the debugger itself, with the game as its only backend. +It also does not stop a running game. A test is synchronous JavaScript and a frame is the game's trigger loop run +to its end, so neither can pause in the middle. Instead the test runs once with everything it did written down, +and the debugger walks the record. + +- **The record** (`compiler/gameDebug.ts`, `RecordingHandle`). It wraps the host's test handle for one test, and + writes down each frame of the run: + - *Steps.* scm-sim's cost tap now also says each record as it runs (`CostTap.trace`), with the line A4's grouping + gives it. Records of one line in a row make one step. A loop's turns come out as its lines over and over. + - *Writes.* `EudTap.listen` now says what a dword held before. Every dword of the script's memory a step changed + is kept with its value before and after. "The script's memory" is the start of `STRx` to the end of the payload: + variables, arrays, the heap, made texts and the string table. With both values, a step back puts writes back, + so it is as quick as a step forward. + - *What no record said.* At each frame's end the memory is compared with the record's copy. A difference becomes + the frame's last step ("the game itself"), so the copy cannot drift. None has turned up on the probes. + - *The test's line* that asked for the frame. This is read from V8's call sites, not from stack text: under Node + with source maps, formatting a stack every frame cost ten times what the game did. That took a 216-frame + record from 4.6 s to 0.5 s. + - *What was shown* on the text lines. +- **Pictures.** These stay on the host's side and are never sent with the record: + - The handle can now keep the game and put it back (`keep()` / `restore()`): the engine's snapshot plus the + payload range it does not cover. + - A game is kept every 48 frames, and when the kept games pass 256 MB, half go and the spacing doubles. + - `GameSim` now tells the handle of its own writes between frames (`wrote()`: keys, the mouse, a scratch + location, hit points), and the recorder keeps those with the test's actions, typed lines and calls. + - A picture of frame F is then the nearest kept game, those things done again at their frames, and the frames + played. Mid-frame it is the frame before's end. + - The analysis page holds the run in a session (`scmjs.sim.debug`, `picture`; the plugin's `debug()`). The + viewer page starts the same map the same way and draws a kept game put back over it (`scmjs.view.debug`, + `show`). It refuses one whose frame-0 state differs. +- **The walk** (`compiler/debugModel.ts`, `DebugCursor`): + - a line or a frame, either way; + - continue to a breakpoint or a watched variable's change, either way; + - the variables at the cursor, read as `sim.program()` reads them, over the record's copy of the memory; + - what each line wrote in the frame so far. +- **In the workspace** (`debugger.ts`): + - *Starting.* *Debug this test in the game* is on each test's row in Testing, and *Debug Test at Cursor* is in + the palette and the editor's menu. A failed test opens at its failure. + - *The Debug view* has a bar of controls, a timeline (the frames' cost, breakpoint hits in red, shown text in + gold; click or drag), the variables (changes in gold, an eye to watch one), the frame's steps, and the game + with its text lines. + - *The editor* shows breakpoints in the margin (F9 or a click), the line about to run and the test's line lit, + and beside each line run in the frame, the values it wrote. + - *Keys:* F5 and F6 continue forwards and back; F10 and F11 step a line and a frame, with Shift for back; + Shift+F5 stops. + - The marks show only while the script is the text that was run. +- **Found on the way:** the build's variable map crashed on any program with a loop over units (`LoopUnit` has no + `decl`). Fixed in `trigscript.py`. The units probe could not run in the game before this. +- **Measured** (under Node, `scripts/debug-game.mts`, `probes/game-debug.ts`): recording costs about 2.5 ms a + frame, kept games included. + + | Test | Frames | Steps | Writes | Record | Time | + | --- | --- | --- | --- | --- | --- | + | waves | 216 | 268 | 121,213 | 1.7 MB | 536 ms | + | the failing test | 146 | 183 | 96,936 | 1.4 MB | 349 ms | + + Most writes are eudplib's own. A picture takes 45 ms (played on from the last) to 110 ms (from a kept game). + At six frames of each test, both ways, the picture's memory was what the record said, and the variables read + from each agreed. +- **Not done:** + - A test of several players. The debugger runs one game. + - Units are known only by their slot: the record keeps the script's memory, not the unit table. + - Conditional breakpoints, hover values, a call stack. The stack's frames are cells too, so the record has + them, but nothing reads them yet. + - On a copy that may only watch (StarEdit's download), there is no picture: a picture of a map's run is the map + played. Everything else works. + ## Part B — the language Each slice states what changes in the IR, the simulator and the Python, and has a probe. From b0f333496ef42e488c3d205be7d164a59a025503 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 13:44:24 -0600 Subject: [PATCH 022/238] TrigScript 4, A5: fixes from the debugger's first run in a built editor - The game's view is a frame: made in a detached box it never loaded, and the panel takes a view's body out when another view is shown, so it lives in an overlay laid over its place in the Debug view (Debugger.gameHost, placed each animation frame while a session has a view). - The Debug view takes the focus (tabIndex -1), and keeps it when a button of its own is drawn anew: F10 and the rest went to the page. - A session grows the panel to 340 px (Shell.growPanel). - Continue backwards on a watched variable stood still: it found the change the cursor already stood after. - The bar says when a continue backwards reached the start. Co-Authored-By: Claude Opus 5.5 --- compiler/debugModel.ts | 5 ++- debugger.ts | 80 +++++++++++++++++++++++++++++++++++---- editor.ts | 2 +- ko.ts | 1 + shell.ts | 10 +++++ tests/debug-model.test.ts | 4 ++ 6 files changed, 92 insertions(+), 10 deletions(-) diff --git a/compiler/debugModel.ts b/compiler/debugModel.ts index 5239efe..7550674 100644 --- a/compiler/debugModel.ts +++ b/compiler/debugModel.ts @@ -174,6 +174,7 @@ export class DebugCursor { return null; }; let { k, s } = this.at; + const from = { k, s }; // Positions in order: each step of a frame, then its end. A position is checked as it is reached, never where // the cursor stood; `passed` is the step gone over on the way to it. const next = (): number | null => { @@ -190,7 +191,9 @@ export class DebugCursor { const passed = next(); if (Number.isNaN(passed)) break; // A watched variable a passed step wrote, and whose value it changed: stop just after that step. - const name = passed !== null && cells.size ? touches(passed) : null; + // (Backwards, the change the cursor stands just after is where it is already: the one before it is next.) + const here = dir < 0 && passed !== null && k === from.k && passed + 1 === from.s; + const name = passed !== null && cells.size && !here ? touches(passed) : null; if (name && passed !== null) { this.moveTo({ k, s: passed }); const before = this.watchText(name); diff --git a/debugger.ts b/debugger.ts index ccea714..872ef8a 100644 --- a/debugger.ts +++ b/debugger.ts @@ -56,6 +56,7 @@ export interface DebugStart { } export const DEBUG_STYLE = ` +.tsd .tsd-debug:focus { outline: none; } .tsd .tsd-debug { box-sizing: border-box; height: 100%; display: flex; flex-direction: column; min-height: 0; font-size: var(--fs-md); } .tsd .tsd-debug-bar { flex: none; display: flex; align-items: center; gap: 2px; padding: 2px 8px; border-bottom: 1px solid var(--border); white-space: nowrap; overflow: hidden; } .tsd .tsd-debug-bar .tsd-icon-button { display: inline-flex; } @@ -93,7 +94,8 @@ export const DEBUG_STYLE = ` .tsd .tsd-debug-col .tsd-empty { padding: 6px 10px; color: var(--text-faint); white-space: normal; line-height: 1.45; } .tsd .tsd-debug-game { flex: 1.3 1 0; display: flex; flex-direction: column; } .tsd .tsd-debug-game .tsd-view { position: relative; flex: 1; min-height: 120px; background: #000; } -.tsd .tsd-debug-game .tsd-view > * { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; } +.tsd .tsd-debug-overlay { position: absolute; z-index: 4; display: none; background: #000; } +.tsd .tsd-debug-overlay > div, .tsd .tsd-debug-overlay iframe { display: block; width: 100%; height: 100%; border: 0; } .tsd .tsd-debug-game .tsd-shown { flex: none; max-height: 40%; overflow: auto; padding: 2px 10px; font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--text-dim); } .tsd .tsd-debug-game .tsd-shown div { white-space: pre-wrap; } .trigscript-debug-line { background: rgba(214, 178, 74, 0.18); } @@ -107,6 +109,8 @@ export const DEBUG_STYLE = ` const NS = "http://www.w3.org/2000/svg"; +/** How tall the panel is made for a session: the bar, the timeline, and room for the columns and the game. */ +const DEBUG_PANEL = 340; /** How long the cursor must stay before a picture of its frame is asked for (scrubbing asks for none on the way). */ const PICTURE_MS = 60; @@ -132,12 +136,21 @@ export class Debugger { private readonly bar: HTMLElement; private readonly time: HTMLElement; private readonly cols: HTMLElement; + private readonly root: HTMLElement; /** The three columns, made once a session (the game's view is a frame: moved, it would load again). */ private readonly varsCol: HTMLElement; private readonly stepsCol: HTMLElement; private readonly gameCol: HTMLElement; private readonly viewBox: HTMLElement; private readonly shownBox: HTMLElement; + /** + * Where the game's view lives: over the workspace, laid over `viewBox` while the Debug view shows and hidden + * otherwise. The view is a frame, which loads again whenever it is moved or taken out of the page, and the panel + * takes a view's body out when another is shown. + */ + private readonly overlay: HTMLElement; + private placing = 0; + private placed = ""; constructor(deps: DebuggerDeps) { this.d = deps; @@ -152,7 +165,11 @@ export class Debugger { this.viewBox = el("div"); this.shownBox = el("div", { className: "tsd-shown", title: t("The text lines up to this frame") }); this.gameCol = el("div", { className: "tsd-debug-col tsd-debug-game" }, el("h5", {}, t("The game")), this.viewBox, this.shownBox); - this.body.append(el("div", { className: "tsd-debug" }, this.bar, this.time, this.cols)); + this.overlay = el("div", { className: "tsd-debug-overlay" }); + shell.root.append(this.overlay); + // (Focusable, so that a click anywhere in the view leaves the keys with the workspace: F10 and the rest.) + this.root = el("div", { className: "tsd-debug", tabIndex: -1 }, this.bar, this.time, this.cols); + this.body.append(this.root); this.time.addEventListener("pointerdown", (e) => { if (!this.session) return; this.time.setPointerCapture(e.pointerId); @@ -167,28 +184,67 @@ export class Debugger { get active(): boolean { return this.session !== null; } + /** + * Where a view of the game for the next session goes (`DebugStart.view`'s element must be this): in the page from + * the start, since a frame made outside it never loads. + */ + gameHost(): HTMLElement { + const host = this.d.el("div"); + this.overlay.replaceChildren(host); + return host; + } + + /** Keeps the game's view over its place in the Debug view, and out of sight while that is not shown. */ + private place = () => { + this.placing = 0; + const s = this.session; + if (!s || !s.view || typeof s.view === "string") { this.overlay.style.display = "none"; this.placed = ""; return; } + const root = this.d.shell.root, box = this.viewBox; + const r = box.isConnected ? box.getBoundingClientRect() : null; + let key = "none"; + if (r && r.width > 0 && r.height > 0) { + const o = root.getBoundingClientRect(); + key = `${Math.round(r.left - o.left)},${Math.round(r.top - o.top)},${Math.round(r.width)},${Math.round(r.height)}`; + } + if (key !== this.placed) { + this.placed = key; + if (key === "none") this.overlay.style.display = "none"; + else { + const [x, y, w, h] = key.split(","); + Object.assign(this.overlay.style, { display: "block", left: `${x}px`, top: `${y}px`, width: `${w}px`, height: `${h}px` }); + } + } + this.placing = requestAnimationFrame(this.place); + }; + /** A recorded run to walk: the cursor at its start, or at its end when the test failed (where debugging begins). */ start(s: DebugStart): void { - this.end(); + const keep = s.view && typeof s.view !== "string" ? s.view.element : null; + this.end(keep); const cursor = new DebugCursor(s.recording, s.variables); this.session = { ...s, cursor, costs: cursor.frameCosts(), last: new Map(), stop: null, picture: { timer: null, asked: -1, shown: -1 } }; const { el, t } = this.d; - this.viewBox.replaceChildren(s.view && typeof s.view !== "string" ? s.view.element : el("div", { className: "tsd-empty" }, typeof s.view === "string" ? s.view : t("No picture of the game here."))); - this.viewBox.className = s.view && typeof s.view !== "string" ? "tsd-view" : ""; + this.viewBox.replaceChildren(...(keep ? [] : [el("div", { className: "tsd-empty" }, typeof s.view === "string" ? s.view : t("No picture of the game here."))])); + this.viewBox.className = keep ? "tsd-view" : ""; + if (!keep) this.overlay.replaceChildren(); + if (keep && !this.placing) this.placing = requestAnimationFrame(this.place); this.cols.replaceChildren(this.varsCol, this.stepsCol, this.gameCol); if (s.recording.result.status === "failed" && s.recording.frames) cursor.toFrame(s.recording.frames - 1); else this.firstLine(); this.d.shell.showPanel("debug"); + this.d.shell.growPanel(DEBUG_PANEL); this.moved(); } - /** The session is over: the engine's game let go, the editor's marks taken away. */ - end(): void { + /** The session is over: the engine's game let go, the editor's marks taken away (`keep`: a view made for the next). */ + end(keep: HTMLElement | null = null): void { + if (!keep) { this.overlay.replaceChildren(); this.overlay.style.display = "none"; this.placed = ""; } + if (this.placing) { cancelAnimationFrame(this.placing); this.placing = 0; } const s = this.session; if (!s) return; if (s.picture.timer) clearTimeout(s.picture.timer); s.pictures?.close(); - if (s.view && typeof s.view !== "string") s.view.dispose(); + if (s.view && typeof s.view !== "string" && s.view.element !== keep) s.view.dispose(); this.viewBox.replaceChildren(); this.session = null; this.render(); @@ -325,6 +381,13 @@ export class Debugger { } private render() { + // (A button pressed here is drawn anew: the keys stay with the view rather than going to the page.) + const focused = this.root.contains(document.activeElement); + this.draw(); + if (focused && !this.root.contains(document.activeElement)) this.root.focus({ preventScroll: true }); + } + + private draw() { const { el, t } = this.d; const s = this.session; if (!s) { @@ -340,6 +403,7 @@ export class Debugger { if (!this.current()) return { text: t("The script changed since this run: its lines may have moved. Debug the test again to walk the script as it is."), failed: true }; if (s.stop?.kind === "breakpoint") return { text: t("Stopped at the breakpoint on line {line}.", { line: s.stop.place.line }), failed: false }; if (s.stop?.kind === "watch") return { text: t("{name} changed: {before} → {after}", { name: s.stop.name, before: s.stop.before, after: s.stop.after }), failed: false }; + if (s.stop?.kind === "start") return { text: t("The start of the run."), failed: false }; if (s.stop?.kind === "end") return { text: rec.result.status === "failed" ? t("The end of the run: {message}", { message: rec.result.message ?? t("the test failed") }) : t("The end of the run: the test passed."), failed: rec.result.status === "failed" }; if (rec.cut && !c.known) return { text: t("The record stops at frame {frame} ({why}): the variables are not known past it.", { frame: rec.cut.frame, why: rec.cut.why }), failed: true }; return rec.result.status === "failed" ? { text: t("{test} failed: {message}", { test: rec.test.name, message: rec.result.message ?? "" }), failed: true } : { text: t("{test} passed.", { test: rec.test.name }), failed: false }; diff --git a/editor.ts b/editor.ts index 810d296..47efb29 100644 --- a/editor.ts +++ b/editor.ts @@ -884,7 +884,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa let view: DebugGameView | string | null = null; if (engine.view) { try { - const host = el("div"); + const host = debug.gameHost(); const v = engine.view(host) as ScmSimDebugView; if (typeof v.debug === "function" && typeof v.show === "function") { const shown = await v.debug(bytes).then(() => null, (err: Error) => err.message); diff --git a/ko.ts b/ko.ts index 79c4296..f4838fc 100644 --- a/ko.ts +++ b/ko.ts @@ -310,6 +310,7 @@ export const KO: Record = { "{name} changed: {before} → {after}": "{name} 바뀜: {before} → {after}", "The end of the run: {message}": "실행의 끝: {message}", "the test failed": "테스트 실패", + "The start of the run.": "실행의 처음.", "The end of the run: the test passed.": "실행의 끝: 테스트 통과.", "The record stops at frame {frame} ({why}): the variables are not known past it.": "기록이 {frame}번째 프레임에서 끝납니다({why}). 그 뒤의 변수 값은 알 수 없습니다.", "{test} failed: {message}": "{test} 실패: {message}", diff --git a/shell.ts b/shell.ts index 3df9135..ac2f5ef 100644 --- a/shell.ts +++ b/shell.ts @@ -191,6 +191,8 @@ export interface Shell { showPanel(id?: string): void; /** What the view's shortcut does: show it, or hide the panel when it is already what is shown. */ togglePanel(id?: string): void; + /** The panel at least this tall (as far as the editor leaves room), for a view that needs it: the debugger's. */ + growPanel(height: number): void; statusItem(side: "left" | "right"): StatusItemHandle; notify(spec: NotificationSpec): void; dismiss(key: string): void; @@ -619,6 +621,14 @@ export function createShell(options: ShellOptions): Shell { }, showPanel, togglePanel, + growPanel(height) { + const most = Math.max(PANEL_MIN, main.clientHeight - 120); + const next = Math.min(most, Math.max(layout.panelHeight, height)); + if (next === layout.panelHeight) return; + layout.panelHeight = next; + applyLayout(); + saveLayout(); + }, statusItem(side) { const slot = el("span", { hidden: true, style: "display: contents" }); (side === "left" ? statusLeft : statusRight).append(slot); diff --git a/tests/debug-model.test.ts b/tests/debug-model.test.ts index 65a0255..893e697 100644 --- a/tests/debug-model.test.ts +++ b/tests/debug-model.test.ts @@ -127,6 +127,10 @@ describe("walking a debugged run", () => { expect(c.where.k).toBe(3); expect(c.continueBack(new Set(), ["a"])).toEqual({ kind: "watch", name: "a", before: "10", after: "20" }); expect(c.where.k).toBe(3); + // Again: the change before that one, not the one the cursor stands after. + expect(c.continueBack(new Set(), ["a"])).toEqual({ kind: "watch", name: "a", before: "0", after: "10" }); + expect(c.where.k).toBe(1); + expect(c.continueBack(new Set(), ["a"])).toEqual({ kind: "start" }); }); it("says what each line wrote in the frame so far, and what the frames cost", () => { From 2b6915031bec5ff66c4808cf4df3f72f85c00bea Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 13:52:07 -0600 Subject: [PATCH 023/238] TrigScript 4, A5: the Debug view keeps the keys after a click on the timeline, and opens a run on its first line - The timeline's graph is drawn anew under the pointer, so the browser gave the focus to the page: the view takes it on pointerdown. - A passing test's session opened on frame 1's end (no line runs there): it opens on the run's first line of the script. - The game's column does not scroll and the view is clipped to it (it was drawn over the status bar). - A test: a breakpoint stops before the watched change its line makes. Co-Authored-By: Claude Opus 5.5 --- debugger.ts | 25 +++++++++++++++++++------ tests/debug-model.test.ts | 8 ++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/debugger.ts b/debugger.ts index 872ef8a..3bc4841 100644 --- a/debugger.ts +++ b/debugger.ts @@ -92,8 +92,8 @@ export const DEBUG_STYLE = ` .tsd .tsd-debug-col li:hover .tsd-icon-button, .tsd .tsd-debug-col li .tsd-icon-button.tsd-on { visibility: visible; } .tsd .tsd-debug-col .tsd-icon-button.tsd-on { color: var(--gold); } .tsd .tsd-debug-col .tsd-empty { padding: 6px 10px; color: var(--text-faint); white-space: normal; line-height: 1.45; } -.tsd .tsd-debug-game { flex: 1.3 1 0; display: flex; flex-direction: column; } -.tsd .tsd-debug-game .tsd-view { position: relative; flex: 1; min-height: 120px; background: #000; } +.tsd .tsd-debug-game { flex: 1.3 1 0; display: flex; flex-direction: column; overflow: hidden; } +.tsd .tsd-debug-game .tsd-view { position: relative; flex: 1; min-height: 0; background: #000; } .tsd .tsd-debug-overlay { position: absolute; z-index: 4; display: none; background: #000; } .tsd .tsd-debug-overlay > div, .tsd .tsd-debug-overlay iframe { display: block; width: 100%; height: 100%; border: 0; } .tsd .tsd-debug-game .tsd-shown { flex: none; max-height: 40%; overflow: auto; padding: 2px 10px; font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--text-dim); } @@ -172,6 +172,9 @@ export class Debugger { this.body.append(this.root); this.time.addEventListener("pointerdown", (e) => { if (!this.session) return; + // (The graph is drawn anew under the pointer, so the browser would give the focus to the page: the view takes it.) + e.preventDefault(); + this.root.focus({ preventScroll: true }); this.time.setPointerCapture(e.pointerId); this.scrub(e); const move = (m: PointerEvent) => this.scrub(m); @@ -200,11 +203,14 @@ export class Debugger { const s = this.session; if (!s || !s.view || typeof s.view === "string") { this.overlay.style.display = "none"; this.placed = ""; return; } const root = this.d.shell.root, box = this.viewBox; - const r = box.isConnected ? box.getBoundingClientRect() : null; + const b = box.isConnected ? box.getBoundingClientRect() : null; let key = "none"; - if (r && r.width > 0 && r.height > 0) { + if (b) { + // (Only what of it the column shows.) + const c = this.gameCol.getBoundingClientRect(); + const left = Math.max(b.left, c.left), top = Math.max(b.top, c.top), right = Math.min(b.right, c.right), bottom = Math.min(b.bottom, c.bottom); const o = root.getBoundingClientRect(); - key = `${Math.round(r.left - o.left)},${Math.round(r.top - o.top)},${Math.round(r.width)},${Math.round(r.height)}`; + if (right - left > 0 && bottom - top > 0) key = `${Math.round(left - o.left)},${Math.round(top - o.top)},${Math.round(right - left)},${Math.round(bottom - top)}`; } if (key !== this.placed) { this.placed = key; @@ -274,10 +280,17 @@ export class Debugger { prevFrame(): void { this.go((c) => { c.prevFrame(); return null; }); } restart(): void { this.go(() => { this.firstLine(); return null; }); } + /** The run's first line of the script (frames with none run, such as the first, are passed over). */ private firstLine() { const c = this.session!.cursor; c.moveTo({ k: 0, s: c.frameStart(0) }); - if (!c.line() && !this.everyStep && !c.atEnd()) c.stepForward(false); + if (this.everyStep) return; + while (!c.line()) { + const was = c.where; + c.stepForward(false); + const now = c.where; + if (now.k === was.k && now.s === was.s) { c.moveTo({ k: 0, s: c.frameStart(0) }); return; } + } } private go(move: (c: DebugCursor) => DebugStop | null): void { diff --git a/tests/debug-model.test.ts b/tests/debug-model.test.ts index 893e697..b8d6dd0 100644 --- a/tests/debug-model.test.ts +++ b/tests/debug-model.test.ts @@ -133,6 +133,14 @@ describe("walking a debugged run", () => { expect(c.continueBack(new Set(), ["a"])).toEqual({ kind: "start" }); }); + it("stops at a breakpoint before the watched change its line makes", () => { + const c = new DebugCursor(record(), MAP); + // (Not where the cursor stands: the next time line 3 is about to run, before it writes `a`.) + expect(c.continueForward(new Set(["main.ts:3"]), ["a"])).toEqual({ kind: "breakpoint", place: { file: "main.ts", line: 3 } }); + expect(c.where).toEqual({ k: 1, s: 2 }); + expect(value(c, "a")).toBe(0); + }); + it("says what each line wrote in the frame so far, and what the frames cost", () => { const c = new DebugCursor(record(), MAP); c.toFrame(3); From 56c9ecc9723a4551eaac6dcd7bea0655976162e5 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 13:55:24 -0600 Subject: [PATCH 024/238] TrigScript 4, A5: the game's view is cut by every box around its place that clips The Debug view is taller than a short panel, which clips it; the view, laid over the workspace, was not, and covered the status bar. Co-Authored-By: Claude Opus 5.5 --- debugger.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/debugger.ts b/debugger.ts index 3bc4841..a51034b 100644 --- a/debugger.ts +++ b/debugger.ts @@ -206,9 +206,13 @@ export class Debugger { const b = box.isConnected ? box.getBoundingClientRect() : null; let key = "none"; if (b) { - // (Only what of it the column shows.) - const c = this.gameCol.getBoundingClientRect(); - const left = Math.max(b.left, c.left), top = Math.max(b.top, c.top), right = Math.min(b.right, c.right), bottom = Math.min(b.bottom, c.bottom); + // (Only what of it shows: every box around it that clips, up to the workspace, cuts it.) + let left = b.left, top = b.top, right = b.right, bottom = b.bottom; + for (let e = box.parentElement; e && e !== root; e = e.parentElement) { + if (getComputedStyle(e).overflow === "visible") continue; + const c = e.getBoundingClientRect(); + left = Math.max(left, c.left); top = Math.max(top, c.top); right = Math.min(right, c.right); bottom = Math.min(bottom, c.bottom); + } const o = root.getBoundingClientRect(); if (right - left > 0 && bottom - top > 0) key = `${Math.round(left - o.left)},${Math.round(top - o.top)},${Math.round(right - left)},${Math.round(bottom - top)}`; } From 021bc3f50b2da9b827a2f46ae38b383022ca3910 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 13:56:05 -0600 Subject: [PATCH 025/238] TrigScript 4: A5 end to end in a built editor Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index d6b3813..2c8a7bf 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -611,7 +611,26 @@ and the debugger walks the record. Most writes are eudplib's own. A picture takes 45 ms (played on from the last) to 110 ms (from a kept game). At six frames of each test, both ways, the picture's memory was what the record said, and the variables read from each agreed. +- **End to end in a built editor (2026-10-16).** scm-js `main` was built under `/editor/` with scm-sim's dev server + beside it and the local plugin builds, on `probes/game-debug.ts`. + - *Debugging the failing test* took about 11 s from the click (the eudplib build was 3.4 s of it) and opened at + frame 146: "failed: expected 0, got 5". + - *Keys:* every key moved the cursor as it should. F6 with `wave` watched stopped at "wave changed: 2 → 3", then + at "1 → 2". + - *Breakpoints:* a click in the margin set one, and F5 stopped before its line. + - *The picture* was drawn in the game column. The two workers' frame-0 states matched. + - *Picture latency:* 210–250 ms a move, 60 ms of it the debounce. + - *Fixed in that run:* + - The view's frame was made inside a detached box and never loaded. It now lives in an overlay over the game + column. + - The keys stopped after a click in the view or the timeline. + - The panel is now made 340 px tall for a session. + - A second backwards continue on a watched variable stood still. + - A passing test opened on frame 1, where no line runs. + - *Seen:* the simulator and the game disagree on this test (6 Zerglings at Home against 5). That is the kind of + finding A2 was for. - **Not done:** + - The picture's camera stays where the viewer put it first. - A test of several players. The debugger runs one game. - Units are known only by their slot: the record keeps the script's memory, not the unit table. - Conditional breakpoints, hover values, a call stack. The stack's frames are cells too, so the record has From 9edf5f910c3865b9dd35d8415930d8ad5191b403 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 15:04:41 -0600 Subject: [PATCH 026/238] TrigScript 4, R0: the allowlist takes what Remastered's own EUD emulator does data/remastered.json is what a dump of Remastered 1.23.10.13515 says, address by address: its EUD emulator, run on every region it emulates, read and wrote each dword through its own code. Addresses, widths, verdicts and notes only. It is the fourth source of scripts/allowlist.mts, each entry marked from: "dump". verdictOf lets what was played in Remastered decide first, the dump where nothing played says more than "not known", and the list's `outside` (the game ends) where no region covers the address. Entries are indexed by 256-byte block: 1,478 entries check the units probe's 42,745 rows in 190 ms, as 191 did before. The three failures seen in the game are rewritten after the dump probe: a unit's position written is ignored, not fatal; a sprite's position too; unit +0x74 read ends the game. Co-Authored-By: Claude Opus 5.5 --- compiler/allowlist.ts | 46 +- data/allowlist.json | 14813 +++++++++++++++++++++++++++++++++++++- data/remastered.json | 9645 +++++++++++++++++++++++++ scripts/allowlist.mts | 33 +- tests/allowlist.test.ts | 43 +- 5 files changed, 24557 insertions(+), 23 deletions(-) create mode 100644 data/remastered.json diff --git a/compiler/allowlist.ts b/compiler/allowlist.ts index b9fe158..41cc56c 100644 --- a/compiler/allowlist.ts +++ b/compiler/allowlist.ts @@ -29,11 +29,17 @@ export interface AllowEntry { evidence: string[]; /** A value that differs between the players' computers (the mouse, the screen): never to change the game itself. */ local?: boolean; + /** "dump": read off Remastered's own EUD emulator rather than seen in a game; a played entry over the same byte wins. */ + from?: "dump"; + /** What a person needs besides the verdict ("a unit pointer: the game takes only the address of a unit"). */ + note?: string; } export interface Allowlist { version: 1; entries: AllowEntry[]; + /** What Remastered does with an address no entry covers, and why (its emulator has no region there: the game ends). */ + outside?: { verdict: Verdict; evidence: string[] }; } /** [origin, address, write (1) or read (0), mask, times] — origin as the build's map of triggers names it. */ @@ -107,20 +113,47 @@ function covers(e: AllowEntry, byte: number): boolean { return off % e.stride < e.width && Math.floor(off / e.stride) < (e.count ?? 1); } +/** The entries by the 256-byte blocks they reach into, made once a list: the dump's entries cover most of the game. */ +const BLOCK = 8; +const indexes = new WeakMap>(); +function indexOf(list: Allowlist): Map { + let index = indexes.get(list); + if (index) return index; + index = new Map(); + for (const e of list.entries) { + const end = e.base + (e.stride ? e.stride * ((e.count ?? 1) - 1) : 0) + e.width; + for (let b = e.base >>> BLOCK; b <= (end - 1) >>> BLOCK; b++) { + const at = index.get(b); + if (at) at.push(e); else index.set(b, [e]); + } + } + indexes.set(list, index); + return index; +} + /** How grave a verdict is when a row is summed up from its bytes: one byte not known makes the row not known. */ const RANK: Record = { ends: 3, ignored: 2, unknown: 1, allowed: 0 }; /** Which of several entries over one byte decides: a failure seen beats a success, a success beats no word at all. */ const WEIGHT: Record = { ends: 3, ignored: 2, allowed: 1, unknown: 0 }; -/** What the allowlist says of one byte, for a read or a write, and the entry that says it. */ +/** + * What the allowlist says of one byte, for a read or a write, and the entry that says it. What was played in + * Remastered decides first; the dump of its emulator decides where nothing played says more than "not known"; an + * address neither covers takes the list's `outside`. + */ export function verdictOf(list: Allowlist, byte: number, write: boolean): { verdict: Verdict; entry: AllowEntry | null } { - let best: { verdict: Verdict; entry: AllowEntry | null } = { verdict: "unknown", entry: null }; - for (const e of list.entries) { + let played: { verdict: Verdict; entry: AllowEntry | null } = { verdict: "unknown", entry: null }; + let dumped: { verdict: Verdict; entry: AllowEntry | null } = { verdict: "unknown", entry: null }; + for (const e of indexOf(list).get(byte >>> BLOCK) ?? []) { if (!covers(e, byte)) continue; const v = write ? e.write : e.read; - if (best.entry === null || WEIGHT[v] > WEIGHT[best.verdict]) best = { verdict: v, entry: e }; + const best = e.from === "dump" ? dumped : played; + if (best.entry === null || WEIGHT[v] > WEIGHT[best.verdict]) { best.verdict = v; best.entry = e; } } - return best; + if (played.entry && played.verdict !== "unknown") return played; + if (dumped.entry) return dumped; + if (played.entry) return played; + return { verdict: list.outside?.verdict ?? "unknown", entry: null }; } /** @@ -144,7 +177,8 @@ export function checkTrace(trace: Trace, list: Allowlist): TraceCheck { if (RANK[v.verdict] > RANK[worst.verdict]) worst = v; } if (worst.verdict === "allowed") { counted.allowed++; continue; } - add(origin, address, write === 1, worst.verdict, worst.entry, worst.entry?.evidence ?? [], times); + const evidence = worst.entry ? [...worst.entry.evidence, ...(worst.entry.note ? [worst.entry.note] : [])] : worst.verdict === list.outside?.verdict ? list.outside.evidence : []; + add(origin, address, write === 1, worst.verdict, worst.entry, evidence, times); } function add(origin: string, address: number, write: boolean, verdict: Verdict, entry: AllowEntry | null, evidence: string[], times: number) { counted[verdict as "ends" | "ignored" | "unknown"]++; diff --git a/data/allowlist.json b/data/allowlist.json index df05cdd..c4c62dc 100644 --- a/data/allowlist.json +++ b/data/allowlist.json @@ -860,9 +860,34 @@ "count": 1700, "width": 4, "read": "allowed", + "write": "ignored", + "evidence": [ + "the dump probe, steps A and B (2026-10-16): a unit's X and Y written, the game went on and nothing was seen to move", + "Magenta probe 6, key I (2026-09-14) ended the game writing the unit's position and its sprite's together; the dump probe wrote each alone and neither did" + ] + }, + { + "name": "sprite table: position", + "base": 6462892, + "stride": 36, + "count": 2500, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "the dump probe, step C (2026-10-16): a sprite's X and Y written, the game went on and nothing on the screen changed" + ] + }, + { + "name": "unit table: +0x74", + "base": 5885212, + "stride": 336, + "count": 1700, + "width": 4, + "read": "ends", "write": "ends", "evidence": [ - "Magenta probe 6, key I (2026-09-14): writing a unit's position ends the game, \"EUD not supported\"" + "the dump probe, step D (2026-10-16): read, the game ended \"EUD ERROR: 0xFFA632E3\" (the address, bits flipped), as the dump said" ] }, { @@ -2068,6 +2093,14790 @@ "evidence": [ "touched by the probes units (2026-09-18), each played in Remastered with every line as expected" ] + }, + { + "name": "0x200000", + "base": 2097152, + "width": 4096, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x201000", + "base": 2101248, + "width": 24576, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x41e0d0", + "base": 4317392, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x426918", + "base": 4352280, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x427900", + "base": 4356352, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x427cdc", + "base": 4357340, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x45134c", + "base": 4526924, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x451350", + "base": 4526928, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x46044c", + "base": 4588620, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4887c0", + "base": 4753344, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x489204", + "base": 4755972, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x492f90", + "base": 4796304, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4a3898", + "base": 4864152, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4a389c", + "base": 4864156, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4b995c", + "base": 4954460, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4bd5ec", + "base": 4969964, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4bd5f0", + "base": 4969968, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4d68f4", + "base": 5073140, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4d98bc", + "base": 5085372, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4f3288", + "base": 5190280, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4f3360", + "base": 5190496, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4fdfbc", + "base": 5234620, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4fe544", + "base": 5236036, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x4ff900", + "base": 5241088, + "width": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x50cdc0", + "base": 5295552, + "width": 260, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x50e064", + "base": 5300324, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5124d8", + "base": 5317848, + "width": 28, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x512678", + "base": 5318264, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51267c", + "base": 5318268, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x512684", + "base": 5318276, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x512688", + "base": 5318280, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x512708", + "base": 5318408, + "width": 76, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5128f8", + "base": 5318904, + "width": 160, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x513b68", + "base": 5323624, + "width": 52, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x514178", + "base": 5325176, + "width": 1092, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5145bc", + "base": 5326268, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5145c0", + "base": 5326272, + "width": 840, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x514908", + "base": 5327112, + "width": 320, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x514a48", + "base": 5327432, + "width": 688, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x514cf8", + "base": 5328120, + "width": 1316, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51521c", + "base": 5329436, + "width": 8, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5152a8", + "base": 5329576, + "width": 84, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5152fc", + "base": 5329660, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515300", + "base": 5329664, + "width": 72, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515348", + "base": 5329736, + "width": 60, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515384", + "base": 5329796, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515388", + "base": 5329800, + "width": 96, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515af8", + "base": 5331704, + "width": 16, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515b08", + "base": 5331720, + "width": 16, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515b18", + "base": 5331736, + "width": 32, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515b38", + "base": 5331768, + "width": 32, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515b58", + "base": 5331800, + "width": 8, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515b60", + "base": 5331808, + "width": 8, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515b68", + "base": 5331816, + "width": 8, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515b70", + "base": 5331824, + "width": 120, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515be8", + "base": 5331944, + "width": 60, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515c24", + "base": 5332004, + "width": 60, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515c60", + "base": 5332064, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515c74", + "base": 5332084, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515c88", + "base": 5332104, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515c9c", + "base": 5332124, + "width": 40, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515cc4", + "base": 5332164, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515cd8", + "base": 5332184, + "width": 40, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515d00", + "base": 5332224, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515d14", + "base": 5332244, + "width": 60, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515d50", + "base": 5332304, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515d64", + "base": 5332324, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515d68", + "base": 5332328, + "width": 100, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515dd0", + "base": 5332432, + "width": 100, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515e34", + "base": 5332532, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515e38", + "base": 5332536, + "width": 100, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515e9c", + "base": 5332636, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515ea0", + "base": 5332640, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515f2c", + "base": 5332780, + "width": 40, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515f54", + "base": 5332820, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x515f58", + "base": 5332824, + "width": 180, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51600c", + "base": 5333004, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516020", + "base": 5333024, + "width": 180, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5160d4", + "base": 5333204, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5160d8", + "base": 5333208, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516164", + "base": 5333348, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516168", + "base": 5333352, + "width": 120, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5161e0", + "base": 5333472, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51626c", + "base": 5333612, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516270", + "base": 5333616, + "width": 160, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516310", + "base": 5333776, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51639c", + "base": 5333916, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5163a0", + "base": 5333920, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51642c", + "base": 5334060, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516430", + "base": 5334064, + "width": 180, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5164e4", + "base": 5334244, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5164e8", + "base": 5334248, + "width": 180, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51659c", + "base": 5334428, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5165a0", + "base": 5334432, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51662c", + "base": 5334572, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516630", + "base": 5334576, + "width": 200, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5166f8", + "base": 5334776, + "width": 180, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5167ac", + "base": 5334956, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5167b0", + "base": 5334960, + "width": 100, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516814", + "base": 5335060, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516818", + "base": 5335064, + "width": 180, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5168cc", + "base": 5335244, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5168d0", + "base": 5335248, + "width": 160, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516970", + "base": 5335408, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516984", + "base": 5335428, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516988", + "base": 5335432, + "width": 80, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5169d8", + "base": 5335512, + "width": 60, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516a14", + "base": 5335572, + "width": 60, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516a50", + "base": 5335632, + "width": 40, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516a78", + "base": 5335672, + "width": 100, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516adc", + "base": 5335772, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516ae0", + "base": 5335776, + "width": 100, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516b44", + "base": 5335876, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516b48", + "base": 5335880, + "width": 100, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516bac", + "base": 5335980, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516bb0", + "base": 5335984, + "width": 80, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516c00", + "base": 5336064, + "width": 60, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516c3c", + "base": 5336124, + "width": 60, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516c78", + "base": 5336184, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516d04", + "base": 5336324, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516d08", + "base": 5336328, + "width": 180, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516dbc", + "base": 5336508, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516dc0", + "base": 5336512, + "width": 180, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516e74", + "base": 5336692, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516e78", + "base": 5336696, + "width": 160, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516f18", + "base": 5336856, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516fa4", + "base": 5336996, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x516fa8", + "base": 5337000, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517034", + "base": 5337140, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517038", + "base": 5337144, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5170c4", + "base": 5337284, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5170c8", + "base": 5337288, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517154", + "base": 5337428, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517158", + "base": 5337432, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5171e4", + "base": 5337572, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5171e8", + "base": 5337576, + "width": 120, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517260", + "base": 5337696, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5172ec", + "base": 5337836, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5172f0", + "base": 5337840, + "width": 100, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517354", + "base": 5337940, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517358", + "base": 5337944, + "width": 120, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5173d0", + "base": 5338064, + "width": 120, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517448", + "base": 5338184, + "width": 40, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517470", + "base": 5338224, + "width": 80, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5174c0", + "base": 5338304, + "width": 160, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517560", + "base": 5338464, + "width": 80, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5175b0", + "base": 5338544, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51763c", + "base": 5338684, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517640", + "base": 5338688, + "width": 100, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5176a4", + "base": 5338788, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5176a8", + "base": 5338792, + "width": 80, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5176f8", + "base": 5338872, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51770c", + "base": 5338892, + "width": 60, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517748", + "base": 5338952, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5177d4", + "base": 5339092, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5177d8", + "base": 5339096, + "width": 120, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517850", + "base": 5339216, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5178dc", + "base": 5339356, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5178e0", + "base": 5339360, + "width": 180, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517994", + "base": 5339540, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517998", + "base": 5339544, + "width": 180, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517a4c", + "base": 5339724, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517a50", + "base": 5339728, + "width": 100, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517ab4", + "base": 5339828, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517ab8", + "base": 5339832, + "width": 180, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517b6c", + "base": 5340012, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517b70", + "base": 5340016, + "width": 160, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517c10", + "base": 5340176, + "width": 120, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517c88", + "base": 5340296, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517d14", + "base": 5340436, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517d18", + "base": 5340440, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517da4", + "base": 5340580, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517da8", + "base": 5340584, + "width": 160, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517e48", + "base": 5340744, + "width": 120, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517ec0", + "base": 5340864, + "width": 120, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x517f38", + "base": 5340984, + "width": 200, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518000", + "base": 5341184, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518014", + "base": 5341204, + "width": 40, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51803c", + "base": 5341244, + "width": 40, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518068", + "base": 5341288, + "width": 220, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518144", + "base": 5341508, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518148", + "base": 5341512, + "width": 240, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518238", + "base": 5341752, + "width": 240, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518328", + "base": 5341992, + "width": 260, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51842c", + "base": 5342252, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518430", + "base": 5342256, + "width": 80, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518480", + "base": 5342336, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51850c", + "base": 5342476, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518510", + "base": 5342480, + "width": 120, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518588", + "base": 5342600, + "width": 80, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5185d8", + "base": 5342680, + "width": 100, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51863c", + "base": 5342780, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518640", + "base": 5342784, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5186cc", + "base": 5342924, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5186d0", + "base": 5342928, + "width": 120, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x518748", + "base": 5343048, + "width": 140, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5187d4", + "base": 5343188, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5187e8", + "base": 5343208, + "width": 3000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5193a0", + "base": 5346208, + "width": 2736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51a280", + "base": 5350016, + "width": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51a28c", + "base": 5350028, + "width": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51a298", + "base": 5350040, + "width": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51a2a4", + "base": 5350052, + "width": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51a2b0", + "base": 5350064, + "width": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51a2bc", + "base": 5350076, + "width": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51a2c8", + "base": 5350088, + "width": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51a2d4", + "base": 5350100, + "width": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51a424", + "base": 5350436, + "width": 24, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51a440", + "base": 5350464, + "width": 24, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51aab0", + "base": 5352112, + "width": 24, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51aac8", + "base": 5352136, + "width": 24, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51aae0", + "base": 5352160, + "width": 24, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51ab94", + "base": 5352340, + "width": 28, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51abb0", + "base": 5352368, + "width": 16, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51abc0", + "base": 5352384, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51abd4", + "base": 5352404, + "width": 28, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51abf0", + "base": 5352432, + "width": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51abfc", + "base": 5352444, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51ac10", + "base": 5352464, + "width": 28, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51ac2c", + "base": 5352492, + "width": 24, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51ac48", + "base": 5352520, + "width": 20, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51ac5c", + "base": 5352540, + "width": 24, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51ac74", + "base": 5352564, + "width": 16, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51ad18", + "base": 5352728, + "width": 24, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51baf8", + "base": 5356280, + "width": 256, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51ca14", + "base": 5360148, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51ce70", + "base": 5361264, + "width": 28, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51ce8c", + "base": 5361292, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51ce98", + "base": 5361304, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51cea0", + "base": 5361312, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51ced0", + "base": 5361360, + "width": 3996, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x51f2a8", + "base": 5370536, + "width": 19980, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5240d0", + "base": 5390544, + "width": 1000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x52e4c0", + "base": 5432512, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x52e5c8", + "base": 5432776, + "width": 3996, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x0", + "base": 5436776, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x4", + "base": 5436780, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x8", + "base": 5436784, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0xc", + "base": 5436788, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x10", + "base": 5436792, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x14", + "base": 5436796, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x18", + "base": 5436800, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x1c", + "base": 5436804, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x20", + "base": 5436808, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x24", + "base": 5436812, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x28", + "base": 5436816, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x2c", + "base": 5436820, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x30", + "base": 5436824, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x34", + "base": 5436828, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x38", + "base": 5436832, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "image table: +0x3c", + "base": 5436836, + "width": 4, + "stride": 64, + "count": 5000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x57eb68", + "base": 5761896, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x57eb70", + "base": 5761904, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x57eee0", + "base": 5762784, + "width": 432, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x57f090", + "base": 5763216, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x57f0b4", + "base": 5763252, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x57f0d8", + "base": 5763288, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x57f0f0", + "base": 5763312, + "width": 66396, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x58f44c", + "base": 5829708, + "width": 29604, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5967f8", + "base": 5859320, + "width": 144, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x596a18", + "base": 5859864, + "width": 48, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x596a18 +0x30", + "base": 5859912, + "width": 12, + "read": "allowed", + "write": "unknown", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x596a18 +0x3c", + "base": 5859924, + "width": 196, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x597208", + "base": 5861896, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x59720c", + "base": 5861900, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x597210", + "base": 5861904, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x597214", + "base": 5861908, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x597218", + "base": 5861912, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x59721c", + "base": 5861916, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x597220", + "base": 5861920, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x597224", + "base": 5861924, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x597228", + "base": 5861928, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x59722c", + "base": 5861932, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x597230", + "base": 5861936, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x597234", + "base": 5861940, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x597238", + "base": 5861944, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x59723c", + "base": 5861948, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5973a0", + "base": 5862304, + "width": 8192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5993c4", + "base": 5870532, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x5993d4", + "base": 5870548, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x59c6c0", + "base": 5883584, + "width": 1024, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x59cb5c", + "base": 5884764, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x59cb60", + "base": 5884768, + "width": 256, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x59cc78", + "base": 5885048, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x59cc80", + "base": 5885056, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x0", + "base": 5885096, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x4", + "base": 5885100, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x8", + "base": 5885104, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xc", + "base": 5885108, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a sprite pointer: the game takes the address of a sprite; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x10", + "base": 5885112, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x14", + "base": 5885116, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x18", + "base": 5885120, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x1c", + "base": 5885124, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x20", + "base": 5885128, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x24", + "base": 5885132, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x28", + "base": 5885136, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x2c", + "base": 5885140, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x30", + "base": 5885144, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x34", + "base": 5885148, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x38", + "base": 5885152, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x3c", + "base": 5885156, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x40", + "base": 5885160, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x44", + "base": 5885164, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x48", + "base": 5885168, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x4c", + "base": 5885172, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x50", + "base": 5885176, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x54", + "base": 5885180, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x58", + "base": 5885184, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x5c", + "base": 5885188, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x60", + "base": 5885192, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x64", + "base": 5885196, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x68", + "base": 5885200, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x6c", + "base": 5885204, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x70", + "base": 5885208, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x74", + "base": 5885212, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x78", + "base": 5885216, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x7c", + "base": 5885220, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x80", + "base": 5885224, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x84", + "base": 5885228, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x88", + "base": 5885232, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x8c", + "base": 5885236, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x90", + "base": 5885240, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x94", + "base": 5885244, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x98", + "base": 5885248, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x9c", + "base": 5885252, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xa0", + "base": 5885256, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xa4", + "base": 5885260, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xa8", + "base": 5885264, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xac", + "base": 5885268, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xb0", + "base": 5885272, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xb4", + "base": 5885276, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xb8", + "base": 5885280, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xbc", + "base": 5885284, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xc0", + "base": 5885288, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back; differs between elements: ignored, shadow", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xc4", + "base": 5885292, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "differs between elements: allowed, ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xc8", + "base": 5885296, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "differs between elements: allowed, ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xcc", + "base": 5885300, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "differs between elements: allowed, ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xd0", + "base": 5885304, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "differs between elements: allowed, ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xd4", + "base": 5885308, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back; differs between elements: ignored, shadow", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xd8", + "base": 5885312, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "differs between elements: allowed, ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xdc", + "base": 5885316, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xe0", + "base": 5885320, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xe4", + "base": 5885324, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xe8", + "base": 5885328, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xec", + "base": 5885332, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xf0", + "base": 5885336, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xf4", + "base": 5885340, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xf8", + "base": 5885344, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0xfc", + "base": 5885348, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x100", + "base": 5885352, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x104", + "base": 5885356, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x108", + "base": 5885360, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x10c", + "base": 5885364, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x110", + "base": 5885368, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x114", + "base": 5885372, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x118", + "base": 5885376, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x11c", + "base": 5885380, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x120", + "base": 5885384, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x124", + "base": 5885388, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x128", + "base": 5885392, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x12c", + "base": 5885396, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x130", + "base": 5885400, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x134", + "base": 5885404, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x138", + "base": 5885408, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x13c", + "base": 5885412, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x140", + "base": 5885416, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x144", + "base": 5885420, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x148", + "base": 5885424, + "width": 4, + "stride": 336, + "count": 1700, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "unit table: +0x14c", + "base": 5885428, + "width": 4, + "stride": 336, + "count": 1700, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6283f0", + "base": 6456304, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6283f8", + "base": 6456312, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6283fc", + "base": 6456316, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628400", + "base": 6456320, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628404", + "base": 6456324, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628408", + "base": 6456328, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62840c", + "base": 6456332, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628410", + "base": 6456336, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628414", + "base": 6456340, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628418", + "base": 6456344, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62841c", + "base": 6456348, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628420", + "base": 6456352, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628424", + "base": 6456356, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628428", + "base": 6456360, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62842c", + "base": 6456364, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628430", + "base": 6456368, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628434", + "base": 6456372, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628438", + "base": 6456376, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62843c", + "base": 6456380, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628444", + "base": 6456388, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628448", + "base": 6456392, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628454", + "base": 6456404, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628458", + "base": 6456408, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628470", + "base": 6456432, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62848c", + "base": 6456460, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628494", + "base": 6456468, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628498", + "base": 6456472, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284a8", + "base": 6456488, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284ac", + "base": 6456492, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284b8", + "base": 6456504, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284bc", + "base": 6456508, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284c0", + "base": 6456512, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284c4", + "base": 6456516, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284c8", + "base": 6456520, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284cc", + "base": 6456524, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284d0", + "base": 6456528, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284d4", + "base": 6456532, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284d8", + "base": 6456536, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284dc", + "base": 6456540, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284e0", + "base": 6456544, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284e4", + "base": 6456548, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284e8", + "base": 6456552, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284ec", + "base": 6456556, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284f0", + "base": 6456560, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284f4", + "base": 6456564, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284f8", + "base": 6456568, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6284fc", + "base": 6456572, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628500", + "base": 6456576, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628504", + "base": 6456580, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628508", + "base": 6456584, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62850c", + "base": 6456588, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628510", + "base": 6456592, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628514", + "base": 6456596, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628518", + "base": 6456600, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62851c", + "base": 6456604, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628520", + "base": 6456608, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628524", + "base": 6456612, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628528", + "base": 6456616, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62852c", + "base": 6456620, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628530", + "base": 6456624, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628534", + "base": 6456628, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628538", + "base": 6456632, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62853c", + "base": 6456636, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628540", + "base": 6456640, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628544", + "base": 6456644, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628548", + "base": 6456648, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62854c", + "base": 6456652, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628550", + "base": 6456656, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628554", + "base": 6456660, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628558", + "base": 6456664, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62855c", + "base": 6456668, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628560", + "base": 6456672, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628564", + "base": 6456676, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628568", + "base": 6456680, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62856c", + "base": 6456684, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628570", + "base": 6456688, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628574", + "base": 6456692, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628578", + "base": 6456696, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62857c", + "base": 6456700, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628580", + "base": 6456704, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628584", + "base": 6456708, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628588", + "base": 6456712, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62858c", + "base": 6456716, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628590", + "base": 6456720, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628594", + "base": 6456724, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628598", + "base": 6456728, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62859c", + "base": 6456732, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285a0", + "base": 6456736, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285a4", + "base": 6456740, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285a8", + "base": 6456744, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285ac", + "base": 6456748, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285b0", + "base": 6456752, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285b4", + "base": 6456756, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285b8", + "base": 6456760, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285bc", + "base": 6456764, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285c0", + "base": 6456768, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285c4", + "base": 6456772, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285c8", + "base": 6456776, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285cc", + "base": 6456780, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285d0", + "base": 6456784, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285d4", + "base": 6456788, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285d8", + "base": 6456792, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285dc", + "base": 6456796, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285e0", + "base": 6456800, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285e4", + "base": 6456804, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285e8", + "base": 6456808, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285ec", + "base": 6456812, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285f0", + "base": 6456816, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285f4", + "base": 6456820, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285f8", + "base": 6456824, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6285fc", + "base": 6456828, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628600", + "base": 6456832, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628604", + "base": 6456836, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628608", + "base": 6456840, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62860c", + "base": 6456844, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628610", + "base": 6456848, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628614", + "base": 6456852, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628618", + "base": 6456856, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62861c", + "base": 6456860, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628620", + "base": 6456864, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628624", + "base": 6456868, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628628", + "base": 6456872, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62862c", + "base": 6456876, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628630", + "base": 6456880, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628634", + "base": 6456884, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628638", + "base": 6456888, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62863c", + "base": 6456892, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628640", + "base": 6456896, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628644", + "base": 6456900, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628648", + "base": 6456904, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62864c", + "base": 6456908, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628650", + "base": 6456912, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628654", + "base": 6456916, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628658", + "base": 6456920, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x62865c", + "base": 6456924, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628660", + "base": 6456928, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x628664", + "base": 6456932, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x629c90", + "base": 6462608, + "width": 256, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "sprite table: +0x0", + "base": 6462872, + "width": 4, + "stride": 36, + "count": 2500, + "read": "allowed", + "write": "allowed", + "note": "a sprite pointer: the game takes the address of a sprite; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "sprite table: +0x4", + "base": 6462876, + "width": 4, + "stride": 36, + "count": 2500, + "read": "allowed", + "write": "allowed", + "note": "a sprite pointer: the game takes the address of a sprite; anything else is kept beside it and only read back", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "sprite table: +0x8", + "base": 6462880, + "width": 4, + "stride": 36, + "count": 2500, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; differs between elements: allowed, shadow", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "sprite table: +0xc", + "base": 6462884, + "width": 4, + "stride": 36, + "count": 2500, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "sprite table: +0x10", + "base": 6462888, + "width": 4, + "stride": 36, + "count": 2500, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "sprite table: +0x14", + "base": 6462892, + "width": 4, + "stride": 36, + "count": 2500, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "sprite table: +0x18", + "base": 6462896, + "width": 4, + "stride": 36, + "count": 2500, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "sprite table: +0x1c", + "base": 6462900, + "width": 4, + "stride": 36, + "count": 2500, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "sprite table: +0x20", + "base": 6462904, + "width": 4, + "stride": 36, + "count": 2500, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x63fe30", + "base": 6553136, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x63fe34", + "base": 6553140, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x63fed8", + "base": 6553304, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x63ff40", + "base": 6553408, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x63ff78", + "base": 6553464, + "width": 828, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6402b8", + "base": 6554296, + "width": 656, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x640550", + "base": 6554960, + "width": 796, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x64086c", + "base": 6555756, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x640b58", + "base": 6556504, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x640b5c", + "base": 6556508, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x640b60", + "base": 6556512, + "width": 2836, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x64169c", + "base": 6559388, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "order queue: +0x0", + "base": 6559392, + "width": 4, + "stride": 20, + "count": 2000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "order queue: +0x4", + "base": 6559396, + "width": 4, + "stride": 20, + "count": 2000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "order queue: +0x8", + "base": 6559400, + "width": 4, + "stride": 20, + "count": 2000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "order queue: +0xc", + "base": 6559404, + "width": 4, + "stride": 20, + "count": 2000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "order queue: +0x10", + "base": 6559408, + "width": 4, + "stride": 20, + "count": 2000, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x64b2e0", + "base": 6599392, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x64b2e4", + "base": 6599396, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x0", + "base": 6599400, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x4", + "base": 6599404, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x8", + "base": 6599408, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0xc", + "base": 6599412, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x10", + "base": 6599416, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x14", + "base": 6599420, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x18", + "base": 6599424, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x1c", + "base": 6599428, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x20", + "base": 6599432, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x24", + "base": 6599436, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x28", + "base": 6599440, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x2c", + "base": 6599444, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x30", + "base": 6599448, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x34", + "base": 6599452, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x38", + "base": 6599456, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x3c", + "base": 6599460, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x40", + "base": 6599464, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x44", + "base": 6599468, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x48", + "base": 6599472, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x4c", + "base": 6599476, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x50", + "base": 6599480, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x54", + "base": 6599484, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x58", + "base": 6599488, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x5c", + "base": 6599492, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x60", + "base": 6599496, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x64", + "base": 6599500, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x68", + "base": 6599504, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "bullet table: +0x6c", + "base": 6599508, + "width": 4, + "stride": 112, + "count": 100, + "read": "allowed", + "write": "ignored", + "note": "kept beside the game: read back, never used by it; tried on elements not in use", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x64debc", + "base": 6610620, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x64eed8", + "base": 6614744, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x64eedc", + "base": 6614748, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6509a0", + "base": 6621600, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6509b0", + "base": 6621616, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x654880", + "base": 6637696, + "width": 512, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x654aa0", + "base": 6638240, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6556e4", + "base": 6641380, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x655700", + "base": 6641408, + "width": 64, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x655740", + "base": 6641472, + "width": 124, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6557bc", + "base": 6641596, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6557c0", + "base": 6641600, + "width": 124, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x65583c", + "base": 6641724, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x655840", + "base": 6641728, + "width": 124, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6558c0", + "base": 6641856, + "width": 124, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x655940", + "base": 6641984, + "width": 124, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6559bc", + "base": 6642108, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6559c0", + "base": 6642112, + "width": 124, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x655a40", + "base": 6642240, + "width": 124, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x655ac0", + "base": 6642368, + "width": 124, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x655b3c", + "base": 6642492, + "width": 64, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x655b7c", + "base": 6642556, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x655b80", + "base": 6642560, + "width": 124, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x655bfc", + "base": 6642684, + "width": 64, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x655c58", + "base": 6642776, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x655e80", + "base": 6643328, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656198", + "base": 6644120, + "width": 88, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6561f0", + "base": 6644208, + "width": 88, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656248", + "base": 6644296, + "width": 88, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6562a0", + "base": 6644384, + "width": 88, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6562f8", + "base": 6644472, + "width": 88, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656350", + "base": 6644560, + "width": 44, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x65637c", + "base": 6644604, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656380", + "base": 6644608, + "width": 88, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6563d8", + "base": 6644696, + "width": 88, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656430", + "base": 6644784, + "width": 88, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656488", + "base": 6644872, + "width": 44, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6564b4", + "base": 6644916, + "width": 44, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6564e0", + "base": 6644960, + "width": 132, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656568", + "base": 6645096, + "width": 260, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656670", + "base": 6645360, + "width": 132, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6566f4", + "base": 6645492, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6566f8", + "base": 6645496, + "width": 132, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x65677c", + "base": 6645628, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656780", + "base": 6645632, + "width": 260, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656884", + "base": 6645892, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656888", + "base": 6645896, + "width": 260, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x65698c", + "base": 6646156, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656990", + "base": 6646160, + "width": 132, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656a14", + "base": 6646292, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656a18", + "base": 6646296, + "width": 520, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656c20", + "base": 6646816, + "width": 132, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656ca4", + "base": 6646948, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656ca8", + "base": 6646952, + "width": 520, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656eb0", + "base": 6647472, + "width": 260, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656fb4", + "base": 6647732, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x656fb8", + "base": 6647736, + "width": 132, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x65703c", + "base": 6647868, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x657040", + "base": 6647872, + "width": 132, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6570c4", + "base": 6648004, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6570c8", + "base": 6648008, + "width": 260, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6571cc", + "base": 6648268, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6571d0", + "base": 6648272, + "width": 132, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x657254", + "base": 6648404, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x657258", + "base": 6648408, + "width": 132, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6572dc", + "base": 6648540, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6572e0", + "base": 6648544, + "width": 260, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6573e4", + "base": 6648804, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6573e8", + "base": 6648808, + "width": 132, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x65746c", + "base": 6648940, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x657470", + "base": 6648944, + "width": 520, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x657678", + "base": 6649464, + "width": 260, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x657780", + "base": 6649728, + "width": 260, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x657888", + "base": 6649992, + "width": 132, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x65790c", + "base": 6650124, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x657910", + "base": 6650128, + "width": 132, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x657998", + "base": 6650264, + "width": 260, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x657a9c", + "base": 6650524, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x657aa0", + "base": 6650528, + "width": 4096, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x65fc18", + "base": 6683672, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x65fd00", + "base": 6683904, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x65fec8", + "base": 6684360, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x65ffb0", + "base": 6684592, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x660178", + "base": 6685048, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x660260", + "base": 6685280, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x660428", + "base": 6685736, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6605f0", + "base": 6686192, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6606d8", + "base": 6686424, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6607c0", + "base": 6686656, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x660988", + "base": 6687112, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x660a70", + "base": 6687344, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x660c38", + "base": 6687800, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x660e00", + "base": 6688256, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x660fc8", + "base": 6688712, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6610ac", + "base": 6688940, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6610b0", + "base": 6688944, + "width": 912, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x661440", + "base": 6689856, + "width": 212, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x661514", + "base": 6690068, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x661518", + "base": 6690072, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6616e0", + "base": 6690528, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6617c4", + "base": 6690756, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6617c8", + "base": 6690760, + "width": 1824, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x661ee8", + "base": 6692584, + "width": 212, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x661fbc", + "base": 6692796, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x661fc0", + "base": 6692800, + "width": 212, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662094", + "base": 6693012, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662098", + "base": 6693016, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66217c", + "base": 6693244, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662180", + "base": 6693248, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662264", + "base": 6693476, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662268", + "base": 6693480, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662350", + "base": 6693712, + "width": 912, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6626e0", + "base": 6694624, + "width": 384, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662860", + "base": 6695008, + "width": 912, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662bf0", + "base": 6695920, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662db8", + "base": 6696376, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662e9c", + "base": 6696604, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662ea0", + "base": 6696608, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662f84", + "base": 6696836, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x662f88", + "base": 6696840, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663150", + "base": 6697296, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663234", + "base": 6697524, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663238", + "base": 6697528, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66331c", + "base": 6697756, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663320", + "base": 6697760, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663404", + "base": 6697988, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663408", + "base": 6697992, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6635d0", + "base": 6698448, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6636b4", + "base": 6698676, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6636b8", + "base": 6698680, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66379c", + "base": 6698908, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6637a0", + "base": 6698912, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663884", + "base": 6699140, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663888", + "base": 6699144, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663a50", + "base": 6699600, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663b38", + "base": 6699832, + "width": 212, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663c0c", + "base": 6700044, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663c10", + "base": 6700048, + "width": 212, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663ce4", + "base": 6700260, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663ce8", + "base": 6700264, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663dcc", + "base": 6700492, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663dd0", + "base": 6700496, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x663eb8", + "base": 6700728, + "width": 456, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x664080", + "base": 6701184, + "width": 912, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x664410", + "base": 6702096, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6644f4", + "base": 6702324, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6644f8", + "base": 6702328, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6645dc", + "base": 6702556, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6645e0", + "base": 6702560, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6646c4", + "base": 6702788, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6646c8", + "base": 6702792, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6647ac", + "base": 6703020, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6647b0", + "base": 6703024, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x664894", + "base": 6703252, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x664898", + "base": 6703256, + "width": 228, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66497c", + "base": 6703484, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x664980", + "base": 6703488, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x664a40", + "base": 6703680, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x664b00", + "base": 6703872, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x664bc0", + "base": 6704064, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x664c80", + "base": 6704256, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x664d40", + "base": 6704448, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x664e00", + "base": 6704640, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x664ec0", + "base": 6704832, + "width": 380, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665040", + "base": 6705216, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665100", + "base": 6705408, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6651c0", + "base": 6705600, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665280", + "base": 6705792, + "width": 380, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665400", + "base": 6706176, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6654c0", + "base": 6706368, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665580", + "base": 6706560, + "width": 380, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665700", + "base": 6706944, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6657c0", + "base": 6707136, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665880", + "base": 6707328, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665940", + "base": 6707520, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665a00", + "base": 6707712, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665ac0", + "base": 6707904, + "width": 388, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665c48", + "base": 6708296, + "width": 520, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665e50", + "base": 6708816, + "width": 388, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x665fd8", + "base": 6709208, + "width": 388, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x666160", + "base": 6709600, + "width": 1028, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x666160 +0x404", + "base": 6710628, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x666160 +0x408", + "base": 6710632, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x666570", + "base": 6710640, + "width": 520, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x666778", + "base": 6711160, + "width": 3996, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x667718", + "base": 6715160, + "width": 1000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x667b00", + "base": 6716160, + "width": 3996, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x668aa0", + "base": 6720160, + "width": 3996, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x669a40", + "base": 6724160, + "width": 1000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x669e28", + "base": 6725160, + "width": 1000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66a210", + "base": 6726160, + "width": 3996, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66b1b0", + "base": 6730160, + "width": 3996, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66c150", + "base": 6734160, + "width": 1000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66c538", + "base": 6735160, + "width": 3996, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66d4d8", + "base": 6739160, + "width": 1000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66d8c0", + "base": 6740160, + "width": 3996, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66e860", + "base": 6744160, + "width": 1000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66ec48", + "base": 6745160, + "width": 3748, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66ec48 +0xea4", + "base": 6748908, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66ec48 +0xea8", + "base": 6748912, + "width": 244, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66fd0c", + "base": 6749452, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x66ff78", + "base": 6750072, + "width": 27200, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68ac74", + "base": 6859892, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68ac8c", + "base": 6859916, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c104", + "base": 6865156, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c108", + "base": 6865160, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c10c", + "base": 6865164, + "width": 52, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c140", + "base": 6865216, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c144", + "base": 6865220, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c148", + "base": 6865224, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c14c", + "base": 6865228, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c1e0", + "base": 6865376, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c1f0", + "base": 6865392, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c1f4", + "base": 6865396, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c1fc", + "base": 6865404, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c204", + "base": 6865412, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c208", + "base": 6865416, + "width": 24, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c224", + "base": 6865444, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c234", + "base": 6865460, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x68c238", + "base": 6865464, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6bb210", + "base": 7057936, + "width": 1824, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6bd3d0", + "base": 7066576, + "width": 6804, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6c9858", + "base": 7116888, + "width": 212, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6c9930", + "base": 7117104, + "width": 836, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6c9c78", + "base": 7117944, + "width": 420, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6c9e20", + "base": 7118368, + "width": 212, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6c9ef8", + "base": 7118584, + "width": 836, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ca240", + "base": 7119424, + "width": 212, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ca318", + "base": 7119640, + "width": 420, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ca4bc", + "base": 7120060, + "width": 48, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ca4ec", + "base": 7120108, + "width": 48, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ca51c", + "base": 7120156, + "width": 48, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ca550", + "base": 7120208, + "width": 252, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ca64c", + "base": 7120460, + "width": 24, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ca94c", + "base": 7121228, + "width": 48, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6caf98", + "base": 7122840, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6cafa0", + "base": 7122848, + "width": 10752, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6cd9a0", + "base": 7133600, + "width": 516, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6cdba8", + "base": 7134120, + "width": 516, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6cddc0", + "base": 7134656, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6cddc4", + "base": 7134660, + "width": 8, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6cdfd4", + "base": 7135188, + "width": 36, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ce000", + "base": 7135232, + "width": 192, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ce320", + "base": 7136032, + "width": 1024, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ce720", + "base": 7137056, + "width": 1024, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ceb20", + "base": 7138080, + "width": 28, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ceff0", + "base": 7139312, + "width": 8, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ceff8", + "base": 7139320, + "width": 1200, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d0f14", + "base": 7147284, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d0f24", + "base": 7147300, + "width": 4, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d0f30", + "base": 7147312, + "width": 636, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200", + "base": 7148032, + "width": 52, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x34", + "base": 7148084, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1238", + "base": 7148088, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x3c", + "base": 7148092, + "width": 36, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x60", + "base": 7148128, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1260", + "base": 7148128, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x64", + "base": 7148132, + "width": 18380, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x4830", + "base": 7166512, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d5a30", + "base": 7166512, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x4834", + "base": 7166516, + "width": 56, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x486c", + "base": 7166572, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x4870", + "base": 7166576, + "width": 616, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x4ad8", + "base": 7167192, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d5cd8", + "base": 7167192, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x4adc", + "base": 7167196, + "width": 312, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x4c14", + "base": 7167508, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d5e14", + "base": 7167508, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x4c18", + "base": 7167512, + "width": 4, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x4c1c", + "base": 7167516, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x4c20", + "base": 7167520, + "width": 168, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x4cc8", + "base": 7167688, + "width": 4, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d5ec8", + "base": 7167688, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x4ccc", + "base": 7167692, + "width": 1224, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x5194", + "base": 7168916, + "width": 24, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d6394", + "base": 7168916, + "width": 24, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6d1200 +0x51ac", + "base": 7168940, + "width": 19576, + "read": "ends", + "write": "ends", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6de000", + "base": 7200768, + "width": 1216, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6de4c0", + "base": 7201984, + "width": 1216, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6de980", + "base": 7203200, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ef320", + "base": 7271200, + "width": 1216, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x6ef7e0", + "base": 7272416, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x700180", + "base": 7340416, + "width": 1216, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x700640", + "base": 7341632, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x710fe0", + "base": 7409632, + "width": 1216, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7114a0", + "base": 7410848, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x721e40", + "base": 7478848, + "width": 1216, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x722300", + "base": 7480064, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x732ca0", + "base": 7548064, + "width": 1216, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x733160", + "base": 7549280, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x743b00", + "base": 7617280, + "width": 1216, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x743fc0", + "base": 7618496, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x754960", + "base": 7686496, + "width": 1216, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x754e20", + "base": 7687712, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7657c0", + "base": 7755712, + "width": 1216, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x765c80", + "base": 7756928, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x776620", + "base": 7824928, + "width": 736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x776900", + "base": 7825664, + "width": 736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x776be0", + "base": 7826400, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x787580", + "base": 7894400, + "width": 736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x787860", + "base": 7895136, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x798200", + "base": 7963136, + "width": 736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7984e0", + "base": 7963872, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7a8e80", + "base": 8031872, + "width": 736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7a9160", + "base": 8032608, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7b9b00", + "base": 8100608, + "width": 736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7b9de0", + "base": 8101344, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7ca780", + "base": 8169344, + "width": 736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7caa60", + "base": 8170080, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7db400", + "base": 8238080, + "width": 736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7db6e0", + "base": 8238816, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7ec080", + "base": 8306816, + "width": 736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7ec360", + "base": 8307552, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7fcd00", + "base": 8375552, + "width": 736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x7fcfe0", + "base": 8376288, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x80d980", + "base": 8444288, + "width": 736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x80dc60", + "base": 8445024, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x81e600", + "base": 8513024, + "width": 736, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x81e8e0", + "base": 8513760, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x82f280", + "base": 8581760, + "width": 796, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x82f5a0", + "base": 8582560, + "width": 796, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x82f8c0", + "base": 8583360, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x840260", + "base": 8651360, + "width": 796, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x840580", + "base": 8652160, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x850f20", + "base": 8720160, + "width": 796, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x851240", + "base": 8720960, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x861be0", + "base": 8788960, + "width": 796, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x861f00", + "base": 8789760, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8728a0", + "base": 8857760, + "width": 796, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x872bc0", + "base": 8858560, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x883560", + "base": 8926560, + "width": 796, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x883880", + "base": 8927360, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x894220", + "base": 8995360, + "width": 796, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x894540", + "base": 8996160, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8a4ee0", + "base": 9064160, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8a50a0", + "base": 9064608, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8a5260", + "base": 9065056, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8b5c00", + "base": 9133056, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8b5dc0", + "base": 9133504, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8c6760", + "base": 9201504, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8c6920", + "base": 9201952, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8d72c0", + "base": 9269952, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8d7480", + "base": 9270400, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8e7e20", + "base": 9338400, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8e7fe0", + "base": 9338848, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8f8980", + "base": 9406848, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x8f8b40", + "base": 9407296, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x9094e0", + "base": 9475296, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x9096a0", + "base": 9475744, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x91a040", + "base": 9543744, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x91a200", + "base": 9544192, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x92aba0", + "base": 9612192, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x92ad60", + "base": 9612640, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x93b700", + "base": 9680640, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x93b8c0", + "base": 9681088, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x94c260", + "base": 9749088, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x94c420", + "base": 9749536, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x95cdc0", + "base": 9817536, + "width": 440, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x95cf80", + "base": 9817984, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x96d920", + "base": 9885984, + "width": 184, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x96d9e0", + "base": 9886176, + "width": 184, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x96daa0", + "base": 9886368, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x97e440", + "base": 9954368, + "width": 184, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x97e500", + "base": 9954560, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x98eea0", + "base": 10022560, + "width": 184, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x98ef60", + "base": 10022752, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x99f900", + "base": 10090752, + "width": 184, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x99f9c0", + "base": 10090944, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x9b0360", + "base": 10158944, + "width": 184, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x9b0420", + "base": 10159136, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x9c0dc0", + "base": 10227136, + "width": 184, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x9c0e80", + "base": 10227328, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x9d1820", + "base": 10295328, + "width": 184, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x9d18e0", + "base": 10295520, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x9e2280", + "base": 10363520, + "width": 184, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x9e2340", + "base": 10363712, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x9f2ce0", + "base": 10431712, + "width": 184, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x9f2da0", + "base": 10431904, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa03740", + "base": 10499904, + "width": 184, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa03800", + "base": 10500096, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa141a0", + "base": 10568096, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa141b0", + "base": 10568112, + "width": 272, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa142c0", + "base": 10568384, + "width": 272, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa143d0", + "base": 10568656, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa24d70", + "base": 10636656, + "width": 272, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa24e80", + "base": 10636928, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa35820", + "base": 10704928, + "width": 272, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa35930", + "base": 10705200, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa462d0", + "base": 10773200, + "width": 272, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa463e0", + "base": 10773472, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa56d80", + "base": 10841472, + "width": 272, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa56e90", + "base": 10841744, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa67830", + "base": 10909744, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa67840", + "base": 10909760, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa67850", + "base": 10909776, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa67860", + "base": 10909792, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa67870", + "base": 10909808, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xa67880", + "base": 10909824, + "width": 301056, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xab1080", + "base": 11210880, + "width": 131072, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xad1080", + "base": 11341952, + "width": 131072, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xaf1080", + "base": 11473024, + "width": 262144, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xb31080", + "base": 11735168, + "width": 3145728, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xe31080", + "base": 14880896, + "width": 204800, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xe63080", + "base": 15085696, + "width": 81920, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xe77080", + "base": 15167616, + "width": 48, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0xe770b0", + "base": 15167664, + "width": 4828, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1505adf8", + "base": 352693752, + "width": 12, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1505e670", + "base": 352708208, + "width": 1024, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19018f38", + "base": 419532600, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x190452c8", + "base": 419713736, + "width": 128, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19046134", + "base": 419717428, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19046138", + "base": 419717432, + "width": 256, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x190913d8", + "base": 420025304, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x190913e0", + "base": 420025312, + "width": 306904, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x190dc2c0", + "base": 420332224, + "width": 48716, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x190e8110", + "base": 420380944, + "width": 51208, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x190f4920", + "base": 420432160, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x190f4930", + "base": 420432176, + "width": 5000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x190f5cc0", + "base": 420437184, + "width": 5000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x190f7050", + "base": 420442192, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191079f0", + "base": 420510192, + "width": 5000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19108d80", + "base": 420515200, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19119720", + "base": 420583200, + "width": 5000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1911aab0", + "base": 420588208, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1912b450", + "base": 420656208, + "width": 5000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1912c7e0", + "base": 420661216, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1913d180", + "base": 420729216, + "width": 5000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1913e510", + "base": 420734224, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1914eeb0", + "base": 420802224, + "width": 5000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19150240", + "base": 420807232, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19160be0", + "base": 420875232, + "width": 5000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19161f70", + "base": 420880240, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19172910", + "base": 420948240, + "width": 5000, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19173ca0", + "base": 420953248, + "width": 68000, + "read": "allowed", + "write": "ignored", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19184640", + "base": 421021248, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19184650", + "base": 421021264, + "width": 4, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19184660", + "base": 421021280, + "width": 64864, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191943c0", + "base": 421086144, + "width": 8, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191943c8", + "base": 421086152, + "width": 143004, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191b7270", + "base": 421229168, + "width": 29244, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191be4b0", + "base": 421258416, + "width": 86624, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191d3710", + "base": 421345040, + "width": 51032, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191dfe70", + "base": 421396080, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191dfe70 +0x8", + "base": 421396088, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e07e0", + "base": 421398496, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e07e0 +0x8", + "base": 421398504, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e1150", + "base": 421400912, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e1150 +0x8", + "base": 421400920, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e1ac0", + "base": 421403328, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e1ac0 +0x8", + "base": 421403336, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e2430", + "base": 421405744, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e2430 +0x8", + "base": 421405752, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e2da0", + "base": 421408160, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e2da0 +0x8", + "base": 421408168, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e3710", + "base": 421410576, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e3710 +0x8", + "base": 421410584, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e4080", + "base": 421412992, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e4080 +0x8", + "base": 421413000, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e49f0", + "base": 421415408, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e49f0 +0x8", + "base": 421415416, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e5360", + "base": 421417824, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e5360 +0x8", + "base": 421417832, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e5cd0", + "base": 421420240, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e5cd0 +0x8", + "base": 421420248, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e6640", + "base": 421422656, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e6640 +0x8", + "base": 421422664, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e6fb0", + "base": 421425072, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e6fb0 +0x8", + "base": 421425080, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e7920", + "base": 421427488, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e7920 +0x8", + "base": 421427496, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e8290", + "base": 421429904, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e8290 +0x8", + "base": 421429912, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e8c00", + "base": 421432320, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e8c00 +0x8", + "base": 421432328, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e9570", + "base": 421434736, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e9570 +0x8", + "base": 421434744, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e9ee0", + "base": 421437152, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191e9ee0 +0x8", + "base": 421437160, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ea850", + "base": 421439568, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ea850 +0x8", + "base": 421439576, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191eb1c0", + "base": 421441984, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191eb1c0 +0x8", + "base": 421441992, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ebb30", + "base": 421444400, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ebb30 +0x8", + "base": 421444408, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ec4a0", + "base": 421446816, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ec4a0 +0x8", + "base": 421446824, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ece10", + "base": 421449232, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ece10 +0x8", + "base": 421449240, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ed780", + "base": 421451648, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ed780 +0x8", + "base": 421451656, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ee0f0", + "base": 421454064, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ee0f0 +0x8", + "base": 421454072, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191eea60", + "base": 421456480, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191eea60 +0x8", + "base": 421456488, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ef3d0", + "base": 421458896, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ef3d0 +0x8", + "base": 421458904, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191efd40", + "base": 421461312, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191efd40 +0x8", + "base": 421461320, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f06b0", + "base": 421463728, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f06b0 +0x8", + "base": 421463736, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f1020", + "base": 421466144, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f1020 +0x8", + "base": 421466152, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f1990", + "base": 421468560, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f1990 +0x8", + "base": 421468568, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f2300", + "base": 421470976, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f2300 +0x8", + "base": 421470984, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f2c70", + "base": 421473392, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f2c70 +0x8", + "base": 421473400, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f35e0", + "base": 421475808, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f35e0 +0x8", + "base": 421475816, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f3f50", + "base": 421478224, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f3f50 +0x8", + "base": 421478232, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f48c0", + "base": 421480640, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f48c0 +0x8", + "base": 421480648, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f5230", + "base": 421483056, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f5230 +0x8", + "base": 421483064, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f5ba0", + "base": 421485472, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f5ba0 +0x8", + "base": 421485480, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f6510", + "base": 421487888, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f6510 +0x8", + "base": 421487896, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f6e80", + "base": 421490304, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f6e80 +0x8", + "base": 421490312, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f77f0", + "base": 421492720, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f77f0 +0x8", + "base": 421492728, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f8160", + "base": 421495136, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f8160 +0x8", + "base": 421495144, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f8ad0", + "base": 421497552, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f8ad0 +0x8", + "base": 421497560, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f9440", + "base": 421499968, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f9440 +0x8", + "base": 421499976, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f9db0", + "base": 421502384, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191f9db0 +0x8", + "base": 421502392, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fa720", + "base": 421504800, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fa720 +0x8", + "base": 421504808, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fb090", + "base": 421507216, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fb090 +0x8", + "base": 421507224, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fba00", + "base": 421509632, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fba00 +0x8", + "base": 421509640, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fc370", + "base": 421512048, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fc370 +0x8", + "base": 421512056, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fcce0", + "base": 421514464, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fcce0 +0x8", + "base": 421514472, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fd650", + "base": 421516880, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fd650 +0x8", + "base": 421516888, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fdfc0", + "base": 421519296, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fdfc0 +0x8", + "base": 421519304, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fe930", + "base": 421521712, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191fe930 +0x8", + "base": 421521720, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ff2a0", + "base": 421524128, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ff2a0 +0x8", + "base": 421524136, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ffc10", + "base": 421526544, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x191ffc10 +0x8", + "base": 421526552, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19200580", + "base": 421528960, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19200580 +0x8", + "base": 421528968, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19200ef0", + "base": 421531376, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19200ef0 +0x8", + "base": 421531384, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19201860", + "base": 421533792, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19201860 +0x8", + "base": 421533800, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192021d0", + "base": 421536208, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192021d0 +0x8", + "base": 421536216, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19202b40", + "base": 421538624, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19202b40 +0x8", + "base": 421538632, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192034b0", + "base": 421541040, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192034b0 +0x8", + "base": 421541048, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19203e20", + "base": 421543456, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19203e20 +0x8", + "base": 421543464, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19204790", + "base": 421545872, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19204790 +0x8", + "base": 421545880, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19205100", + "base": 421548288, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19205100 +0x8", + "base": 421548296, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19205a70", + "base": 421550704, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19205a70 +0x8", + "base": 421550712, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192063e0", + "base": 421553120, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192063e0 +0x8", + "base": 421553128, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19206d50", + "base": 421555536, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19206d50 +0x8", + "base": 421555544, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192076c0", + "base": 421557952, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192076c0 +0x8", + "base": 421557960, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19208030", + "base": 421560368, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19208030 +0x8", + "base": 421560376, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192089a0", + "base": 421562784, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192089a0 +0x8", + "base": 421562792, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19209310", + "base": 421565200, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19209310 +0x8", + "base": 421565208, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19209c80", + "base": 421567616, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19209c80 +0x8", + "base": 421567624, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920a5f0", + "base": 421570032, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920a5f0 +0x8", + "base": 421570040, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920af60", + "base": 421572448, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920af60 +0x8", + "base": 421572456, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920b8d0", + "base": 421574864, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920b8d0 +0x8", + "base": 421574872, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920c240", + "base": 421577280, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920c240 +0x8", + "base": 421577288, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920cbb0", + "base": 421579696, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920cbb0 +0x8", + "base": 421579704, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920d520", + "base": 421582112, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920d520 +0x8", + "base": 421582120, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920de90", + "base": 421584528, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920de90 +0x8", + "base": 421584536, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920e800", + "base": 421586944, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920e800 +0x8", + "base": 421586952, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920f170", + "base": 421589360, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920f170 +0x8", + "base": 421589368, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920fae0", + "base": 421591776, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1920fae0 +0x8", + "base": 421591784, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19210450", + "base": 421594192, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19210450 +0x8", + "base": 421594200, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19210dc0", + "base": 421596608, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19210dc0 +0x8", + "base": 421596616, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19211730", + "base": 421599024, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19211730 +0x8", + "base": 421599032, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192120a0", + "base": 421601440, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192120a0 +0x8", + "base": 421601448, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19212a10", + "base": 421603856, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19212a10 +0x8", + "base": 421603864, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19213380", + "base": 421606272, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19213380 +0x8", + "base": 421606280, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19213cf0", + "base": 421608688, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19213cf0 +0x8", + "base": 421608696, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19214660", + "base": 421611104, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19214660 +0x8", + "base": 421611112, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19214fd0", + "base": 421613520, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19214fd0 +0x8", + "base": 421613528, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19215940", + "base": 421615936, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19215940 +0x8", + "base": 421615944, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192162b0", + "base": 421618352, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192162b0 +0x8", + "base": 421618360, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19216c20", + "base": 421620768, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19216c20 +0x8", + "base": 421620776, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19217590", + "base": 421623184, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19217590 +0x8", + "base": 421623192, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19217f00", + "base": 421625600, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19217f00 +0x8", + "base": 421625608, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19218870", + "base": 421628016, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19218870 +0x8", + "base": 421628024, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192191e0", + "base": 421630432, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192191e0 +0x8", + "base": 421630440, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19219b50", + "base": 421632848, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19219b50 +0x8", + "base": 421632856, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921a4c0", + "base": 421635264, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921a4c0 +0x8", + "base": 421635272, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921ae30", + "base": 421637680, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921ae30 +0x8", + "base": 421637688, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921b7a0", + "base": 421640096, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921b7a0 +0x8", + "base": 421640104, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921c110", + "base": 421642512, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921c110 +0x8", + "base": 421642520, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921ca80", + "base": 421644928, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921ca80 +0x8", + "base": 421644936, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921d3f0", + "base": 421647344, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921d3f0 +0x8", + "base": 421647352, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921dd60", + "base": 421649760, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921dd60 +0x8", + "base": 421649768, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921e6d0", + "base": 421652176, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921e6d0 +0x8", + "base": 421652184, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921f040", + "base": 421654592, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921f040 +0x8", + "base": 421654600, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921f9b0", + "base": 421657008, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1921f9b0 +0x8", + "base": 421657016, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19220320", + "base": 421659424, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19220320 +0x8", + "base": 421659432, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19220c90", + "base": 421661840, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19220c90 +0x8", + "base": 421661848, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19221600", + "base": 421664256, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19221600 +0x8", + "base": 421664264, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19221f70", + "base": 421666672, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19221f70 +0x8", + "base": 421666680, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192228e0", + "base": 421669088, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192228e0 +0x8", + "base": 421669096, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19223250", + "base": 421671504, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19223250 +0x8", + "base": 421671512, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19223bc0", + "base": 421673920, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19223bc0 +0x8", + "base": 421673928, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19224530", + "base": 421676336, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19224530 +0x8", + "base": 421676344, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19224ea0", + "base": 421678752, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19224ea0 +0x8", + "base": 421678760, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19225810", + "base": 421681168, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19225810 +0x8", + "base": 421681176, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19226180", + "base": 421683584, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19226180 +0x8", + "base": 421683592, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19226af0", + "base": 421686000, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19226af0 +0x8", + "base": 421686008, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19227460", + "base": 421688416, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19227460 +0x8", + "base": 421688424, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19227dd0", + "base": 421690832, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19227dd0 +0x8", + "base": 421690840, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19228740", + "base": 421693248, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19228740 +0x8", + "base": 421693256, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192290b0", + "base": 421695664, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192290b0 +0x8", + "base": 421695672, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19229a20", + "base": 421698080, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x19229a20 +0x8", + "base": 421698088, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1922a390", + "base": 421700496, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1922a390 +0x8", + "base": 421700504, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1922ad00", + "base": 421702912, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1922ad00 +0x8", + "base": 421702920, + "width": 2400, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1922b670", + "base": 421705328, + "width": 32, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1922b690", + "base": 421705360, + "width": 2896, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1922c1e0", + "base": 421708256, + "width": 262144, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1926c1e0", + "base": 421970400, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1926c1e0 +0x8", + "base": 421970408, + "width": 316, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1926c330", + "base": 421970736, + "width": 32, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1926c350", + "base": 421970768, + "width": 14896, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1926fd80", + "base": 421985664, + "width": 65536, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1927fd80", + "base": 422051200, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1927fd80 +0x8", + "base": 422051208, + "width": 316, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1927fed0", + "base": 422051536, + "width": 32, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1927fef0", + "base": 422051568, + "width": 46800, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1928b5c0", + "base": 422098368, + "width": 65536, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1929b5c0", + "base": 422163904, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1929b5c0 +0x8", + "base": 422163912, + "width": 316, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1929b710", + "base": 422164240, + "width": 32, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x1929b730", + "base": 422164272, + "width": 20032, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192a0570", + "base": 422184304, + "width": 32768, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192a8570", + "base": 422217072, + "width": 324, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192a86c0", + "base": 422217408, + "width": 32, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192a86e0", + "base": 422217440, + "width": 48, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192a8710", + "base": 422217488, + "width": 512, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192a8910", + "base": 422218000, + "width": 8, + "read": "allowed", + "write": "ignored", + "note": "read back, but kept beside the game", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" + }, + { + "name": "0x192a8910 +0x8", + "base": 422218008, + "width": 316, + "read": "allowed", + "write": "allowed", + "evidence": [ + "Remastered 1.23.10.13515's own EUD emulator, run on a dump of the game (2026-10-16)" + ], + "from": "dump" } - ] + ], + "outside": { + "verdict": "ends", + "evidence": [ + "no region of Remastered 1.23.10.13515's EUD emulator covers it (a dump of the game, 2026-10-16); the game ends with \"EUD ERROR\" and the address with its bits flipped" + ] + } } diff --git a/data/remastered.json b/data/remastered.json new file mode 100644 index 0000000..73d6500 --- /dev/null +++ b/data/remastered.json @@ -0,0 +1,9645 @@ +{ +"build": "1.23.10.13515", +"source": "Remastered's own EUD emulator, run on a dump of the running game (2026-10-16): every region it emulates, each dword read and written through its own code", +"outside": "ends", +"entries": [ +{ +"name": "0x200000", +"base": 2097152, +"width": 4096, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x201000", +"base": 2101248, +"width": 24576, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x41e0d0", +"base": 4317392, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x426918", +"base": 4352280, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x427900", +"base": 4356352, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x427cdc", +"base": 4357340, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x45134c", +"base": 4526924, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x451350", +"base": 4526928, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x46044c", +"base": 4588620, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4887c0", +"base": 4753344, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x489204", +"base": 4755972, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x492f90", +"base": 4796304, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4a3898", +"base": 4864152, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4a389c", +"base": 4864156, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4b995c", +"base": 4954460, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4bd5ec", +"base": 4969964, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4bd5f0", +"base": 4969968, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4d68f4", +"base": 5073140, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4d98bc", +"base": 5085372, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4f3288", +"base": 5190280, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4f3360", +"base": 5190496, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4fdfbc", +"base": 5234620, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4fe544", +"base": 5236036, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x4ff900", +"base": 5241088, +"width": 12, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x50cdc0", +"base": 5295552, +"width": 260, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x50e064", +"base": 5300324, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5124d8", +"base": 5317848, +"width": 28, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x512678", +"base": 5318264, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51267c", +"base": 5318268, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x512684", +"base": 5318276, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x512688", +"base": 5318280, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x512708", +"base": 5318408, +"width": 76, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x5128f8", +"base": 5318904, +"width": 160, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x513b68", +"base": 5323624, +"width": 52, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x514178", +"base": 5325176, +"width": 1092, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5145bc", +"base": 5326268, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5145c0", +"base": 5326272, +"width": 840, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x514908", +"base": 5327112, +"width": 320, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x514a48", +"base": 5327432, +"width": 688, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x514cf8", +"base": 5328120, +"width": 1316, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51521c", +"base": 5329436, +"width": 8, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5152a8", +"base": 5329576, +"width": 84, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5152fc", +"base": 5329660, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515300", +"base": 5329664, +"width": 72, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515348", +"base": 5329736, +"width": 60, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515384", +"base": 5329796, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515388", +"base": 5329800, +"width": 96, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515af8", +"base": 5331704, +"width": 16, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515b08", +"base": 5331720, +"width": 16, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515b18", +"base": 5331736, +"width": 32, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515b38", +"base": 5331768, +"width": 32, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515b58", +"base": 5331800, +"width": 8, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515b60", +"base": 5331808, +"width": 8, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515b68", +"base": 5331816, +"width": 8, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515b70", +"base": 5331824, +"width": 120, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515be8", +"base": 5331944, +"width": 60, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515c24", +"base": 5332004, +"width": 60, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515c60", +"base": 5332064, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515c74", +"base": 5332084, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515c88", +"base": 5332104, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515c9c", +"base": 5332124, +"width": 40, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515cc4", +"base": 5332164, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515cd8", +"base": 5332184, +"width": 40, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515d00", +"base": 5332224, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515d14", +"base": 5332244, +"width": 60, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515d50", +"base": 5332304, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515d64", +"base": 5332324, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515d68", +"base": 5332328, +"width": 100, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515dd0", +"base": 5332432, +"width": 100, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515e34", +"base": 5332532, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515e38", +"base": 5332536, +"width": 100, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515e9c", +"base": 5332636, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515ea0", +"base": 5332640, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515f2c", +"base": 5332780, +"width": 40, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515f54", +"base": 5332820, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x515f58", +"base": 5332824, +"width": 180, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51600c", +"base": 5333004, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516020", +"base": 5333024, +"width": 180, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5160d4", +"base": 5333204, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5160d8", +"base": 5333208, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516164", +"base": 5333348, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516168", +"base": 5333352, +"width": 120, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5161e0", +"base": 5333472, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51626c", +"base": 5333612, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516270", +"base": 5333616, +"width": 160, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516310", +"base": 5333776, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51639c", +"base": 5333916, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5163a0", +"base": 5333920, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51642c", +"base": 5334060, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516430", +"base": 5334064, +"width": 180, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5164e4", +"base": 5334244, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5164e8", +"base": 5334248, +"width": 180, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51659c", +"base": 5334428, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5165a0", +"base": 5334432, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51662c", +"base": 5334572, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516630", +"base": 5334576, +"width": 200, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5166f8", +"base": 5334776, +"width": 180, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5167ac", +"base": 5334956, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5167b0", +"base": 5334960, +"width": 100, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516814", +"base": 5335060, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516818", +"base": 5335064, +"width": 180, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5168cc", +"base": 5335244, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5168d0", +"base": 5335248, +"width": 160, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516970", +"base": 5335408, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516984", +"base": 5335428, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516988", +"base": 5335432, +"width": 80, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5169d8", +"base": 5335512, +"width": 60, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516a14", +"base": 5335572, +"width": 60, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516a50", +"base": 5335632, +"width": 40, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516a78", +"base": 5335672, +"width": 100, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516adc", +"base": 5335772, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516ae0", +"base": 5335776, +"width": 100, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516b44", +"base": 5335876, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516b48", +"base": 5335880, +"width": 100, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516bac", +"base": 5335980, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516bb0", +"base": 5335984, +"width": 80, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516c00", +"base": 5336064, +"width": 60, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516c3c", +"base": 5336124, +"width": 60, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516c78", +"base": 5336184, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516d04", +"base": 5336324, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516d08", +"base": 5336328, +"width": 180, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516dbc", +"base": 5336508, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516dc0", +"base": 5336512, +"width": 180, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516e74", +"base": 5336692, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516e78", +"base": 5336696, +"width": 160, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516f18", +"base": 5336856, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516fa4", +"base": 5336996, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x516fa8", +"base": 5337000, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517034", +"base": 5337140, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517038", +"base": 5337144, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5170c4", +"base": 5337284, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5170c8", +"base": 5337288, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517154", +"base": 5337428, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517158", +"base": 5337432, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5171e4", +"base": 5337572, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5171e8", +"base": 5337576, +"width": 120, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517260", +"base": 5337696, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5172ec", +"base": 5337836, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5172f0", +"base": 5337840, +"width": 100, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517354", +"base": 5337940, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517358", +"base": 5337944, +"width": 120, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5173d0", +"base": 5338064, +"width": 120, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517448", +"base": 5338184, +"width": 40, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517470", +"base": 5338224, +"width": 80, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5174c0", +"base": 5338304, +"width": 160, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517560", +"base": 5338464, +"width": 80, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5175b0", +"base": 5338544, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51763c", +"base": 5338684, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517640", +"base": 5338688, +"width": 100, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5176a4", +"base": 5338788, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5176a8", +"base": 5338792, +"width": 80, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5176f8", +"base": 5338872, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51770c", +"base": 5338892, +"width": 60, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517748", +"base": 5338952, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5177d4", +"base": 5339092, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5177d8", +"base": 5339096, +"width": 120, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517850", +"base": 5339216, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5178dc", +"base": 5339356, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5178e0", +"base": 5339360, +"width": 180, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517994", +"base": 5339540, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517998", +"base": 5339544, +"width": 180, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517a4c", +"base": 5339724, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517a50", +"base": 5339728, +"width": 100, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517ab4", +"base": 5339828, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517ab8", +"base": 5339832, +"width": 180, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517b6c", +"base": 5340012, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517b70", +"base": 5340016, +"width": 160, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517c10", +"base": 5340176, +"width": 120, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517c88", +"base": 5340296, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517d14", +"base": 5340436, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517d18", +"base": 5340440, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517da4", +"base": 5340580, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517da8", +"base": 5340584, +"width": 160, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517e48", +"base": 5340744, +"width": 120, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517ec0", +"base": 5340864, +"width": 120, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x517f38", +"base": 5340984, +"width": 200, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518000", +"base": 5341184, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518014", +"base": 5341204, +"width": 40, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51803c", +"base": 5341244, +"width": 40, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518068", +"base": 5341288, +"width": 220, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518144", +"base": 5341508, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518148", +"base": 5341512, +"width": 240, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518238", +"base": 5341752, +"width": 240, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518328", +"base": 5341992, +"width": 260, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51842c", +"base": 5342252, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518430", +"base": 5342256, +"width": 80, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518480", +"base": 5342336, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51850c", +"base": 5342476, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518510", +"base": 5342480, +"width": 120, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518588", +"base": 5342600, +"width": 80, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5185d8", +"base": 5342680, +"width": 100, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51863c", +"base": 5342780, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518640", +"base": 5342784, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5186cc", +"base": 5342924, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5186d0", +"base": 5342928, +"width": 120, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x518748", +"base": 5343048, +"width": 140, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5187d4", +"base": 5343188, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5187e8", +"base": 5343208, +"width": 3000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5193a0", +"base": 5346208, +"width": 2736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51a280", +"base": 5350016, +"width": 12, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51a28c", +"base": 5350028, +"width": 12, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51a298", +"base": 5350040, +"width": 12, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51a2a4", +"base": 5350052, +"width": 12, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51a2b0", +"base": 5350064, +"width": 12, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51a2bc", +"base": 5350076, +"width": 12, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51a2c8", +"base": 5350088, +"width": 12, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51a2d4", +"base": 5350100, +"width": 12, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51a424", +"base": 5350436, +"width": 24, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51a440", +"base": 5350464, +"width": 24, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51aab0", +"base": 5352112, +"width": 24, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51aac8", +"base": 5352136, +"width": 24, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51aae0", +"base": 5352160, +"width": 24, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51ab94", +"base": 5352340, +"width": 28, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51abb0", +"base": 5352368, +"width": 16, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51abc0", +"base": 5352384, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51abd4", +"base": 5352404, +"width": 28, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51abf0", +"base": 5352432, +"width": 12, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51abfc", +"base": 5352444, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51ac10", +"base": 5352464, +"width": 28, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51ac2c", +"base": 5352492, +"width": 24, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51ac48", +"base": 5352520, +"width": 20, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51ac5c", +"base": 5352540, +"width": 24, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51ac74", +"base": 5352564, +"width": 16, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51ad18", +"base": 5352728, +"width": 24, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51baf8", +"base": 5356280, +"width": 256, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51ca14", +"base": 5360148, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51ce70", +"base": 5361264, +"width": 28, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x51ce8c", +"base": 5361292, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51ce98", +"base": 5361304, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x51cea0", +"base": 5361312, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x51ced0", +"base": 5361360, +"width": 3996, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x51f2a8", +"base": 5370536, +"width": 19980, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x5240d0", +"base": 5390544, +"width": 1000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x52e4c0", +"base": 5432512, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x52e5c8", +"base": 5432776, +"width": 3996, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "image table: +0x0", +"base": 5436776, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x4", +"base": 5436780, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x8", +"base": 5436784, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0xc", +"base": 5436788, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x10", +"base": 5436792, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x14", +"base": 5436796, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x18", +"base": 5436800, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x1c", +"base": 5436804, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x20", +"base": 5436808, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x24", +"base": 5436812, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x28", +"base": 5436816, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x2c", +"base": 5436820, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x30", +"base": 5436824, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x34", +"base": 5436828, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x38", +"base": 5436832, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "image table: +0x3c", +"base": 5436836, +"width": 4, +"stride": 64, +"count": 5000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "0x57eb68", +"base": 5761896, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x57eb70", +"base": 5761904, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x57eee0", +"base": 5762784, +"width": 432, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x57f090", +"base": 5763216, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x57f0b4", +"base": 5763252, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x57f0d8", +"base": 5763288, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x57f0f0", +"base": 5763312, +"width": 66396, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x58f44c", +"base": 5829708, +"width": 29604, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5967f8", +"base": 5859320, +"width": 144, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x596a18", +"base": 5859864, +"width": 48, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x596a18 +0x30", +"base": 5859912, +"width": 12, +"read": "allowed", +"write": "unknown" +}, +{ +"name": "0x596a18 +0x3c", +"base": 5859924, +"width": 196, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x597208", +"base": 5861896, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x59720c", +"base": 5861900, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x597210", +"base": 5861904, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x597214", +"base": 5861908, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x597218", +"base": 5861912, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x59721c", +"base": 5861916, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x597220", +"base": 5861920, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x597224", +"base": 5861924, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x597228", +"base": 5861928, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x59722c", +"base": 5861932, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x597230", +"base": 5861936, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x597234", +"base": 5861940, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x597238", +"base": 5861944, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x59723c", +"base": 5861948, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5973a0", +"base": 5862304, +"width": 8192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5993c4", +"base": 5870532, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x5993d4", +"base": 5870548, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x59c6c0", +"base": 5883584, +"width": 1024, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x59cb5c", +"base": 5884764, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x59cb60", +"base": 5884768, +"width": 256, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x59cc78", +"base": 5885048, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x59cc80", +"base": 5885056, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x0", +"base": 5885096, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back" +}, +{ +"name": "unit table: +0x4", +"base": 5885100, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back" +}, +{ +"name": "unit table: +0x8", +"base": 5885104, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0xc", +"base": 5885108, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a sprite pointer: the game takes the address of a sprite; anything else is kept beside it and only read back" +}, +{ +"name": "unit table: +0x10", +"base": 5885112, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x14", +"base": 5885116, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back" +}, +{ +"name": "unit table: +0x18", +"base": 5885120, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x1c", +"base": 5885124, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x20", +"base": 5885128, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x24", +"base": 5885132, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x28", +"base": 5885136, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x2c", +"base": 5885140, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x30", +"base": 5885144, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x34", +"base": 5885148, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x38", +"base": 5885152, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x3c", +"base": 5885156, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x40", +"base": 5885160, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x44", +"base": 5885164, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x48", +"base": 5885168, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x4c", +"base": 5885172, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x50", +"base": 5885176, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x54", +"base": 5885180, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x58", +"base": 5885184, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x5c", +"base": 5885188, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back" +}, +{ +"name": "unit table: +0x60", +"base": 5885192, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x64", +"base": 5885196, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x68", +"base": 5885200, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back" +}, +{ +"name": "unit table: +0x6c", +"base": 5885204, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back" +}, +{ +"name": "unit table: +0x70", +"base": 5885208, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back" +}, +{ +"name": "unit table: +0x74", +"base": 5885212, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x78", +"base": 5885216, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x7c", +"base": 5885220, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back" +}, +{ +"name": "unit table: +0x80", +"base": 5885224, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back" +}, +{ +"name": "unit table: +0x84", +"base": 5885228, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x88", +"base": 5885232, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x8c", +"base": 5885236, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x90", +"base": 5885240, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x94", +"base": 5885244, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x98", +"base": 5885248, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x9c", +"base": 5885252, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0xa0", +"base": 5885256, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0xa4", +"base": 5885260, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0xa8", +"base": 5885264, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0xac", +"base": 5885268, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0xb0", +"base": 5885272, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0xb4", +"base": 5885276, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0xb8", +"base": 5885280, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0xbc", +"base": 5885284, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0xc0", +"base": 5885288, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back; differs between elements: ignored, shadow" +}, +{ +"name": "unit table: +0xc4", +"base": 5885292, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "differs between elements: allowed, ignored" +}, +{ +"name": "unit table: +0xc8", +"base": 5885296, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "differs between elements: allowed, ignored" +}, +{ +"name": "unit table: +0xcc", +"base": 5885300, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "differs between elements: allowed, ignored" +}, +{ +"name": "unit table: +0xd0", +"base": 5885304, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "differs between elements: allowed, ignored" +}, +{ +"name": "unit table: +0xd4", +"base": 5885308, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back; differs between elements: ignored, shadow" +}, +{ +"name": "unit table: +0xd8", +"base": 5885312, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "differs between elements: allowed, ignored" +}, +{ +"name": "unit table: +0xdc", +"base": 5885316, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0xe0", +"base": 5885320, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0xe4", +"base": 5885324, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0xe8", +"base": 5885328, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0xec", +"base": 5885332, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back" +}, +{ +"name": "unit table: +0xf0", +"base": 5885336, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0xf4", +"base": 5885340, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0xf8", +"base": 5885344, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0xfc", +"base": 5885348, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x100", +"base": 5885352, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x104", +"base": 5885356, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x108", +"base": 5885360, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x10c", +"base": 5885364, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x110", +"base": 5885368, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x114", +"base": 5885372, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x118", +"base": 5885376, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x11c", +"base": 5885380, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed", +"note": "a unit pointer: the game takes the address of a unit; anything else is kept beside it and only read back" +}, +{ +"name": "unit table: +0x120", +"base": 5885384, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x124", +"base": 5885388, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x128", +"base": 5885392, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x12c", +"base": 5885396, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "unit table: +0x130", +"base": 5885400, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x134", +"base": 5885404, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x138", +"base": 5885408, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x13c", +"base": 5885412, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x140", +"base": 5885416, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x144", +"base": 5885420, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x148", +"base": 5885424, +"width": 4, +"stride": 336, +"count": 1700, +"read": "ends", +"write": "ends" +}, +{ +"name": "unit table: +0x14c", +"base": 5885428, +"width": 4, +"stride": 336, +"count": 1700, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6283f0", +"base": 6456304, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x6283f8", +"base": 6456312, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6283fc", +"base": 6456316, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628400", +"base": 6456320, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628404", +"base": 6456324, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628408", +"base": 6456328, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62840c", +"base": 6456332, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628410", +"base": 6456336, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628414", +"base": 6456340, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628418", +"base": 6456344, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62841c", +"base": 6456348, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628420", +"base": 6456352, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628424", +"base": 6456356, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628428", +"base": 6456360, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62842c", +"base": 6456364, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628430", +"base": 6456368, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628434", +"base": 6456372, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628438", +"base": 6456376, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62843c", +"base": 6456380, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628444", +"base": 6456388, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x628448", +"base": 6456392, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x628454", +"base": 6456404, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x628458", +"base": 6456408, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x628470", +"base": 6456432, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x62848c", +"base": 6456460, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x628494", +"base": 6456468, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x628498", +"base": 6456472, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x6284a8", +"base": 6456488, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x6284ac", +"base": 6456492, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x6284b8", +"base": 6456504, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284bc", +"base": 6456508, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284c0", +"base": 6456512, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284c4", +"base": 6456516, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284c8", +"base": 6456520, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284cc", +"base": 6456524, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284d0", +"base": 6456528, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284d4", +"base": 6456532, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284d8", +"base": 6456536, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284dc", +"base": 6456540, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284e0", +"base": 6456544, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284e4", +"base": 6456548, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284e8", +"base": 6456552, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284ec", +"base": 6456556, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284f0", +"base": 6456560, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284f4", +"base": 6456564, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284f8", +"base": 6456568, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6284fc", +"base": 6456572, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628500", +"base": 6456576, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628504", +"base": 6456580, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628508", +"base": 6456584, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62850c", +"base": 6456588, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628510", +"base": 6456592, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628514", +"base": 6456596, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628518", +"base": 6456600, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62851c", +"base": 6456604, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628520", +"base": 6456608, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628524", +"base": 6456612, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628528", +"base": 6456616, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62852c", +"base": 6456620, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628530", +"base": 6456624, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628534", +"base": 6456628, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628538", +"base": 6456632, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62853c", +"base": 6456636, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628540", +"base": 6456640, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628544", +"base": 6456644, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628548", +"base": 6456648, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62854c", +"base": 6456652, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628550", +"base": 6456656, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628554", +"base": 6456660, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628558", +"base": 6456664, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62855c", +"base": 6456668, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628560", +"base": 6456672, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628564", +"base": 6456676, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628568", +"base": 6456680, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62856c", +"base": 6456684, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628570", +"base": 6456688, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628574", +"base": 6456692, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628578", +"base": 6456696, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62857c", +"base": 6456700, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628580", +"base": 6456704, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628584", +"base": 6456708, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628588", +"base": 6456712, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62858c", +"base": 6456716, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628590", +"base": 6456720, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628594", +"base": 6456724, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628598", +"base": 6456728, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62859c", +"base": 6456732, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285a0", +"base": 6456736, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285a4", +"base": 6456740, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285a8", +"base": 6456744, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285ac", +"base": 6456748, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285b0", +"base": 6456752, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285b4", +"base": 6456756, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285b8", +"base": 6456760, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285bc", +"base": 6456764, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285c0", +"base": 6456768, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285c4", +"base": 6456772, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285c8", +"base": 6456776, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285cc", +"base": 6456780, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285d0", +"base": 6456784, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285d4", +"base": 6456788, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285d8", +"base": 6456792, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285dc", +"base": 6456796, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285e0", +"base": 6456800, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285e4", +"base": 6456804, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285e8", +"base": 6456808, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285ec", +"base": 6456812, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285f0", +"base": 6456816, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285f4", +"base": 6456820, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285f8", +"base": 6456824, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6285fc", +"base": 6456828, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628600", +"base": 6456832, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628604", +"base": 6456836, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628608", +"base": 6456840, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62860c", +"base": 6456844, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628610", +"base": 6456848, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628614", +"base": 6456852, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628618", +"base": 6456856, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62861c", +"base": 6456860, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628620", +"base": 6456864, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628624", +"base": 6456868, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628628", +"base": 6456872, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62862c", +"base": 6456876, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628630", +"base": 6456880, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628634", +"base": 6456884, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628638", +"base": 6456888, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62863c", +"base": 6456892, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628640", +"base": 6456896, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628644", +"base": 6456900, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628648", +"base": 6456904, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62864c", +"base": 6456908, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628650", +"base": 6456912, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628654", +"base": 6456916, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628658", +"base": 6456920, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x62865c", +"base": 6456924, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628660", +"base": 6456928, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x628664", +"base": 6456932, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x629c90", +"base": 6462608, +"width": 256, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "sprite table: +0x0", +"base": 6462872, +"width": 4, +"stride": 36, +"count": 2500, +"read": "allowed", +"write": "allowed", +"note": "a sprite pointer: the game takes the address of a sprite; anything else is kept beside it and only read back" +}, +{ +"name": "sprite table: +0x4", +"base": 6462876, +"width": 4, +"stride": 36, +"count": 2500, +"read": "allowed", +"write": "allowed", +"note": "a sprite pointer: the game takes the address of a sprite; anything else is kept beside it and only read back" +}, +{ +"name": "sprite table: +0x8", +"base": 6462880, +"width": 4, +"stride": 36, +"count": 2500, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; differs between elements: allowed, shadow" +}, +{ +"name": "sprite table: +0xc", +"base": 6462884, +"width": 4, +"stride": 36, +"count": 2500, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it" +}, +{ +"name": "sprite table: +0x10", +"base": 6462888, +"width": 4, +"stride": 36, +"count": 2500, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "sprite table: +0x14", +"base": 6462892, +"width": 4, +"stride": 36, +"count": 2500, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it" +}, +{ +"name": "sprite table: +0x18", +"base": 6462896, +"width": 4, +"stride": 36, +"count": 2500, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it" +}, +{ +"name": "sprite table: +0x1c", +"base": 6462900, +"width": 4, +"stride": 36, +"count": 2500, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it" +}, +{ +"name": "sprite table: +0x20", +"base": 6462904, +"width": 4, +"stride": 36, +"count": 2500, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it" +}, +{ +"name": "0x63fe30", +"base": 6553136, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x63fe34", +"base": 6553140, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x63fed8", +"base": 6553304, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x63ff40", +"base": 6553408, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x63ff78", +"base": 6553464, +"width": 828, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6402b8", +"base": 6554296, +"width": 656, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x640550", +"base": 6554960, +"width": 796, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x64086c", +"base": 6555756, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x640b58", +"base": 6556504, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x640b5c", +"base": 6556508, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x640b60", +"base": 6556512, +"width": 2836, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x64169c", +"base": 6559388, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "order queue: +0x0", +"base": 6559392, +"width": 4, +"stride": 20, +"count": 2000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "order queue: +0x4", +"base": 6559396, +"width": 4, +"stride": 20, +"count": 2000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "order queue: +0x8", +"base": 6559400, +"width": 4, +"stride": 20, +"count": 2000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "order queue: +0xc", +"base": 6559404, +"width": 4, +"stride": 20, +"count": 2000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "order queue: +0x10", +"base": 6559408, +"width": 4, +"stride": 20, +"count": 2000, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "0x64b2e0", +"base": 6599392, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x64b2e4", +"base": 6599396, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "bullet table: +0x0", +"base": 6599400, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x4", +"base": 6599404, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x8", +"base": 6599408, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0xc", +"base": 6599412, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x10", +"base": 6599416, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x14", +"base": 6599420, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x18", +"base": 6599424, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x1c", +"base": 6599428, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x20", +"base": 6599432, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x24", +"base": 6599436, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x28", +"base": 6599440, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x2c", +"base": 6599444, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x30", +"base": 6599448, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x34", +"base": 6599452, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x38", +"base": 6599456, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x3c", +"base": 6599460, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x40", +"base": 6599464, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x44", +"base": 6599468, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x48", +"base": 6599472, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x4c", +"base": 6599476, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x50", +"base": 6599480, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x54", +"base": 6599484, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x58", +"base": 6599488, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x5c", +"base": 6599492, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x60", +"base": 6599496, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x64", +"base": 6599500, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x68", +"base": 6599504, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "bullet table: +0x6c", +"base": 6599508, +"width": 4, +"stride": 112, +"count": 100, +"read": "allowed", +"write": "ignored", +"note": "kept beside the game: read back, never used by it; tried on elements not in use" +}, +{ +"name": "0x64debc", +"base": 6610620, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x64eed8", +"base": 6614744, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x64eedc", +"base": 6614748, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6509a0", +"base": 6621600, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6509b0", +"base": 6621616, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x654880", +"base": 6637696, +"width": 512, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x654aa0", +"base": 6638240, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6556e4", +"base": 6641380, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x655700", +"base": 6641408, +"width": 64, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x655740", +"base": 6641472, +"width": 124, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6557bc", +"base": 6641596, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6557c0", +"base": 6641600, +"width": 124, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x65583c", +"base": 6641724, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x655840", +"base": 6641728, +"width": 124, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6558c0", +"base": 6641856, +"width": 124, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x655940", +"base": 6641984, +"width": 124, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6559bc", +"base": 6642108, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6559c0", +"base": 6642112, +"width": 124, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x655a40", +"base": 6642240, +"width": 124, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x655ac0", +"base": 6642368, +"width": 124, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x655b3c", +"base": 6642492, +"width": 64, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x655b7c", +"base": 6642556, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x655b80", +"base": 6642560, +"width": 124, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x655bfc", +"base": 6642684, +"width": 64, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x655c58", +"base": 6642776, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x655e80", +"base": 6643328, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656198", +"base": 6644120, +"width": 88, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6561f0", +"base": 6644208, +"width": 88, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656248", +"base": 6644296, +"width": 88, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6562a0", +"base": 6644384, +"width": 88, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6562f8", +"base": 6644472, +"width": 88, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656350", +"base": 6644560, +"width": 44, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x65637c", +"base": 6644604, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656380", +"base": 6644608, +"width": 88, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6563d8", +"base": 6644696, +"width": 88, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656430", +"base": 6644784, +"width": 88, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656488", +"base": 6644872, +"width": 44, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6564b4", +"base": 6644916, +"width": 44, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6564e0", +"base": 6644960, +"width": 132, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656568", +"base": 6645096, +"width": 260, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656670", +"base": 6645360, +"width": 132, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6566f4", +"base": 6645492, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6566f8", +"base": 6645496, +"width": 132, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x65677c", +"base": 6645628, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656780", +"base": 6645632, +"width": 260, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656884", +"base": 6645892, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656888", +"base": 6645896, +"width": 260, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x65698c", +"base": 6646156, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656990", +"base": 6646160, +"width": 132, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656a14", +"base": 6646292, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656a18", +"base": 6646296, +"width": 520, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656c20", +"base": 6646816, +"width": 132, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656ca4", +"base": 6646948, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656ca8", +"base": 6646952, +"width": 520, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656eb0", +"base": 6647472, +"width": 260, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656fb4", +"base": 6647732, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x656fb8", +"base": 6647736, +"width": 132, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x65703c", +"base": 6647868, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x657040", +"base": 6647872, +"width": 132, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6570c4", +"base": 6648004, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6570c8", +"base": 6648008, +"width": 260, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6571cc", +"base": 6648268, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6571d0", +"base": 6648272, +"width": 132, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x657254", +"base": 6648404, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x657258", +"base": 6648408, +"width": 132, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6572dc", +"base": 6648540, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6572e0", +"base": 6648544, +"width": 260, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6573e4", +"base": 6648804, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6573e8", +"base": 6648808, +"width": 132, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x65746c", +"base": 6648940, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x657470", +"base": 6648944, +"width": 520, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x657678", +"base": 6649464, +"width": 260, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x657780", +"base": 6649728, +"width": 260, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x657888", +"base": 6649992, +"width": 132, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x65790c", +"base": 6650124, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x657910", +"base": 6650128, +"width": 132, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x657998", +"base": 6650264, +"width": 260, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x657a9c", +"base": 6650524, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x657aa0", +"base": 6650528, +"width": 4096, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x65fc18", +"base": 6683672, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x65fd00", +"base": 6683904, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x65fec8", +"base": 6684360, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x65ffb0", +"base": 6684592, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x660178", +"base": 6685048, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x660260", +"base": 6685280, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x660428", +"base": 6685736, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6605f0", +"base": 6686192, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6606d8", +"base": 6686424, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6607c0", +"base": 6686656, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x660988", +"base": 6687112, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x660a70", +"base": 6687344, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x660c38", +"base": 6687800, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x660e00", +"base": 6688256, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x660fc8", +"base": 6688712, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6610ac", +"base": 6688940, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6610b0", +"base": 6688944, +"width": 912, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x661440", +"base": 6689856, +"width": 212, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x661514", +"base": 6690068, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x661518", +"base": 6690072, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6616e0", +"base": 6690528, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6617c4", +"base": 6690756, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6617c8", +"base": 6690760, +"width": 1824, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x661ee8", +"base": 6692584, +"width": 212, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x661fbc", +"base": 6692796, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x661fc0", +"base": 6692800, +"width": 212, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662094", +"base": 6693012, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662098", +"base": 6693016, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x66217c", +"base": 6693244, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662180", +"base": 6693248, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662264", +"base": 6693476, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662268", +"base": 6693480, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662350", +"base": 6693712, +"width": 912, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6626e0", +"base": 6694624, +"width": 384, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662860", +"base": 6695008, +"width": 912, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662bf0", +"base": 6695920, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662db8", +"base": 6696376, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662e9c", +"base": 6696604, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662ea0", +"base": 6696608, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662f84", +"base": 6696836, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x662f88", +"base": 6696840, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663150", +"base": 6697296, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663234", +"base": 6697524, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663238", +"base": 6697528, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x66331c", +"base": 6697756, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663320", +"base": 6697760, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663404", +"base": 6697988, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663408", +"base": 6697992, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6635d0", +"base": 6698448, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6636b4", +"base": 6698676, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6636b8", +"base": 6698680, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x66379c", +"base": 6698908, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6637a0", +"base": 6698912, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663884", +"base": 6699140, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663888", +"base": 6699144, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663a50", +"base": 6699600, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663b38", +"base": 6699832, +"width": 212, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663c0c", +"base": 6700044, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663c10", +"base": 6700048, +"width": 212, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663ce4", +"base": 6700260, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663ce8", +"base": 6700264, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663dcc", +"base": 6700492, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663dd0", +"base": 6700496, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x663eb8", +"base": 6700728, +"width": 456, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x664080", +"base": 6701184, +"width": 912, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x664410", +"base": 6702096, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6644f4", +"base": 6702324, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6644f8", +"base": 6702328, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6645dc", +"base": 6702556, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6645e0", +"base": 6702560, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6646c4", +"base": 6702788, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6646c8", +"base": 6702792, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6647ac", +"base": 6703020, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6647b0", +"base": 6703024, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x664894", +"base": 6703252, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x664898", +"base": 6703256, +"width": 228, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x66497c", +"base": 6703484, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x664980", +"base": 6703488, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x664a40", +"base": 6703680, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x664b00", +"base": 6703872, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x664bc0", +"base": 6704064, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x664c80", +"base": 6704256, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x664d40", +"base": 6704448, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x664e00", +"base": 6704640, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x664ec0", +"base": 6704832, +"width": 380, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x665040", +"base": 6705216, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x665100", +"base": 6705408, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6651c0", +"base": 6705600, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x665280", +"base": 6705792, +"width": 380, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x665400", +"base": 6706176, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6654c0", +"base": 6706368, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x665580", +"base": 6706560, +"width": 380, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x665700", +"base": 6706944, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6657c0", +"base": 6707136, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x665880", +"base": 6707328, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x665940", +"base": 6707520, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x665a00", +"base": 6707712, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x665ac0", +"base": 6707904, +"width": 388, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x665c48", +"base": 6708296, +"width": 520, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x665e50", +"base": 6708816, +"width": 388, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x665fd8", +"base": 6709208, +"width": 388, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x666160", +"base": 6709600, +"width": 1028, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x666160 +0x404", +"base": 6710628, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x666160 +0x408", +"base": 6710632, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x666570", +"base": 6710640, +"width": 520, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x666778", +"base": 6711160, +"width": 3996, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x667718", +"base": 6715160, +"width": 1000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x667b00", +"base": 6716160, +"width": 3996, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x668aa0", +"base": 6720160, +"width": 3996, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x669a40", +"base": 6724160, +"width": 1000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x669e28", +"base": 6725160, +"width": 1000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x66a210", +"base": 6726160, +"width": 3996, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x66b1b0", +"base": 6730160, +"width": 3996, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x66c150", +"base": 6734160, +"width": 1000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x66c538", +"base": 6735160, +"width": 3996, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x66d4d8", +"base": 6739160, +"width": 1000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x66d8c0", +"base": 6740160, +"width": 3996, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x66e860", +"base": 6744160, +"width": 1000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x66ec48", +"base": 6745160, +"width": 3748, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x66ec48 +0xea4", +"base": 6748908, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x66ec48 +0xea8", +"base": 6748912, +"width": 244, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x66fd0c", +"base": 6749452, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x66ff78", +"base": 6750072, +"width": 27200, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x68ac74", +"base": 6859892, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68ac8c", +"base": 6859916, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c104", +"base": 6865156, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c108", +"base": 6865160, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c10c", +"base": 6865164, +"width": 52, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c140", +"base": 6865216, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c144", +"base": 6865220, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c148", +"base": 6865224, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c14c", +"base": 6865228, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c1e0", +"base": 6865376, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c1f0", +"base": 6865392, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c1f4", +"base": 6865396, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c1fc", +"base": 6865404, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c204", +"base": 6865412, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c208", +"base": 6865416, +"width": 24, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c224", +"base": 6865444, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c234", +"base": 6865460, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x68c238", +"base": 6865464, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x6bb210", +"base": 7057936, +"width": 1824, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6bd3d0", +"base": 7066576, +"width": 6804, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x6c9858", +"base": 7116888, +"width": 212, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6c9930", +"base": 7117104, +"width": 836, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6c9c78", +"base": 7117944, +"width": 420, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6c9e20", +"base": 7118368, +"width": 212, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6c9ef8", +"base": 7118584, +"width": 836, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ca240", +"base": 7119424, +"width": 212, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ca318", +"base": 7119640, +"width": 420, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ca4bc", +"base": 7120060, +"width": 48, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ca4ec", +"base": 7120108, +"width": 48, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ca51c", +"base": 7120156, +"width": 48, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ca550", +"base": 7120208, +"width": 252, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ca64c", +"base": 7120460, +"width": 24, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ca94c", +"base": 7121228, +"width": 48, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6caf98", +"base": 7122840, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6cafa0", +"base": 7122848, +"width": 10752, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6cd9a0", +"base": 7133600, +"width": 516, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6cdba8", +"base": 7134120, +"width": 516, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6cddc0", +"base": 7134656, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6cddc4", +"base": 7134660, +"width": 8, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6cdfd4", +"base": 7135188, +"width": 36, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ce000", +"base": 7135232, +"width": 192, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ce320", +"base": 7136032, +"width": 1024, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ce720", +"base": 7137056, +"width": 1024, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ceb20", +"base": 7138080, +"width": 28, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ceff0", +"base": 7139312, +"width": 8, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x6ceff8", +"base": 7139320, +"width": 1200, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x6d0f14", +"base": 7147284, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6d0f24", +"base": 7147300, +"width": 4, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x6d0f30", +"base": 7147312, +"width": 636, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x6d1200", +"base": 7148032, +"width": 52, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x6d1200 +0x34", +"base": 7148084, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6d1238", +"base": 7148088, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6d1200 +0x3c", +"base": 7148092, +"width": 36, +"read": "ends", +"write": "ends" +}, +{ +"name": "0x6d1200 +0x60", +"base": 7148128, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6d1260", +"base": 7148128, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6d1200 +0x64", +"base": 7148132, +"width": 18380, +"read": "ends", +"write": "ends" +}, +{ +"name": "0x6d1200 +0x4830", +"base": 7166512, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6d5a30", +"base": 7166512, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6d1200 +0x4834", +"base": 7166516, +"width": 56, +"read": "ends", +"write": "ends" +}, +{ +"name": "0x6d1200 +0x486c", +"base": 7166572, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6d1200 +0x4870", +"base": 7166576, +"width": 616, +"read": "ends", +"write": "ends" +}, +{ +"name": "0x6d1200 +0x4ad8", +"base": 7167192, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6d5cd8", +"base": 7167192, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6d1200 +0x4adc", +"base": 7167196, +"width": 312, +"read": "ends", +"write": "ends" +}, +{ +"name": "0x6d1200 +0x4c14", +"base": 7167508, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6d5e14", +"base": 7167508, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6d1200 +0x4c18", +"base": 7167512, +"width": 4, +"read": "ends", +"write": "ends" +}, +{ +"name": "0x6d1200 +0x4c1c", +"base": 7167516, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6d1200 +0x4c20", +"base": 7167520, +"width": 168, +"read": "ends", +"write": "ends" +}, +{ +"name": "0x6d1200 +0x4cc8", +"base": 7167688, +"width": 4, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6d5ec8", +"base": 7167688, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6d1200 +0x4ccc", +"base": 7167692, +"width": 1224, +"read": "ends", +"write": "ends" +}, +{ +"name": "0x6d1200 +0x5194", +"base": 7168916, +"width": 24, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x6d6394", +"base": 7168916, +"width": 24, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6d1200 +0x51ac", +"base": 7168940, +"width": 19576, +"read": "ends", +"write": "ends" +}, +{ +"name": "0x6de000", +"base": 7200768, +"width": 1216, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6de4c0", +"base": 7201984, +"width": 1216, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6de980", +"base": 7203200, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x6ef320", +"base": 7271200, +"width": 1216, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x6ef7e0", +"base": 7272416, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x700180", +"base": 7340416, +"width": 1216, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x700640", +"base": 7341632, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x710fe0", +"base": 7409632, +"width": 1216, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x7114a0", +"base": 7410848, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x721e40", +"base": 7478848, +"width": 1216, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x722300", +"base": 7480064, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x732ca0", +"base": 7548064, +"width": 1216, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x733160", +"base": 7549280, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x743b00", +"base": 7617280, +"width": 1216, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x743fc0", +"base": 7618496, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x754960", +"base": 7686496, +"width": 1216, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x754e20", +"base": 7687712, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x7657c0", +"base": 7755712, +"width": 1216, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x765c80", +"base": 7756928, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x776620", +"base": 7824928, +"width": 736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x776900", +"base": 7825664, +"width": 736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x776be0", +"base": 7826400, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x787580", +"base": 7894400, +"width": 736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x787860", +"base": 7895136, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x798200", +"base": 7963136, +"width": 736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x7984e0", +"base": 7963872, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x7a8e80", +"base": 8031872, +"width": 736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x7a9160", +"base": 8032608, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x7b9b00", +"base": 8100608, +"width": 736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x7b9de0", +"base": 8101344, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x7ca780", +"base": 8169344, +"width": 736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x7caa60", +"base": 8170080, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x7db400", +"base": 8238080, +"width": 736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x7db6e0", +"base": 8238816, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x7ec080", +"base": 8306816, +"width": 736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x7ec360", +"base": 8307552, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x7fcd00", +"base": 8375552, +"width": 736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x7fcfe0", +"base": 8376288, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x80d980", +"base": 8444288, +"width": 736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x80dc60", +"base": 8445024, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x81e600", +"base": 8513024, +"width": 736, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x81e8e0", +"base": 8513760, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x82f280", +"base": 8581760, +"width": 796, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x82f5a0", +"base": 8582560, +"width": 796, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x82f8c0", +"base": 8583360, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x840260", +"base": 8651360, +"width": 796, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x840580", +"base": 8652160, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x850f20", +"base": 8720160, +"width": 796, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x851240", +"base": 8720960, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x861be0", +"base": 8788960, +"width": 796, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x861f00", +"base": 8789760, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x8728a0", +"base": 8857760, +"width": 796, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x872bc0", +"base": 8858560, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x883560", +"base": 8926560, +"width": 796, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x883880", +"base": 8927360, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x894220", +"base": 8995360, +"width": 796, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x894540", +"base": 8996160, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x8a4ee0", +"base": 9064160, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x8a50a0", +"base": 9064608, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x8a5260", +"base": 9065056, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x8b5c00", +"base": 9133056, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x8b5dc0", +"base": 9133504, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x8c6760", +"base": 9201504, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x8c6920", +"base": 9201952, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x8d72c0", +"base": 9269952, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x8d7480", +"base": 9270400, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x8e7e20", +"base": 9338400, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x8e7fe0", +"base": 9338848, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x8f8980", +"base": 9406848, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x8f8b40", +"base": 9407296, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x9094e0", +"base": 9475296, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x9096a0", +"base": 9475744, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x91a040", +"base": 9543744, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x91a200", +"base": 9544192, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x92aba0", +"base": 9612192, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x92ad60", +"base": 9612640, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x93b700", +"base": 9680640, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x93b8c0", +"base": 9681088, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x94c260", +"base": 9749088, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x94c420", +"base": 9749536, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x95cdc0", +"base": 9817536, +"width": 440, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x95cf80", +"base": 9817984, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x96d920", +"base": 9885984, +"width": 184, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x96d9e0", +"base": 9886176, +"width": 184, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x96daa0", +"base": 9886368, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x97e440", +"base": 9954368, +"width": 184, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x97e500", +"base": 9954560, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x98eea0", +"base": 10022560, +"width": 184, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x98ef60", +"base": 10022752, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x99f900", +"base": 10090752, +"width": 184, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x99f9c0", +"base": 10090944, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x9b0360", +"base": 10158944, +"width": 184, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x9b0420", +"base": 10159136, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x9c0dc0", +"base": 10227136, +"width": 184, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x9c0e80", +"base": 10227328, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x9d1820", +"base": 10295328, +"width": 184, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x9d18e0", +"base": 10295520, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x9e2280", +"base": 10363520, +"width": 184, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x9e2340", +"base": 10363712, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x9f2ce0", +"base": 10431712, +"width": 184, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x9f2da0", +"base": 10431904, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xa03740", +"base": 10499904, +"width": 184, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa03800", +"base": 10500096, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xa141a0", +"base": 10568096, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa141b0", +"base": 10568112, +"width": 272, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa142c0", +"base": 10568384, +"width": 272, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa143d0", +"base": 10568656, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xa24d70", +"base": 10636656, +"width": 272, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa24e80", +"base": 10636928, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xa35820", +"base": 10704928, +"width": 272, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa35930", +"base": 10705200, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xa462d0", +"base": 10773200, +"width": 272, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa463e0", +"base": 10773472, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xa56d80", +"base": 10841472, +"width": 272, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa56e90", +"base": 10841744, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xa67830", +"base": 10909744, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa67840", +"base": 10909760, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa67850", +"base": 10909776, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa67860", +"base": 10909792, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa67870", +"base": 10909808, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xa67880", +"base": 10909824, +"width": 301056, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xab1080", +"base": 11210880, +"width": 131072, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xad1080", +"base": 11341952, +"width": 131072, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xaf1080", +"base": 11473024, +"width": 262144, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xb31080", +"base": 11735168, +"width": 3145728, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xe31080", +"base": 14880896, +"width": 204800, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xe63080", +"base": 15085696, +"width": 81920, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0xe77080", +"base": 15167616, +"width": 48, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0xe770b0", +"base": 15167664, +"width": 4828, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x1505adf8", +"base": 352693752, +"width": 12, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1505e670", +"base": 352708208, +"width": 1024, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x19018f38", +"base": 419532600, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x190452c8", +"base": 419713736, +"width": 128, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19046134", +"base": 419717428, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19046138", +"base": 419717432, +"width": 256, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x190913d8", +"base": 420025304, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x190913e0", +"base": 420025312, +"width": 306904, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x190dc2c0", +"base": 420332224, +"width": 48716, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x190e8110", +"base": 420380944, +"width": 51208, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x190f4920", +"base": 420432160, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x190f4930", +"base": 420432176, +"width": 5000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x190f5cc0", +"base": 420437184, +"width": 5000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x190f7050", +"base": 420442192, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x191079f0", +"base": 420510192, +"width": 5000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19108d80", +"base": 420515200, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x19119720", +"base": 420583200, +"width": 5000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1911aab0", +"base": 420588208, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x1912b450", +"base": 420656208, +"width": 5000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1912c7e0", +"base": 420661216, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x1913d180", +"base": 420729216, +"width": 5000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1913e510", +"base": 420734224, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x1914eeb0", +"base": 420802224, +"width": 5000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19150240", +"base": 420807232, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x19160be0", +"base": 420875232, +"width": 5000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19161f70", +"base": 420880240, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x19172910", +"base": 420948240, +"width": 5000, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19173ca0", +"base": 420953248, +"width": 68000, +"read": "allowed", +"write": "ignored" +}, +{ +"name": "0x19184640", +"base": 421021248, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19184650", +"base": 421021264, +"width": 4, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19184660", +"base": 421021280, +"width": 64864, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191943c0", +"base": 421086144, +"width": 8, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191943c8", +"base": 421086152, +"width": 143004, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191b7270", +"base": 421229168, +"width": 29244, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191be4b0", +"base": 421258416, +"width": 86624, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191d3710", +"base": 421345040, +"width": 51032, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191dfe70", +"base": 421396080, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191dfe70 +0x8", +"base": 421396088, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e07e0", +"base": 421398496, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e07e0 +0x8", +"base": 421398504, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e1150", +"base": 421400912, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e1150 +0x8", +"base": 421400920, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e1ac0", +"base": 421403328, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e1ac0 +0x8", +"base": 421403336, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e2430", +"base": 421405744, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e2430 +0x8", +"base": 421405752, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e2da0", +"base": 421408160, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e2da0 +0x8", +"base": 421408168, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e3710", +"base": 421410576, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e3710 +0x8", +"base": 421410584, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e4080", +"base": 421412992, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e4080 +0x8", +"base": 421413000, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e49f0", +"base": 421415408, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e49f0 +0x8", +"base": 421415416, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e5360", +"base": 421417824, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e5360 +0x8", +"base": 421417832, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e5cd0", +"base": 421420240, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e5cd0 +0x8", +"base": 421420248, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e6640", +"base": 421422656, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e6640 +0x8", +"base": 421422664, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e6fb0", +"base": 421425072, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e6fb0 +0x8", +"base": 421425080, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e7920", +"base": 421427488, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e7920 +0x8", +"base": 421427496, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e8290", +"base": 421429904, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e8290 +0x8", +"base": 421429912, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e8c00", +"base": 421432320, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e8c00 +0x8", +"base": 421432328, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e9570", +"base": 421434736, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e9570 +0x8", +"base": 421434744, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191e9ee0", +"base": 421437152, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191e9ee0 +0x8", +"base": 421437160, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191ea850", +"base": 421439568, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191ea850 +0x8", +"base": 421439576, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191eb1c0", +"base": 421441984, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191eb1c0 +0x8", +"base": 421441992, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191ebb30", +"base": 421444400, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191ebb30 +0x8", +"base": 421444408, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191ec4a0", +"base": 421446816, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191ec4a0 +0x8", +"base": 421446824, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191ece10", +"base": 421449232, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191ece10 +0x8", +"base": 421449240, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191ed780", +"base": 421451648, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191ed780 +0x8", +"base": 421451656, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191ee0f0", +"base": 421454064, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191ee0f0 +0x8", +"base": 421454072, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191eea60", +"base": 421456480, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191eea60 +0x8", +"base": 421456488, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191ef3d0", +"base": 421458896, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191ef3d0 +0x8", +"base": 421458904, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191efd40", +"base": 421461312, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191efd40 +0x8", +"base": 421461320, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f06b0", +"base": 421463728, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f06b0 +0x8", +"base": 421463736, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f1020", +"base": 421466144, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f1020 +0x8", +"base": 421466152, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f1990", +"base": 421468560, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f1990 +0x8", +"base": 421468568, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f2300", +"base": 421470976, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f2300 +0x8", +"base": 421470984, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f2c70", +"base": 421473392, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f2c70 +0x8", +"base": 421473400, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f35e0", +"base": 421475808, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f35e0 +0x8", +"base": 421475816, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f3f50", +"base": 421478224, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f3f50 +0x8", +"base": 421478232, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f48c0", +"base": 421480640, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f48c0 +0x8", +"base": 421480648, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f5230", +"base": 421483056, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f5230 +0x8", +"base": 421483064, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f5ba0", +"base": 421485472, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f5ba0 +0x8", +"base": 421485480, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f6510", +"base": 421487888, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f6510 +0x8", +"base": 421487896, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f6e80", +"base": 421490304, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f6e80 +0x8", +"base": 421490312, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f77f0", +"base": 421492720, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f77f0 +0x8", +"base": 421492728, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f8160", +"base": 421495136, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f8160 +0x8", +"base": 421495144, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f8ad0", +"base": 421497552, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f8ad0 +0x8", +"base": 421497560, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f9440", +"base": 421499968, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f9440 +0x8", +"base": 421499976, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191f9db0", +"base": 421502384, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191f9db0 +0x8", +"base": 421502392, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191fa720", +"base": 421504800, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191fa720 +0x8", +"base": 421504808, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191fb090", +"base": 421507216, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191fb090 +0x8", +"base": 421507224, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191fba00", +"base": 421509632, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191fba00 +0x8", +"base": 421509640, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191fc370", +"base": 421512048, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191fc370 +0x8", +"base": 421512056, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191fcce0", +"base": 421514464, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191fcce0 +0x8", +"base": 421514472, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191fd650", +"base": 421516880, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191fd650 +0x8", +"base": 421516888, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191fdfc0", +"base": 421519296, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191fdfc0 +0x8", +"base": 421519304, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191fe930", +"base": 421521712, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191fe930 +0x8", +"base": 421521720, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191ff2a0", +"base": 421524128, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191ff2a0 +0x8", +"base": 421524136, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x191ffc10", +"base": 421526544, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x191ffc10 +0x8", +"base": 421526552, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19200580", +"base": 421528960, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19200580 +0x8", +"base": 421528968, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19200ef0", +"base": 421531376, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19200ef0 +0x8", +"base": 421531384, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19201860", +"base": 421533792, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19201860 +0x8", +"base": 421533800, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192021d0", +"base": 421536208, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x192021d0 +0x8", +"base": 421536216, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19202b40", +"base": 421538624, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19202b40 +0x8", +"base": 421538632, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192034b0", +"base": 421541040, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x192034b0 +0x8", +"base": 421541048, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19203e20", +"base": 421543456, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19203e20 +0x8", +"base": 421543464, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19204790", +"base": 421545872, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19204790 +0x8", +"base": 421545880, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19205100", +"base": 421548288, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19205100 +0x8", +"base": 421548296, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19205a70", +"base": 421550704, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19205a70 +0x8", +"base": 421550712, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192063e0", +"base": 421553120, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x192063e0 +0x8", +"base": 421553128, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19206d50", +"base": 421555536, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19206d50 +0x8", +"base": 421555544, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192076c0", +"base": 421557952, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x192076c0 +0x8", +"base": 421557960, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19208030", +"base": 421560368, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19208030 +0x8", +"base": 421560376, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192089a0", +"base": 421562784, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x192089a0 +0x8", +"base": 421562792, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19209310", +"base": 421565200, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19209310 +0x8", +"base": 421565208, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19209c80", +"base": 421567616, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19209c80 +0x8", +"base": 421567624, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1920a5f0", +"base": 421570032, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1920a5f0 +0x8", +"base": 421570040, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1920af60", +"base": 421572448, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1920af60 +0x8", +"base": 421572456, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1920b8d0", +"base": 421574864, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1920b8d0 +0x8", +"base": 421574872, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1920c240", +"base": 421577280, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1920c240 +0x8", +"base": 421577288, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1920cbb0", +"base": 421579696, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1920cbb0 +0x8", +"base": 421579704, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1920d520", +"base": 421582112, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1920d520 +0x8", +"base": 421582120, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1920de90", +"base": 421584528, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1920de90 +0x8", +"base": 421584536, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1920e800", +"base": 421586944, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1920e800 +0x8", +"base": 421586952, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1920f170", +"base": 421589360, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1920f170 +0x8", +"base": 421589368, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1920fae0", +"base": 421591776, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1920fae0 +0x8", +"base": 421591784, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19210450", +"base": 421594192, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19210450 +0x8", +"base": 421594200, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19210dc0", +"base": 421596608, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19210dc0 +0x8", +"base": 421596616, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19211730", +"base": 421599024, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19211730 +0x8", +"base": 421599032, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192120a0", +"base": 421601440, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x192120a0 +0x8", +"base": 421601448, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19212a10", +"base": 421603856, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19212a10 +0x8", +"base": 421603864, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19213380", +"base": 421606272, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19213380 +0x8", +"base": 421606280, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19213cf0", +"base": 421608688, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19213cf0 +0x8", +"base": 421608696, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19214660", +"base": 421611104, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19214660 +0x8", +"base": 421611112, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19214fd0", +"base": 421613520, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19214fd0 +0x8", +"base": 421613528, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19215940", +"base": 421615936, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19215940 +0x8", +"base": 421615944, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192162b0", +"base": 421618352, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x192162b0 +0x8", +"base": 421618360, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19216c20", +"base": 421620768, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19216c20 +0x8", +"base": 421620776, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19217590", +"base": 421623184, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19217590 +0x8", +"base": 421623192, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19217f00", +"base": 421625600, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19217f00 +0x8", +"base": 421625608, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19218870", +"base": 421628016, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19218870 +0x8", +"base": 421628024, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192191e0", +"base": 421630432, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x192191e0 +0x8", +"base": 421630440, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19219b50", +"base": 421632848, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19219b50 +0x8", +"base": 421632856, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1921a4c0", +"base": 421635264, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1921a4c0 +0x8", +"base": 421635272, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1921ae30", +"base": 421637680, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1921ae30 +0x8", +"base": 421637688, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1921b7a0", +"base": 421640096, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1921b7a0 +0x8", +"base": 421640104, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1921c110", +"base": 421642512, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1921c110 +0x8", +"base": 421642520, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1921ca80", +"base": 421644928, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1921ca80 +0x8", +"base": 421644936, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1921d3f0", +"base": 421647344, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1921d3f0 +0x8", +"base": 421647352, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1921dd60", +"base": 421649760, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1921dd60 +0x8", +"base": 421649768, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1921e6d0", +"base": 421652176, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1921e6d0 +0x8", +"base": 421652184, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1921f040", +"base": 421654592, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1921f040 +0x8", +"base": 421654600, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1921f9b0", +"base": 421657008, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1921f9b0 +0x8", +"base": 421657016, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19220320", +"base": 421659424, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19220320 +0x8", +"base": 421659432, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19220c90", +"base": 421661840, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19220c90 +0x8", +"base": 421661848, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19221600", +"base": 421664256, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19221600 +0x8", +"base": 421664264, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19221f70", +"base": 421666672, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19221f70 +0x8", +"base": 421666680, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192228e0", +"base": 421669088, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x192228e0 +0x8", +"base": 421669096, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19223250", +"base": 421671504, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19223250 +0x8", +"base": 421671512, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19223bc0", +"base": 421673920, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19223bc0 +0x8", +"base": 421673928, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19224530", +"base": 421676336, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19224530 +0x8", +"base": 421676344, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19224ea0", +"base": 421678752, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19224ea0 +0x8", +"base": 421678760, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19225810", +"base": 421681168, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19225810 +0x8", +"base": 421681176, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19226180", +"base": 421683584, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19226180 +0x8", +"base": 421683592, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19226af0", +"base": 421686000, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19226af0 +0x8", +"base": 421686008, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19227460", +"base": 421688416, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19227460 +0x8", +"base": 421688424, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19227dd0", +"base": 421690832, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19227dd0 +0x8", +"base": 421690840, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19228740", +"base": 421693248, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19228740 +0x8", +"base": 421693256, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192290b0", +"base": 421695664, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x192290b0 +0x8", +"base": 421695672, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x19229a20", +"base": 421698080, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x19229a20 +0x8", +"base": 421698088, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1922a390", +"base": 421700496, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1922a390 +0x8", +"base": 421700504, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1922ad00", +"base": 421702912, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1922ad00 +0x8", +"base": 421702920, +"width": 2400, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1922b670", +"base": 421705328, +"width": 32, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1922b690", +"base": 421705360, +"width": 2896, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1922c1e0", +"base": 421708256, +"width": 262144, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1926c1e0", +"base": 421970400, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1926c1e0 +0x8", +"base": 421970408, +"width": 316, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1926c330", +"base": 421970736, +"width": 32, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1926c350", +"base": 421970768, +"width": 14896, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1926fd80", +"base": 421985664, +"width": 65536, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1927fd80", +"base": 422051200, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1927fd80 +0x8", +"base": 422051208, +"width": 316, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1927fed0", +"base": 422051536, +"width": 32, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1927fef0", +"base": 422051568, +"width": 46800, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1928b5c0", +"base": 422098368, +"width": 65536, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1929b5c0", +"base": 422163904, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x1929b5c0 +0x8", +"base": 422163912, +"width": 316, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1929b710", +"base": 422164240, +"width": 32, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x1929b730", +"base": 422164272, +"width": 20032, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192a0570", +"base": 422184304, +"width": 32768, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192a8570", +"base": 422217072, +"width": 324, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192a86c0", +"base": 422217408, +"width": 32, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192a86e0", +"base": 422217440, +"width": 48, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192a8710", +"base": 422217488, +"width": 512, +"read": "allowed", +"write": "allowed" +}, +{ +"name": "0x192a8910", +"base": 422218000, +"width": 8, +"read": "allowed", +"write": "ignored", +"note": "read back, but kept beside the game" +}, +{ +"name": "0x192a8910 +0x8", +"base": 422218008, +"width": 316, +"read": "allowed", +"write": "allowed" +} +] +} \ No newline at end of file diff --git a/scripts/allowlist.mts b/scripts/allowlist.mts index 1be67f7..209f81a 100644 --- a/scripts/allowlist.mts +++ b/scripts/allowlist.mts @@ -1,6 +1,6 @@ /** * Writes data/allowlist.json: what Remastered is known to allow, each entry with what it rests on (see - * compiler/allowlist.ts). Three sources, in this order: + * compiler/allowlist.ts). Four sources, in this order: * * 1. Magenta's catalogue (plugin-magenta's src/catalogue/eud.json): every entry with its address, its Remastered * read/write and whether a probe verified it. A verified yes is allowed; anything else is not known. @@ -9,8 +9,12 @@ * probe here was played in Remastered with every line as expected, so whatever its build reads or writes was * allowed there. A trace is made from today's build of the probe, not the build that was played; the families of * addresses are the same, and a probe played again is what makes a new one certain. + * 4. data/remastered.json: Remastered's own EUD emulator, run on a dump of the running game (2026-10-16): every + * region it emulates and what it does with each dword read and written, and that anything outside its regions + * ends the game. Facts about addresses only. Entries from it carry `from: "dump"`; what was played (1–3) wins + * over the same byte. * - * npx tsx scripts/allowlist.mts .trace.json> [--magenta=] [--out=] + * npx tsx scripts/allowlist.mts .trace.json> [--magenta=] [--remastered=] [--out=] */ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { basename, join, resolve } from "node:path"; @@ -57,7 +61,9 @@ for (const e of magenta.entries) { // 2. What the game was seen to refuse or ignore (Magenta's docs/candidates.md). entries.push( - { name: "unit table: position", base: 0x59ccd0, ...ROWS, width: 4, read: "allowed", write: "ends", evidence: ["Magenta probe 6, key I (2026-09-14): writing a unit's position ends the game, \"EUD not supported\""] }, + { name: "unit table: position", base: 0x59ccd0, ...ROWS, width: 4, read: "allowed", write: "ignored", evidence: ["the dump probe, steps A and B (2026-10-16): a unit's X and Y written, the game went on and nothing was seen to move", "Magenta probe 6, key I (2026-09-14) ended the game writing the unit's position and its sprite's together; the dump probe wrote each alone and neither did"] }, + { name: "sprite table: position", base: 0x629d98 + 0x14, stride: 36, count: 2500, width: 4, read: "allowed", write: "ignored", evidence: ["the dump probe, step C (2026-10-16): a sprite's X and Y written, the game went on and nothing on the screen changed"] }, + { name: "unit table: +0x74", base: 0x59cd1c, ...ROWS, width: 4, read: "ends", write: "ends", evidence: ["the dump probe, step D (2026-10-16): read, the game ended \"EUD ERROR: 0xFFA632E3\" (the address, bits flipped), as the dump said"] }, { name: "unit table: cloak flags", base: 0x59cd85, ...ROWS, width: 1, read: "allowed", write: "ignored", evidence: ["Magenta probe 6, keys Y and H (2026-09-14): status flags 0x100 and 0x200 written, no visible cloak"] }, { name: "game speed", base: 0x6cdfd4, width: 4, read: "allowed", write: "ignored", evidence: ["Magenta probe 5, key 9 (2026-09-14): written, the game kept its speed"] }, ); @@ -106,7 +112,24 @@ for (const r of runs) { }); } -const list: Allowlist = { version: 1, entries }; +// 4. The dump of Remastered's emulator. +const remasteredPath = args.find((a) => a.startsWith("--remastered="))?.slice(13) ?? join(root, "data", "remastered.json"); +let outside: Allowlist["outside"]; +let dumped = 0; +if (existsSync(remasteredPath)) { + const r = JSON.parse(readFileSync(remasteredPath, "utf8")) as { + build: string; source: string; outside: Verdict; + entries: { name: string; base: number; width: number; stride?: number; count?: number; read: Verdict; write: Verdict; note?: string }[]; + }; + const evidence = [`Remastered ${r.build}'s own EUD emulator, run on a dump of the game (2026-10-16)`]; + for (const e of r.entries) { + entries.push({ ...e, evidence, from: "dump" }); + dumped++; + } + outside = { verdict: r.outside, evidence: [`no region of Remastered ${r.build}'s EUD emulator covers it (a dump of the game, 2026-10-16); the game ends with "EUD ERROR" and the address with its bits flipped`] }; +} + +const list: Allowlist = { version: 1, entries, ...(outside ? { outside } : {}) }; const out = args.find((a) => a.startsWith("--out="))?.slice(6) ?? join(root, "data", "allowlist.json"); writeFileSync(out, JSON.stringify(list, null, 1) + "\n"); -console.log(`${out}: ${entries.length} entries — ${magenta.entries.length} from Magenta's catalogue, 3 failures, ${runs.length} runs from ${probes} probes' traces`); +console.log(`${out}: ${entries.length} entries — ${magenta.entries.length} from Magenta's catalogue, 6 seen in the game, ${runs.length} runs from ${probes} probes' traces, ${dumped} from the dump of Remastered's emulator`); diff --git a/tests/allowlist.test.ts b/tests/allowlist.test.ts index 60fe1a0..cb7d0e5 100644 --- a/tests/allowlist.test.ts +++ b/tests/allowlist.test.ts @@ -6,6 +6,8 @@ import { join } from "node:path"; import { checkTrace, describeFinding, verdictOf, type Allowlist, type Trace, type TraceRow } from "../compiler/allowlist"; const list = JSON.parse(readFileSync(join(import.meta.dirname, "..", "data", "allowlist.json"), "utf8")) as Allowlist; +/** The list as it was before the dump of Remastered's emulator: only what was played. */ +const played: Allowlist = { version: 1, entries: list.entries.filter((e) => e.from !== "dump") }; const UNIT = (i: number, off: number) => 0x59cca8 + 336 * i + off; const trace = (rows: TraceRow[]): Trace => ({ frames: 100, strings: { from: 0x19190000, to: 0x191b0000 }, heap: { from: 0x30000000, to: 0x40000000 }, rows }); @@ -13,13 +15,30 @@ describe("the allowlist", () => { it("has Magenta's verified fields allowed and the failures seen in the game", () => { expect(verdictOf(list, UNIT(5, 8), true).verdict).toBe("allowed"); // a unit's hit points expect(verdictOf(list, UNIT(5, 0x28), false).verdict).toBe("allowed"); // its position, read - expect(verdictOf(list, UNIT(5, 0x28), true).verdict).toBe("ends"); // … and written + expect(verdictOf(list, UNIT(5, 0x28), true).verdict).toBe("ignored"); // … and written: the game goes on, nothing moves + expect(verdictOf(list, UNIT(5, 0x74), false).verdict).toBe("ends"); // a field Remastered does not emulate expect(verdictOf(list, 0x6cdfd4, true).verdict).toBe("ignored"); // the game speed - expect(verdictOf(list, 0x7fff0000, false).verdict).toBe("unknown"); }); it("names what each verdict rests on", () => { const e = verdictOf(list, UNIT(5, 0x28), true).entry!; - expect(e.evidence.join(" ")).toMatch(/Magenta probe 6/); + expect(e.evidence.join(" ")).toMatch(/the dump probe.*Magenta probe 6/); + }); + it("ends the game on an address none of Remastered's emulated regions covers", () => { + expect(verdictOf(list, 0x7fff0000, false)).toEqual({ verdict: "ends", entry: null }); + expect(list.outside?.evidence.join(" ")).toMatch(/no region of Remastered .* emulator/); + }); + it("takes the dump of Remastered's emulator where nothing played says more, and what was played over it", () => { + const ptr = verdictOf(list, UNIT(5, 0x5c), true); // the order target: no probe, the dump says a unit pointer + expect(ptr).toMatchObject({ verdict: "allowed", entry: { from: "dump" } }); + expect(ptr.entry!.note).toMatch(/unit/); + const both: Allowlist = { version: 1, entries: [ + { name: "dumped", base: 0x100, width: 4, read: "allowed", write: "ends", evidence: ["dump"], from: "dump" }, + { name: "played", base: 0x100, width: 4, read: "allowed", write: "allowed", evidence: ["a probe"] }, + { name: "not known", base: 0x104, width: 4, read: "unknown", write: "unknown", evidence: ["catalogue, not verified"] }, + { name: "dumped too", base: 0x104, width: 4, read: "allowed", write: "ignored", evidence: ["dump"], from: "dump" }, + ] }; + expect(verdictOf(both, 0x100, true)).toMatchObject({ verdict: "allowed", entry: { name: "played" } }); + expect(verdictOf(both, 0x104, true)).toMatchObject({ verdict: "ignored", entry: { name: "dumped too" } }); }); }); @@ -33,11 +52,15 @@ describe("checkTrace", () => { const r = checkTrace(trace([["40:7", 0x30a30010, 1, 0xffffffff, 1]]), list); expect(r.findings).toMatchObject([{ line: 40, verdict: "unknown", write: true }]); }); - it("finds a write Remastered ends the game on, and says why", () => { - const r = checkTrace(trace([["7:1", UNIT(3, 0x28), 1, 0xffffffff, 1], ["7:1", UNIT(9, 0x28), 1, 0x0000ffff, 1]]), list); + it("finds an access Remastered ends the game on, and says why", () => { + const r = checkTrace(trace([["7:1", UNIT(3, 0x74), 0, 0xffffffff, 1], ["7:1", UNIT(9, 0x74), 0, 0x0000ffff, 1]]), list); expect(r.findings).toHaveLength(1); - expect(r.findings[0]).toMatchObject({ line: 7, verdict: "ends", entry: "unit table: position", addresses: 2, times: 2 }); - expect(describeFinding(r.findings[0])).toMatch(/^line 7: writes unit table: position .* Remastered ends the game on this \(Magenta probe 6/); + expect(r.findings[0]).toMatchObject({ line: 7, verdict: "ends", entry: "unit table: +0x74", addresses: 2, times: 2 }); + expect(describeFinding(r.findings[0])).toMatch(/^line 7: reads unit table: \+0x74 .* Remastered ends the game on this \(the dump probe, step D/); + }); + it("says why an address outside every emulated region ends the game", () => { + const r = checkTrace(trace([["12:1", 0x7fff0000, 1, 0xffffffff, 1]]), list); + expect(describeFinding(r.findings[0])).toMatch(/^line 12: writes 0x7fff0000 — Remastered ends the game on this \(no region of Remastered/); }); it("holds a masked write byte by byte: the cloak bits and the invincible bit share a dword", () => { const flags = UNIT(2, 0xdc); @@ -45,7 +68,7 @@ describe("checkTrace", () => { expect(checkTrace(trace([["3:1", flags, 1, 0x04000000, 1]]), list).findings).toEqual([]); }); it("says what nobody has tried, and puts the gravest first", () => { - const r = checkTrace(trace([["20:1", 0x7fff0000, 0, 0xffffffff, 1], ["5:1", 0x6cdfd4, 1, 0xffffffff, 1], ["9:1", UNIT(0, 0x28), 1, 0xffffffff, 1]]), list); + const r = checkTrace(trace([["20:1", 0x7fff0000, 0, 0xffffffff, 1], ["5:1", 0x6cdfd4, 1, 0xffffffff, 1], ["9:1", UNIT(0, 0x74), 0, 0xffffffff, 1]]), played); expect(r.findings.map((f) => f.verdict)).toEqual(["ends", "ignored", "unknown"]); expect(describeFinding(r.findings[2])).toBe("line 20: reads 0x7fff0000 — not seen in Remastered: needs a probe"); }); @@ -57,8 +80,8 @@ describe("checkTrace", () => { describe("what no entry covers", () => { it("is said by the table of the game it lies in, one finding a field however many units", () => { - const off = 0x7c; // a field no probe and no catalogue entry has touched - const r = checkTrace(trace([0, 1, 2, 500].map((i): TraceRow => ["38:3", UNIT(i, off), 0, 0xffffffff, 1])), list); + const off = 0x7c; // a field no probe and no catalogue entry has touched (the dump covers it: left out here) + const r = checkTrace(trace([0, 1, 2, 500].map((i): TraceRow => ["38:3", UNIT(i, off), 0, 0xffffffff, 1])), played); expect(r.findings).toHaveLength(1); expect(describeFinding(r.findings[0])).toBe("line 38: reads unit table +0x7c (4 dwords from 0x59cd24) — not seen in Remastered: needs a probe"); }); From 13fa741d9238645bcb5fa88af08dfb53187ab198 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 15:04:41 -0600 Subject: [PATCH 027/238] TrigScript 4, R0: the dump probe Hand-written eudplib that takes Magenta's nudge apart: a unit's X, its Y, its sprite's X and Y, each written alone, then a read of unit +0x74 that the dump says Remastered refuses. Played 2026-10-16: A to C went on with nothing seen to move; D ended the game with "EUD ERROR: 0xFFA632E3", the refused address with its bits flipped. Co-Authored-By: Claude Opus 5.5 --- probes/remastered-dump.py | 91 +++++++++++++++++++++++++++++++++++++++ probes/remastered-dump.ts | 15 +++++++ 2 files changed, 106 insertions(+) create mode 100644 probes/remastered-dump.py create mode 100644 probes/remastered-dump.ts diff --git a/probes/remastered-dump.py b/probes/remastered-dump.py new file mode 100644 index 0000000..c8d542d --- /dev/null +++ b/probes/remastered-dump.py @@ -0,0 +1,91 @@ +""" +[remasteredDump] + +What the dump of Remastered's EUD emulator (TrigScript 4, 2026-10-16) predicts, played. Hand-written eudplib. + +Magenta's probe 6 wrote a unit's position and then its sprite's position in one go (`nudge`) and the game ended +with "EUD not supported". Run on a dump of the game, Remastered's own emulator accepts both writes: the unit's +position lands in the game's unit, the sprite's lands in memory the emulator keeps beside the game. So the steps +here take nudge apart, one write each, and end on a read the emulator refuses, which no probe has made before: + + A 5 the first unit's X written (two bytes, as nudge did): predicted to go through + B 15 its Y written the same way: predicted to go through + C 25 its sprite's X and Y written: predicted to go through and change nothing on the screen + D 35 a read of the unit's field +0x74: predicted to END the game ("EUD not supported") + +Built beside probes/remastered-dump.ts, which says the same. +""" +from eudplib import * +from eudplib.core.mapdata.stringmap import ForceAddString + +UNITS = 0x59CCA8 # the unit table; slot 0 is the first unit placed +POS = UNITS + 0x28 # x: u16, y: u16 +SPRITE = UNITS + 0x0C # the unit's sprite, a pointer +UNEMULATED = UNITS + 0x74 + +tick = EUDVariable() +second = EUDVariable() + + +SLOT = ForceAddString(" " * 255) # one string of the table, room for a line made in the game (the strings spike's way) + + +def _say(*parts): + f_dbstr_print(GetMapStringAddr(SLOT), *parts) + DoActions(DisplayText(SLOT)) + + +def _at(n): + return EUDIf()(second == n) + + +def onPluginStart(): + pass + + +def beforeTriggerExec(): + tick.__iadd__(1) + if EUDIf()(tick >= 24): + tick << 0 + second.__iadd__(1) + + if _at(2): + _say("\x04The dump probe: four steps, about 35 seconds. Watch the first unit placed (unit table slot 0).") + EUDEndIf() + + if _at(5): + x = f_wread(POS) + f_wwrite(POS, x + 64) + _say("\x07A\x04: unit 0's X written, ", x, " -> ", f_wread(POS), ". \x07Still playing?\x04 Does the unit look moved?") + EUDEndIf() + + if _at(15): + y = f_wread(POS + 2) + f_wwrite(POS + 2, y + 64) + _say("\x07B\x04: unit 0's Y written, ", y, " -> ", f_wread(POS + 2), ". \x07Still playing?\x04 Select it, click near it: where is it?") + EUDEndIf() + + if _at(25): + sprite = f_dwread(SPRITE) + if EUDIf()(sprite >= 1): + sx = f_wread(sprite + 0x14) + sy = f_wread(sprite + 0x16) + f_wwrite(sprite + 0x14, sx + 64) + f_wwrite(sprite + 0x16, sy + 64) + _say("\x07C\x04: its sprite's X and Y written (read ", sx, ",", sy, "). \x07Still playing?\x04 Did anything on the screen change?") + EUDEndIf() + EUDEndIf() + + if _at(33): + _say("\x04D: in two seconds a read the dump says Remastered refuses. The game \x06should end\x04 with \"EUD not supported\".") + EUDEndIf() + + if _at(35): + v = f_dwread(UNEMULATED) + _say("\x07D\x04: read ", v, ". \x06If you can read this, the dump was wrong about +0x74.") + EUDEndIf() + EUDEndIf() + + +def afterTriggerExec(): + pass diff --git a/probes/remastered-dump.ts b/probes/remastered-dump.ts new file mode 100644 index 0000000..e6314ab --- /dev/null +++ b/probes/remastered-dump.ts @@ -0,0 +1,15 @@ +// The dump probe (TrigScript 4, 2026-10-16): what a dump of Remastered's own EUD emulator predicts, played. The +// script below does next to nothing; what is tried is probes/remastered-dump.py, hand-written eudplib built into the +// same map. It takes Magenta's `nudge` (probe 6, which ended the game) apart, one write each: +// A 5 the first unit's X written: predicted to go through +// B 15 its Y written: predicted to go through +// C 25 its sprite's X and Y written: predicted to go through and change nothing on the screen +// D 35 a read of the unit's field +0x74: predicted to END the game ("EUD not supported") +// Built with +// EXTRA_PLUGIN=remasteredDump=probes/remastered-dump.py npx tsx scripts/build-fixture.mts probes/remastered-dump.ts fixtures/eud/remastered-dump.scx +import { program, displayText, P1 } from "trigscript"; + +program(() => { + sleep(seconds(1)); + displayText("The dump probe: steps A to D, about 35 seconds. Each says what to look for."); +}, { owner: P1 }); From f3ae133bb870343455fda9a19e1aed7971f84303 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 15:04:41 -0600 Subject: [PATCH 028/238] TrigScript 4: R0 as built Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 70 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 2c8a7bf..fb0485c 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1,6 +1,7 @@ # TrigScript 4 — the plan -Written 2026-10-15. Status: nothing built. The question it answers: now that there is a +Written 2026-10-15. Status: Part A built (A0–A5, each with its *As built*), and Remastered's +emulator read from a dump (*R0*, 2026-10-16); Part B not started. The question it answers: now that there is a lifted copy of the game's own code (scm-sim's exact engine) beside eudplib, what does TrigScript become if it uses the game itself as its checker, and how far can the language go towards the TypeScript people actually write. @@ -62,7 +63,7 @@ have already met: | Probe | 1.16.1 meaning | Remastered | | --- | --- | --- | -| Magenta 6, I | a unit's position written | ends the game ("EUD not supported") | +| Magenta 6, I | a unit's position written | ~~ends the game~~ — the dump probe (R0) wrote it alone: the game goes on, nothing moves | | Magenta 10, 1–3 | the tile buffer written through the MTXM pointer | nothing changes on screen | | Magenta 10, 4 | sprite image flags (tint) | ends the game | | Magenta 6, J K | a unit's own top speed | no effect; only the type's table counts | @@ -75,9 +76,12 @@ be read from the file. The user agreed on 2026-10-15 that Remastered may be take through Ghidra to settle rules the compiler needs, provided none of it ships. The way in would be a dump of the running process, once it has unpacked itself, read in Ghidra; the rules found go into the allowlist with "Remastered, disassembled" as their evidence, and -a probe still confirms each family before a library offers it. Until that is done, the -lifted game tells us **what an address means and what the triggers did with it**; -only a played probe tells us **whether Remastered allows it**. Part A turns that split +a probe still confirms each family before a library offers it. That was done on +2026-10-16 (*R0* below): the allowlist now knows, for every address, whether Remastered +lets a read or a write through, and a probe is needed only to learn what the game *does* +with it. The lifted game tells us **what an address means and what the triggers did with +it**; the dump tells us **whether Remastered allows it**; a played probe tells us **what it +looks like**. Part A turns that split into a file (the allowlist) and a check, so the gap is seen at the line that falls into it rather than in a game that ends. @@ -638,6 +642,60 @@ and the debugger walks the record. - On a copy that may only watch (StarEdit's download), there is no picture: a picture of a map's run is the map played. Everything else works. +### R0. Remastered's emulator, read from a dump (2026-10-16) + +Asked for on 2026-10-16 ("start with the Remastered dump"), before B1, so that the +allowlist stops saying "not known" for most of the game. + +- **The dump.** Remastered 1.23.10.13515, the 64-bit executable, in a single-player game + on an EUD map. A small read-only dumper (Windows Python, `ReadProcessMemory`) wrote the + executable's image and every readable region of the heap (2 GB). Its code section is + encrypted on disk and plain in memory. The dump, the tools and everything read from it + stay in a private folder outside every repository. +- **What Remastered's EUD support is.** An emulator object, reached from one global, + holding a tree of 12,290 *regions*: each a 1.16.1 address range, a class (34 of them, + each with its own read and write) and the game's own object behind it. The Deaths + condition and the Set Deaths action look the address up; a dword in no region, or one + whose class refuses it, ends the game. The object tables (1,700 units, 2,500 sprites, + 5,000 images, 100 bullets, the order queue) are a region per object. Writes are checked + before the class sees them: unaligned dwords are split, a flag makes a region's writes + accepted and dropped, and the mask is applied as A0 found by trying + (`(old & ~mask) | (op(old & mask, value & mask) & mask)`). +- **Run, not read.** Rather than read 34 classes of decompiled code, the emulator itself was + run under Unicorn from the dump: the image at its own address, heap pages mapped from the + dump when touched, the game's main thread block for its thread-local data, and Windows + stood in for (locks, the heap; the one other call is `GetKeyState`, for the keyboard + region). Every dword of every region (the object tables sampled at four objects) was read + and then written through Remastered's own entry points, and the memory before and after + compared: 1,581,605 dwords in 20 minutes. A write lands in **the game's own object**, in + **memory the emulator keeps beside it** (read back, never used by the game), or nowhere. +- **Pointers are typed.** A unit's pointer fields (+0x00, +0x04, +0x14, +0x5C order target, + +0x68 … +0x11C) take the address of a unit and set the game's real pointer; +0x0C takes a + sprite. Anything else written there is kept beside the game. So "make this unit target + that one" works through memory. +- **Fields that end the game when read:** unit +0x74, +0x78, +0x88, +0xB0–0xBC, +0xF0, + +0xF4, +0x100, +0x108, +0x10C, +0x130–0x148; and most of the block from 0x6D1200. + Writes to the terrain buffers behind the tile pointers are dropped, as Magenta probe 10 + found by playing. +- **The probe** (`probes/remastered-dump.py`, played 2026-10-16) took Magenta's `nudge` + apart: A, B (a unit's X, then Y) and C (its sprite's X and Y) went through with nothing + seen to move; D (a read of unit +0x74) ended the game, as the dump said, with "EUD ERROR: + 0xFFA632E3", which is 0x0059CD1C with its bits flipped. **The error code is the refused + address**, so a line can be named from it. Probe 6's "a position write ends the game" + was wrong; what ended it is not known. +- **In the allowlist.** `data/remastered.json` (1,285 entries: addresses, widths, verdicts, + notes; no code or bytes of the game) is the fourth source of `scripts/allowlist.mts`, + each entry `from: "dump"`. What was played wins over it byte by byte; where they differ + it is "played: ignored, dump: allowed" (the write reaches the game and nothing visible + happens), never the other way. An address no region covers is `outside`: the game ends. + The units probe's trace now has no finding at all, where before most unit fields were + "not known". `verdictOf` indexes entries by 256-byte block (190 ms for 42,745 rows). +- **Still open:** the keyboard region (filled from Windows on each read, so local by + construction); images, bullets and orders were sampled at objects not in use; the + error-code-to-line feature; the dump is one build, so a Remastered patch means a new dump + (the tools take an hour end to end). Ghidra's project of the dump exists for B7's struct + names. + ## Part B — the language Each slice states what changes in the IR, the simulator and the Python, and has a probe. @@ -867,7 +925,7 @@ build, contract 2) ships first, then plugin-scm-sim, then TrigScript. several people on one instance; 2,200–4,100 frames a second in Node; snapshots ~45 ms / 0.6 MB; hooks by address at generation; `callAt` between turns; `triggerView`; extended Deaths reads work and writes are refused by 1.16.1's check. -- Remastered's executable is encrypted; no disassembly of it is planned. +- Remastered's executable is encrypted on disk and decrypted in memory; its EUD emulator was read from a dump of the running game and run under Unicorn (R0, 2026-10-16). Nothing of it ships: only addresses and verdicts, in `data/remastered.json`. - Magenta's played probes 5–11 (2026-09-14/15): position, tint and image writes end the game; tile buffer writes are not drawn; per-unit speed and cloak flags do nothing; key hold does not exist; the scratch-box order trick and the damage/heal verb work. From 8098600093ce3b65ecbed169e47922663f77b4f9 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 15:11:56 -0600 Subject: [PATCH 029/238] TrigScript 4: the showpieces that steer Part B Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 114 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index fb0485c..2eb3225 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1,7 +1,8 @@ # TrigScript 4 — the plan Written 2026-10-15. Status: Part A built (A0–A5, each with its *As built*), and Remastered's -emulator read from a dump (*R0*, 2026-10-16); Part B not started. The question it answers: now that there is a +emulator read from a dump (*R0*, 2026-10-16); Part B steered by three showpiece maps +(*Showpieces*, 2026-10-16), B1 started. The question it answers: now that there is a lifted copy of the game's own code (scm-sim's exact engine) beside eudplib, what does TrigScript become if it uses the game itself as its checker, and how far can the language go towards the TypeScript people actually write. @@ -866,6 +867,117 @@ already says so. | Computer players written in TrigScript | B1 tasks, unit orders through the scratch-box trick | the trick is verified (Magenta); a behaviour library is new | | Camera work for one player | `centerView`, B5 | ordinary actions; B5 makes them safe | +## Showpieces + +Added 2026-10-16, after Part A was built. Part A is tooling: it makes TrigScript right, and +a map maker sees little of it. What a map maker sees is what a map can do. So Part B is +steered by three demonstration maps, each a map someone would want to play, each built +from the slices it needs, in the order those slices come. A slice that no showpiece needs +waits behind one that does. Each showpiece is also a test: it runs in the lifted game +(A2) and in several copies (A3), its cost per frame is under the budget (A4), and it is +played in Remastered before it is shown to anyone. + +### S1. Act out a cutscene + +The map maker plays the scene in the editor's game: moves the units, sends them into the +fight, places the camera. TrigScript writes the script that plays it back. + +```ts +// Written by Record Scene, 0:42 of play, 3 players' units, 61 orders. +export async function ambush() { + centerView(locations.Ridge); + order(group.raiders, "move", locations.Ford); + await sleep(frames(96)); + order(group.raiders, "attack", locations.Camp); + … +} +``` + +- **The recording** is scm-sim's: the person plays the map in the Replay view's game, and + every command they give is a replay command with its frame. A cutscene is a replay of + the person's own play. +- **Writing it back:** a trigger cannot give every command a player can. What it can give + is the game's Order (move, patrol, attack) to a location, through the scratch-location + trick Magenta verified, plus Create Unit, Center View and the transmission. A recorded + command becomes the nearest of those; the rest (a spell, a build) are said at their + line and left out. Units are named by groups the recorder makes (`group.raiders`), which + are units found by type, owner and place when the scene starts. +- **Checking it:** the written script runs in the lifted game and the units' places are + compared with the recording, frame by frame. The script will not match exactly (an order + through a trigger lands on a trigger pass, a player's command on its turn), so the + comparison has a tolerance, and the view shows where the two part. +- **Needs:** B1 (the script is an `async` function), B2 for "when the raiders arrive", and + scm-sim's view taking commands from the editor (it plays the person's own copy: rendering, + so the StarEdit rule applies). +- **Probe:** none of its own for the playback (only ordinary actions). Its probe is the + scene played in Remastered beside the recording. + +### S2. A live interface in the game's text + +The objectives panel, the leaderboard's label and a unit type's name are re-read every +time the game draws them, and a made text can be written into them for one player (the +strings spike, 2026-09-19). That is a screen. S2 gives it a TypeScript way to be drawn: +JSX. + +```tsx +ui.objectives(P1, () => ( + + Wave {wave} of 10 + + {heroes.map((h) => {h.name}: {h.kills} kills)} + +)); +``` + +- **The compiler** reads the script as TSX. An element is a text built from its children; + `` is characters in colour codes; `color` is the game's colour codes. Nothing of + React runs in the game: the tree is lowered to a template (IR `template`), known parts at + build time and the rest as values. +- **Redrawn when it changes:** the function is a reactive view. The compiler knows which + variables it reads, and the lowering rewrites the text only on a frame when one of them + changed, so a panel that is not changing costs a comparison a frame. +- **Needs:** 3.9's texts (built), B3 for `map` with a callback over an array that grows, + and the made-text field writes (built). No new address. +- **Probe:** one, for how many characters and colour codes a panel holds before the game + cuts it, and whether a rewrite every frame flickers. + +### S3. A hero moved with the keys + +W A S D move a hero, the mouse aims, a key casts. The map plays as an action game. + +```ts +on.key("W", (p) => heroOf(p).order("move", ahead(heroOf(p), 64))); +const aim = await sync(localMouse()); // Local → Point, the same on every machine +``` + +- **Keys held:** Magenta 7 found that Remastered sends one event a press and no key-up, so + "held" does not reach other machines today. R0 found that the keyboard region of + Remastered's emulator calls `GetKeyState`: reading it gives the key's state on the local + machine, now. That is a `Local` (B5), and `sync()` makes it shared a few frames + later. +- **Movement** is a location centred ahead of the hero and Order to it (positions are never + written; Magenta 6 and R0). B6 gives the directions and distances. +- **Checking it:** A3 runs the map in two copies with keys held on one, and the copies must + stay the same. That is the whole point of B5's type, shown working. +- **Needs:** B2 (`on.key`), B5 (`Local`, `sync`), B6 (directions). +- **Probe:** the first, before B5 is built: hold a key and read the keyboard region every + frame in Remastered; then the same in a two-person game, to see each machine reads only + its own. + +### Smaller things that show well + +Each is small and makes the editor feel like it knows the game. They go in where a slice +passes near them. + +| Idea | Built from | Where | +| --- | --- | --- | +| EUD error code to line: Remastered's "EUD ERROR: 0x…" is the refused address inverted (R0); the editor reads the code and points at the line that touches that address | the trigger map and trace (A1), R0 | now: a day | +| Typed chat patterns: `on.chat("-give {n}", (p, { n }) => …)` with `n: number` worked out by template literal types | B2 | B2 | +| A line's values over time beside it, as a small graph, from the debugger's recording | A5 | after A5's open items | +| Changes kept while the game runs: edit the script, rebuild, carry the variables over by name into a snapshot and go on | A2's variable map, scm-sim snapshots | after B1 (a task's place has to carry over too) | +| Fuzzing: random inputs on every seat at the lifted game's speed, looking for desyncs, games that end and failed expectations, then the smallest input that still fails | A2, A3 | after B5 | +| A memory field's hover says what Remastered does with it ("read: fine, from the dump; write: ends the game, probe 6") | R0, Ghidra structs (the analysis has to be run again) | B7 | + ## Slices | # | Slice | What it proves | Size | From 500ec8d7de788c57091f73f49a81eab066f6d754 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 15:36:38 -0600 Subject: [PATCH 030/238] TrigScript 4, B1: async functions, await, until() and Promise.all / race / any An async function is inlined where it is awaited; until() waits a frame at a time; Promise.all / race / any become IR 15's parallel statement, each branch a coroutine of its own (python Coroutine, a generator in the simulator). A race's value is the first branch's, and its other branches stop. probes/game-async.ts passes in the lifted game and in the simulator at the same frames. Co-Authored-By: Claude Opus 5.5 --- compiler/declarations.ts | 13 ++- compiler/eud.ts | 11 ++ compiler/generated/trigscriptPy.ts | 2 +- compiler/hoist.ts | 29 ++++- compiler/input.ts | 1 + compiler/ir.ts | 12 +- compiler/numbers.ts | 1 + compiler/recursion.ts | 1 + compiler/runtime.ts | 3 +- compiler/simulateIr.ts | 23 ++++ compiler/structured.ts | 177 +++++++++++++++++++++++++++-- docs/ir.md | 5 +- probes/game-async.ts | 63 ++++++++++ python/trigscript.py | 149 +++++++++++++++++------- tests/async.test.ts | 139 ++++++++++++++++++++++ tests/eud-build.test.ts | 29 ++++- tests/functions.test.ts | 2 +- tests/script-structured.test.ts | 2 +- tests/service.test.ts | 4 +- 19 files changed, 593 insertions(+), 73 deletions(-) create mode 100644 probes/game-async.ts create mode 100644 tests/async.test.ts diff --git a/compiler/declarations.ts b/compiler/declarations.ts index c4ca739..552cb46 100644 --- a/compiler/declarations.ts +++ b/compiler/declarations.ts @@ -229,7 +229,7 @@ ${kw}function trigger(players: Player | readonly Player[], conditions: Condition * A map with a program in it needs StarCraft: Remastered: the programs are built into the * saved map by the eudplib plugin. trigger() makes ordinary triggers that play anywhere. */ -${kw}function program(body: () => void, options?: ProgramOptions): void; +${kw}function program(body: () => void | Promise, options?: ProgramOptions): void; /** * A function that runs in the game, for programs to call — from any file, imported like any * other: \`export const award = game((p: Player, n: number) => { setResources(p, "add", n, "ore"); })\`. @@ -252,10 +252,17 @@ ${kw}function frames(n: number): Duration; ${kw}function cycles(n: number): Duration; /** * Pause the program, inside program() only: the statements after it run that much later, and nothing - * else of this program runs meanwhile (other programs and triggers go on). \`while (true) { spawn(); sleep(seconds(15)); }\` + * else of this program runs meanwhile (other programs and triggers go on). \`while (true) { spawn(); await sleep(seconds(15)); }\` * is a wave every fifteen seconds. Unlike wait(), it takes nothing from the triggers: a Wait uses the player's one wait slot. + * In an async function it is awaited; in a program that is not async, \`sleep(…)\` on its own pauses as before. */ -${kw}function sleep(duration: Duration): void; +${kw}function sleep(duration: Duration): Promise; +/** + * Wait until a condition holds, inside an async program only: \`await until(() => countUnits(P8, units.ZergZergling) === 0)\`. + * The function is asked once now and then once every frame; the program goes on in the frame it answers true. + * A condition on its own is asked again the same way: \`await until(bring(P1, units.AnyUnit, locations.Exit, ">=", 1))\`. + */ +${kw}function until(test: (() => boolean) | Condition): Promise; /** True on the frame its condition becomes true, false until it becomes false and true again. Inside program(), in an if: \`if (rose(bring(…)))\`. */ ${kw}function rose(condition: Condition | boolean): boolean; /** True the first time its condition holds, never again. Inside program(), in an if. */ diff --git a/compiler/eud.ts b/compiler/eud.ts index c0af4d5..b075e45 100644 --- a/compiler/eud.ts +++ b/compiler/eud.ts @@ -84,6 +84,7 @@ function expressions(body: Stmt[], visit: (e: NumExpr | BoolExpr) => void, pick? case "do": s.body.forEach(stmt); expr(s.cond); break; case "for": if (s.cond) expr(s.cond); s.update.forEach(stmt); s.body.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; + case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; case "switch": expr(s.value); s.cases.forEach((c) => c.body.forEach(stmt)); break; case "return": if (s.value) any(s.value); break; case "action": for (const v of s.variables ?? []) expr(v.expr); if (s.text) text(s.text); break; @@ -106,6 +107,7 @@ function statements(body: Stmt[], visit: (s: Stmt) => void) { case "while": case "do": case "unitLoop": case "textLoop": s.body.forEach(stmt); break; case "for": s.body.forEach(stmt); s.update.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; + case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; case "switch": s.cases.forEach((c) => c.body.forEach(stmt)); break; case "call": s.call.body.forEach(stmt); break; case "block": s.body.forEach(stmt); break; @@ -120,6 +122,7 @@ function checkUnitLoops(body: Stmt[], out: ProgramDiagnostic[]) { statements(body, (s) => { if (s.kind !== "unitLoop") return; statements(s.body, (inner) => { + if (inner.kind === "parallel") out.push({ at: inner.at, message: "await inside a loop over units: the loop looks at the game's units as they are in one frame and cannot be left half way. Collect the units into an array first, and await in a loop over that array." }); if (inner.kind === "sleep") out.push({ at: inner.at, message: "sleep() inside a loop over units: the loop looks at the game's units as they are in one frame and cannot be left half way. To do something to one unit at a time, find it again after each sleep: while (true) { const u = first(…); if (!u) break; u.kill(); sleep(seconds(1)); }" }); }); }); @@ -130,6 +133,7 @@ function checkTextLoops(body: Stmt[], out: ProgramDiagnostic[]) { statements(body, (s) => { if (s.kind !== "textLoop") return; statements(s.body, (inner) => { + if (inner.kind === "parallel") out.push({ at: inner.at, message: "await inside a for…of over a text: the loop walks the text once, within the frame. A loop over its places can wait between turns: for (let i = 0; i < s.length; i++) { … }" }); if (inner.kind === "sleep") out.push({ at: inner.at, message: "sleep() inside a for…of over a text: the loop walks the text once, within the frame. A loop over its places can sleep between turns: for (let i = 0; i < s.length; i++) { const ch = s[i]; … sleep(frames(2)); }" }); }); }); @@ -180,6 +184,7 @@ function checkWidths(body: Stmt[], out: ProgramDiagnostic[], decls: Map i.forEach(stmt)); break; + case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; case "switch": s.cases.forEach((c) => c.body.forEach(stmt)); break; case "call": for (const p of s.call.params) fits(p.decl.id, p.init, s.call.at); s.call.body.forEach(stmt); break; case "block": s.body.forEach(stmt); break; @@ -198,6 +203,7 @@ function remarks(body: Stmt[], out: LineHint[]) { case "while": case "do": case "unitLoop": case "textLoop": s.body.forEach(stmt); break; case "for": s.body.forEach(stmt); s.update.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; + case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; case "switch": s.cases.forEach((c) => c.body.forEach(stmt)); break; case "call": s.call.body.forEach(stmt); break; case "block": s.body.forEach(stmt); break; @@ -224,6 +230,7 @@ function assigned(body: Stmt[], functions: Map, into = new Set case "while": case "for": s.body.forEach(stmt); if (s.kind === "for") s.update.forEach(stmt); break; case "do": s.body.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; + case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; case "switch": s.cases.forEach((c) => c.body.forEach(stmt)); break; case "call": call(s.call); break; case "block": s.body.forEach(stmt); break; @@ -295,6 +302,8 @@ function sleepsOnEveryPath(body: Stmt[]): boolean { case "if": if (s.else && sleepsOnEveryPath(s.then) && sleepsOnEveryPath(s.else)) return true; break; case "block": if (sleepsOnEveryPath(s.body)) return true; break; case "unrolled": if (s.iterations.some(sleepsOnEveryPath)) return true; break; + // `all` waits for every branch, so one that always sleeps is enough; `race` goes on when the first ends, so every one has to. + case "parallel": if (s.mode === "all" ? s.branches.some(sleepsOnEveryPath) : s.branches.length > 0 && s.branches.every(sleepsOnEveryPath)) return true; break; case "switch": { const hasDefault = s.cases.some((c) => c.value === null); if (hasDefault && s.cases.every((c) => sleepsOnEveryPath(c.body))) return true; @@ -323,6 +332,7 @@ function checkSleeps(body: Stmt[], out: ProgramDiagnostic[], functions: Map i.forEach(stmt)); break; + case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; case "switch": s.cases.forEach((c) => c.body.forEach(stmt)); break; case "call": s.call.body.forEach(stmt); break; case "block": s.body.forEach(stmt); break; @@ -397,6 +407,7 @@ export function serializeIr(programs: Program[], strings: readonly ScriptString[ case "do": return { ...s, body: s.body.map(stmt), cond: expr(s.cond) }; case "for": return { ...s, ...(s.cond ? { cond: expr(s.cond) } : {}), update: s.update.map(stmt), body: s.body.map(stmt) }; case "unrolled": return { ...s, iterations: s.iterations.map((i) => i.map(stmt)) }; + case "parallel": return { ...s, branches: s.branches.map((b) => b.map(stmt)) }; case "switch": return { ...s, value: expr(s.value), cases: s.cases.map((c) => ({ ...c, body: c.body.map(stmt) })) }; case "return": return s.value ? { ...s, value: any(s.value) } : s; case "action": return { ...s, record: action(s.record) as unknown as ActionRecord, ...(s.variables ? { variables: s.variables.map((v) => ({ ...v, expr: expr(v.expr) })) } : {}), ...(s.text ? { text: text(s.text) } : {}) }; diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index 0fe46e9..ae8ce66 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 14\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# 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 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# 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\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\nclass Storage:\n \"\"\"A variable's cell: plain, or a 12-slot row of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n self.store = EUDArray([0] * 12) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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, per_player, player_of):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.bits = decl.get(\"bits\")\n values = decl.get(\"values\")\n self.rowed = per_player and not decl.get(\"shared\") and values is None\n self.player_of = player_of\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 self.store = EUDArray([int(v) & U32 for v in values] if values is not None else [0] * (self.length * (12 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 player's row first.\"\"\"\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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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\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\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.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 12-slot rows of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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\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 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 self.arrays = {a[\"id\"]: SliceStorage(a, self) if a.get(\"slice\") else InnerListStorage(a, self) if a.get(\"through\") else (ListStorage if a.get(\"dynamic\") else ArrayStorage)(a, self.per_player, self.player_of) for a in program.get(\"arrays\", [])}\n self.state = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.wait = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state\n self.resumes = [] # (index, Forward) for every sleep\n self.frame_end = None\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 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 declare(self, decl):\n if decl.get(\"kind\") == \"unit\":\n s = UnitStorage(decl, self.per_player, self.player_of)\n elif decl.get(\"kind\") == \"text\" and decl.get(\"text\") != \"id\":\n s = TextStorage(decl, self.per_player, self.player_of)\n else:\n s = Storage(decl, self.per_player, self.player_of)\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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n def get_state(self):\n return self.state[self.player] if self.per_player else self.state\n\n def set_state(self, value):\n if self.per_player:\n self.state[self.player] = value\n else:\n self.state << value\n\n def get_wait(self):\n return self.wait[self.player] if self.per_player else self.wait\n\n def set_wait(self, value):\n if self.per_player:\n self.wait[self.player] = value\n else:\n self.wait << value\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 == \"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 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\"] == \"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] = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: the latch\n latch = self.latches[key]\n\n def get():\n return latch[self.player] if self.per_player else latch\n\n def put(v):\n if self.per_player:\n latch[self.player] = v\n else:\n latch << v\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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"action\":\n self.action(st)\n elif k == \"print\":\n self.print_(st)\n elif k == \"centerLocation\":\n self.center_location(st)\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 index = len(self.resumes) + 1\n resume = Forward()\n self.resumes.append((index, resume))\n self.set_state(index)\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 self.set_wait(frames - 1)\n EUDJump(self.frame_end)\n resume << 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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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 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 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 = [], []\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\":\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 else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n group, room, first = [], 0, [True]\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 <= T[\"scratch\"] + TEXT_BYTES):\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 << T[\"append\"](pos, value.addr)\n else:\n if EUDIf()(pos <= T[\"scratch\"] + TEXT_BYTES):\n pos << T[\"append\"](pos, value.addr)\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 return pos, temps\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 == \"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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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.rowed))\n elif isinstance(s, TextStorage):\n for cell in (s.addr, s.block, s.len):\n pairs.append(self.cell_pair(cell, s.rowed))\n elif isinstance(s, Storage):\n pairs.append(self.cell_pair(s.store, s.rowed))\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.rowed))\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, rowed):\n if rowed:\n return (lambda: cell[self.player_of()]), (lambda v: cell.__setitem__(self.player_of(), 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 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 self.set_state(DONE)\n EUDJump(self.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 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 self.frame_end = Forward()\n start = Forward()\n skip = Forward()\n # Still waiting: count down and leave.\n EUDJumpIfNot(as_var(self.get_wait()) >= 1, skip)\n self.set_wait(as_var(self.get_wait()) - 1)\n EUDJump(self.frame_end)\n skip << NextTrigger()\n # The body once, resumes recorded as sleeps are met; the jump table is filled in after.\n table = Forward()\n EUDJump(table)\n start << NextTrigger()\n try:\n self.block(self.p[\"body\"], {})\n except Leave:\n pass\n # The body ended: the program stops for good, as the classic backend's does.\n self.set_state(DONE)\n EUDJump(self.frame_end)\n table << NextTrigger()\n EUDJumpIf(as_var(self.get_state()) == DONE, self.frame_end)\n for index, resume in self.resumes:\n EUDJumpIf(as_var(self.get_state()) == index, resume)\n EUDJump(start)\n self.frame_end << NextTrigger()\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 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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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\"):\n continue\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_of(s).items()}})\n for id_, a in low.arrays.items():\n if isinstance(a, (InnerListStorage, SliceStorage)):\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 15\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# 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 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# 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\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\nclass Storage:\n \"\"\"A variable's cell: plain, or a 12-slot row of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n self.store = EUDArray([0] * 12) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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, per_player, player_of):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.bits = decl.get(\"bits\")\n values = decl.get(\"values\")\n self.rowed = per_player and not decl.get(\"shared\") and values is None\n self.player_of = player_of\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 self.store = EUDArray([int(v) & U32 for v in values] if values is not None else [0] * (self.length * (12 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 player's row first.\"\"\"\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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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\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\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.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 12-slot rows of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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\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. The program is one; each branch of a Promise.all / race is\n another, which the program runs inside its own frame for as long as it is waiting at that statement.\"\"\"\n\n def __init__(self, lowering):\n self.lowering = lowering\n self.state = EUDArray([0] * 12) if lowering.per_player else EUDVariable(0) # initial: program state\n self.wait = EUDArray([0] * 12) if lowering.per_player else EUDVariable(0) # initial: program state\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[self.lowering.player] if self.lowering.per_player else self.state\n\n def set_state(self, value):\n if self.lowering.per_player:\n self.state[self.lowering.player] = value\n else:\n self.state << value\n\n def get_wait(self):\n return self.wait[self.lowering.player] if self.lowering.per_player else self.wait\n\n def set_wait(self, value):\n if self.lowering.per_player:\n self.wait[self.lowering.player] = value\n else:\n self.wait << value\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 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 self.arrays = {a[\"id\"]: SliceStorage(a, self) if a.get(\"slice\") else InnerListStorage(a, self) if a.get(\"through\") else (ListStorage if a.get(\"dynamic\") else ArrayStorage)(a, self.per_player, self.player_of) for a in program.get(\"arrays\", [])}\n # The program's own coroutine, and the one being lowered (a branch of a Promise.all / race inside it).\n self.top = Coroutine(self)\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 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 declare(self, decl):\n if decl.get(\"kind\") == \"unit\":\n s = UnitStorage(decl, self.per_player, self.player_of)\n elif decl.get(\"kind\") == \"text\" and decl.get(\"text\") != \"id\":\n s = TextStorage(decl, self.per_player, self.player_of)\n else:\n s = Storage(decl, self.per_player, self.player_of)\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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 == \"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 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\"] == \"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] = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: the latch\n latch = self.latches[key]\n\n def get():\n return latch[self.player] if self.per_player else latch\n\n def put(v):\n if self.per_player:\n latch[self.player] = v\n else:\n latch << v\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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"action\":\n self.action(st)\n elif k == \"print\":\n self.print_(st)\n elif k == \"centerLocation\":\n self.center_location(st)\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 co.set_state(index)\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.set_wait(frames - 1)\n EUDJump(co.frame_end)\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) 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.set_state(index)\n parent.set_wait(0)\n EUDJump(parent.frame_end)\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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 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 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 = [], []\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\":\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 else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n group, room, first = [], 0, [True]\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 <= T[\"scratch\"] + TEXT_BYTES):\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 << T[\"append\"](pos, value.addr)\n else:\n if EUDIf()(pos <= T[\"scratch\"] + TEXT_BYTES):\n pos << T[\"append\"](pos, value.addr)\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 return pos, temps\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 == \"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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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.rowed))\n elif isinstance(s, TextStorage):\n for cell in (s.addr, s.block, s.len):\n pairs.append(self.cell_pair(cell, s.rowed))\n elif isinstance(s, Storage):\n pairs.append(self.cell_pair(s.store, s.rowed))\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.rowed))\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, rowed):\n if rowed:\n return (lambda: cell[self.player_of()]), (lambda v: cell.__setitem__(self.player_of(), 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 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 self.top.set_state(DONE)\n EUDJump(self.top.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 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 self.coroutine(self.top, self.p[\"body\"])\n\n def coroutine(self, co, body):\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, {})\n except Leave:\n pass\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 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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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\"):\n continue\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_of(s).items()}})\n for id_, a in low.arrays.items():\n if isinstance(a, (InnerListStorage, SliceStorage)):\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\n"; diff --git a/compiler/hoist.ts b/compiler/hoist.ts index 3f47db5..af16e87 100644 --- a/compiler/hoist.ts +++ b/compiler/hoist.ts @@ -108,7 +108,7 @@ function owningDeclaration(ts: typeof TS, decl: TS.Declaration): TS.Node { /** The library calls that must not appear inside a program body: they would run at build time, silently. */ const FORBIDDEN_INSIDE = new Set(["trigger", "program", "hyperTriggers", "game"]); /** The library calls the game answers: never hoisted, the structured compiler lowers them. */ -const GAME_CALLS = new Set(["random", "sleep", "rose", "once", "shared"]); +const GAME_CALLS = new Set(["random", "sleep", "until", "rose", "once", "shared"]); /** * Calls that read the game — `minerals(P1)`, and a comparing condition without its comparison * (`deaths(P1, unit)`) — or show text a program fills in (`print`). The call is never a @@ -135,6 +135,23 @@ export function isCallbackArgument(ts: typeof TS, node: TS.Node): node is TS.Arr return ts.isCallExpression(call) && call.arguments.includes(at as TS.Expression) && ts.isPropertyAccessExpression(call.expression) && CALLBACK_METHODS.has(call.expression.name.text); } +/** + * A function that is part of what a program waits for: the one `until(…)` asks every frame, the one given to `.then(…)`, + * and an async arrow called where it is written — `(async () => { … })()` in a `Promise.all`. Like a callback, its body + * is the program's and is walked, not run when the script is built. + */ +export function isTaskFunction(ts: typeof TS, checker: TS.TypeChecker, node: TS.Node): node is TS.ArrowFunction | TS.FunctionExpression { + if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) return false; + let at: TS.Node = node; + while (ts.isParenthesizedExpression(at.parent)) at = at.parent; + const call = at.parent; + if (!ts.isCallExpression(call)) return false; + if (call.expression === at) return !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); + if (!call.arguments.includes(at as TS.Expression)) return false; + if (ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === "then") return true; + return libraryCallName(ts, checker, call) === "until"; +} + export function planProgram(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunction | TS.FunctionExpression, options: PlanOptions = {}): ProgramPlan { // A list a callback writes to (`xs.forEach((x) => out.push(x))`) is the program's, and that is found out after its // `const` was planned as a value of the script: the plan is made again knowing it. The set only grows, so this ends. @@ -160,7 +177,7 @@ function planOnce(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunctio plan.body = ts.factory.createBlock([]); } if (arrow.parameters.length && !options.parameters) error(arrow.parameters[0], "The program's arrow takes no parameters."); - if (arrow.asteriskToken || arrow.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) error(arrow, `A ${what} body cannot be async or a generator.`); + if (arrow.asteriskToken) error(arrow, `A ${what} body cannot be a generator.`); /* ── Bindings ── */ const declare = (node: TS.Node) => plan.game.add(node); @@ -293,6 +310,7 @@ function planOnce(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunctio // what its function does, and what this one does is the game's. Run when the script is built it would make actions // nobody receives, and the line would silently do nothing; it is the program's loop, unrolled. if (isCallbackArgument(ts, n) && doesSomething(n)) { ok = false; return; } + if ((ts.isArrowFunction(n) || ts.isFunctionExpression(n)) && n.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { ok = false; return; } if (isFunctionValue(n)) { scan(n); return; } if (ts.isIdentifier(n)) { // A property name (`obj.name`, `{ name: v }`) is not a value reference. @@ -341,11 +359,14 @@ function planOnce(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunctio // `random()`, `sleep()`, … are the game's: their callee is never a value, however it is spelt (`ts.random()` through a namespace import). // Of a method call the object is the value, not the method: `cost.get(u.type)` has to leave `cost` — the Map — in reach, and a // method read off its object on its own has lost it. - if (!lib || !GAME_CALLS.has(lib)) value(ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) ? e.expression.expression : e.expression, items); + let callee: TS.Expression = e.expression; + while (ts.isParenthesizedExpression(callee)) callee = callee.expression; + if (isTaskFunction(ts, checker, callee)) callback(callee, items); + else if (!lib || !GAME_CALLS.has(lib)) value(ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) ? e.expression.expression : e.expression, items); for (const a of e.arguments ?? []) { let fn: TS.Expression = a; while (ts.isParenthesizedExpression(fn)) fn = fn.expression; - if (isCallbackArgument(ts, fn)) callback(fn, items); else value(a, items); + if (isCallbackArgument(ts, fn) || isTaskFunction(ts, checker, fn)) callback(fn, items); else value(a, items); } return; } diff --git a/compiler/input.ts b/compiler/input.ts index e0883c4..53583ed 100644 --- a/compiler/input.ts +++ b/compiler/input.ts @@ -251,6 +251,7 @@ export function inputsOf(programs: Program[]): { sources: InputSource[]; mouse: case "do": s.body.forEach(stmt); expr(s.cond); break; case "for": if (s.cond) expr(s.cond); s.update.forEach(stmt); s.body.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; + case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; case "switch": expr(s.value); s.cases.forEach((c) => c.body.forEach(stmt)); break; case "return": if (s.value) any(s.value); break; case "action": for (const v of s.variables ?? []) expr(v.expr); if (s.text) text(s.text); break; diff --git a/compiler/ir.ts b/compiler/ir.ts index 9be9a21..5f7cfde 100644 --- a/compiler/ir.ts +++ b/compiler/ir.ts @@ -10,6 +10,7 @@ import type { ActionRecord, ConditionRecord } from "../vendor/triggers"; import type { InputSource } from "./input"; /** + * 15: `parallel` — `await Promise.all([…])` / `Promise.race([…])`: branches that run side by side, each a coroutine of its own inside the program's frame. * 14: a text in the cells of three arrays — `textAt`, `storeText`, `releaseText` — which is what lets a row of an array of records hold one. * 13: texts — a `text` variable (`VarDecl.text`: the id of a text of the built map's table, or a text that was made: where it is, the block of the heap it owns, its length in characters), `TextExpr`, `assignText`, `textLoop`, a text among a `print`'s parts, in an action's text field (`action.text`) and as a unit type's name; `textLength` / `textIndexOf` / `textCode` among the numbers, `textCompare` / `textTest` among the conditions; `ArrayDecl.texts`. * 12: arrays inside things — `ArrayDecl.slice` (a window on another array's cells) and `ArrayDecl.through` (a growing array whose handle is a cell of four others). @@ -24,7 +25,7 @@ import type { InputSource } from "./input"; * 3: reads (`read`), `random(n)` as a number, the bitwise operators and `print` with its text in parts. * 2: a record's text and sound are written out in the JSON (1 had the map's string indices); `cyclesPerSecond` is gone. */ -export const IR_VERSION = 14; +export const IR_VERSION = 15; /** Where a node came from; `column` is 1-based like `line`. */ export interface At { file: string; line: number; column: number } @@ -488,6 +489,14 @@ export type Stmt = | { kind: "return"; value?: NumExpr | BoolExpr | UnitExpr | TextExpr; at: At; label: string } /** `cycles` is a count of frames (`frames(n)`; `cycles(n)` is the older word for the same). */ | { kind: "sleep"; ms?: number; cycles?: number; at: At; label: string } + /** + * `await Promise.all([…])` (`all`) / `await Promise.race([…])` (`race`): each branch is a coroutine of its own, started + * here, in order, within this frame. From then on, every frame the program is here, each branch that has not ended + * goes on from where it slept, in order, and the program goes on past this statement on the frame the last branch + * ends (`all`) or the first does (`race`, whose other branches then stop where they are). A branch's `sleep` is its + * own; `break`, `continue` and `return` never leave a branch. + */ + | { kind: "parallel"; mode: "all" | "race"; branches: Stmt[][]; at: At; label: string } /** A trigger action; each of `variables` names a field that takes an expression's value instead of the record's. */ /** * `text`: the action's text is the program's. One of the table goes into the action as its id. One that was made is @@ -591,6 +600,7 @@ export function declarations(body: Stmt[]): VarDecl[] { case "do": s.body.forEach(stmt); init(s.cond); break; case "for": if (s.cond) init(s.cond); s.update.forEach(stmt); s.body.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; + case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; case "switch": expr(s.value); s.cases.forEach((c) => c.body.forEach(stmt)); break; case "return": if (s.value) init(s.value); break; case "action": for (const v of s.variables ?? []) expr(v.expr); if (s.text) text(s.text); break; diff --git a/compiler/numbers.ts b/compiler/numbers.ts index 6249a33..66a47d0 100644 --- a/compiler/numbers.ts +++ b/compiler/numbers.ts @@ -278,6 +278,7 @@ export function typeNumbers(program: Program): ProgramDiagnostic[] { case "do": s.body.forEach(stmt); s.cond = bool(s.cond); break; case "for": if (s.cond) s.cond = bool(s.cond); s.update.forEach(stmt); s.body.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; + case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; case "switch": s.value = num(s.value)[0]; s.cases.forEach((c) => c.body.forEach(stmt)); break; case "return": if (s.value && result) s.value = valueFor(result, s.value, s.at); break; case "action": for (const v of s.variables ?? []) v.expr = floor(v.expr, s.at); if (s.text) s.text = text(s.text); break; diff --git a/compiler/recursion.ts b/compiler/recursion.ts index e0e39a9..0147e10 100644 --- a/compiler/recursion.ts +++ b/compiler/recursion.ts @@ -373,6 +373,7 @@ export function settleRecursion(program: Program): ProgramDiagnostic[] { continue; } case "unrolled": s.iterations = s.iterations.map((i) => body(i, returns)); break; + case "parallel": s.branches = s.branches.map((b) => body(b, returns)); break; case "block": s.body = body(s.body, returns); break; case "textLoop": errors.push({ at: s.at, message: `${f.name} calls itself, and this loop over a text holds such a call: the loop's place in the text is not something a call can keep. Walk it by place — for (let i = 0; i < s.length; i++) — which a call keeps as it keeps any number.` }); diff --git a/compiler/runtime.ts b/compiler/runtime.ts index 2ce5321..11699ee 100644 --- a/compiler/runtime.ts +++ b/compiler/runtime.ts @@ -427,6 +427,7 @@ export function createRuntime(names: ScriptNames, collector: Collector, options: // The word from before 3.0, when a program's clock was the trigger cycle; a program's clock is the frame now. rt.cycles = (n: unknown): DurationValue => ({ __trigscript: "duration", cycles: Math.max(1, Math.round(number(n, "cycles"))) }); rt.sleep = () => { throw new ScriptError("sleep() pauses a program: use it inside program(), as a statement — sleep(seconds(2))."); }; + rt.until = () => { throw new ScriptError("until() waits inside a program: await until(() => …), inside program(async () => { … })."); }; rt.rose = () => { throw new ScriptError("rose() is true on the cycle its condition becomes true: use it inside program(), in an if."); }; rt.once = () => { throw new ScriptError("once() is true the first time its condition holds: use it inside program(), in an if."); }; rt.shared = () => { throw new ScriptError("shared() marks a variable every player of a per-player program shares: let total = shared(0), inside program()."); }; @@ -691,7 +692,7 @@ export function runtimeNames(names: ScriptNames): string[] { out.push("CurrentPlayer", "AllPlayers"); out.push(...CONDITION_IDENTS.keys(), ...ACTION_IDENTS.keys(), "preserve"); out.push("condition", "action", "memory", "setMemory", "disabled", "not", "trigger", "hyperTriggers", "program", "game", "random"); - out.push("seconds", "minutes", "frames", "cycles", "sleep", "rose", "once", "shared", "clamp", "u32", "i32"); + out.push("seconds", "minutes", "frames", "cycles", "sleep", "until", "rose", "once", "shared", "clamp", "u32", "i32"); out.push(...READER_NAMES, "races", "slots", "name", "color", "print"); out.push(...UNIT_CALL_NAMES, "colors", ...INPUT_CALL_NAMES); return out; diff --git a/compiler/simulateIr.ts b/compiler/simulateIr.ts index ca68b57..4e4a959 100644 --- a/compiler/simulateIr.ts +++ b/compiler/simulateIr.ts @@ -977,6 +977,29 @@ class ProgramRun { for (let i = 0; i < n; i++) yield; return "next"; } + case "parallel": { + // Each branch a coroutine of its own: started here in order, then once a frame each one still running goes on, in + // order. A body run apart (a recursive call) inside a branch is handed up to the run's loop and its outcome back. + const branches = s.branches.map((b) => this.block(b, {})); + const done = branches.map(() => false); + for (;;) { + for (let i = 0; i < branches.length; i++) { + if (done[i]) continue; + let send: unknown; + for (;;) { + const r = branches[i].next(send); + send = undefined; + if (r.done) { done[i] = true; break; } + if (r.value === undefined) break; + send = yield r.value; + } + // The first to end wins a race; the others stop where they are. + if (done[i] && s.mode === "race") return "next"; + } + if (done.every(Boolean)) return "next"; + yield; + } + } case "action": { const record: ActionRecord = { ...s.record }; // A unit count is that many units (the game does the action once for each), whatever a byte could hold. diff --git a/compiler/structured.ts b/compiler/structured.ts index c75d687..9368da6 100644 --- a/compiler/structured.ts +++ b/compiler/structured.ts @@ -367,7 +367,8 @@ export class Structured { private unwrap(expr: TS.Expression): TS.Expression { const { ts } = this; for (;;) { - if (ts.isParenthesizedExpression(expr) || ts.isAsExpression(expr) || ts.isTypeAssertionExpression(expr) || ts.isNonNullExpression(expr) || ts.isSatisfiesExpression(expr)) expr = expr.expression; + // `await f()` is what f gives: an async function is inlined, and where it waits the program waits. + if (ts.isParenthesizedExpression(expr) || ts.isAsExpression(expr) || ts.isTypeAssertionExpression(expr) || ts.isNonNullExpression(expr) || ts.isSatisfiesExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression; else return expr; } } @@ -843,6 +844,9 @@ export class Structured { const over = this.overOf(init.expression.expression); if (over) { this.searchCall(init, over, init.expression.name.text, over.kind === "array" ? over.a.kind : "number"); continue; } } + // `const v = await Promise.race([wave(), sleep(…)])` is a number or nothing. + const race = !kind ? this.promiseCall(init) : undefined; + 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; } 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; @@ -2650,12 +2654,13 @@ export class Structured { } /** The function a method is given: written there, or the name of one declared in the body. */ - private functionOf(arg: TS.Expression | undefined, method: string, e: TS.Node): { parameters: readonly TS.ParameterDeclaration[]; body: TS.Block | TS.Expression; closure: Scope | null; name?: string } | null { + private functionOf(arg: TS.Expression | undefined, method: string, e: TS.Node, waits = false): { parameters: readonly TS.ParameterDeclaration[]; body: TS.Block | TS.Expression; closure: Scope | null; name?: string } | null { const { ts } = this; if (!arg) { this.c.error(e, `${method}() takes a function: xs.${method}((x) => …).`); return null; } const fn = this.unwrap(arg); if (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) { - if (fn.asteriskToken || fn.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { this.c.error(fn, "Generators and async functions are not supported in a program."); return null; } + if (fn.asteriskToken) { this.c.error(fn, "Generators are not supported in a program yet."); return null; } + if (!waits && fn.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { this.c.error(fn, `An async function given to ${method}(): the method is one loop within the frame, and nothing waits for what the function gives. A for…of over the same list can await between its turns.`); return null; } return { parameters: fn.parameters, body: fn.body, closure: this.scope }; } if (ts.isIdentifier(fn)) { @@ -2671,9 +2676,10 @@ export class Structured { * its place, the list. A variable it uses from outside is the program's own cell, so nothing is captured and nothing * is made when the map is played. `kind` is what it has to give back; "any" takes what it says it returns. */ - private callback(arg: TS.Expression | undefined, method: string, bound: Binding[], kind: Kind | "void" | "any", e: TS.CallExpression): Call | undefined { + private callback(arg: TS.Expression | undefined, method: string, bound: Binding[], kind: Kind | "void" | "any", e: TS.CallExpression, waits = false): Call | undefined { const { ts } = this; - const fn = this.functionOf(arg, method, e); + // `waits`: an async arrow called where it is written, whose body is the program's own and may wait. + const fn = this.functionOf(arg, method, e, waits); if (!fn) return undefined; if (kind === "any") { const signature = this.c.checker.getSignaturesOfType(this.c.checker.getTypeAtLocation(arg!), ts.SignatureKind.Call)[0]; @@ -2716,8 +2722,8 @@ export class Structured { scope.bind(p, b); }); if (!ok) return undefined; - this.inCallback++; - try { out.body = this.walkFunction(fn.body, kind, this.body, scope, () => patterns.forEach((take) => take())); } finally { this.inCallback--; } + if (!waits) this.inCallback++; + try { out.body = this.walkFunction(fn.body, kind, this.body, scope, () => patterns.forEach((take) => take())); } finally { if (!waits) this.inCallback--; } return out; } @@ -3575,6 +3581,7 @@ export class Structured { /** What a `u8` / `u16` / `u32` annotation declares, read off the brand in the type; nothing for a plain number, which is signed. */ private widthOf(type: TS.Type): { bits?: 8 | 16; unsigned?: boolean } { + type = this.awaited(type); for (const t of type.isIntersection() ? type.types : [type]) { const p = t.getProperty("__kind"); if (!p) continue; @@ -3587,8 +3594,14 @@ export class Structured { return {}; } + /** What a promise gives once it is awaited (an async function's `Promise` is a number); any other type as it is. */ + private awaited(type: TS.Type): TS.Type { + return this.c.checker.getAwaitedType(type) ?? type; + } + private kindOf(type: TS.Type): Kind | null { const { ts } = this; + type = this.awaited(type); // `c ? 1 : 0` is `0 | 1`: a union of numbers is a number. const isNumber = (t: TS.Type): boolean => (t.flags & ts.TypeFlags.NumberLike) !== 0 || (t.isIntersection() && t.types.some(isNumber)) || (t.isUnion() && t.types.every(isNumber)); if (type.flags & ts.TypeFlags.BooleanLike) return "boolean"; @@ -3656,6 +3669,7 @@ export class Structured { /** Whether a type is a string's: `string`, a text written out, a union of those (`undefined` beside it is what `at()` adds, and is nothing here). */ private isTextType(type: TS.Type): boolean { const { ts } = this; + type = this.awaited(type); const bare = (type.isUnion() ? type.types : [type]).filter((t) => (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) === 0); return bare.length > 0 && bare.every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0); } @@ -4127,8 +4141,145 @@ export class Structured { this.emit({ kind: "sleep", ...(d.cycles !== undefined ? { cycles: d.cycles } : {}), ...(d.ms !== undefined ? { ms: d.ms } : {}), at: this.at(call), label: this.label(call) }, call); } - private expressionStatement(expr: TS.Expression) { + /** `await until(() => condition)`: the program waits, asking again every frame, until the condition holds — not at all when it holds now. */ + private untilStatement(call: TS.CallExpression) { + const { ts } = this; + if (this.inCallback > 0) { this.c.error(call, "until() inside a function given to an array method: the method is one loop within the frame. A for…of over the same list can await between its turns."); return; } + if (call.arguments.length !== 1) { this.c.error(call, "until() takes a function that answers true or false: await until(() => countUnits(P8, units.ZergZergling) === 0)."); return; } + const arg = call.arguments[0]; + const fn = this.unwrap(arg); + let cond: BoolExpr | null; + if (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) cond = this.predicate(arg, "until", [], call); + else if (isCondition(this.evaluate(arg)?.value)) cond = this.bool(arg); + else { this.c.error(arg, "until() takes a function, which is asked again every frame: until(() => …). A condition — until(bring(…)) — is asked again on its own."); return; } + if (!cond) return; + const at = this.at(call), label = this.label(call); + this.emit({ kind: "while", cond: { kind: "not", expr: cond }, body: [{ kind: "sleep", cycles: 1, at, label }], at, label }, call); + } + + /** `Promise.all(…)`, `Promise.race(…)`, `Promise.any(…)`: which one, when the call is one of the language's own Promise. */ + private promiseCall(e: TS.Expression): "all" | "race" | "any" | undefined { + const { ts } = this; + if (!ts.isCallExpression(e) || !ts.isPropertyAccessExpression(e.expression) || !ts.isIdentifier(e.expression.expression) || e.expression.expression.text !== "Promise") return undefined; + if (this.gameDeclaration(e.expression.expression)) return undefined; + const name = e.expression.name.text; + return name === "all" || name === "race" || name === "any" ? name : undefined; + } + + /** Whether an expression's type is a promise: what an async function, sleep(), until() and Promise.all give. */ + private isPromise(e: TS.Expression): boolean { + const type = this.c.checker.getTypeAtLocation(e); + return this.awaited(type) !== type; + } + + /** Whether a node is inside an async function: `program(async () => …)`, an `async function`, an async arrow called where it is written. */ + private inAsync(node: TS.Node): boolean { + const { ts } = this; + for (let n = node.parent; n; n = n.parent) if (ts.isFunctionLike(n)) return !!ts.getModifiers(n as TS.FunctionLikeDeclaration)?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); + return false; + } + + /** A promise as a statement on its own: in a program, what is not awaited never runs beside it. */ + private notAwaited(e: TS.CallExpression) { + const text = e.getText(); + const mode = this.promiseCall(e); + const what = this.isLibraryCall(e, "sleep") ? "sleep()" : this.isLibraryCall(e, "until") ? "until()" : mode ? `Promise.${mode}()` : `${this.unwrap(e.expression).getText()}()`; + if (this.inAsync(e)) this.c.error(e, `${what} gives a promise, and nothing waits for it here: await ${text}. A program cannot yet start something and leave it running beside it.`); + else this.c.error(e, `${what} waits, and a program waits in an async function: program(async () => { … await ${text}; … }).`); + } + + /** + * What is awaited, as statements of the program — or of a branch, inside Promise.all / race: sleep(), until(), an async + * call, an async arrow called where it is written, a Promise.all / race inside another, `….then(() => value)`. `into`: + * the result of a race, which the branch sets as it ends. + */ + private task(expr: TS.Expression, into?: { decl: VarDecl; kind: "number" | "boolean" }) { + const { ts } = this; + const e = this.unwrap(expr); + const valueless = (what: string) => { if (into) this.c.error(e, `${what} gives no value, and Promise.race gives the value of the branch that ends first: give this one a value with .then(() => …).`); }; + const keep = (call: Call | undefined, node: TS.Node) => { + if (!call) return; + this.emit({ kind: "call", call, at: call.at, label: call.label }, node); + if (!into) return; + if (!call.result || call.result.kind !== into.kind) { this.c.error(node, `Every branch of a race whose value is kept gives a ${into.kind}.`); return; } + const value = { kind: "var" as const, id: call.result.decl.id }; + this.emit(into.kind === "number" ? { kind: "assign", target: into.decl.id, value, at: call.at, label: call.label } : { kind: "assignBool", target: into.decl.id, value, at: call.at, label: call.label }, node); + }; + if (ts.isCallExpression(e)) { + if (this.isLibraryCall(e, "sleep")) { this.sleepStatement(e); valueless("sleep()"); return; } + if (this.isLibraryCall(e, "until")) { this.untilStatement(e); valueless("until()"); return; } + const mode = this.promiseCall(e); + if (mode) { + if (mode === "all") valueless("Promise.all()"); + this.parallel(e, mode, mode === "all" ? undefined : into); + return; + } + if (ts.isPropertyAccessExpression(e.expression) && e.expression.name.text === "then" && this.isPromise(e.expression.expression)) { + if (e.arguments.length !== 1) { this.c.error(e, ".then() takes one function: sleep(seconds(90)).then(() => false)."); return; } + const fn = this.unwrap(e.arguments[0]); + if ((ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && fn.parameters.length) { this.c.error(fn, "The function given to .then() takes nothing here. To use what a call gives, keep it first: const v = await f(); then use v."); return; } + this.task(e.expression.expression); + keep(this.callback(e.arguments[0], "then", [], into?.kind ?? "any", e), e); + return; + } + const callee = this.unwrap(e.expression); + if (ts.isArrowFunction(callee) || ts.isFunctionExpression(callee)) { + if (e.arguments.length || callee.parameters.length) { this.c.error(e, "An async arrow called where it is written takes nothing: (async () => { … })()."); return; } + keep(this.callback(e.expression, "an async function", [], into?.kind ?? "any", e, true), e); + return; + } + } + if (into) { + const value = into.kind === "number" ? this.num(expr) : this.boolValue(expr); + if (!value) return; + const at = this.at(e), label = this.label(e); + this.emit(into.kind === "number" ? { kind: "assign", target: into.decl.id, value: value as NumExpr, at, label } : { kind: "assignBool", target: into.decl.id, value: value as BoolExpr, at, label }, e); + return; + } + this.expressionStatement(expr, true); + } + + /** + * `await Promise.all([…])` / `race` / `any`: each item a branch of its own, side by side (`parallel` in the IR). Nothing in + * a program fails, so the first branch to end is the first to succeed, and `any` is a race. + */ + private parallel(call: TS.CallExpression, mode: "all" | "race" | "any", into?: { decl: VarDecl; kind: "number" | "boolean" }) { + const { ts } = this; + const name = `Promise.${mode}`; + if (this.inCallback > 0) { this.c.error(call, `${name}() inside a function given to an array method: the method is one loop within the frame.`); return; } + const list = call.arguments.length === 1 ? this.unwrap(call.arguments[0]) : undefined; + if (!list || !ts.isArrayLiteralExpression(list)) { this.c.error(call, `${name}() takes a list written where it is called: ${name}([wave(1), sleep(seconds(90))]).`); return; } + if (!list.elements.length) { this.c.error(list, mode === "all" ? "Promise.all([]) has nothing to wait for." : `${name}([]) never ends.`); return; } + const branches: Stmt[][] = []; + for (const el of list.elements) { + if (ts.isSpreadElement(el) || ts.isOmittedExpression(el)) { this.c.error(el, `Each item of ${name}() is written out: what runs side by side is settled when the script is built.`); return; } + branches.push(this.collect(() => this.task(el, into))); + } + this.emit({ kind: "parallel", mode: mode === "all" ? "all" : "race", branches, at: this.at(call), label: this.label(call) }, call); + } + + /** `const first = await Promise.race([…])`: the value of the branch that ends first, in a variable of its own. */ + private raceValue(e: TS.CallExpression, kind: "number" | "boolean"): NumExpr | BoolExpr | null { + const { ts } = this; + const mode = this.promiseCall(e)!; + if (mode === "all") { this.c.error(e, "Promise.all() gives no value yet: await it as a statement, and keep what each branch finds in variables of the program."); return null; } + let up: TS.Node = e.parent; + while (ts.isParenthesizedExpression(up)) up = up.parent; + if (!ts.isAwaitExpression(up)) { this.c.error(e, `Promise.${mode}() gives a promise: await it — const v = await Promise.${mode}([…]).`); return null; } + const at = this.at(e), label = this.label(e); + const t = this.newVar(`(Promise.${mode} result)`, kind, at, { temp: true }); + this.emit({ kind: "declare", decl: t, init: kind === "number" ? num(0) : FALSE, at, label }, e); + this.parallel(e, mode, { decl: t, kind }); + return kind === "number" ? varRef(t) : { kind: "var", id: t.id }; + } + + private expressionStatement(expr: TS.Expression, awaited = false) { const { ts } = this; + let bare = expr; + while (ts.isParenthesizedExpression(bare)) bare = bare.expression; + if (ts.isAwaitExpression(bare)) { this.task(bare.expression); return; } + // A promise nothing waits for. sleep() in a function that is not async is the language before 4: it pauses there. + if (!awaited && ts.isCallExpression(bare) && this.isPromise(bare) && !(this.isLibraryCall(bare, "sleep") && !this.inAsync(bare))) { this.notAwaited(bare); return; } const e = this.unwrap(expr); const h = this.evaluate(expr); if (h) { this.hoistedStatement(e, h); return; } @@ -5310,7 +5461,7 @@ export class Structured { if (cls.typeParameters?.length) this.c.error(cls.typeParameters[0], "A class of a program takes no type parameters: what each field holds has to be known when the script is built."); for (const m of cls.members) { if (ts.isClassStaticBlockDeclaration(m)) { this.c.error(m, "A static block is not supported in a program; give the static fields their values where they are declared."); continue; } - if (ts.isMethodDeclaration(m) && (m.asteriskToken || ts.getModifiers(m)?.some((x) => x.kind === ts.SyntaxKind.AsyncKeyword))) this.c.error(m, "Generators and async functions are not supported in a program."); + if (ts.isMethodDeclaration(m) && (m.asteriskToken || ts.getModifiers(m)?.some((x) => x.kind === ts.SyntaxKind.AsyncKeyword))) this.c.error(m, "A generator or an async method is not supported in a class yet; an async function declared in the program can take the instance."); if (!ts.isPropertyDeclaration(m) || !this.isStatic(m)) continue; const key = this.memberKey(m.name); if (!key) { this.c.error(m.name, "A field's name is written out."); continue; } @@ -5562,7 +5713,7 @@ export class Structured { if (!body) { this.c.error(call, "The function has no body."); return undefined; } // A copy of a body inside a copy of a body, sixteen times over: what happens from here is decided once the arguments are known. const deep = this.inlineDepth >= MAX_INLINE_DEPTH; - if ((ts.isFunctionDeclaration(decl) || ts.isArrowFunction(decl) || ts.isFunctionExpression(decl) || ts.isMethodDeclaration(decl)) && (decl.asteriskToken || decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword))) { this.c.error(decl, "Generators and async functions are not supported in a program."); return undefined; } + if ((ts.isFunctionDeclaration(decl) || ts.isArrowFunction(decl) || ts.isFunctionExpression(decl) || ts.isMethodDeclaration(decl)) && decl.asteriskToken) { this.c.error(decl, "Generators are not supported in a program yet."); return undefined; } const kind: Kind | "text" | "void" = as.nothing ? "void" : this.kindOf(this.c.checker.getTypeAtLocation(call)) ?? (this.isTextType(this.c.checker.getTypeAtLocation(call)) ? "text" : "void"); const line = this.line(call); const out: Call = { ...(name ? { name } : {}), at: this.at(call), label: this.label(call), params: [], body: [] }; @@ -5916,7 +6067,7 @@ export class Structured { (this.c as { error: StructuredContext["error"] }).error = error; } if (caught.length) return `with its parameters as variables of the game it does not compile — ${caught[0].replace(/\.$/, "")}`; - if (mentions(fn.body, "sleep")) return "it sleeps, and the program wakes up inside it"; + if (mentions(fn.body, "sleep") || mentions(fn.body, "parallel")) return "it sleeps, and the program wakes up inside it"; if (mentions(fn.body, "edge")) return "rose() / once() remember what they saw, a call each"; this.mark(fn, decl); this.functions.push(fn); @@ -6227,6 +6378,7 @@ export class Structured { const { ts } = this; const ofText = this.textNumber(e); if (ofText !== undefined) return ofText; + if (this.promiseCall(e)) return this.raceValue(e, "number") as NumExpr | null; const method = this.methodCall(e); if (method !== undefined) return this.numberOf(method, e); if (ts.isIdentifier(e.expression) && (e.expression.text === "parseInt" || e.expression.text === "Number" || e.expression.text === "parseFloat") && !this.gameDeclaration(e.expression)) { @@ -6782,7 +6934,8 @@ export class Structured { } if (this.isLibraryCall(e, "rose")) return this.edge(e, "rose"); if (this.isLibraryCall(e, "once")) return this.edge(e, "once"); - if (this.isLibraryCall(e, "sleep")) { this.c.error(e, "sleep() is a statement, not a condition."); return FALSE; } + if (this.isLibraryCall(e, "sleep") || this.isLibraryCall(e, "until")) { this.c.error(e, `${this.isLibraryCall(e, "sleep") ? "sleep" : "until"}() is a statement, not a condition: await it on a line of its own.`); return FALSE; } + if (this.promiseCall(e)) return (this.raceValue(e, "boolean") as BoolExpr | null) ?? FALSE; const read = this.readCall(e); if (read !== undefined) return read ? this.readBool(read, e) : FALSE; const callee = this.evaluate(e.expression)?.value; diff --git a/docs/ir.md b/docs/ir.md index cdd8982..13162d5 100644 --- a/docs/ir.md +++ b/docs/ir.md @@ -3,7 +3,7 @@ What a `program(() => { … })` body means, written down as data. The compiler's front end (`compiler/structured.ts`) turns the TypeScript into this, `python/trigscript.py` lowers it to eudplib when the map is saved, and `compiler/simulateIr.ts` interprets it for -Simulate and the tests. **Version 14** (13 had no text in the cells of an array: no `textAt`, `storeText`, `releaseText`; 12 had no texts: a text was the parts of a `print` and nothing else; 11 had no `slice` and no `through`: every array was cells of its own; 10 had no recursion: no `recursive`, no `saves`, and the heap's top set aside for a stack nothing used; 9 had no functions that are called: every `call` carried a body of its own; 8 had no `unitAt` / `unitPart`, so no array could hold a unit; 7 had no arrays that grow; 6 had no arrays; 5 had unsigned numbers only, the two-sided reading of `+` and `−`, no `>>>` and no `unsigned` anywhere; 4 had no input, no `centerLocation`, and one `variable` on an action where 5 has a list; 3 had no units and no tables; 2 had no reads, no `random(n)`, no bitwise operators +Simulate and the tests. **Version 15** (14 had no `parallel`: nothing ran beside a program's body; 13 had no text in the cells of an array: no `textAt`, `storeText`, `releaseText`; 12 had no texts: a text was the parts of a `print` and nothing else; 11 had no `slice` and no `through`: every array was cells of its own; 10 had no recursion: no `recursive`, no `saves`, and the heap's top set aside for a stack nothing used; 9 had no functions that are called: every `call` carried a body of its own; 8 had no `unitAt` / `unitPart`, so no array could hold a unit; 7 had no arrays that grow; 6 had no arrays; 5 had unsigned numbers only, the two-sided reading of `+` and `−`, no `>>>` and no `unsigned` anywhere; 4 had no input, no `centerLocation`, and one `variable` on an action where 5 has a list; 3 had no units and no tables; 2 had no reads, no `random(n)`, no bitwise operators and no `print`; 1 had the map's string indices in the records and a `cyclesPerSecond` on the program, for the death-counter backend 3.0 removed). The types are in `compiler/ir.ts`; this is the reference for anyone reading the lowering or writing @@ -179,7 +179,8 @@ frame's `saves`, and a `slice` is never declared. | `switch` | `value`, `cases: { value, body }[]` | Cases tested in order; `value` null is `default`, `NaN` a case whose value could not be read (no test, still fallen into). Bodies fall through unless they `break`. | | `break`, `continue` | | Of the nearest loop (`break` also of a `switch`). | | `return` | `value?` | Inside a `call` body: writes the result and leaves the call. | -| `sleep` | `ms?`, `cycles?` | Park the program: a duration in milliseconds, or in frames (`cycles`). `cycles: 1` goes on in the very next frame. | +| `sleep` | `ms?`, `cycles?` | Park the program: a duration in milliseconds, or in frames (`cycles`). `cycles: 1` goes on in the very next frame. Inside a `parallel` branch it parks that branch alone. | +| `parallel` | `mode`, `branches` | `await Promise.all([…])` (`all`) or `await Promise.race([…])` / `Promise.any` (`race`). Each branch is a coroutine of its own with its own place to go on from and its own wait: started here, in order, in this frame; then, every frame the program is at this statement, each branch that has not ended goes on, in order. The program goes on past it in the frame the last branch ends (`all`) or the first (`race`, which leaves the others where they are: they never run again). A branch is never left by `break`, `continue` or `return`. Not inside a called function, a `unitLoop` or a `textLoop`. Since version 15. | | `print` | `parts`, `to`, `position` | Text with values in it. `parts` are `{ kind: "text", text }`, `{ kind: "number", expr, unsigned? }` (its digits, with a minus sign when it is below zero and not `unsigned`), `{ kind: "name", player }` and `{ kind: "color", player }` (the colour code of the player's colour), a player being a slot or 13 for the current player. `to` is who sees it: a slot, 13, All Players (17) or a force (18–21). `position` is `chat` or `center`, the line the game's own errors use. Every number is evaluated before anything is shown. | | `action` | `record`, `variables?` | A trigger action. Each of `variables` names a field of the record that takes an expression's value: `{ field, bits: 8, 16 or 32, name, expr }` — the unit count of `createUnit` and friends is an 8-bit field, a unit type a 16-bit one (the lowering stops it at 228), an amount with a modifier a 32-bit one. Version 4 had one, as `variable`. | | `centerLocation` | `location`, `x`, `y` | Centre a location (1-based) on a point of the map in pixels, its size kept. | diff --git a/probes/game-async.ts b/probes/game-async.ts new file mode 100644 index 0000000..efa759d --- /dev/null +++ b/probes/game-async.ts @@ -0,0 +1,63 @@ +// TrigScript 4, B1: async functions, await, until(), Promise.all and Promise.race, run in the lifted game as well as in +// the simulator — the two have to agree frame for frame. +// npx tsx scripts/test-game.mts probes/game-async.ts +// A program first runs on frame 2. Built like a probe (Player 1 Terran, Player 2 a Zerg computer, location 1 "Home"). +import { test, expect, program, displayText, setResources, createUnit, countUnits, sleep, until, frames, units, P1, P2, type Location } from "trigscript"; + +const HOME = 1 as Location; + +program(async () => { + let n = 0; + async function wave(k: number): Promise { + createUnit(P2, units.ZergZergling, k, HOME); + await until(() => countUnits(P2, units.ZergZergling, HOME) === 0); + return k; + } + // Frame 2: n is 1; frame 6: 2; frame 10: 3, and the second branch sees it in the same frame; frame 14 the first ends. + await Promise.all([ + (async () => { for (let i = 0; i < 3; i++) { n++; setResources(P1, "set", n, "gas"); await sleep(frames(4)); } })(), + (async () => { await until(() => n >= 3); displayText("three"); })(), + ]); + displayText("all done"); + // From frame 14: the second branch wins on frame 24; the third never prints. + const w = await Promise.race([ + (async () => { await sleep(frames(20)); return 1; })(), + sleep(frames(10)).then(() => 2), + (async () => { await sleep(frames(30)); displayText("late"); return 3; })(), + ]); + setResources(P1, "set", w * 100, "ore"); + // A wave of two Zerglings the test kills, raced against a long sleep. + const cleared = await Promise.race([wave(2), sleep(frames(500)).then(() => -1)]); + setResources(P1, "set", cleared, "gas"); + displayText(cleared > 0 ? "wave cleared" : "wave lost"); +}, { owner: P1 }); + +test("Promise.all: the branches side by side, until() in the frame its answer turns", (sim) => { + sim.frames(9); + expect(sim.resources(P1).gas).toBe(2); + expect(sim).not.toHavePrinted("three"); + sim.frames(2); + expect(sim.resources(P1).gas).toBe(3); + expect(sim).toHavePrinted("three"); + expect(sim).not.toHavePrinted("all done"); + sim.frames(4); + expect(sim).toHavePrinted("all done"); +}); + +test("Promise.race: the first to end gives the value, the others stop", (sim) => { + sim.frames(23); + expect(sim.resources(P1).ore).toBe(0); + sim.frames(1); + expect(sim.resources(P1).ore).toBe(200); + sim.frames(40); + expect(sim).not.toHavePrinted("late"); +}); + +test("an awaited wave: until() over the game's units", (sim) => { + sim.frames(30); + expect(sim.count(P2, units.ZergZergling, HOME)).toBe(2); + for (const u of sim.units({ owner: P2, type: units.ZergZergling })) sim.kill(u); + sim.frames(30); + expect(sim.resources(P1).gas).toBe(2); + expect(sim).toHavePrinted("wave cleared"); +}); diff --git a/python/trigscript.py b/python/trigscript.py index 0da33e9..d70f75a 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -68,7 +68,7 @@ from eudplib.core.mapdata.stringmap import ForceAddString from eudplib.memio.rwcommon import br1, br2, bw1 -IR_VERSION = 14 +IR_VERSION = 15 FRAMES_PER_SECOND = 24 # Where the game keeps what a read reads (1.16.1 addresses, which Remastered emulates). The player # tables are the ones Magenta's probes 5 and 8 read in the game. @@ -999,6 +999,44 @@ def fresh(value=0): return v +class Coroutine: + """Where a coroutine is: the label it goes on from (0 its start, DONE once it ended) and the frames it still waits, + a cell each, a row of twelve in a per-player program. The program is one; each branch of a Promise.all / race is + another, which the program runs inside its own frame for as long as it is waiting at that statement.""" + + def __init__(self, lowering): + self.lowering = lowering + self.state = EUDArray([0] * 12) if lowering.per_player else EUDVariable(0) # initial: program state + self.wait = EUDArray([0] * 12) if lowering.per_player else EUDVariable(0) # initial: program state + self.resumes = [] # (index, Forward) for every place it can go on from + self.frame_end = None + + def get_state(self): + return self.state[self.lowering.player] if self.lowering.per_player else self.state + + def set_state(self, value): + if self.lowering.per_player: + self.state[self.lowering.player] = value + else: + self.state << value + + def get_wait(self): + return self.wait[self.lowering.player] if self.lowering.per_player else self.wait + + def set_wait(self, value): + if self.lowering.per_player: + self.wait[self.lowering.player] = value + else: + self.wait << value + + def resume_point(self): + """A place to go on from: its number, which the state takes, and its label, to be placed where it is.""" + index = len(self.resumes) + 1 + label = Forward() + self.resumes.append((index, label)) + return index, label + + class Lowering: def __init__(self, program): self.p = program @@ -1008,10 +1046,9 @@ def __init__(self, program): self.player = None self.vars = {} self.arrays = {a["id"]: SliceStorage(a, self) if a.get("slice") else InnerListStorage(a, self) if a.get("through") else (ListStorage if a.get("dynamic") else ArrayStorage)(a, self.per_player, self.player_of) for a in program.get("arrays", [])} - self.state = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state - self.wait = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: program state - self.resumes = [] # (index, Forward) for every sleep - self.frame_end = None + # The program's own coroutine, and the one being lowered (a branch of a Promise.all / race inside it). + self.top = Coroutine(self) + self.co = self.top self.latches = {} # The functions that are called, by id, and each one's EUDFunc once something has called it. self.functions = {f["id"]: f for f in program.get("functions", [])} @@ -1048,24 +1085,6 @@ def var(self, id_, node=None): raise Fail("trigscript: unknown variable %r%s" % (id_, where(node))) return s - def get_state(self): - return self.state[self.player] if self.per_player else self.state - - def set_state(self, value): - if self.per_player: - self.state[self.player] = value - else: - self.state << value - - def get_wait(self): - return self.wait[self.player] if self.per_player else self.wait - - def set_wait(self, value): - if self.per_player: - self.wait[self.player] = value - else: - self.wait << value - # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ── def num(self, e): k = e["kind"] @@ -1992,6 +2011,8 @@ def statement(self, st, ctx): raise Leave() elif k == "sleep": self.sleep(st) + elif k == "parallel": + self.parallel(st) elif k == "action": self.action(st) elif k == "print": @@ -2087,16 +2108,48 @@ def sleep(self, st): frames = int(st["cycles"]) else: frames = max(1, int(round(float(st.get("ms", 0)) * FRAMES_PER_SECOND / 1000))) - index = len(self.resumes) + 1 - resume = Forward() - self.resumes.append((index, resume)) - self.set_state(index) + co = self.co + index, resume = co.resume_point() + co.set_state(index) # The frame after this one is one frame later: sleep(frames(1)) goes on in the next frame, as the # simulator has it. (Until 3.4 the wait was one frame longer, so a loop sleeping a frame ran every other.) - self.set_wait(frames - 1) - EUDJump(self.frame_end) + co.set_wait(frames - 1) + EUDJump(co.frame_end) resume << NextTrigger() + def parallel(self, st): + """Promise.all / race: each branch a coroutine of its own, started here and run from here, in order, every frame + the program waits at this statement; the program goes on when every branch has ended (all) or the first has + (race, which leaves the others where they are). The program waits here through a place of its own to go on + from, which this same statement is: the first frame falls into it, the later ones jump to it.""" + if self.in_function: + raise Fail("trigscript: await inside a function that is called%s" % where(st)) + parent = self.co + kids = [Coroutine(self) for _ in st["branches"]] + for k in kids: + k.set_state(0) + k.set_wait(0) + index, again = parent.resume_point() + again << NextTrigger() + after = Forward() + for k, body in zip(kids, st["branches"]): + skip = Forward() + EUDJumpIf(as_var(k.get_state()) == DONE, skip) + self.coroutine(k, body) + if st["mode"] == "race": + EUDJumpIfNot(as_var(k.get_state()) == DONE, skip) + for other in kids: + if other is not k: + other.set_state(DONE) + EUDJump(after) + skip << NextTrigger() + if st["mode"] == "all": + EUDJumpIf([as_var(k.get_state()) == DONE for k in kids], after) + parent.set_state(index) + parent.set_wait(0) + EUDJump(parent.frame_end) + after << NextTrigger() + def action(self, st): r = st["record"] 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"]) @@ -2625,8 +2678,8 @@ def keep(self, call): 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)) st["depth"] << 0 st["at"] << st["base"] - self.set_state(DONE) - EUDJump(self.frame_end) + self.top.set_state(DONE) + EUDJump(self.top.frame_end) EUDEndIf() pairs = self.saved_cells(saves, call) at = st["at"] @@ -2734,31 +2787,39 @@ def call(self, call): # ── one frame ── def frame(self): - self.frame_end = Forward() + self.coroutine(self.top, self.p["body"]) + + def coroutine(self, co, body): + """One frame of a coroutine: count its wait down and leave, or go on from where it stopped until it sleeps or + ends. Its body is lowered here, once, and the places to go on from are recorded as they are met; the jump + table is filled in after.""" + outer = self.co + self.co = co + co.frame_end = Forward() start = Forward() skip = Forward() # Still waiting: count down and leave. - EUDJumpIfNot(as_var(self.get_wait()) >= 1, skip) - self.set_wait(as_var(self.get_wait()) - 1) - EUDJump(self.frame_end) + EUDJumpIfNot(as_var(co.get_wait()) >= 1, skip) + co.set_wait(as_var(co.get_wait()) - 1) + EUDJump(co.frame_end) skip << NextTrigger() - # The body once, resumes recorded as sleeps are met; the jump table is filled in after. table = Forward() EUDJump(table) start << NextTrigger() try: - self.block(self.p["body"], {}) + self.block(body, {}) except Leave: pass - # The body ended: the program stops for good, as the classic backend's does. - self.set_state(DONE) - EUDJump(self.frame_end) + # The body ended: it stops for good, as the classic backend's program does. + co.set_state(DONE) + EUDJump(co.frame_end) table << NextTrigger() - EUDJumpIf(as_var(self.get_state()) == DONE, self.frame_end) - for index, resume in self.resumes: - EUDJumpIf(as_var(self.get_state()) == index, resume) + EUDJumpIf(as_var(co.get_state()) == DONE, co.frame_end) + for index, resume in co.resumes: + EUDJumpIf(as_var(co.get_state()) == index, resume) EUDJump(start) - self.frame_end << NextTrigger() + co.frame_end << NextTrigger() + self.co = outer def run(self): if self.per_player: diff --git a/tests/async.test.ts b/tests/async.test.ts new file mode 100644 index 0000000..5929d67 --- /dev/null +++ b/tests/async.test.ts @@ -0,0 +1,139 @@ +/** + * TrigScript 4, slice B1: async functions and await. An async function is inlined where it is awaited, so where it waits + * the program waits; `Promise.all` / `race` / `any` run their branches side by side, each a coroutine of its own inside + * the program's frame (`parallel` in the IR). The interpreter here; `eud-build.test.ts` builds the same through eudplib. + */ +import ts from "typescript"; +import { describe, expect, it } from "vitest"; +import { compileScript, type CompileResult } from "../compiler/compiler"; +import { defaultScriptNames } from "../compiler/names"; +import { simulatePrograms } from "../compiler/simulateIr"; +import { ActionType } from "../vendor/triggers"; +import { defaultLib } from "../bundle/lib.mjs"; + +const LIB = defaultLib(); +const NAMES = defaultScriptNames(); +const raw = (body: string, options = ""): CompileResult => compileScript(ts, { "main.ts": `program(async () => {${body}}${options ? `, ${options}` : ""});` }, NAMES, { lib: LIB }); +const compile = (body: string, options = ""): CompileResult => { const r = raw(body, options); expect(r.diagnostics.map((d) => d.message)).toEqual([]); return r; }; +const messages = (body: string, source?: string): string[] => (source !== undefined ? compileScript(ts, { "main.ts": source }, NAMES, { lib: LIB }) : raw(body)).diagnostics.map((d) => d.message); +/** What the program printed, as "frame:text", over that many frames. */ +const printed = (r: CompileResult, frames: number): string[] => + simulatePrograms(r.ir, frames, { strings: r.strings }).events.filter((e) => e.action.type === ActionType.DisplayText).map((e) => `${e.cycle}:${e.text}`); + +describe("async functions and await", () => { + it("an awaited async function waits where it sleeps and gives back its value", () => { + const r = compile(` + async function wave(n: number): Promise { print(\`wave \${n}\`); await sleep(frames(2)); return n * 10; } + const a = await wave(1); + print(\`got \${a}\`); + const b = await wave(2); + print(\`got \${b}\`);`); + expect(printed(r, 10)).toEqual(["0:wave 1", "2:got 10", "2:wave 2", "4:got 20"]); + }); + + it("an async function is never one called copy: it is inlined at each await, as a function that sleeps always was", () => { + const r = compile(` + async function pause(): Promise { await sleep(frames(1)); } + await pause(); await pause(); print("done");`); + expect(r.ir[0].functions ?? []).toEqual([]); + expect(printed(r, 5)).toEqual(["2:done"]); + }); + + it("until() asks every frame and goes on in the frame its answer is true — at once when it already is", () => { + const r = compile(` + let n = 0; + await until(() => n === 0); + print("at once"); + await Promise.all([ + (async () => { for (let i = 0; i < 3; i++) { n++; await sleep(frames(1)); } })(), + (async () => { await until(() => n >= 3); print(\`n \${n}\`); })(), + ]); + print("both");`); + expect(printed(r, 10)).toEqual(["0:at once", "2:n 3", "3:both"]); + }); + + it("Promise.all goes on in the frame its last branch ends; the branches run in order, each with its own sleeps", () => { + const r = compile(` + await Promise.all([ + (async () => { print("a0"); await sleep(frames(3)); print("a3"); })(), + (async () => { print("b0"); await sleep(frames(1)); print("b1"); await sleep(frames(1)); print("b2"); })(), + sleep(frames(2)), + ]); + print("all");`); + expect(printed(r, 10)).toEqual(["0:a0", "0:b0", "1:b1", "2:b2", "3:a3", "3:all"]); + }); + + it("Promise.race gives the value of the first branch to end, and the others stop where they are", () => { + const r = compile(` + async function wave(n: number): Promise { await sleep(frames(n)); return n; } + const a = await Promise.race([wave(5), sleep(frames(3)).then(() => -1)]); + print(\`a \${a}\`); + const b = await Promise.race([wave(2), sleep(frames(3)).then(() => -1), (async () => { await sleep(frames(4)); print("late"); return 0; })()]); + print(\`b \${b}\`);`); + expect(printed(r, 20)).toEqual(["3:a -1", "5:b 2"]); + }); + + it("a race of booleans, and Promise.any as a race (nothing in a program fails)", () => { + const r = compile(` + let hp = 3; + const ok = await Promise.any([until(() => hp <= 0).then(() => false), sleep(frames(2)).then(() => true)]); + if (ok) print("held");`); + expect(printed(r, 5)).toEqual(["2:held"]); + }); + + it("the plan's wave loop: a race in a for loop, the lost wave stopped each time", () => { + const r = compile(` + let left = 0; + async function wave(n: number): Promise { + left = n; + print(\`wave \${n}\`); + while (left > 0) { left--; await sleep(frames(1)); } + return true; + } + for (let n = 1; n <= 3; n++) { + const done = await Promise.race([wave(n * 2), sleep(frames(5)).then(() => false)]); + if (!done) { print(\`lost \${n}\`); break; } + print(\`cleared \${n}\`); + }`); + expect(printed(r, 30)).toEqual(["0:wave 2", "2:cleared 1", "2:wave 4", "6:cleared 2", "6:wave 6", "11:lost 3"]); + }); + + it("each player of a per-player program has its own branches and its own place in them", () => { + const r = compile(` + let mine = 0; + await Promise.all([ + (async () => { for (let i = 0; i < 3; i++) { mine++; await sleep(frames(1)); } })(), + sleep(frames(1)), + ]); + print(\`mine \${mine}\`);`, "{ owner: AllPlayers }"); + const sim = simulatePrograms(r.ir, 10, { strings: r.strings, players: [0, 1] }); + const lines = sim.events.filter((e) => e.action.type === ActionType.DisplayText).map((e) => `${e.cycle}:${e.player}:${e.text}`); + expect(lines).toEqual(["3:0:mine 3", "3:1:mine 3"]); + }); + + it("sleep() on its own still pauses a program that is not async", () => { + const r = compileScript(ts, { "main.ts": `program(() => { sleep(frames(2)); print("on"); });` }, NAMES, { lib: LIB }); + expect(r.diagnostics).toEqual([]); + expect(printed(r, 5)).toEqual(["2:on"]); + }); +}); + +describe("async functions and await: what is said", () => { + it("a promise nothing waits for", () => { + expect(messages(`sleep(frames(1));`)).toEqual(["sleep() gives a promise, and nothing waits for it here: await sleep(frames(1)). A program cannot yet start something and leave it running beside it."]); + expect(messages(`async function f(): Promise { await sleep(frames(1)); } f();`)).toEqual(["f() gives a promise, and nothing waits for it here: await f(). A program cannot yet start something and leave it running beside it."]); + expect(messages("", `program(() => { async function f(): Promise { await sleep(frames(1)); } f(); });`)).toEqual(["f() waits, and a program waits in an async function: program(async () => { … await f(); … })."]); + }); + + it("what Promise.all and race take, and what they give", () => { + expect(messages(`await Promise.all([]);`)).toEqual(["Promise.all([]) has nothing to wait for."]); + expect(messages(`const ps = [sleep(frames(1))]; await Promise.all(ps);`)).toContain("Promise.all() takes a list written where it is called: Promise.all([wave(1), sleep(seconds(90))])."); + expect(messages(`const v = await Promise.race([sleep(frames(1)), sleep(frames(2)).then(() => 1)]); print(\`\${v}\`);`)).toContain("v would be number | void: 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(() => …)."); + expect(messages(`const v: number = await Promise.all([sleep(frames(1)).then(() => 1)]) as never; print(\`\${v}\`);`)).toEqual(["Promise.all() gives no value yet: await it as a statement, and keep what each branch finds in variables of the program."]); + }); + + it("await where the game cannot leave and come back", () => { + expect(messages(`for (const u of unitsAt(locations.Anywhere, { owner: P1 })) { await sleep(frames(1)); u.kill(); }`).join("\n")).toContain("sleep() inside a loop over units"); + expect(messages(`[1, 2].forEach(async (x) => { await sleep(frames(x)); });`).join("\n")).toContain("An async function given to forEach()"); + }); +}); diff --git a/tests/eud-build.test.ts b/tests/eud-build.test.ts index 55e29b8..5a4e7f7 100644 --- a/tests/eud-build.test.ts +++ b/tests/eud-build.test.ts @@ -282,6 +282,33 @@ const FIXTURES: Record = { sleep(frames(12)); } }, { owner: AllPlayers });`, + tasks: `program(async () => { + let n = 0; + async function wave(k: number): Promise { + createUnit(P8, units.ZergZergling, k, locations.Anywhere); + await until(() => countUnits(P8, units.ZergZergling) === 0); + return k; + } + for (let i = 1; i <= 3; i++) { + const won = await Promise.race([wave(i * 2), sleep(seconds(30)).then(() => -1)]); + if (won < 0) defeat(); + await Promise.all([ + (async () => { while (n < i * 10) { n++; await sleep(frames(1)); } })(), + sleep(seconds(2)), + ]); + } + victory(); + });`, + tasksPerPlayer: `program(async () => { + let mine = 0; + let total = shared(0); + await Promise.all([ + (async () => { for (let i = 0; i < 5; i++) { mine++; await sleep(frames(3)); } })(), + (async () => { await until(() => mine >= 5); total += 1; })(), + ]); + const first = await Promise.race([until(() => total >= 2).then(() => true), sleep(seconds(5)).then(() => false)]); + if (first) setResources(CurrentPlayer, "add", 50, "ore"); + }, { owner: AllPlayers });`, }; function build(name: string, src: string): { out: number; triggers: number } { @@ -499,6 +526,6 @@ describe("the IR as the lowering reads it", () => { const ir = JSON.parse(serializeIr(r.ir, r.strings)); const actions = ir.programs[0].body.filter((s: { kind: string }) => s.kind === "action").map((s: { record: { text: unknown; wav: unknown } }) => [s.record.text, s.record.wav]); expect(actions).toEqual([["hello", 0], [0, "sound\\x.wav"]]); - expect(ir.version).toBe(14); + expect(ir.version).toBe(15); }); }); diff --git a/tests/functions.test.ts b/tests/functions.test.ts index b67436b..12a7ecd 100644 --- a/tests/functions.test.ts +++ b/tests/functions.test.ts @@ -175,7 +175,7 @@ describe("functions: arrays, and functions inside functions", () => { it("the functions go to the lowering with the program, their text written out", () => { const r = compile("function say(n: number) { displayText(`n`); setDeaths(P1, units.TerranMarine, 'set', n); } let k = 1; say(k); say(k + 1);"); const ir = JSON.parse(serializeIr(r.ir, r.strings)); - expect(ir.version).toBe(14); + expect(ir.version).toBe(15); expect(ir.programs[0].functions).toHaveLength(1); expect(JSON.stringify(ir.programs[0].functions)).toContain('"text":"n"'); }); diff --git a/tests/script-structured.test.ts b/tests/script-structured.test.ts index efe3938..c71ad1e 100644 --- a/tests/script-structured.test.ts +++ b/tests/script-structured.test.ts @@ -82,7 +82,7 @@ describe("structured: loops and branches", () => { expect(r.triggers).toEqual([]); expect(r.strings).toEqual([]); expect(r.ir).toHaveLength(1); - expect(r.ir[0]).toMatchObject({ version: 14, owner: 0, owners: [0], perPlayer: false }); + expect(r.ir[0]).toMatchObject({ version: 15, owner: 0, owners: [0], perPlayer: false }); const sim = run(r, 6); expect(sim.events.map((e) => `${e.cycle}:${ActionType.Victory === e.action.type ? "Victory" : e.action.type}`)).toEqual(["2:Victory"]); expect(value(sim, "n")).toBe(6); diff --git a/tests/service.test.ts b/tests/service.test.ts index ac6272c..f698c3e 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -264,9 +264,9 @@ describe("service: saving applies the script and builds its programs", () => { expect(compiles).toBe(before); expect(Object.keys(input.plugins)).toEqual(["trigscript", "eudTurbo"]); expect(input.plugins.trigscript).toEqual({ ir: "/work/files/trigscript.json" }); - expect(input.sources!.trigscript).toContain("IR_VERSION = 14"); + expect(input.sources!.trigscript).toContain("IR_VERSION = 15"); const ir = JSON.parse(input.files!["trigscript.json"]); - expect(ir.version).toBe(14); + expect(ir.version).toBe(15); expect(ir.programs).toHaveLength(1); expect(JSON.stringify(ir)).toContain('"text":"tick"'); // The library going away takes the contribution with it; coming back, it is made again. From d93eb58e72027c3ceeac0b4465c8be5a8576af3e Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 15:36:38 -0600 Subject: [PATCH 031/238] TrigScript 4: B1's first part as built Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 2eb3225..f8bafe2 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -742,6 +742,57 @@ Open: whether `Promise.race` cancels the tasks that lost. JavaScript does not ca them, and a wave that keeps running after its race was lost is rarely what the author meant. See *Open decisions*. +**As built, first part (2026-10-16, under Node and in the lifted game; branch `b1-async`).** +The design above was built in a smaller form. No task lives on the heap: every task is +known when the script is built, so its place and its wait are two cells, like a program's. + +- **An async function is inlined where it is awaited.** A function that sleeps has been + inlined since 3.0, so `await wave(n)` puts `wave`'s body in the program. Where it waits, + the program waits. `await f()` reads as `f()` in the front end. The type checker's + awaited type gives a call's kind, so `Promise` is a number. An async function is + never one called copy. +- **`until(test)`** is `while (!test()) sleep(frames(1))`. It asks once at once, then once + a frame, and goes on in the frame the answer is true. A condition (`until(bring(…))`) + is taken as it is. No new IR. +- **`Promise.all` / `race` / `any`** are the one new statement, `parallel` (IR 15). Each + item is a branch, and each branch is a coroutine of its own: its state and wait cells + (rows of twelve in a per-player program), its own jump table, and its own frame end. A + branch's `sleep` parks that branch alone. The program waits at the statement through a + resume point of its own, and the statement *is* that point: the first frame falls into + it and later frames jump to it. There, each branch that has not ended goes on, in + order. `all` goes on when every branch has ended. `race` goes on at the first one, and + the others are set to "ended". `any` is a race, since nothing in a program fails. An + item may be an async call, `sleep()`, `until()`, another `Promise.all` / `race`, an + async arrow called where it is written, or any of these with `.then(() => value)`. + `const v = await Promise.race([…])` keeps the winner's value: each branch sets a + variable as it ends, and only the winner gets to the end. +- **The lowering.** `python/trigscript.py` gained a `Coroutine` (state, wait, resume points, + frame end). The program's frame is the first coroutine, and a branch is another, lowered + with the same code. The simulator runs each branch as a generator of its own and hands + up any body run apart (recursion) from inside a branch. +- **What is said.** In an async function, a promise nothing awaits is an error that names + the `await` to write. Without async, an async call says to make the program async. + `sleep()` on its own in a program that is not async still pauses, as in 3.x. A race + whose value is kept and has a branch without one says to add `.then(() => …)`. + `Promise.all` has no value yet. `await` inside a loop over units or a text is refused, + as `sleep` is. +- **Checked.** `tests/async.test.ts` (12) and two eudplib build fixtures (`tasks`, + `tasksPerPlayer`). `probes/game-async.ts` runs in the lifted game and in the simulator, + and both pass all three tests at the same frames: the branches side by side, `until()` + in the frame its answer turns, a race's value and its losers stopped, and a wave whose + Zerglings the test kills. Cost: a waiting `Promise.all` of two branches is about 60 + conditions and actions a frame. +- **Decided while building, for the user to confirm:** a race's losers stop where they + are. JavaScript does not cancel them, but here a branch is a fixed pair of cells, so a + race met again in a loop could not run a second copy beside the first. Stopping is also + what the plan's own example means. (Open decisions, below.) +- **Not yet.** Starting a task and leaving it running (`spawn`, or an async call nothing + awaits) needs tasks that are not known when the script is built, which is the heap + frame of the design above. Also not yet: generators, `const [a, b] = await + Promise.all(…)`, async methods of a class, a parameter for `.then`'s function, and + recursion through `await` (it is the inline limit's error today). These are the second + part of B1, together with the hint for an async function that never awaits. + ### B2. Events (4.4.0) ```ts @@ -1018,7 +1069,10 @@ build, contract 2) ships first, then plugin-scm-sim, then TrigScript. nothing of the game is drawn. A5's "the game beside the code" draws, so it needs the person's own copy, as Test Map does. - **Does `Promise.race` cancel the losers?** JavaScript: no. Proposed: no, as JavaScript, - plus an explicit `task.cancel()` on what `spawn(…)` returns. + plus an explicit `task.cancel()` on what `spawn(…)` returns. *Built otherwise in B1's + first part (2026-10-16), for the user to confirm:* the losers stop, because a branch is + a fixed pair of cells and a race met again cannot run beside its last run. Once `spawn` + exists, a loser that should go on can be spawned. - **What a closure may capture.** Proposed above (B3): `const` by value, `let` only when the closure does not outlive its frame. The alternative is reference-counted environments: closer to JavaScript, more Python, and a new kind of fault. From d23fa20120dbf7b130842f6e88aa7003817aeecc Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 16:54:31 -0600 Subject: [PATCH 032/238] TrigScript 4: race losers stop, confirmed Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index f8bafe2..723c1ea 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -782,7 +782,7 @@ known when the script is built, so its place and its wait are two cells, like a in the frame its answer turns, a race's value and its losers stopped, and a wave whose Zerglings the test kills. Cost: a waiting `Promise.all` of two branches is about 60 conditions and actions a frame. -- **Decided while building, for the user to confirm:** a race's losers stop where they +- **Decided while building, confirmed by the user (2026-10-16):** a race's losers stop where they are. JavaScript does not cancel them, but here a branch is a fixed pair of cells, so a race met again in a loop could not run a second copy beside the first. Stopping is also what the plan's own example means. (Open decisions, below.) @@ -1069,8 +1069,8 @@ build, contract 2) ships first, then plugin-scm-sim, then TrigScript. nothing of the game is drawn. A5's "the game beside the code" draws, so it needs the person's own copy, as Test Map does. - **Does `Promise.race` cancel the losers?** JavaScript: no. Proposed: no, as JavaScript, - plus an explicit `task.cancel()` on what `spawn(…)` returns. *Built otherwise in B1's - first part (2026-10-16), for the user to confirm:* the losers stop, because a branch is + plus an explicit `task.cancel()` on what `spawn(…)` returns. *Decided otherwise in B1's + first part (2026-10-16), and confirmed by the user:* the losers stop, because a branch is a fixed pair of cells and a race met again cannot run beside its last run. Once `spawn` exists, a loser that should go on can be spawned. - **What a closure may capture.** Proposed above (B3): `const` by value, `let` only when From fab715ee3b35a0d2438366fa871579d2de7a5bca Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 17:21:50 -0600 Subject: [PATCH 033/238] =?UTF-8?q?TrigScript=204,=20B1:=20tasks=20?= =?UTF-8?q?=E2=80=94=20start(),=20void,=20a=20Task's=20running=20/=20cance?= =?UTF-8?q?l()=20/=20await?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A task is a copy of an async function's body that runs beside the program: each start() has slots, a row of the body's cells each, and runs its running tasks every frame after the program's turn (IR 16). Checked in the lifted game by probes/game-tasks.ts. Co-Authored-By: Claude Opus 5.5 --- compiler/declarations.ts | 21 ++ compiler/eud.ts | 10 + compiler/generated/trigscriptPy.ts | 2 +- compiler/hoist.ts | 7 +- compiler/input.ts | 3 + compiler/ir.ts | 36 ++- compiler/numbers.ts | 3 + compiler/runtime.ts | 3 +- compiler/simulateIr.ts | 170 +++++++++++- compiler/structured.ts | 173 +++++++++++- docs/ir.md | 30 ++- probes/game-tasks.ts | 66 +++++ python/trigscript.py | 418 +++++++++++++++++++++++------ tests/async.test.ts | 4 +- tests/eud-build.test.ts | 26 +- tests/functions.test.ts | 2 +- tests/script-structured.test.ts | 2 +- tests/service.test.ts | 4 +- tests/tasks.test.ts | 124 +++++++++ 19 files changed, 995 insertions(+), 109 deletions(-) create mode 100644 probes/game-tasks.ts create mode 100644 tests/tasks.test.ts diff --git a/compiler/declarations.ts b/compiler/declarations.ts index 552cb46..872f349 100644 --- a/compiler/declarations.ts +++ b/compiler/declarations.ts @@ -263,6 +263,27 @@ ${kw}function sleep(duration: Duration): Promise; * A condition on its own is asked again the same way: \`await until(bring(P1, units.AnyUnit, locations.Exit, ">=", 1))\`. */ ${kw}function until(test: (() => boolean) | Condition): Promise; +/** + * A task that start() started: \`t.cancel()\` stops it where it is, \`t.running\` is true until it ends or is cancelled, + * and \`await t\` waits for it to end. Kept in a variable, it is a number of the program, 0 for none. + */ +${kw}interface Task extends Promise { + readonly __task: true; + /** Stop the task where it is. Nothing happens when it has ended already. */ + cancel(): void; + /** True until the task ends or is cancelled. */ + readonly running: boolean; +} +/** + * Start a task and go on without waiting for it, inside an async program only: \`start(wave(3))\`, or + * \`start(async () => { … })\`. The arguments are worked out now; the task runs beside the program, from this frame on, + * and keeps running after the program ends. \`void wave(3)\` starts one too, without a Task to keep. + * + * Each start() is a body of its own, and \`max\` of its tasks (16 unless said) can run at once: a start with all of + * them running starts nothing, and the game says so. \`for (…) start(wave(n), { max: 40 })\`. A loop unrolled when the + * script is built has a start() for each of its turns. + */ +${kw}function start(task: Promise | (() => Promise), options?: { max?: number }): Task; /** True on the frame its condition becomes true, false until it becomes false and true again. Inside program(), in an if: \`if (rose(bring(…)))\`. */ ${kw}function rose(condition: Condition | boolean): boolean; /** True the first time its condition holds, never again. Inside program(), in an if. */ diff --git a/compiler/eud.ts b/compiler/eud.ts index b075e45..4897c71 100644 --- a/compiler/eud.ts +++ b/compiler/eud.ts @@ -57,6 +57,7 @@ function expressions(body: Stmt[], visit: (e: NumExpr | BoolExpr) => void, pick? case "not": expr(e.expr); break; case "test": expr(e.expr); break; case "edge": expr(e.cond); break; + case "running": expr(e.task); break; case "call": call(e.call); break; default: break; } @@ -85,6 +86,8 @@ function expressions(body: Stmt[], visit: (e: NumExpr | BoolExpr) => void, pick? case "for": if (s.cond) expr(s.cond); s.update.forEach(stmt); s.body.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; + case "start": call(s.call); break; + case "cancel": expr(s.task); break; case "switch": expr(s.value); s.cases.forEach((c) => c.body.forEach(stmt)); break; case "return": if (s.value) any(s.value); break; case "action": for (const v of s.variables ?? []) expr(v.expr); if (s.text) text(s.text); break; @@ -108,6 +111,7 @@ function statements(body: Stmt[], visit: (s: Stmt) => void) { case "for": s.body.forEach(stmt); s.update.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; + case "start": s.call.body.forEach(stmt); break; case "switch": s.cases.forEach((c) => c.body.forEach(stmt)); break; case "call": s.call.body.forEach(stmt); break; case "block": s.body.forEach(stmt); break; @@ -185,6 +189,7 @@ function checkWidths(body: Stmt[], out: ProgramDiagnostic[], decls: Map i.forEach(stmt)); break; case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; + case "start": for (const p of s.call.params) fits(p.decl.id, p.init, s.call.at); s.call.body.forEach(stmt); break; case "switch": s.cases.forEach((c) => c.body.forEach(stmt)); break; case "call": for (const p of s.call.params) fits(p.decl.id, p.init, s.call.at); s.call.body.forEach(stmt); break; case "block": s.body.forEach(stmt); break; @@ -204,6 +209,7 @@ function remarks(body: Stmt[], out: LineHint[]) { case "for": s.body.forEach(stmt); s.update.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; + case "start": s.call.body.forEach(stmt); break; case "switch": s.cases.forEach((c) => c.body.forEach(stmt)); break; case "call": s.call.body.forEach(stmt); break; case "block": s.body.forEach(stmt); break; @@ -333,6 +339,7 @@ function checkSleeps(body: Stmt[], out: ProgramDiagnostic[], functions: Map i.forEach(stmt)); break; case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; + case "start": s.call.body.forEach(stmt); break; case "switch": s.cases.forEach((c) => c.body.forEach(stmt)); break; case "call": s.call.body.forEach(stmt); break; case "block": s.body.forEach(stmt); break; @@ -380,6 +387,7 @@ export function serializeIr(programs: Program[], strings: readonly ScriptString[ case "and": case "or": return { ...e, items: e.items.map(expr) }; case "not": return { ...e, expr: expr(e.expr) }; case "edge": return { ...e, cond: expr(e.cond) }; + case "running": return { ...e, task: expr(e.task) }; default: return e; } }; @@ -408,6 +416,8 @@ export function serializeIr(programs: Program[], strings: readonly ScriptString[ case "for": return { ...s, ...(s.cond ? { cond: expr(s.cond) } : {}), update: s.update.map(stmt), body: s.body.map(stmt) }; case "unrolled": return { ...s, iterations: s.iterations.map((i) => i.map(stmt)) }; case "parallel": return { ...s, branches: s.branches.map((b) => b.map(stmt)) }; + case "start": return { ...s, call: call(s.call) }; + case "cancel": return { ...s, task: expr(s.task) }; case "switch": return { ...s, value: expr(s.value), cases: s.cases.map((c) => ({ ...c, body: c.body.map(stmt) })) }; case "return": return s.value ? { ...s, value: any(s.value) } : s; case "action": return { ...s, record: action(s.record) as unknown as ActionRecord, ...(s.variables ? { variables: s.variables.map((v) => ({ ...v, expr: expr(v.expr) })) } : {}), ...(s.text ? { text: text(s.text) } : {}) }; diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index ae8ce66..518f96a 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 15\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# 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 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# 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\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\nclass Storage:\n \"\"\"A variable's cell: plain, or a 12-slot row of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n self.store = EUDArray([0] * 12) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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, per_player, player_of):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.bits = decl.get(\"bits\")\n values = decl.get(\"values\")\n self.rowed = per_player and not decl.get(\"shared\") and values is None\n self.player_of = player_of\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 self.store = EUDArray([int(v) & U32 for v in values] if values is not None else [0] * (self.length * (12 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 player's row first.\"\"\"\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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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, per_player, player_of):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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\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\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.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player and not decl.get(\"shared\")\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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 12-slot rows of a per-player program.\"\"\"\n\n def __init__(self, decl, per_player, player_of):\n self.decl = decl\n self.rowed = per_player\n self.player_of = player_of\n make = (lambda: EUDArray([0] * 12)) 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\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. The program is one; each branch of a Promise.all / race is\n another, which the program runs inside its own frame for as long as it is waiting at that statement.\"\"\"\n\n def __init__(self, lowering):\n self.lowering = lowering\n self.state = EUDArray([0] * 12) if lowering.per_player else EUDVariable(0) # initial: program state\n self.wait = EUDArray([0] * 12) if lowering.per_player else EUDVariable(0) # initial: program state\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[self.lowering.player] if self.lowering.per_player else self.state\n\n def set_state(self, value):\n if self.lowering.per_player:\n self.state[self.lowering.player] = value\n else:\n self.state << value\n\n def get_wait(self):\n return self.wait[self.lowering.player] if self.lowering.per_player else self.wait\n\n def set_wait(self, value):\n if self.lowering.per_player:\n self.wait[self.lowering.player] = value\n else:\n self.wait << value\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 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 self.arrays = {a[\"id\"]: SliceStorage(a, self) if a.get(\"slice\") else InnerListStorage(a, self) if a.get(\"through\") else (ListStorage if a.get(\"dynamic\") else ArrayStorage)(a, self.per_player, self.player_of) for a in program.get(\"arrays\", [])}\n # The program's own coroutine, and the one being lowered (a branch of a Promise.all / race inside it).\n self.top = Coroutine(self)\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 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 declare(self, decl):\n if decl.get(\"kind\") == \"unit\":\n s = UnitStorage(decl, self.per_player, self.player_of)\n elif decl.get(\"kind\") == \"text\" and decl.get(\"text\") != \"id\":\n s = TextStorage(decl, self.per_player, self.player_of)\n else:\n s = Storage(decl, self.per_player, self.player_of)\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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 == \"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 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\"] == \"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] = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: the latch\n latch = self.latches[key]\n\n def get():\n return latch[self.player] if self.per_player else latch\n\n def put(v):\n if self.per_player:\n latch[self.player] = v\n else:\n latch << v\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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"action\":\n self.action(st)\n elif k == \"print\":\n self.print_(st)\n elif k == \"centerLocation\":\n self.center_location(st)\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 co.set_state(index)\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.set_wait(frames - 1)\n EUDJump(co.frame_end)\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) 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.set_state(index)\n parent.set_wait(0)\n EUDJump(parent.frame_end)\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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 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 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 = [], []\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\":\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 else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n group, room, first = [], 0, [True]\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 <= T[\"scratch\"] + TEXT_BYTES):\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 << T[\"append\"](pos, value.addr)\n else:\n if EUDIf()(pos <= T[\"scratch\"] + TEXT_BYTES):\n pos << T[\"append\"](pos, value.addr)\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 return pos, temps\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 == \"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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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.rowed))\n elif isinstance(s, TextStorage):\n for cell in (s.addr, s.block, s.len):\n pairs.append(self.cell_pair(cell, s.rowed))\n elif isinstance(s, Storage):\n pairs.append(self.cell_pair(s.store, s.rowed))\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.rowed))\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, rowed):\n if rowed:\n return (lambda: cell[self.player_of()]), (lambda v: cell.__setitem__(self.player_of(), 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 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 self.top.set_state(DONE)\n EUDJump(self.top.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 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 self.coroutine(self.top, self.p[\"body\"])\n\n def coroutine(self, co, body):\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, {})\n except Leave:\n pass\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 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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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\"):\n continue\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_of(s).items()}})\n for id_, a in low.arrays.items():\n if isinstance(a, (InnerListStorage, SliceStorage)):\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 16\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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\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\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: EUDArray([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: EUDArray([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\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\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 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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"centerLocation\":\n self.center_location(st)\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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 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 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 = [], []\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\":\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 else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n group, room, first = [], 0, [True]\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 <= T[\"scratch\"] + TEXT_BYTES):\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 << T[\"append\"](pos, value.addr)\n else:\n if EUDIf()(pos <= T[\"scratch\"] + TEXT_BYTES):\n pos << T[\"append\"](pos, value.addr)\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 return pos, temps\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 == \"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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 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 EUDJumpIf(self.again >= 1, sweep)\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 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_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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\n"; diff --git a/compiler/hoist.ts b/compiler/hoist.ts index af16e87..dbba4d0 100644 --- a/compiler/hoist.ts +++ b/compiler/hoist.ts @@ -108,7 +108,7 @@ function owningDeclaration(ts: typeof TS, decl: TS.Declaration): TS.Node { /** The library calls that must not appear inside a program body: they would run at build time, silently. */ const FORBIDDEN_INSIDE = new Set(["trigger", "program", "hyperTriggers", "game"]); /** The library calls the game answers: never hoisted, the structured compiler lowers them. */ -const GAME_CALLS = new Set(["random", "sleep", "until", "rose", "once", "shared"]); +const GAME_CALLS = new Set(["random", "sleep", "until", "start", "rose", "once", "shared"]); /** * Calls that read the game — `minerals(P1)`, and a comparing condition without its comparison * (`deaths(P1, unit)`) — or show text a program fills in (`print`). The call is never a @@ -137,7 +137,7 @@ export function isCallbackArgument(ts: typeof TS, node: TS.Node): node is TS.Arr /** * A function that is part of what a program waits for: the one `until(…)` asks every frame, the one given to `.then(…)`, - * and an async arrow called where it is written — `(async () => { … })()` in a `Promise.all`. Like a callback, its body + * an async arrow called where it is written — `(async () => { … })()` in a `Promise.all` — and the one start(…) runs as a task. Like a callback, its body * is the program's and is walked, not run when the script is built. */ export function isTaskFunction(ts: typeof TS, checker: TS.TypeChecker, node: TS.Node): node is TS.ArrowFunction | TS.FunctionExpression { @@ -149,7 +149,8 @@ export function isTaskFunction(ts: typeof TS, checker: TS.TypeChecker, node: TS. if (call.expression === at) return !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); if (!call.arguments.includes(at as TS.Expression)) return false; if (ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === "then") return true; - return libraryCallName(ts, checker, call) === "until"; + const lib = libraryCallName(ts, checker, call); + return lib === "until" || (lib === "start" && call.arguments[0] === at); } export function planProgram(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunction | TS.FunctionExpression, options: PlanOptions = {}): ProgramPlan { diff --git a/compiler/input.ts b/compiler/input.ts index 53583ed..319ac87 100644 --- a/compiler/input.ts +++ b/compiler/input.ts @@ -224,6 +224,7 @@ export function inputsOf(programs: Program[]): { sources: InputSource[]; mouse: case "and": case "or": e.items.forEach(expr); break; case "not": case "test": expr(e.expr); break; case "edge": expr(e.cond); break; + case "running": expr(e.task); break; case "call": call(e.call); break; default: break; } @@ -252,6 +253,8 @@ export function inputsOf(programs: Program[]): { sources: InputSource[]; mouse: case "for": if (s.cond) expr(s.cond); s.update.forEach(stmt); s.body.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; + case "start": call(s.call); break; + case "cancel": expr(s.task); break; case "switch": expr(s.value); s.cases.forEach((c) => c.body.forEach(stmt)); break; case "return": if (s.value) any(s.value); break; case "action": for (const v of s.variables ?? []) expr(v.expr); if (s.text) text(s.text); break; diff --git a/compiler/ir.ts b/compiler/ir.ts index 5f7cfde..1a137ab 100644 --- a/compiler/ir.ts +++ b/compiler/ir.ts @@ -10,6 +10,7 @@ import type { ActionRecord, ConditionRecord } from "../vendor/triggers"; import type { InputSource } from "./input"; /** + * 16: tasks — `start`: a task that runs beside what started it, a copy of an async function's body with the arguments it was started with; `cancel` and `running` on what `start()` gives back. A task's variables and arrays (`VarDecl.task`, `ArrayDecl.task`) are a row a task, as many rows as tasks of it can run at once. * 15: `parallel` — `await Promise.all([…])` / `Promise.race([…])`: branches that run side by side, each a coroutine of its own inside the program's frame. * 14: a text in the cells of three arrays — `textAt`, `storeText`, `releaseText` — which is what lets a row of an array of records hold one. * 13: texts — a `text` variable (`VarDecl.text`: the id of a text of the built map's table, or a text that was made: where it is, the block of the heap it owns, its length in characters), `TextExpr`, `assignText`, `textLoop`, a text among a `print`'s parts, in an action's text field (`action.text`) and as a unit type's name; `textLength` / `textIndexOf` / `textCode` among the numbers, `textCompare` / `textTest` among the conditions; `ArrayDecl.texts`. @@ -25,7 +26,7 @@ import type { InputSource } from "./input"; * 3: reads (`read`), `random(n)` as a number, the bitwise operators and `print` with its text in parts. * 2: a record's text and sound are written out in the JSON (1 had the map's string indices); `cyclesPerSecond` is gone. */ -export const IR_VERSION = 15; +export const IR_VERSION = 16; /** Where a node came from; `column` is 1-based like `line`. */ export interface At { file: string; line: number; column: number } @@ -52,6 +53,8 @@ export interface VarDecl { unsigned?: boolean; /** A backend's scratch value that dies with the statement (a function's result). */ temp?: boolean; + /** A variable of a task's body (`start`, its `task`): a cell for each of its slots, within each player's row. */ + task?: number; at: At; } @@ -73,6 +76,8 @@ export interface ArrayDecl { values?: number[]; /** A list of texts the script has, which a program indexes with a variable (`titles[level]`): `values` are their places here, the lowering's their ids in the built map's table. */ texts?: string[]; + /** An array declared in a task's body (`start`, its `task`): its cells once for each of its slots. */ + task?: number; /** * The array grows: something pushes to it, pops from it or sets its length. Its cells are a block of the * programs' heap (`HEAP_CELLS`), reached through a handle — where the block is, how many cells are in use, how @@ -118,6 +123,8 @@ export const HEAP_SMALLEST = 4; * largest frame any program has — and only there when some function recurses, so the limit is the same whatever the * programs' arrays hold: the simulator and the game stop at the same call. */ +/** How many tasks of one `start` can run at once, unless the start says (`{ max }`, 1 … 255). */ +export const TASK_MAX = 16; export const STACK_DEPTH = 1024; export const STACK_DEPTH_MIN = 16; export const STACK_DEPTH_MAX = 65536; @@ -364,6 +371,8 @@ export type BoolExpr = | { kind: "not"; expr: BoolExpr } /** `random()`: a coin toss, fresh at every evaluation. */ | { kind: "random"; at: At } + /** `t.running`: the task the number stands for (what `start()` gave) has not ended, nor been cancelled. 0 is no task. */ + | { kind: "running"; task: NumExpr; at: At } /** `if (target)`: the variable holds a unit and that unit is still on the map. */ | { kind: "unitAlive"; unit: UnitExpr; at: At; label: string } /** `u == target`: both name the same unit of the game (and there is one). */ @@ -497,6 +506,19 @@ export type Stmt = * own; `break`, `continue` and `return` never leave a branch. */ | { kind: "parallel"; mode: "all" | "race"; branches: Stmt[][]; at: At; label: string } + /** + * `start(wave(n))`: a task — `call`, an async function's body inlined, its parameters copies of this start's arguments — + * that runs beside what started it. Each `start` statement is a body of its own, `task` its number (they are numbered + * from 1 in the order they are met, a task inside another's body after it), with `max` slots: that many of its tasks can + * run at once, and a start with every slot taken starts nothing (the game says so once). A task starts in the frame it + * is started in, once the program has given the frame back (it slept, waited or ended); from then on it goes on every + * frame, after the program, the tasks in order of their number and slot. It runs on after the program ends. `handle`: + * the variable that takes a number for the task — `slot | task << 8 | generation << 16`, the generation one more at + * every start in the slot — or 0 when nothing started. + */ + | { kind: "start"; task: number; max: number; call: Call; handle?: string; at: At; label: string } + /** `t.cancel()`: the task stops where it is. Nothing happens when it has ended, or the number is no task's. */ + | { kind: "cancel"; task: NumExpr; at: At; label: string } /** A trigger action; each of `variables` names a field that takes an expression's value instead of the record's. */ /** * `text`: the action's text is the program's. One of the table goes into the action as its id. One that was made is @@ -557,6 +579,15 @@ export function eachCall(root: unknown, visit: (c: Call) => void): void { for (const v of Object.values(o)) if (v && typeof v === "object") eachCall(v, visit); } +/** Every `start` of a piece of IR, in the order of their numbers: outer ones before the ones in their bodies. */ +export function eachStart(root: unknown, visit: (s: Extract) => void): void { + if (Array.isArray(root)) { for (const x of root) eachStart(x, visit); return; } + if (!root || typeof root !== "object") return; + const o = root as Record; + if (o.kind === "start" && typeof o.task === "number" && o.call && typeof o.call === "object") visit(o as Extract); + for (const v of Object.values(o)) if (v && typeof v === "object") eachStart(v, visit); +} + export const isUnitExpr = (e: NumExpr | BoolExpr | UnitExpr | TextExpr): e is UnitExpr => e.kind === "unitNull" || e.kind === "unitVar" || e.kind === "pick" || e.kind === "unitAt" || (e.kind === "call" && e.call.result?.kind === "unit"); @@ -601,6 +632,8 @@ export function declarations(body: Stmt[]): VarDecl[] { case "for": if (s.cond) init(s.cond); s.update.forEach(stmt); s.body.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; + case "start": call(s.call); break; + case "cancel": expr(s.task); break; case "switch": expr(s.value); s.cases.forEach((c) => c.body.forEach(stmt)); break; case "return": if (s.value) init(s.value); break; case "action": for (const v of s.variables ?? []) expr(v.expr); if (s.text) text(s.text); break; @@ -661,6 +694,7 @@ export function declarations(body: Stmt[]): VarDecl[] { case "ternary": bool(b.cond); bool(b.whenTrue); bool(b.whenFalse); break; case "textCompare": text(b.left); text(b.right); break; case "textTest": text(b.of); text(b.find); break; + case "running": expr(b.task); break; case "call": call(b.call); break; default: break; } diff --git a/compiler/numbers.ts b/compiler/numbers.ts index 66a47d0..1f7e015 100644 --- a/compiler/numbers.ts +++ b/compiler/numbers.ts @@ -171,6 +171,7 @@ export function typeNumbers(program: Program): ProgramDiagnostic[] { case "and": case "or": e.items = e.items.map(bool); return e; case "not": e.expr = bool(e.expr); return e; case "edge": e.cond = bool(e.cond); return e; + case "running": e.task = num(e.task)[0]; return e; case "ternary": e.cond = bool(e.cond); e.whenTrue = bool(e.whenTrue); e.whenFalse = bool(e.whenFalse); return e; case "unitAlive": case "unitFlag": unit(e.unit); return e; case "unitSame": unit(e.left); unit(e.right); return e; @@ -279,6 +280,8 @@ export function typeNumbers(program: Program): ProgramDiagnostic[] { case "for": if (s.cond) s.cond = bool(s.cond); s.update.forEach(stmt); s.body.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; + case "start": call(s.call); break; + case "cancel": s.task = num(s.task)[0]; break; case "switch": s.value = num(s.value)[0]; s.cases.forEach((c) => c.body.forEach(stmt)); break; case "return": if (s.value && result) s.value = valueFor(result, s.value, s.at); break; case "action": for (const v of s.variables ?? []) v.expr = floor(v.expr, s.at); if (s.text) s.text = text(s.text); break; diff --git a/compiler/runtime.ts b/compiler/runtime.ts index 11699ee..969f1a6 100644 --- a/compiler/runtime.ts +++ b/compiler/runtime.ts @@ -427,6 +427,7 @@ export function createRuntime(names: ScriptNames, collector: Collector, options: // The word from before 3.0, when a program's clock was the trigger cycle; a program's clock is the frame now. rt.cycles = (n: unknown): DurationValue => ({ __trigscript: "duration", cycles: Math.max(1, Math.round(number(n, "cycles"))) }); rt.sleep = () => { throw new ScriptError("sleep() pauses a program: use it inside program(), as a statement — sleep(seconds(2))."); }; + rt.start = () => { throw new ScriptError("start() starts a task inside a program: start(wave(3)), inside program(async () => { … })."); }; rt.until = () => { throw new ScriptError("until() waits inside a program: await until(() => …), inside program(async () => { … })."); }; rt.rose = () => { throw new ScriptError("rose() is true on the cycle its condition becomes true: use it inside program(), in an if."); }; rt.once = () => { throw new ScriptError("once() is true the first time its condition holds: use it inside program(), in an if."); }; @@ -692,7 +693,7 @@ export function runtimeNames(names: ScriptNames): string[] { out.push("CurrentPlayer", "AllPlayers"); out.push(...CONDITION_IDENTS.keys(), ...ACTION_IDENTS.keys(), "preserve"); out.push("condition", "action", "memory", "setMemory", "disabled", "not", "trigger", "hyperTriggers", "program", "game", "random"); - out.push("seconds", "minutes", "frames", "cycles", "sleep", "until", "rose", "once", "shared", "clamp", "u32", "i32"); + out.push("seconds", "minutes", "frames", "cycles", "sleep", "until", "start", "rose", "once", "shared", "clamp", "u32", "i32"); out.push(...READER_NAMES, "races", "slots", "name", "color", "print"); out.push(...UNIT_CALL_NAMES, "colors", ...INPUT_CALL_NAMES); return out; diff --git a/compiler/simulateIr.ts b/compiler/simulateIr.ts index 4e4a959..339b48a 100644 --- a/compiler/simulateIr.ts +++ b/compiler/simulateIr.ts @@ -36,7 +36,7 @@ */ import { ActionType, Comparison, ConditionType, SetModifier, SWITCH_COUNT, SwitchAction, SwitchState, type ActionRecord, type ConditionRecord } from "../vendor/triggers"; import { emptyAction, ResourceType } from "../vendor/triggers"; -import { HEAP_CELLS, HEAP_SMALLEST, STACK_DEPTH, TEXT_BYTES, TEXT_FIELD_BYTES, heapCells, isTextExpr, stackDepth } from "./ir"; +import { HEAP_CELLS, HEAP_SMALLEST, STACK_DEPTH, TEXT_BYTES, TEXT_FIELD_BYTES, eachStart, heapCells, isTextExpr, programDeclarations, stackDepth } from "./ir"; import type { TextExpr, ArrayDecl, At, BoolExpr, Call, NumExpr, Program, ReadSource, Stmt, TableCell, TextPart, UnitExpr, UnitFilter, UnitNumField, UnitVerb, VarDecl } from "./ir"; import { cellMax } from "./tables"; import { inputsOf, keyName, matchChat, parseChatPattern, type ChatPattern, type InputSource, type MouseButton } from "./input"; @@ -122,6 +122,21 @@ class Halt extends Error {} /** The program stops for good, as the game stops it: its stack ran out. Said as a fault where it happened. */ class Stopped extends Error {} +/** A slot of a `start`: the task in it (none: free), its generation, the frame it last ran in, and its own cells. */ +interface TaskSlot { + exec: Exec | null; + apart: Exec[]; + gen: number; + ran: number; + vars: Map; + units: Map; + texts: Map; + arrays: Map; + latches: Map; +} +/** A `start` statement's tasks: its slots, and the ids of the variables and arrays a slot has a copy of. */ +interface TaskPool { start: Extract; vars: string[]; arrays: ArrayDecl[]; slots: TaskSlot[]; said: boolean } + /** One program's run: its variables, its latches, and the coroutine that is its body. */ /** What a recursive function holds, kept for the length of a call that may come back into it. */ interface Frame { @@ -197,7 +212,8 @@ class ProgramRun { readonly bits = new Map(); /** The `u32` variables. */ readonly unsigned = new Set(); - readonly latches = new Map(); + /** Every latch's state; a task has its own, swapped in while it runs. */ + latches = new Map(); /** The program's arrays: their cells, as a variable's value is kept. */ readonly arrays: Map = new (class extends Map { run!: ProgramRun; @@ -210,7 +226,13 @@ class ProgramRun { /** How many calls deep the program is in functions that call themselves: frames on the stack. Nothing is on it between frames. */ depth = 0; /** The bodies being run apart from the one that called them, innermost last (`Apart`). */ - private readonly apart: Exec[] = []; + private apart: Exec[] = []; + /** The `start` statements' tasks, in order of their numbers. */ + private readonly pools: TaskPool[] = []; + /** Frames the tasks have run, for a slot to tell whether it has had its turn in this one. */ + private taskFrame = 0; + /** A task was started during this frame's turn of the tasks: they are gone through again, for it to start in this frame too. */ + private again = false; readonly sim: ProgramSimulation; readonly index: number; readonly program: Program; @@ -235,6 +257,14 @@ class ProgramRun { this.arrays.set(decl.id, held); } this.body = this.run(); + const decls = programDeclarations(this.program); + const starts: Extract[] = []; + eachStart([program.body, program.functions ?? []], (st) => starts.push(st)); + starts.sort((a, b) => a.task - b.task); + for (const start of starts) { + const slots = Array.from({ length: start.max }, (): TaskSlot => ({ exec: null, apart: [], gen: 0, ran: 0, vars: new Map(), units: new Map(), texts: new Map(), arrays: new Map(), latches: new Map() })); + this.pools.push({ start, vars: decls.filter((d) => d.task === start.task).map((d) => d.id), arrays: (program.arrays ?? []).filter((a) => a.task === start.task), slots, said: false }); + } } private *run(): Exec { @@ -243,26 +273,95 @@ class ProgramRun { return "next"; } - /** One frame: resume the body until it gives the frame back or ends. */ + /** One frame: resume the body until it gives the frame back or ends, then each task that is running. */ tick(): void { - if (this.done || !this.body) return; this.steps = 0; + if (!this.done && this.body) { + try { + if (this.drive(this.body)) { this.done = true; this.body = null; } + } catch (err) { + this.apart.length = 0; + if (!(err instanceof Stopped)) throw err; + this.done = true; + this.body = null; + this.depth = 0; + } + } + this.runTasks(); + } + + /** Whether a task of the program is still running. */ + get tasksRunning(): boolean { return this.pools.some((p) => p.slots.some((s) => s.exec)); } + + /** + * The tasks' turn, after the program's: each that is running goes on until it gives the frame back or ends, in order of + * their start's number and then of their slot. A task started meanwhile has its first turn in this frame too. + */ + private runTasks(): void { + if (!this.pools.length) return; + this.taskFrame++; + do { + this.again = false; + for (const pool of this.pools) { + for (const slot of pool.slots) { + if (!slot.exec || slot.ran === this.taskFrame) continue; + slot.ran = this.taskFrame; + this.runSlot(pool, slot); + } + } + } while (this.again); + } + + /** A task's turn: its cells in place of the slot's, its body driven as the program's is. */ + private runSlot(pool: TaskPool, slot: TaskSlot): void { + const exec = slot.exec!; + const outer = this.apart, latches = this.latches; + this.swap(pool, slot, true); + this.apart = slot.apart; + this.latches = slot.latches; try { - if (this.drive()) { this.done = true; this.body = null; } + if (this.drive(exec)) { slot.exec = null; slot.apart.length = 0; } } catch (err) { - this.apart.length = 0; + slot.apart.length = 0; if (!(err instanceof Stopped)) throw err; - this.done = true; - this.body = null; + slot.exec = null; this.depth = 0; + } finally { + this.apart = outer; + this.latches = latches; + this.swap(pool, slot, false); + } + } + + /** A slot's cells into the run's (`into`), or back out of them: between turns nothing of a task is among the run's. */ + private swap(pool: TaskPool, slot: TaskSlot, into: boolean): void { + const move = (run: Map, own: Map, id: string) => { + if (into) { if (own.has(id)) run.set(id, own.get(id)!); else run.delete(id); } + else { if (run.has(id)) own.set(id, run.get(id)!); else own.delete(id); run.delete(id); } + }; + for (const id of pool.vars) { move(this.vars, slot.vars, id); move(this.unitVars, slot.units, id); move(this.textVars, slot.texts, id); } + for (const a of pool.arrays) { + if (into) this.arrays.set(a.id, slot.arrays.get(a.id) ?? this.newHeld(a)); + else { const held = (this.arrays as Map).get(a.id); if (held) slot.arrays.set(a.id, held); } } } + private newHeld(decl: ArrayDecl): Held { + return { decl, room: 0, cells: decl.values ? [...decl.values] : decl.dynamic ? [] : new Array(decl.length).fill(decl.kind === "number" ? 0 : false) }; + } + + /** The pool and slot a task's number names, while that task is in it; undefined for a number that is no running task's. */ + private taskOf(handle: number): { pool: TaskPool; slot: TaskSlot } | undefined { + const pool = this.pools.find((p) => p.start.task === ((handle >>> 8) & 0xff)); + const slot = pool?.slots[handle & 0xff]; + return pool && slot?.exec && slot.gen === handle >>> 16 ? { pool, slot } : undefined; + } + /** The run until it gives the frame back (false) or ends (true): the body, and above it each body that was handed over to be run apart. */ - private drive(): boolean { + private drive(body: Exec): boolean { let send: unknown; for (;;) { - const top = this.apart[this.apart.length - 1] ?? this.body!; + const top = this.apart[this.apart.length - 1] ?? body; const r = top.next(send); send = undefined; if (r.done) { @@ -671,6 +770,7 @@ class ProgramRun { return false; } case "ternary": return (yield* this.bool(e.cond)) ? yield* this.bool(e.whenTrue) : yield* this.bool(e.whenFalse); + case "running": return !!this.taskOf((yield* this.num(e.task)) >>> 0); case "call": return Boolean(yield* this.call(e.call)); case "textCompare": { const a = yield* this.text(e.left); @@ -699,6 +799,14 @@ class ProgramRun { else this.store(decl.id, yield* this.init(e as NumExpr | BoolExpr, decl.kind)); } + /** A task's body, as its slot runs it: what it returns into is the task's own, and nothing reads it. */ + private *taskBody(c: Call): Exec { + if (c.result) this.declare(c.result.decl); + const flow = yield* this.block(c.body, { fn: { result: c.result?.decl } }); + if (flow === "break" || flow === "continue") throw new Error(`${flow} inside a task reached its end (line ${c.at.line}).`); + return "next"; + } + private *call(c: Call): Gen { if (c.result) this.declare(c.result.decl); const fn = c.fn ? this.program.functions?.find((f) => f.id === c.fn) : undefined; @@ -1016,6 +1124,44 @@ class ProgramRun { return "next"; } case "centerLocation": { const x = yield* this.amount(s.x); const y = yield* this.amount(s.y); this.sim.centre(s.location, x, y); return "next"; } + case "start": { + const pool = this.pools.find((p) => p.start === s); + if (!pool) throw new Error(`The start of task ${s.task} is not one of the program's (line ${s.at.line}).`); + // The arguments as they are now, before any parameter is set. + const values: (Value | SimUnit | HeldText | null)[] = []; + for (const p of s.call.params) values.push(p.decl.kind === "unit" ? yield* this.unit(p.init as UnitExpr) : p.decl.kind === "text" ? this.owned(yield* this.text(p.init as TextExpr), s.at) : this.kept(yield* this.init(p.init as NumExpr | BoolExpr, p.decl.kind), p.decl)); + const i = pool.slots.findIndex((slot) => !slot.exec); + if (i < 0) { + for (const v of values) if (v && typeof v === "object" && "room" in v && v.room) this.sim.heap.give(v.room); + if (!pool.said) this.sim.faults.push({ cycle: this.sim.cycle, frame: this.sim.frame, program: this.index, at: s.at, message: `${s.call.name ?? "The task"} did not start: all ${s.max} of this start's tasks are running. start(…, { max: … }) lets more run at once.` }); + pool.said = true; + if (s.handle) this.store(s.handle, 0); + return "next"; + } + const slot = pool.slots[i]; + slot.gen = slot.gen >= 0xffff ? 1 : slot.gen + 1; + s.call.params.forEach((p, k) => { + this.declare(p.decl); + if (p.decl.kind === "unit") slot.units.set(p.decl.id, values[k] as SimUnit | null); + else if (p.decl.kind === "text") { const old = slot.texts.get(p.decl.id); if (old?.room) this.sim.heap.give(old.room); slot.texts.set(p.decl.id, values[k] as HeldText); } + else slot.vars.set(p.decl.id, values[k] as Value); + // (declare() put the parameter among the run's own: it is the slot's.) + this.vars.delete(p.decl.id); + this.unitVars.delete(p.decl.id); + this.textVars.delete(p.decl.id); + }); + slot.exec = this.taskBody(s.call); + slot.apart = []; + slot.ran = 0; + this.again = true; + if (s.handle) this.store(s.handle, i | (s.task << 8) | (slot.gen << 16)); + return "next"; + } + case "cancel": { + const t = this.taskOf((yield* this.num(s.task)) >>> 0); + if (t) { t.slot.exec = null; t.slot.apart.length = 0; } + return "next"; + } case "call": { yield* this.call(s.call); return "next"; } case "block": return yield* this.block(s.body, ctx); case "remark": return "next"; @@ -1398,7 +1544,7 @@ export class ProgramSimulation { } /** Whether every program has ended. */ - finished(): boolean { return this.runs.every((r) => r.done); } + finished(): boolean { return this.runs.every((r) => r.done && !r.tasksRunning); } /** A program's run: the program by its place in the list or its name, and of a per-player program the player's (the first by default). */ runOf(program: number | string = 0, player?: number): ProgramRun | undefined { diff --git a/compiler/structured.ts b/compiler/structured.ts index 9368da6..7c916d7 100644 --- a/compiler/structured.ts +++ b/compiler/structured.ts @@ -66,7 +66,7 @@ import { hasTextMark, isAction, isBuilder, isChat, isCondition, isDuration, isGa import { cellMax } from "./tables"; import { HANDLE_PARTS, Scope, TEXT_PARTS, THIS, UNIT_PARTS, type Binding, type Place, type RowField, type RowShape } from "./scope"; import { ACTIONS_WITH_MODIFIER, LowerError } from "./lower"; -import { I32_MAX, I32_MIN, IR_VERSION, TEXT_BYTES, U32_MAX, UNIT_FLAGS, UNIT_NUM_FIELDS, UNIT_WRITABLE, eachCall, textHasId, type ActionVariable, type ArithOp, type ArrayDecl, type At, type BoolExpr, type Call, type CompareOp, type FuncDecl, type NumExpr, type Program, type Stmt, type TextExpr, type TextPart, type UnitExpr, type UnitFlag, type UnitNumField, type UnitVerb, type VarDecl } from "./ir"; +import { I32_MAX, I32_MIN, IR_VERSION, TASK_MAX, TEXT_BYTES, U32_MAX, UNIT_FLAGS, UNIT_NUM_FIELDS, UNIT_WRITABLE, declarations, eachCall, eachStart, programDeclarations, textHasId, type ActionVariable, type ArithOp, type ArrayDecl, type At, type BoolExpr, type Call, type CompareOp, type FuncDecl, type NumExpr, type Program, type Stmt, type TextExpr, type TextPart, type UnitExpr, type UnitFlag, type UnitNumField, type UnitVerb, type VarDecl } from "./ir"; /** The outcome of a thunk, kept so it runs once whatever asks. */ type Outcome = { ok: true; value: unknown } | { ok: false; error: unknown }; @@ -257,6 +257,10 @@ export class Structured { /** The body being walked: the program's, or the game function being inlined. */ private body: Body; private inlineDepth = 0; + /** The `start` statements so far: the next one's number is one more. */ + private taskCount = 0; + /** The inline depth at which the next call is a task's (`start(wave(n))`): its parameters are copies of the arguments, never the caller's variables. */ + private copyArgsAt = -1; private scope: Scope = new Scope(null); /** The program's outermost scope: what an inlined function of the body closes over. */ private readonly topScope = this.scope; @@ -295,6 +299,7 @@ export class Structured { this.c.error(statements[statements.length - 1] ?? this.body.plan.body, err.message); } this.settleFunctions(program); + this.checkTasks(program); return { program, nodeOf: (node) => this.nodes.get(node) }; } @@ -3601,6 +3606,8 @@ export class Structured { private kindOf(type: TS.Type): Kind | null { const { ts } = this; + // What start() gives back: a number of the program that stands for the task. + if (this.isTaskType(type)) return "number"; type = this.awaited(type); // `c ? 1 : 0` is `0 | 1`: a union of numbers is a number. const isNumber = (t: TS.Type): boolean => (t.flags & ts.TypeFlags.NumberLike) !== 0 || (t.isIntersection() && t.types.some(isNumber)) || (t.isUnion() && t.types.every(isNumber)); @@ -3612,6 +3619,11 @@ export class Structured { return null; } + /** Whether a type is the Task start() gives back. */ + private isTaskType(type: TS.Type): boolean { + return !!type.getProperty("__task") && (type.flags & this.ts.TypeFlags.Any) === 0; + } + private isUnitTyped(e: TS.Expression): boolean { return this.kindOf(this.c.checker.getTypeAtLocation(e)) === "unit"; } @@ -4184,7 +4196,7 @@ export class Structured { const text = e.getText(); const mode = this.promiseCall(e); const what = this.isLibraryCall(e, "sleep") ? "sleep()" : this.isLibraryCall(e, "until") ? "until()" : mode ? `Promise.${mode}()` : `${this.unwrap(e.expression).getText()}()`; - if (this.inAsync(e)) this.c.error(e, `${what} gives a promise, and nothing waits for it here: await ${text}. A program cannot yet start something and leave it running beside it.`); + if (this.inAsync(e)) this.c.error(e, `${what} gives a promise, and nothing waits for it here: await ${text}${mode || what === "sleep()" || what === "until()" ? "" : ` — or, to leave it running beside the program, start(${text})`}.`); else this.c.error(e, `${what} waits, and a program waits in an async function: program(async () => { … await ${text}; … }).`); } @@ -4205,6 +4217,15 @@ export class Structured { const value = { kind: "var" as const, id: call.result.decl.id }; this.emit(into.kind === "number" ? { kind: "assign", target: into.decl.id, value, at: call.at, label: call.label } : { kind: "assignBool", target: into.decl.id, value, at: call.at, label: call.label }, node); }; + if (this.isTaskType(this.c.checker.getTypeAtLocation(e))) { + // `await t`, `await start(…)`: until the task has ended. + const t = this.num(e); + if (!t) return; + const at = this.at(e), label = this.label(e); + this.emit({ kind: "while", cond: { kind: "running", task: t, at }, body: [{ kind: "sleep", cycles: 1, at, label }], at, label }, e); + valueless("A task"); + return; + } if (ts.isCallExpression(e)) { if (this.isLibraryCall(e, "sleep")) { this.sleepStatement(e); valueless("sleep()"); return; } if (this.isLibraryCall(e, "until")) { this.untilStatement(e); valueless("until()"); return; } @@ -4273,11 +4294,145 @@ export class Structured { return kind === "number" ? varRef(t) : { kind: "var", id: t.id }; } + /** `start(…)`'s `{ max }`: how many of its tasks can run at once, 16 unless said. */ + private startMax(arg: TS.Expression | undefined): number | null { + if (!arg) return TASK_MAX; + const h = this.evaluate(arg); + const max = h && typeof h.value === "object" && h.value !== null ? (h.value as { max?: unknown }).max : undefined; + if (!h || (max !== undefined && (typeof max !== "number" || !Number.isInteger(max) || max < 1 || max > 255))) { + this.c.error(arg, "start()'s options are written out, as { max: 40 }: how many of this start's tasks can run at once, a whole number from 1 to 255."); + return null; + } + return (max as number | undefined) ?? TASK_MAX; + } + + /** `start(task, options?)` as a statement, or into `handle` — the variable that takes the number the task is known by. */ + private startCall(call: TS.CallExpression, handle?: VarDecl) { + if (call.arguments.length < 1 || call.arguments.length > 2) { this.c.error(call, "start() takes a task, and options if you like: start(wave(3)), start(async () => { … }, { max: 4 })."); return; } + const max = this.startMax(call.arguments[1]); + if (max === null) return; + this.startTask(call.arguments[0], call, max, handle); + } + + /** `const t = start(…)`: the task's number, in a variable of its own. */ + private startValue(e: TS.CallExpression): NumExpr { + const at = this.at(e), label = this.label(e); + const t = this.newVar("(task)", "number", at, { temp: true }); + this.emit({ kind: "declare", decl: t, init: num(0), at, label }, e); + this.startCall(e, t); + return varRef(t); + } + + /** + * A task, started here and left running (`start` in the IR): `start(wave(n))` — the async function's body inlined for + * this start alone, its parameters copies of the arguments as they are now — or `start(async () => { … })`. The body's + * variables and arrays are the task's: a cell for each of its slots, so tasks of the same start keep theirs apart. + */ + private startTask(expr: TS.Expression, node: TS.Node, max: number, handle?: VarDecl) { + const { ts } = this; + const e = this.unwrap(expr); + const task = ++this.taskCount; + const refuse = () => this.c.error(e, `start() takes a call of an async function — start(wave(3)) — or an async function: start(async () => { … }). For anything else, start an async function that awaits it: start(async () => { await ${e.getText()}; }).`); + let call: Call | undefined; + const fn = ts.isCallExpression(e) ? this.unwrap(e.expression) : e; + const caught = (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) ? this.loopCapture(fn, node) : undefined; + if (caught) { this.c.error(caught, `This task reads ${caught.text}, which the loop around the start() changes: JavaScript gives each turn its own, and here the task would find it as it is when it reads it. Hand it over as an argument instead — an async function that takes it, start(f(${caught.text})).`); return; } + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { + if (e.parameters.length) { this.c.error(e, "The function start() runs takes nothing: start(async () => { … })."); return; } + if (!ts.isCallExpression(node)) { refuse(); return; } + call = this.callback(expr, "start", [], "any", node, true); + } else if (ts.isCallExpression(e) && this.isPromise(e)) { + // The call's arguments are worked out at the start: the parameters are copies, whatever the function does with them. + this.copyArgsAt = this.inlineDepth; + let stmts: Stmt[]; + try { stmts = this.collect(() => this.task(e)); } finally { this.copyArgsAt = -1; } + // Only a call of a function of the script is a body to run as a task; sleep(), until(), Promise.all and .then() are not. + call = stmts.length === 1 && stmts[0].kind === "call" && !stmts[0].call.fn ? stmts[0].call : undefined; + if (!call) { if (stmts.length) refuse(); return; } + } else { refuse(); return; } + if (!call) return; + // What the body declares is the task's own: one of each a slot. A start inside it marked its own task's first. + for (const d of [...call.params.map((p) => p.decl), ...(call.result ? [call.result.decl] : []), ...declarations(call.body)]) d.task ??= task; + const arrays = new Set(); + const visit = (root: unknown): void => { + if (Array.isArray(root)) { root.forEach(visit); return; } + if (!root || typeof root !== "object") return; + const o = root as Record; + if (o.kind === "declareArray" && typeof o.array === "string") arrays.add(o.array); + Object.values(o).forEach((v) => { if (v && typeof v === "object") visit(v); }); + }; + visit(call.body); + for (const a of this.arrays) if (arrays.has(a.id)) a.task ??= task; + this.emit({ kind: "start", task, max, call, ...(handle ? { handle: handle.id } : {}), at: this.at(node), label: this.label(node) }, node); + } + + /** + * A task's body may use the program's variables, its own, and those of the tasks it starts — not those of a task it is + * started from: which slot of that one's is in its cells is settled when that one runs, not when this one does. + */ + private checkTasks(program: Program) { + const owner = new Map(); + for (const d of programDeclarations(program)) if (d.task !== undefined) owner.set(d.id, { task: d.task, name: d.name }); + for (const a of this.arrays) if (a.task !== undefined) owner.set(a.id, { task: a.task, name: a.name }); + eachStart(program.body, (st) => { + const mine = new Set([st.task]); + eachStart(st.call.body, (inner) => mine.add(inner.task)); + let found: string | undefined; + const visit = (root: unknown): void => { + if (found || !root || typeof root !== "object") return; + if (Array.isArray(root)) { root.forEach(visit); return; } + const o = root as Record; + for (const key of ["id", "target", "array", "handle"]) { + const v = o[key]; + const of = typeof v === "string" ? owner.get(v) : undefined; + if (of && !mine.has(of.task)) { found = of.name; return; } + } + Object.values(o).forEach(visit); + }; + visit(st.call.body); + const node = this.nodes.get(st); + if (found && node) this.c.error(node, `This task uses ${found}, a variable of the task it is started from: which of that task's copies it would find is not settled. Hand it over as an argument instead — start(f(${found})).`); + }); + } + + /** A variable of the program, declared inside a loop around `at`, that `fn` reads: what a task must not close over. */ + private loopCapture(fn: TS.ArrowFunction | TS.FunctionExpression, at: TS.Node): TS.Identifier | undefined { + const { ts } = this; + const loops: TS.Node[] = []; + for (let n: TS.Node | undefined = at.parent; n; n = n.parent) { + if (ts.isIterationStatement(n, false)) loops.push(n); + if (ts.isFunctionLike(n)) break; + } + if (!loops.length) return undefined; + let found: TS.Identifier | undefined; + const visit = (n: TS.Node): void => { + if (found) return; + if (ts.isIdentifier(n)) { + const d = this.c.checker.getSymbolAtLocation(n)?.valueDeclaration; + if (d && ts.isVariableDeclaration(d) && !(d.pos >= fn.pos && d.end <= fn.end) && loops.some((l) => d.pos >= l.pos && d.end <= l.end) && this.bindingOf(n)?.kind === "var") found = n; + } + ts.forEachChild(n, visit); + }; + visit(fn.body); + return found; + } + + /** `t.cancel()`: the task stops where it is. */ + private cancelTask(e: TS.CallExpression, receiver: TS.Expression) { + if (e.arguments.length) { this.c.error(e, "cancel() takes nothing."); return; } + const task = this.num(receiver); + if (task) this.emit({ kind: "cancel", task, at: this.at(e), label: this.label(e) }, e); + } + private expressionStatement(expr: TS.Expression, awaited = false) { const { ts } = this; let bare = expr; while (ts.isParenthesizedExpression(bare)) bare = bare.expression; if (ts.isAwaitExpression(bare)) { this.task(bare.expression); return; } + // `void wave(3)`: started, and nothing waits for it — what JavaScript does with a promise nobody keeps. + if (ts.isVoidExpression(bare) && this.isPromise(bare.expression)) { this.startTask(bare.expression, bare, TASK_MAX); return; } + if (ts.isCallExpression(bare) && this.isLibraryCall(bare, "start")) { this.startCall(bare); return; } + if (ts.isCallExpression(bare) && ts.isPropertyAccessExpression(bare.expression) && bare.expression.name.text === "cancel" && this.isTaskType(this.c.checker.getTypeAtLocation(bare.expression.expression))) { this.cancelTask(bare, bare.expression.expression); return; } // A promise nothing waits for. sleep() in a function that is not async is the language before 4: it pauses there. if (!awaited && ts.isCallExpression(bare) && this.isPromise(bare) && !(this.isLibraryCall(bare, "sleep") && !this.inAsync(bare))) { this.notAwaited(bare); return; } const e = this.unwrap(expr); @@ -5710,6 +5865,9 @@ export class Structured { for (const item of items) { spread.set(given.length, item); given.push(arg); } } const what = name ?? "The function"; + // The call start() runs as a task: its arguments are worked out at the start, and the caller's variables may change after. + const copies = this.copyArgsAt === this.inlineDepth; + if (copies) this.copyArgsAt = -1; if (!body) { this.c.error(call, "The function has no body."); return undefined; } // A copy of a body inside a copy of a body, sixteen times over: what happens from here is decided once the arguments are known. const deep = this.inlineDepth >= MAX_INLINE_DEPTH; @@ -5735,6 +5893,7 @@ export class Structured { }; // Which instance comes back is settled where the call is: a copy of the body a call. if (as.instance) asCalled("it returns an instance, and which one is settled at each call"); + if (copies) asCalled("it runs as a task, a copy of its body for each start()"); if (as.before?.length) asCalled("it is called on what another call gives, which runs first and inside it"); // `function len({ x, y }: Point)`: taken apart when the function starts, inside its body — a called function's too. const patterns: (() => void)[] = []; @@ -5800,7 +5959,7 @@ export class Structured { // A text of the program: the caller's own variable when the function only reads it, else a variable of the parameter's own that starts as a copy. // A function that is called has the parameter for a variable either way, set to a copy at each call. const given = this.bindingOf(arg); - if (given?.kind === "var" && given.v.kind === "text" && !this.assigns(body, p)) { scope.bind(p, given); asCalled({ init: { kind: "textVar", id: given.v.id }, label }); return; } + if (given?.kind === "var" && given.v.kind === "text" && !this.assigns(body, p) && !copies) { scope.bind(p, given); asCalled({ init: { kind: "textVar", id: given.v.id }, label }); return; } const value = this.text(arg); if (!value) { ok = false; return; } asCalled({ init: value, label }); @@ -5853,7 +6012,7 @@ export class Structured { const init = variable.kind === "number" ? varRef(variable) : variable.kind === "unit" ? unitRef(variable) : boolRef(variable); asCalled({ init, label }); // By value, as in TypeScript. A parameter the function never assigns can read the caller's variable directly; one it assigns gets a copy. - if (!this.assigns(body, p)) { scope.bind(p, { kind: "var", v: variable }); return; } + if (!this.assigns(body, p) && !copies) { scope.bind(p, { kind: "var", v: variable }); return; } const copy = this.newVar(p.name.text, variable.kind, this.sourceOfIn(target, p.name), { ...(variable.bits ? { bits: variable.bits } : {}), ...(variable.unsigned ? { unsigned: true } : {}) }); out.params.push({ decl: copy, init, label }); scope.bind(p, { kind: "var", v: copy }); @@ -6068,6 +6227,7 @@ export class Structured { } if (caught.length) return `with its parameters as variables of the game it does not compile — ${caught[0].replace(/\.$/, "")}`; if (mentions(fn.body, "sleep") || mentions(fn.body, "parallel")) return "it sleeps, and the program wakes up inside it"; + if (mentions(fn.body, "start")) return "it starts a task"; if (mentions(fn.body, "edge")) return "rose() / once() remember what they saw, a call each"; this.mark(fn, decl); this.functions.push(fn); @@ -6379,6 +6539,7 @@ export class Structured { const ofText = this.textNumber(e); if (ofText !== undefined) return ofText; if (this.promiseCall(e)) return this.raceValue(e, "number") as NumExpr | null; + if (this.isLibraryCall(e, "start")) return this.startValue(e); const method = this.methodCall(e); if (method !== undefined) return this.numberOf(method, e); if (ts.isIdentifier(e.expression) && (e.expression.text === "parseInt" || e.expression.text === "Number" || e.expression.text === "parseFloat") && !this.gameDeclaration(e.expression)) { @@ -6820,6 +6981,10 @@ export class Structured { const h = this.evaluate(expr); if (h) return this.hoistedBool(h, e); if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken) return { kind: "not", expr: this.boolInner(e.operand, depth + 1) }; + if (ts.isPropertyAccessExpression(e) && e.name.text === "running" && this.isTaskType(this.c.checker.getTypeAtLocation(e.expression))) { + const task = this.num(e.expression); + return task ? { kind: "running", task, at: this.at(e) } : FALSE; + } if ((ts.isCallExpression(e) || ts.isPropertyAccessExpression(e)) && !this.isTextTyped(e)) { // `if (s.alive())`, `if (s.ready)`: a method or a getter, tested as any call is. const kind = this.kindOf(this.c.checker.getTypeAtLocation(e)); diff --git a/docs/ir.md b/docs/ir.md index 13162d5..9d752cf 100644 --- a/docs/ir.md +++ b/docs/ir.md @@ -3,7 +3,7 @@ What a `program(() => { … })` body means, written down as data. The compiler's front end (`compiler/structured.ts`) turns the TypeScript into this, `python/trigscript.py` lowers it to eudplib when the map is saved, and `compiler/simulateIr.ts` interprets it for -Simulate and the tests. **Version 15** (14 had no `parallel`: nothing ran beside a program's body; 13 had no text in the cells of an array: no `textAt`, `storeText`, `releaseText`; 12 had no texts: a text was the parts of a `print` and nothing else; 11 had no `slice` and no `through`: every array was cells of its own; 10 had no recursion: no `recursive`, no `saves`, and the heap's top set aside for a stack nothing used; 9 had no functions that are called: every `call` carried a body of its own; 8 had no `unitAt` / `unitPart`, so no array could hold a unit; 7 had no arrays that grow; 6 had no arrays; 5 had unsigned numbers only, the two-sided reading of `+` and `−`, no `>>>` and no `unsigned` anywhere; 4 had no input, no `centerLocation`, and one `variable` on an action where 5 has a list; 3 had no units and no tables; 2 had no reads, no `random(n)`, no bitwise operators +Simulate and the tests. **Version 16** (15 had no tasks: no `start`, `cancel`, `running`, and no `task` on a declaration; 14 had no `parallel`: nothing ran beside a program's body; 13 had no text in the cells of an array: no `textAt`, `storeText`, `releaseText`; 12 had no texts: a text was the parts of a `print` and nothing else; 11 had no `slice` and no `through`: every array was cells of its own; 10 had no recursion: no `recursive`, no `saves`, and the heap's top set aside for a stack nothing used; 9 had no functions that are called: every `call` carried a body of its own; 8 had no `unitAt` / `unitPart`, so no array could hold a unit; 7 had no arrays that grow; 6 had no arrays; 5 had unsigned numbers only, the two-sided reading of `+` and `−`, no `>>>` and no `unsigned` anywhere; 4 had no input, no `centerLocation`, and one `variable` on an action where 5 has a list; 3 had no units and no tables; 2 had no reads, no `random(n)`, no bitwise operators and no `print`; 1 had the map's string indices in the records and a `cyclesPerSecond` on the program, for the death-counter backend 3.0 removed). The types are in `compiler/ir.ts`; this is the reference for anyone reading the lowering or writing @@ -60,7 +60,7 @@ the statement as the source has it ("L12: while (x < 3)"), for a log or a debugg ## Variables ``` -VarDecl { id, name, kind: "number" | "boolean" | "unit" | "text", text?: "id" | "made", shared, bits?: 8 | 16, unsigned?, temp?, at } +VarDecl { id, name, kind: "number" | "boolean" | "unit" | "text", text?: "id" | "made", shared, bits?: 8 | 16, unsigned?, temp?, task?, at } ``` `id` is unique within the program (`name#n`); `name` is the source's. `shared` is a @@ -69,12 +69,13 @@ VarDecl { id, name, kind: "number" | "boolean" | "unit" | "text", text?: "id" | call's result). A variable exists from its `declare` statement on; a backend allocates storage in that order. A `unit` variable holds a unit of the game or none; its `declare`, a parameter's `init` and a `return` carry a unit expression (below). A `text` variable -is kept the way `text` says (*Texts*, below). +is kept the way `text` says (*Texts*, below). `task` marks a variable of a task's body (*Tasks*, +below): it has a cell for each slot of that `start`, within each player's row in a per-player program. ## Arrays ``` -ArrayDecl { id, name, kind: "number" | "boolean", length, shared, bits?: 8 | 16, unsigned?, values?: number[], dynamic?, slice?, through?, at } +ArrayDecl { id, name, kind: "number" | "boolean", length, shared, bits?: 8 | 16, unsigned?, values?: number[], dynamic?, slice?, through?, task?, at } ``` `Program.arrays` lists every array of the program, those of inlined functions included, so a @@ -181,6 +182,8 @@ frame's `saves`, and a `slice` is never declared. | `return` | `value?` | Inside a `call` body: writes the result and leaves the call. | | `sleep` | `ms?`, `cycles?` | Park the program: a duration in milliseconds, or in frames (`cycles`). `cycles: 1` goes on in the very next frame. Inside a `parallel` branch it parks that branch alone. | | `parallel` | `mode`, `branches` | `await Promise.all([…])` (`all`) or `await Promise.race([…])` / `Promise.any` (`race`). Each branch is a coroutine of its own with its own place to go on from and its own wait: started here, in order, in this frame; then, every frame the program is at this statement, each branch that has not ended goes on, in order. The program goes on past it in the frame the last branch ends (`all`) or the first (`race`, which leaves the others where they are: they never run again). A branch is never left by `break`, `continue` or `return`. Not inside a called function, a `unitLoop` or a `textLoop`. Since version 15. | +| `start` | `task`, `max`, `call`, `handle?` | `start(wave(n))`, `void wave(n)`: a task that runs beside what started it (*Tasks*, below). `call` is the async function's body inlined, its `params` copies of this start's arguments, worked out here; the body runs in the task. `handle` takes the task's number, or 0 when every slot was running. Since version 16. | +| `cancel` | `task` (a number) | `t.cancel()`: the task stops where it is; nothing happens for a number that is no running task's. Since version 16. | | `print` | `parts`, `to`, `position` | Text with values in it. `parts` are `{ kind: "text", text }`, `{ kind: "number", expr, unsigned? }` (its digits, with a minus sign when it is below zero and not `unsigned`), `{ kind: "name", player }` and `{ kind: "color", player }` (the colour code of the player's colour), a player being a slot or 13 for the current player. `to` is who sees it: a slot, 13, All Players (17) or a force (18–21). `position` is `chat` or `center`, the line the game's own errors use. Every number is evaluated before anything is shown. | | `action` | `record`, `variables?` | A trigger action. Each of `variables` names a field of the record that takes an expression's value: `{ field, bits: 8, 16 or 32, name, expr }` — the unit count of `createUnit` and friends is an 8-bit field, a unit type a 16-bit one (the lowering stops it at 228), an amount with a modifier a 32-bit one. Version 4 had one, as `variable`. | | `centerLocation` | `location`, `x`, `y` | Centre a location (1-based) on a point of the map in pixels, its size kept. | @@ -188,6 +191,24 @@ frame's `saves`, and a `slice` is never declared. | `block` | `body` | Scoping only. | | `remark` | `text`, `short?` | A word for the editor about the line: a loop unrolled when the script was applied. | +### Tasks + +Each `start` statement is a body of its own — a loop unrolled when the script is built has one +for each turn — numbered from 1 in the order the front end meets them (`task`; one inside +another's body comes after it), with `max` slots. A slot is a row of the cells of everything +the body declares (`VarDecl.task`, `ArrayDecl.task`), plus its place to go on from, its wait, +its generation and the frame it last had its turn in. A `start` works its arguments out, takes +the lowest free slot — one more generation, its parameters set, its place the body's start — +and gives `slot | task << 8 | generation << 16`. With every slot running nothing starts, and +the game says so once. + +Each frame, after the program's own turn (whether it slept, waited or ended), every running +task goes on until it sleeps, waits or ends: by `task`, then by slot. A task started during +that turn has its first turn in the same frame — the slots are gone through again until none +was started. Tasks run on after the program has ended. A task's body may read the program's +variables, but not those of a task it was started from (the front end refuses that), and a +`sleep`, an `await` or a `parallel` in it is the task's own. + ## Expressions Numbers (`NumExpr`): @@ -226,6 +247,7 @@ Booleans (`BoolExpr`): | `not` | `expr` | | `random` | a coin toss, fresh at every evaluation | | `unitAlive` | `unit` — `if (target)`: there is a unit and it is still on the map | +| `running` | `task` (a number) — `t.running`: the task that number stands for has not ended or been cancelled | | `unitSame` | `left`, `right` — both name one unit of the game (none is never the same as anything) | | `unitFlag` | `unit`, `flag: hallucinated | cloaked | burrowed | invincible | underAttack`; false when the unit is none or gone | | `edge` | `edge: rose | once`, `cond` | diff --git a/probes/game-tasks.ts b/probes/game-tasks.ts new file mode 100644 index 0000000..6f88d2b --- /dev/null +++ b/probes/game-tasks.ts @@ -0,0 +1,66 @@ +// TrigScript 4, B1's second part: tasks — start(), void, a Task's running / cancel() / await, max — run in the lifted +// game as well as in the simulator; the two have to agree frame for frame. +// npx tsx scripts/test-game.mts probes/game-tasks.ts +// A program first runs on frame 2. Built like a probe (Player 1 Terran, Player 2 a Zerg computer, location 1 "Home"). +import { test, expect, program, displayText, print, setResources, sleep, start, frames, P1 } from "trigscript"; + +program(async () => { + async function add(k: number, each: number) { for (let i = 0; i < k; i++) { setResources(P1, "add", each, "gas"); await sleep(frames(1)); } } + async function forever() { while (true) { setResources(P1, "add", 1, "ore"); await sleep(frames(1)); } } + async function hold() { await sleep(frames(50)); } + // Frame 2: two tasks of the one start, +1 gas for three frames and +10 for five, each with its own i. + let n = 2; + for (let j = 0; j < n; j++) start(add(3 + j * 2, 1 + j * 9)); + await sleep(frames(10)); + // Frame 12: +1 ore a frame until frame 17, when the program, whose turn comes first, cancels it. + const f = start(forever()); + await sleep(frames(5)); + f.cancel(); + displayText(f.running ? "still running" : "stopped"); + // Three starts of a start with room for two. + let made = 0; + for (let j = 0; j <= n; j++) { const t = start(hold(), { max: 2 }); if (t.running) made++; } + print(`made ${made}`); + // +100 gas on frames 17 and 18; the task ends on frame 19 after the program's turn, which sees it on frame 20. + const w = start(add(2, 100)); + await w; + displayText("awaited"); + void (async () => { await sleep(frames(20)); displayText("after the end"); })(); +}, { owner: P1 }); + +test("two tasks of one start keep their own cells", (sim) => { + sim.frames(5); + expect(sim.resources(P1).gas).toBe(43); + sim.frames(2); + expect(sim.resources(P1).gas).toBe(53); + sim.frames(5); + expect(sim.resources(P1).gas).toBe(53); +}); + +test("cancel() stops a task where it is", (sim) => { + sim.frames(17); + expect(sim.resources(P1).ore).toBe(5); + sim.frames(6); + expect(sim.resources(P1).ore).toBe(5); + expect(sim).toHavePrinted("stopped"); + expect(sim).toHaveFaulted("did not start"); +}); + +test("max: a start with every slot running starts nothing", (sim) => { + sim.frames(18); + expect(sim).toHavePrinted("made 2"); + expect(sim).toHaveFaulted("hold did not start"); +}); + +test("await a task, and a task that runs on after the program ends", (sim) => { + sim.frames(19); + expect(sim).toHaveFaulted("did not start"); + expect(sim).not.toHavePrinted("awaited"); + sim.frames(1); + expect(sim).toHavePrinted("awaited"); + expect(sim.resources(P1).gas).toBe(253); + sim.frames(19); + expect(sim).not.toHavePrinted("after the end"); + sim.frames(1); + expect(sim).toHavePrinted("after the end"); +}); diff --git a/python/trigscript.py b/python/trigscript.py index d70f75a..c2a6ace 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -68,7 +68,7 @@ from eudplib.core.mapdata.stringmap import ForceAddString from eudplib.memio.rwcommon import br1, br2, bw1 -IR_VERSION = 15 +IR_VERSION = 16 FRAMES_PER_SECOND = 24 # Where the game keeps what a read reads (1.16.1 addresses, which Remastered emulates). The player # tables are the ones Magenta's probes 5 and 8 read in the game. @@ -134,15 +134,46 @@ class Leave(Exception): """Raised through the lowering when a statement ends the straight line (break / continue / return).""" +class Rows: + """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 + start (`Pool`), and a task's in a per-player program one a slot within each player's. Called, it is the index.""" + + def __init__(self, lowering, per_player, pool): + self.lowering, self.per_player, self.pool = lowering, per_player, pool + self.count = (12 if per_player else 1) * (pool.max if pool else 1) + + def __call__(self): + if self.pool: + return self.pool.row + return self.lowering.player_of() + + +class RowCell: + """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.""" + + def __init__(self, rows, initial=0): + self.rows = rows + self.store = EUDArray([initial] * rows.count) if rows else EUDVariable(initial) # initial: the lowering's own state + + def get(self): + return self.store[self.rows()] if self.rows else self.store + + def set(self, value): + if self.rows: + self.store[self.rows()] = value + else: + self.store << value + + class Storage: - """A variable's cell: plain, or a 12-slot row of a per-player program.""" + """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.""" - def __init__(self, decl, per_player, player_of): + def __init__(self, decl, rows): self.decl = decl self.bits = decl.get("bits") - self.rowed = per_player and not decl.get("shared") - self.player_of = player_of - self.store = EUDArray([0] * 12) if self.rowed else EUDVariable(0) # initial: the variable's own cell + self.rowed = rows is not None + self.player_of = rows + self.store = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell def get(self): return self.store[self.player_of()] if self.rowed else self.store @@ -161,20 +192,20 @@ class ArrayStorage: the script computed when it was built, which is in the map as it loads and which nothing writes. An index past either end (read from 0 up, so one below zero too) reads 0 and stores nothing.""" - def __init__(self, decl, per_player, player_of): + def __init__(self, decl, rows): self.decl = decl self.length = int(decl["length"]) self.bits = decl.get("bits") values = decl.get("values") - self.rowed = per_player and not decl.get("shared") and values is None - self.player_of = player_of + self.rowed = rows is not None and values is None + self.player_of = rows if decl.get("texts") is not None: # A list of texts the script has: a cell is the text's id in the built map's table. values = [text_id(t) for t in decl["texts"]] - self.store = EUDArray([int(v) & U32 for v in values] if values is not None else [0] * (self.length * (12 if self.rowed else 1))) # initial: a table's values, or cells declareArray sets + self.store = EUDArray([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 def at(self, index): - """The cell's place among the store's: the player's row first.""" + """The cell's place among the store's: the row first (the player's, the task's).""" if not self.rowed: return index p = self.player_of() @@ -418,12 +449,12 @@ class ListStorage: 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 a per-player program. Reads and stores are bounded by the cells in use; a store at exactly the length is a push.""" - def __init__(self, decl, per_player, player_of): + def __init__(self, decl, rows): self.decl = decl self.bits = decl.get("bits") - self.rowed = per_player and not decl.get("shared") - self.player_of = player_of - make = (lambda: EUDArray([0] * 12)) if self.rowed else (lambda: EUDVariable(0)) # initial: no block + self.rowed = rows is not None + self.player_of = rows + make = (lambda: EUDArray([0] * rows.count)) if self.rowed else (lambda: EUDVariable(0)) # initial: no block self.handle = {name: make() for name in ("ptr", "len", "room", "k")} self.heap = heap() @@ -879,13 +910,13 @@ def length(self): class TextStorage: - """A text variable that is made: three cells, or three rows of twelve in a per-player program.""" + """A text variable that is made: three cells, or three rows (of twelve in a per-player program, of a task's slots).""" - def __init__(self, decl, per_player, player_of): + def __init__(self, decl, rows): self.decl = decl - self.rowed = per_player and not decl.get("shared") - self.player_of = player_of - make = (lambda: EUDArray([0] * 12)) if self.rowed else (lambda: EUDVariable(0)) # initial: no text yet, and no block + self.rowed = rows is not None + self.player_of = rows + make = (lambda: EUDArray([0] * rows.count)) if self.rowed else (lambda: EUDVariable(0)) # initial: no text yet, and no block self.addr, self.block, self.len = make(), make(), make() def cell(self, cell): @@ -931,13 +962,13 @@ def none(self): class UnitStorage: - """A unit variable: three cells, or three 12-slot rows of a per-player program.""" + """A unit variable: three cells, or three rows (12-slot of a per-player program, of a task's slots).""" - def __init__(self, decl, per_player, player_of): + def __init__(self, decl, rows): self.decl = decl - self.rowed = per_player - self.player_of = player_of - make = (lambda: EUDArray([0] * 12)) if self.rowed else (lambda: EUDVariable(0)) # initial: no unit + self.rowed = rows is not None + self.player_of = rows + make = (lambda: EUDArray([0] * rows.count)) if self.rowed else (lambda: EUDVariable(0)) # initial: no unit self.ptr, self.epd, self.uid = make(), make(), make() def get(self): @@ -1001,33 +1032,40 @@ def fresh(value=0): class Coroutine: """Where a coroutine is: the label it goes on from (0 its start, DONE once it ended) and the frames it still waits, - a cell each, a row of twelve in a per-player program. The program is one; each branch of a Promise.all / race is - another, which the program runs inside its own frame for as long as it is waiting at that statement.""" + 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 + of a Promise.all / race is another, which runs inside the frame of what waits at that statement; a task is one too. + `task`: the coroutine is a task's own, which a cancel() may have ended while it ran: parking leaves an end an end.""" - def __init__(self, lowering): + def __init__(self, lowering, rows, initial=0, task=False): self.lowering = lowering - self.state = EUDArray([0] * 12) if lowering.per_player else EUDVariable(0) # initial: program state - self.wait = EUDArray([0] * 12) if lowering.per_player else EUDVariable(0) # initial: program state + self.state = RowCell(rows, initial) + self.wait = RowCell(rows, 0) + self.task = task self.resumes = [] # (index, Forward) for every place it can go on from self.frame_end = None def get_state(self): - return self.state[self.lowering.player] if self.lowering.per_player else self.state + return self.state.get() def set_state(self, value): - if self.lowering.per_player: - self.state[self.lowering.player] = value - else: - self.state << value + self.state.set(value) def get_wait(self): - return self.wait[self.lowering.player] if self.lowering.per_player else self.wait + return self.wait.get() def set_wait(self, value): - if self.lowering.per_player: - self.wait[self.lowering.player] = value + self.wait.set(value) + + def park(self, index, wait): + """Stop here for this frame, to go on from `index` once `wait` more frames have passed.""" + if self.task: + if EUDIfNot()(as_var(self.get_state()) == DONE): + self.set_state(index) + EUDEndIf() else: - self.wait << value + self.set_state(index) + self.set_wait(wait) + EUDJump(self.frame_end) def resume_point(self): """A place to go on from: its number, which the state takes, and its label, to be placed where it is.""" @@ -1037,6 +1075,45 @@ def resume_point(self): return index, label +class Pool: + """A `start` statement's tasks: `max` slots, each a row of the cells of the task's body - its coroutine's place + (DONE: the slot is free), the generation of the task in it, the frame it last had its turn in. `slot` is the slot + being run or started, `row` its row among the cells (the player's rows first in a per-player program, from `base`).""" + + def __init__(self, lowering, st): + self.st = st + self.task = int(st["task"]) + self.max = int(st["max"]) + self.slot = EUDVariable(0) # initial: the pool's own state + self.row = EUDVariable(0) # initial: the pool's own state + self.base = EUDVariable(0) # initial: the pool's own state + self.said = EUDVariable(0) # initial: nothing said yet + self.rows = Rows(lowering, lowering.per_player, self) + self.co = Coroutine(lowering, self.rows, DONE, task=True) + self.gen = RowCell(self.rows, 0) + self.ran = RowCell(self.rows, 0) + # How many of its slots are running (the player's, in a per-player program), and that count where the tasks' + # turn can test it every time round: the slots after the last running one are not looked at. + self.live = RowCell(Rows(lowering, True, None) if lowering.per_player else None, 0) + self.count = self.live.store if not lowering.per_player else EUDVariable(0) # initial: the pool's own state + + def add(self, n): + """One more task running (1) or one fewer (-1).""" + if self.count is self.live.store: + self.count += n + return + self.live.set(as_var(self.live.get()) + n) + self.count += n + + def select(self, i): + self.slot << i + self.row << self.base + i + + def at(self, cell, slot): + """A cell of the slot `slot`, without selecting it: the one being run stays where it is.""" + return cell.store[self.base + slot] + + class Lowering: def __init__(self, program): self.p = program @@ -1045,9 +1122,29 @@ def __init__(self, program): self.slots = owner_slots(program) self.player = None self.vars = {} - self.arrays = {a["id"]: SliceStorage(a, self) if a.get("slice") else InnerListStorage(a, self) if a.get("through") else (ListStorage if a.get("dynamic") else ArrayStorage)(a, self.per_player, self.player_of) for a in program.get("arrays", [])} - # The program's own coroutine, and the one being lowered (a branch of a Promise.all / race inside it). - self.top = Coroutine(self) + # The `start` statements' tasks, by their number, in that order; the one whose body is being lowered. + starts = [] + each_start([program.get("body", []), program.get("functions", [])], starts.append) + self.pools = {} + self.pool = None + for st in sorted(starts, key=lambda st: int(st["task"])): + self.pools[int(st["task"])] = Pool(self, st) + self.tick = EUDVariable(0) # initial: frames the tasks have run + self.again = EUDVariable(0) # initial: no task started during the tasks' turn + self.arrays = {} + for a in program.get("arrays", []): + if a.get("slice"): + self.arrays[a["id"]] = SliceStorage(a, self) + elif a.get("through"): + self.arrays[a["id"]] = InnerListStorage(a, self) + else: + self.arrays[a["id"]] = (ListStorage if a.get("dynamic") else ArrayStorage)(a, self.rows_of(a)) + # A task's parameters are set where it is started, which may be lowered after its body: theirs are made now. + for pool in self.pools.values(): + for prm in pool.st["call"]["params"]: + self.declare(prm["decl"]) + # The program's own coroutine, and the one being lowered (a branch of a Promise.all / race inside it, a task). + self.top = Coroutine(self, self.rows()) self.co = self.top self.latches = {} # The functions that are called, by id, and each one's EUDFunc once something has called it. @@ -1063,13 +1160,26 @@ def player_of(self): raise Fail("trigscript: a per-player variable outside the player loop") return self.player + def rows(self): + """The rows a cell of the lowering's own has where the lowering is: a player's, a task's slot's, or none.""" + return Rows(self, self.per_player, self.pool) if self.per_player or self.pool else None + + def rows_of(self, decl): + """The rows of a variable's or an array's cells: one a player unless it is shared, one a slot of its task.""" + pool = self.pools.get(decl["task"]) if decl.get("task") is not None else None + per_player = self.per_player and not decl.get("shared") + return Rows(self, per_player, pool) if per_player or pool else None + def declare(self, decl): + made = self.vars.get(decl["id"]) + if made is not None and decl.get("task") is not None: + return made # (a task's parameter, made with its pool) if decl.get("kind") == "unit": - s = UnitStorage(decl, self.per_player, self.player_of) + 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) elif decl.get("kind") == "text" and decl.get("text") != "id": - s = TextStorage(decl, self.per_player, self.player_of) + s = TextStorage(decl, self.rows_of(decl)) else: - s = Storage(decl, self.per_player, self.player_of) + s = Storage(decl, self.rows_of(decl)) self.vars[decl["id"]] = s return s @@ -1383,6 +1493,8 @@ def cond(self, e): return (f_rand() & 1) >= 1 if k in ("unitAlive", "unitFlag", "unitSame"): return as_var(self.truth(e)) >= 1 + if k == "running": + return as_var(self.running(e)) >= 1 if k == "edge": return self.edge(e) if k == "ternary": @@ -1417,6 +1529,8 @@ def truth(self, e): return t if e["kind"] == "unitFlag": return self.unit_flag(e) + if e["kind"] == "running": + return self.running(e) if e["kind"] == "unitSame": a, b = self.unit(e["left"]), self.unit(e["right"]) t = fresh(0) @@ -1436,17 +1550,9 @@ def edge(self, e): """rose(c): true on the frame c becomes true; once(c): true the first time it holds.""" key = id(e) if key not in self.latches: - self.latches[key] = EUDArray([0] * 12) if self.per_player else EUDVariable(0) # initial: the latch + self.latches[key] = RowCell(self.rows(), 0) # initial: the latch latch = self.latches[key] - - def get(): - return latch[self.player] if self.per_player else latch - - def put(v): - if self.per_player: - latch[self.player] = v - else: - latch << v + get, put = latch.get, latch.set fired = fresh(0) held = self.truth(e["cond"]) @@ -2013,6 +2119,10 @@ def statement(self, st, ctx): self.sleep(st) elif k == "parallel": self.parallel(st) + elif k == "start": + self.start(st) + elif k == "cancel": + self.cancel(st) elif k == "action": self.action(st) elif k == "print": @@ -2110,11 +2220,9 @@ def sleep(self, st): frames = max(1, int(round(float(st.get("ms", 0)) * FRAMES_PER_SECOND / 1000))) co = self.co index, resume = co.resume_point() - co.set_state(index) # The frame after this one is one frame later: sleep(frames(1)) goes on in the next frame, as the # simulator has it. (Until 3.4 the wait was one frame longer, so a loop sleeping a frame ran every other.) - co.set_wait(frames - 1) - EUDJump(co.frame_end) + co.park(index, frames - 1) resume << NextTrigger() def parallel(self, st): @@ -2125,7 +2233,7 @@ def parallel(self, st): if self.in_function: raise Fail("trigscript: await inside a function that is called%s" % where(st)) parent = self.co - kids = [Coroutine(self) for _ in st["branches"]] + kids = [Coroutine(self, self.rows()) for _ in st["branches"]] for k in kids: k.set_state(0) k.set_wait(0) @@ -2145,9 +2253,7 @@ def parallel(self, st): skip << NextTrigger() if st["mode"] == "all": EUDJumpIf([as_var(k.get_state()) == DONE for k in kids], after) - parent.set_state(index) - parent.set_wait(0) - EUDJump(parent.frame_end) + parent.park(index, 0) after << NextTrigger() def action(self, st): @@ -2643,26 +2749,26 @@ def saved_cells(self, saves, node): continue if isinstance(s, UnitStorage): for cell in (s.ptr, s.epd, s.uid): - pairs.append(self.cell_pair(cell, s.rowed)) + pairs.append(self.cell_pair(cell, s.player_of if s.rowed else None)) elif isinstance(s, TextStorage): for cell in (s.addr, s.block, s.len): - pairs.append(self.cell_pair(cell, s.rowed)) + pairs.append(self.cell_pair(cell, s.player_of if s.rowed else None)) elif isinstance(s, Storage): - pairs.append(self.cell_pair(s.store, s.rowed)) + pairs.append(self.cell_pair(s.store, s.player_of if s.rowed else None)) for id_ in saves.get("arrays", []): a = self.array(id_, node) if isinstance(a, ListStorage): for name in HANDLE: - pairs.append(self.cell_pair(a.handle[name], a.rowed)) + pairs.append(self.cell_pair(a.handle[name], a.player_of if a.rowed else None)) return pairs def saved_texts(self, saves): """The made texts among what a frame keeps: each owns a block, which the frame keeps for it.""" return [s for s in (self.vars.get(id_) for id_ in saves.get("vars", [])) if isinstance(s, TextStorage)] - def cell_pair(self, cell, rowed): - if rowed: - return (lambda: cell[self.player_of()]), (lambda v: cell.__setitem__(self.player_of(), v)) + def cell_pair(self, cell, rows): + if rows: + return (lambda: cell[rows()]), (lambda v: cell.__setitem__(rows(), v)) return (lambda: cell), (lambda v: cell << v) def keep(self, call): @@ -2678,8 +2784,10 @@ def keep(self, call): 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)) st["depth"] << 0 st["at"] << st["base"] - self.top.set_state(DONE) - EUDJump(self.top.frame_end) + # The program stops for good, or, in a task, the task does. + stopped = self.pool.co if self.pool else self.top + stopped.set_state(DONE) + EUDJump(stopped.frame_end) EUDEndIf() pairs = self.saved_cells(saves, call) at = st["at"] @@ -2787,9 +2895,150 @@ def call(self, call): # ── one frame ── def frame(self): + if self.pools: + self.tick += 1 + 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"]) + self.run_tasks() - def coroutine(self, co, body): + def run_tasks(self): + """The tasks' turn, after the program's: each slot whose task is running and has not had its turn in this frame + goes on until it sleeps, waits or ends - by the number of its start, then by slot. A task started meanwhile + sets `again`, and the slots are gone through once more, so that it starts in this frame too.""" + if not self.pools: + return + if self.per_player: + for pool in self.pools.values(): + pool.count << pool.live.get() + sweep = Forward() + sweep << NextTrigger() + self.again << 0 + for pool in self.pools.values(): + i, seen = EUDVariable(), EUDVariable() + i << 0 + seen << 0 + # `seen`: the running slots so far, less those that ended on their turn - so while it is below the count, + # a running slot is still ahead (or one started meanwhile, which the next time round finds). + if EUDWhile()([i <= pool.max - 1, seen < pool.count]): + pool.select(i) + skip = Forward() + EUDJumpIf(as_var(pool.co.get_state()) == DONE, skip) + seen += 1 + EUDJumpIf(as_var(pool.ran.get()) == self.tick, skip) + pool.ran.set(self.tick) + outer = self.pool + self.pool = pool + end = Forward() + call = pool.st["call"] + result = self.declare(call["result"]["decl"]) if call.get("result") else None + self.coroutine(pool.co, call["body"], {"fn": {"result": result, "kind": call["result"]["kind"] if call.get("result") else "void", "end": end}}, end) + self.pool = outer + if EUDIf()(as_var(pool.co.get_state()) == DONE): + pool.add(-1) + seen -= 1 + EUDEndIf() + skip << NextTrigger() + i += 1 + EUDEndWhile() + EUDJumpIf(self.again >= 1, sweep) + + def start(self, st): + """A task into a free slot of its start: the arguments worked out as things are now, then the slot taken - its + generation one more, its parameters set, its place the body's start - and the number the task is known by + into `handle`. With every slot running nothing starts, and the game says so once.""" + pool = self.pools[int(st["task"])] + call = st["call"] + values = [] + for prm in call["params"]: + kind = prm["decl"]["kind"] + target = self.var(prm["decl"]["id"], st) + if kind == "unit": + ref = self.unit(prm["init"]) + uid = ref.uid if ref.uid is not None else f_maskread_epd(ref.epd + OFF_UID, 0xFF00) + values.append(UnitRef(fresh(ref.ptr), fresh(ref.epd), fresh(uid))) + elif kind == "text" and isinstance(target, TextStorage): + v = self.own(self.text(prm["init"])) + values.append(TextVal(fresh(v.addr), fresh(v.block), fresh(v.length()), True)) + elif kind == "text": + values.append(fresh(self.text_id_of(prm["init"]))) + else: + values.append(fresh(self.value(prm["init"], kind))) + found, after = Forward(), Forward() + i = fresh(0) + if EUDWhile()(i <= pool.max - 1): + pool.select(i) + EUDJumpIf(as_var(pool.co.get_state()) == DONE, found) + i += 1 + EUDEndWhile() + # Every slot is running. + for v in values: + if isinstance(v, TextVal): + texts()["release"](v.block) + if EUDIf()(pool.said == 0): + pool.said << 1 + 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)) + EUDEndIf() + if st.get("handle"): + self.var(st["handle"], st).set(0) + EUDJump(after) + found << NextTrigger() + gen = fresh(pool.gen.get()) + if EUDIf()(gen >= 0xFFFF): + gen << 0 + EUDEndIf() + gen += 1 + pool.gen.set(gen) + for prm, v in zip(call["params"], values): + target = self.var(prm["decl"]["id"], st) + if isinstance(v, UnitRef): + target.set(v) + elif isinstance(v, TextVal): + texts()["release"](target.cell(target.block)) + target.write(v.addr, v.block, v.length()) + else: + target.set(v) + pool.co.set_state(0) + pool.co.set_wait(0) + pool.ran.set(self.tick - 1) + pool.add(1) + self.again << 1 + if st.get("handle"): + self.var(st["handle"], st).set(pool.slot + pool.task * 256 + f_bitlshift(fresh(gen), 16)) + after << NextTrigger() + + def task_slot(self, e, act): + """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.""" + h = fresh(self.num(e)) + slot = self.bitwise("&", h, 0xFF) + task = self.bitwise("&", self.bitwise(">>>", h, 8), 0xFF) + gen = self.bitwise(">>>", h, 16) + for pool in self.pools.values(): + if EUDIf()([as_var(task) == pool.task, as_var(slot) <= pool.max - 1]): + if EUDIf()([EUDNot(as_var(pool.at(pool.co.state, slot)) == DONE), as_var(pool.at(pool.gen, slot)) == gen]): + act(pool, slot) + EUDEndIf() + EUDEndIf() + + def running(self, e): + out = fresh(0) + self.task_slot(e["task"], lambda pool, slot: out << 1) + return out + + def cancel(self, st): + def stop(pool, slot): + pool.co.state.store[pool.base + slot] = DONE + # The task whose turn this is ends on it: the tasks' turn counts it then. + if self.pool is pool: + if EUDIfNot()(as_var(slot) == pool.slot): + pool.add(-1) + EUDEndIf() + else: + pool.add(-1) + self.task_slot(st["task"], stop) + + def coroutine(self, co, body, ctx=None, end=None): """One frame of a coroutine: count its wait down and leave, or go on from where it stopped until it sleeps or ends. Its body is lowered here, once, and the places to go on from are recorded as they are met; the jump table is filled in after.""" @@ -2807,9 +3056,11 @@ def coroutine(self, co, body): EUDJump(table) start << NextTrigger() try: - self.block(body, {}) + self.block(body, ctx or {}) except Leave: pass + if end is not None: + end << NextTrigger() # (where a `return` in a task's body goes) # The body ended: it stops for good, as the classic backend's program does. co.set_state(DONE) EUDJump(co.frame_end) @@ -2845,6 +3096,21 @@ def flipped(t): return out +def each_start(node, visit): + """Every `start` statement in a piece of the IR, outer ones before those in their bodies.""" + if isinstance(node, list): + for x in node: + each_start(x, visit) + return + if not isinstance(node, dict): + return + if node.get("kind") == "start" and isinstance(node.get("task"), int) and isinstance(node.get("call"), dict): + visit(node) + for v in node.values(): + if isinstance(v, (dict, list)): + each_start(v, visit) + + def has_call(node): """Whether a call is anywhere in a piece of the IR.""" if isinstance(node, list): @@ -3292,12 +3558,12 @@ def _program_places(k, low): for id_, s in low.vars.items(): # (A loop's unit is the scan's own pointer for one turn, kept nowhere: nothing to read.) d = getattr(s, "decl", None) - if d is None or d.get("temp"): - continue + if d is None or d.get("temp") or d.get("task") is not None: + continue # (a task's variable is a row a slot: nothing a test reads by name) out["vars"].append({"id": id_, "kind": d.get("kind"), "text": d.get("text"), "bits": d.get("bits"), "unsigned": bool(d.get("unsigned")), "rowed": bool(s.rowed), "cells": {name: _place(cell) for name, cell in _cells_of(s).items()}}) for id_, a in low.arrays.items(): - if isinstance(a, (InnerListStorage, SliceStorage)): + if isinstance(a, (InnerListStorage, SliceStorage)) or a.decl.get("task") is not None: continue # (a row of another's cells, or a list inside one: read through what it is in, not here) d = a.decl 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)} diff --git a/tests/async.test.ts b/tests/async.test.ts index 5929d67..4b7e811 100644 --- a/tests/async.test.ts +++ b/tests/async.test.ts @@ -120,8 +120,8 @@ describe("async functions and await", () => { describe("async functions and await: what is said", () => { it("a promise nothing waits for", () => { - expect(messages(`sleep(frames(1));`)).toEqual(["sleep() gives a promise, and nothing waits for it here: await sleep(frames(1)). A program cannot yet start something and leave it running beside it."]); - expect(messages(`async function f(): Promise { await sleep(frames(1)); } f();`)).toEqual(["f() gives a promise, and nothing waits for it here: await f(). A program cannot yet start something and leave it running beside it."]); + expect(messages(`sleep(frames(1));`)).toEqual(["sleep() gives a promise, and nothing waits for it here: await sleep(frames(1))."]); + expect(messages(`async function f(): Promise { await sleep(frames(1)); } f();`)).toEqual(["f() gives a promise, and nothing waits for it here: await f() — or, to leave it running beside the program, start(f())."]); expect(messages("", `program(() => { async function f(): Promise { await sleep(frames(1)); } f(); });`)).toEqual(["f() waits, and a program waits in an async function: program(async () => { … await f(); … })."]); }); diff --git a/tests/eud-build.test.ts b/tests/eud-build.test.ts index 5a4e7f7..1a274ca 100644 --- a/tests/eud-build.test.ts +++ b/tests/eud-build.test.ts @@ -309,6 +309,30 @@ const FIXTURES: Record = { const first = await Promise.race([until(() => total >= 2).then(() => true), sleep(seconds(5)).then(() => false)]); if (first) setResources(CurrentPlayer, "add", 50, "ore"); }, { owner: AllPlayers });`, + started: `program(async () => { + let made = 0; + async function drip(k: number, name: string) { + for (let i = 0; i < k; i++) { createUnit(P8, units.ZergZergling, 1, locations.Anywhere); made++; await sleep(frames(2)); } + print(\`\${name} done\`); + } + const t = start(drip(3, "a"), { max: 4 }); + for (let i = 0; i < made + 2; i++) start(drip(i, "b")); + void drip(1, "c"); + start(async () => { await Promise.all([sleep(frames(1)), until(() => made >= 2)]); start(drip(1, "inner")); }); + await t; + if (t.running) defeat(); + const f = start(async () => { while (true) await sleep(frames(1)); }); + f.cancel(); + victory(); + });`, + startedPerPlayer: `program(async () => { + let mine = 0; + async function add(k: number) { await sleep(frames(k)); mine += k; } + start(add(1)); + start(add(2), { max: 2 }); + await until(() => mine >= 3); + setResources(CurrentPlayer, "add", 50, "ore"); + }, { owner: AllPlayers });`, }; function build(name: string, src: string): { out: number; triggers: number } { @@ -526,6 +550,6 @@ describe("the IR as the lowering reads it", () => { const ir = JSON.parse(serializeIr(r.ir, r.strings)); const actions = ir.programs[0].body.filter((s: { kind: string }) => s.kind === "action").map((s: { record: { text: unknown; wav: unknown } }) => [s.record.text, s.record.wav]); expect(actions).toEqual([["hello", 0], [0, "sound\\x.wav"]]); - expect(ir.version).toBe(15); + expect(ir.version).toBe(16); }); }); diff --git a/tests/functions.test.ts b/tests/functions.test.ts index 12a7ecd..e1b291a 100644 --- a/tests/functions.test.ts +++ b/tests/functions.test.ts @@ -175,7 +175,7 @@ describe("functions: arrays, and functions inside functions", () => { it("the functions go to the lowering with the program, their text written out", () => { const r = compile("function say(n: number) { displayText(`n`); setDeaths(P1, units.TerranMarine, 'set', n); } let k = 1; say(k); say(k + 1);"); const ir = JSON.parse(serializeIr(r.ir, r.strings)); - expect(ir.version).toBe(15); + expect(ir.version).toBe(16); expect(ir.programs[0].functions).toHaveLength(1); expect(JSON.stringify(ir.programs[0].functions)).toContain('"text":"n"'); }); diff --git a/tests/script-structured.test.ts b/tests/script-structured.test.ts index c71ad1e..f2d2f86 100644 --- a/tests/script-structured.test.ts +++ b/tests/script-structured.test.ts @@ -82,7 +82,7 @@ describe("structured: loops and branches", () => { expect(r.triggers).toEqual([]); expect(r.strings).toEqual([]); expect(r.ir).toHaveLength(1); - expect(r.ir[0]).toMatchObject({ version: 15, owner: 0, owners: [0], perPlayer: false }); + expect(r.ir[0]).toMatchObject({ version: 16, owner: 0, owners: [0], perPlayer: false }); const sim = run(r, 6); expect(sim.events.map((e) => `${e.cycle}:${ActionType.Victory === e.action.type ? "Victory" : e.action.type}`)).toEqual(["2:Victory"]); expect(value(sim, "n")).toBe(6); diff --git a/tests/service.test.ts b/tests/service.test.ts index f698c3e..a176561 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -264,9 +264,9 @@ describe("service: saving applies the script and builds its programs", () => { expect(compiles).toBe(before); expect(Object.keys(input.plugins)).toEqual(["trigscript", "eudTurbo"]); expect(input.plugins.trigscript).toEqual({ ir: "/work/files/trigscript.json" }); - expect(input.sources!.trigscript).toContain("IR_VERSION = 15"); + expect(input.sources!.trigscript).toContain("IR_VERSION = 16"); const ir = JSON.parse(input.files!["trigscript.json"]); - expect(ir.version).toBe(15); + expect(ir.version).toBe(16); expect(ir.programs).toHaveLength(1); expect(JSON.stringify(ir)).toContain('"text":"tick"'); // The library going away takes the contribution with it; coming back, it is made again. diff --git a/tests/tasks.test.ts b/tests/tasks.test.ts new file mode 100644 index 0000000..4b45d7c --- /dev/null +++ b/tests/tasks.test.ts @@ -0,0 +1,124 @@ +/** + * TrigScript 4, slice B1's second part: tasks. `start(wave(n))` runs a copy of an async function's body beside the program, + * its parameters copies of the arguments; each start has slots, a row of the body's cells each (`start` in the IR). The + * interpreter here; `eud-build.test.ts` builds the same through eudplib, and `probes/game-tasks.ts` runs it in the game. + */ +import ts from "typescript"; +import { describe, expect, it } from "vitest"; +import { compileScript, type CompileResult } from "../compiler/compiler"; +import { defaultScriptNames } from "../compiler/names"; +import { simulatePrograms } from "../compiler/simulateIr"; +import { ActionType } from "../vendor/triggers"; +import { defaultLib } from "../bundle/lib.mjs"; + +const LIB = defaultLib(); +const NAMES = defaultScriptNames(); +const raw = (body: string, options = ""): CompileResult => compileScript(ts, { "main.ts": `program(async () => {${body}}${options ? `, ${options}` : ""});` }, NAMES, { lib: LIB }); +const compile = (body: string, options = ""): CompileResult => { const r = raw(body, options); expect(r.diagnostics.map((d) => d.message)).toEqual([]); return r; }; +const messages = (body: string): string[] => raw(body).diagnostics.map((d) => d.message); +const run = (r: CompileResult, frames: number) => simulatePrograms(r.ir, frames, { strings: r.strings }); +/** What the program printed, as "frame:text", over that many frames. */ +const printed = (r: CompileResult, frames: number): string[] => + run(r, frames).events.filter((e) => e.action.type === ActionType.DisplayText).map((e) => `${e.cycle}:${e.text}`); + +describe("tasks", () => { + it("start() runs a task beside the program, from the frame it starts in, after the program's turn", () => { + const r = compile(` + async function tick(name: string, n: number) { for (let i = 0; i < n; i++) { print(\`\${name} \${i}\`); await sleep(frames(2)); } } + start(tick("a", 3)); + print("program"); + await sleep(frames(1)); + start(tick("b", 2)); + print("started b");`); + expect(printed(r, 10)).toEqual(["0:program", "0:a 0", "1:started b", "1:b 0", "2:a 1", "3:b 1", "4:a 2"]); + }); + + it("the arguments are worked out at the start: each task keeps its own copy, and its own variables", () => { + const r = compile(` + async function count(n: number) { let left = n; while (left > 0) { await sleep(frames(1)); left--; } print(\`done \${n}\`); } + for (let n = 3; n >= 1; n--) start(count(n));`); + expect(printed(r, 10)).toEqual(["1:done 1", "2:done 2", "3:done 3"]); + }); + + it("the tasks run on after the program ends", () => { + const r = compile(` + start(async () => { await sleep(frames(5)); print("later"); }); + print("end");`); + const sim = run(r, 10); + expect(sim.events.filter((e) => e.action.type === ActionType.DisplayText).map((e) => `${e.cycle}:${e.text}`)).toEqual(["0:end", "5:later"]); + expect(sim.finished()).toBe(true); + }); + + it("void wave() starts a task too, as JavaScript leaves a promise running that nothing keeps", () => { + const r = compile(` + async function later() { await sleep(frames(2)); print("later"); } + void later(); + print("now");`); + expect(printed(r, 5)).toEqual(["0:now", "2:later"]); + }); + + it("a Task: running, cancel() and await", () => { + const r = compile(` + async function forever() { let i = 0; while (true) { i++; await sleep(frames(1)); } } + async function short() { await sleep(frames(3)); } + const f = start(forever()); + const s = start(short()); + print(\`\${f.running ? 1 : 0} \${s.running ? 1 : 0}\`); + await s; + print(\`short ended \${s.running ? 1 : 0}\`); + f.cancel(); + print(\`cancelled \${f.running ? 1 : 0}\`); + await f; + print("awaited");`); + expect(printed(r, 10)).toEqual(["0:1 1", "4:short ended 0", "4:cancelled 0", "4:awaited"]); + }); + + it("a start with every slot running starts nothing and says so; a slot is free again once its task ends", () => { + const r = compile(` + async function wait() { await sleep(frames(2)); print("ended"); } + let k = 3; + for (let i = 0; i < k; i++) { const t = start(wait(), { max: 2 }); print(\`\${t.running ? 1 : 0}\`); } + await sleep(frames(3)); + start(wait(), { max: 2 });`); + const sim = run(r, 10); + expect(sim.events.filter((e) => e.action.type === ActionType.DisplayText).map((e) => `${e.cycle}:${e.text}`)).toEqual(["0:1", "0:1", "0:0", "2:ended", "2:ended", "5:ended"]); + expect(sim.faults.map((f) => f.message)).toEqual(["wait did not start: all 2 of this start's tasks are running. start(…, { max: … }) lets more run at once."]); + }); + + it("a task awaits Promise.all of its own, and starts tasks of its own", () => { + const r = compile(` + async function inner(n: number) { await sleep(frames(n)); print(\`inner \${n}\`); } + async function outer() { + await Promise.all([sleep(frames(1)), (async () => { await sleep(frames(2)); print("branch"); })()]); + start(inner(1)); + print("outer"); + } + start(outer()); + start(outer());`); + expect(printed(r, 10)).toEqual(["2:branch", "2:outer", "2:branch", "2:outer", "3:inner 1", "3:inner 1"]); + }); + + it("each player's tasks are their own in a per-player program", () => { + const r = compile(` + async function hello(n: number) { await sleep(frames(n)); print(\`hello \${n}\`); } + let mine = 0; + start((async () => { for (let i = 0; i < 3; i++) { mine++; await sleep(frames(1)); } })()); + start(hello(2)); + await sleep(frames(4)); + print(\`mine \${mine}\`);`, "{ owner: AllPlayers }"); + const sim = simulatePrograms(r.ir, 10, { strings: r.strings, players: [0, 1] }); + const lines = sim.events.filter((e) => e.action.type === ActionType.DisplayText).map((e) => `${e.cycle}:${e.player}:${e.text}`); + expect(lines).toEqual(["2:0:hello 2", "2:1:hello 2", "4:0:mine 3", "4:1:mine 3"]); + }); + + it("what the language says", () => { + expect(messages(`async function w() { await sleep(frames(1)); } w();`)).toEqual(["w() gives a promise, and nothing waits for it here: await w() — or, to leave it running beside the program, start(w())."]); + expect(messages(`start(sleep(frames(1)));`)[0]).toContain("start() takes a call of an async function"); + expect(messages(`async function w(n: number) { await sleep(frames(n)); start(w(n)); } start(w(1));`)[0]).toContain("calls itself"); + expect(messages(`async function a() { let mine = 1; await sleep(frames(1)); start(async () => { print(\`\${mine}\`); }); } start(a());`)).toEqual(["This task uses mine, a variable of the task it is started from: which of that task's copies it would find is not settled. Hand it over as an argument instead — start(f(mine))."]); + expect(messages(`async function show(n: number) { print(\`\${n}\`); } async function a() { let mine = 1; await sleep(frames(1)); start(show(mine)); } start(a());`)).toEqual([]); + expect(messages(`let n = 3; for (let i = 0; i < n; i++) start(async () => { await sleep(frames(1)); print(\`\${i}\`); });`)).toEqual(["This task reads i, which the loop around the start() changes: JavaScript gives each turn its own, and here the task would find it as it is when it reads it. Hand it over as an argument instead — an async function that takes it, start(f(i))."]); + expect(messages(`for (const i of [1, 2]) start(async () => { await sleep(frames(i)); print(\`\${i}\`); });`)).toEqual([]); + expect(messages(`start(async () => {}, { max: 0 });`)).toEqual(["start()'s options are written out, as { max: 40 }: how many of this start's tasks can run at once, a whole number from 1 to 255."]); + }); +}); From 71a21a7d5cbc5f558f3fd2cf7cfb1f319d37d6cb Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 17:34:34 -0600 Subject: [PATCH 034/238] =?UTF-8?q?TrigScript=204,=20B1:=20generators,=20P?= =?UTF-8?q?romise.all's=20values,=20.then((value)=20=3D>=20=E2=80=A6),=20a?= =?UTF-8?q?sync=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit for…of walks a generator (function*, or async function* with for await): its body inlined, the loop's body at each yield. const [a, b] = await Promise.all([…]) takes each item's value. .then's function may take what the promise gives. A class may have async methods. An async function that never awaits is a hint; a loop that can break out is no longer called a freeze. Checked in the lifted game by probes/game-generators.ts. Co-Authored-By: Claude Opus 5.5 --- compiler/eud.ts | 46 ++++++---- compiler/hoist.ts | 2 + compiler/structured.ts | 175 +++++++++++++++++++++++++++++++++++--- probes/game-generators.ts | 56 ++++++++++++ tests/async.test.ts | 2 +- tests/eud-build.test.ts | 11 +++ tests/tasks.test.ts | 75 ++++++++++++++++ 7 files changed, 338 insertions(+), 29 deletions(-) create mode 100644 probes/game-generators.ts diff --git a/compiler/eud.ts b/compiler/eud.ts index 4897c71..71a13dd 100644 --- a/compiler/eud.ts +++ b/compiler/eud.ts @@ -199,24 +199,21 @@ function checkWidths(body: Stmt[], out: ProgramDiagnostic[], decls: Map { - switch (s.kind) { - case "remark": if (s.short) out.push({ file: s.at.file, line: s.at.line, label: s.short, note: s.text }); break; - case "if": s.then.forEach(stmt); s.else?.forEach(stmt); break; - case "while": case "do": case "unitLoop": case "textLoop": s.body.forEach(stmt); break; - case "for": s.body.forEach(stmt); s.update.forEach(stmt); break; - case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; - case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; - case "start": s.call.body.forEach(stmt); break; - case "switch": s.cases.forEach((c) => c.body.forEach(stmt)); break; - case "call": s.call.body.forEach(stmt); break; - case "block": s.body.forEach(stmt); break; - default: break; + const visit = (root: unknown): void => { + if (Array.isArray(root)) { root.forEach(visit); return; } + if (!root || typeof root !== "object") return; + const o = root as Record; + if (o.kind === "remark") { + // (A function's body inlined at several calls says the same of its line each time: once is enough.) + const r = o as Extract; + if (r.short && !out.some((h) => h.file === r.at.file && h.line === r.at.line && h.label === r.short)) out.push({ file: r.at.file, line: r.at.line, label: r.short, note: r.text }); + return; } + Object.values(o).forEach((v) => { if (v && typeof v === "object") visit(v); }); }; - body.forEach(stmt); + visit(body); } /** The variables a statement list assigns (declares count too), by id — and `THE_GAME` when it takes an action, which may change what a read finds. */ @@ -323,12 +320,29 @@ function sleepsOnEveryPath(body: Stmt[]): boolean { return false; } +/** Whether a loop's body can leave it: a `break` of its own (not of a loop or a switch inside it), or a `return`. */ +function leaves(body: Stmt[]): boolean { + const stmt = (s: Stmt, own: boolean): boolean => { + switch (s.kind) { + case "break": return own; + case "return": return true; + case "if": return s.then.some((x) => stmt(x, own)) || !!s.else?.some((x) => stmt(x, own)); + case "block": return s.body.some((x) => stmt(x, own)); + case "unrolled": return s.iterations.some((i) => i.some((x) => stmt(x, own))); + case "while": case "do": case "for": return s.body.some((x) => stmt(x, false)); + case "switch": return s.cases.some((c) => c.body.some((x) => stmt(x, false))); + default: return false; + } + }; + return body.some((s) => stmt(s, true)); +} + function checkSleeps(body: Stmt[], out: ProgramDiagnostic[], functions: Map) { const stmt = (s: Stmt) => { switch (s.kind) { case "while": case "for": case "do": { const moves = s.cond ? [...reads(s.cond)].some((id) => assigned(s.kind === "for" ? [...s.body, ...s.update] : s.body, functions).has(id)) : false; - if (!moves && !sleepsOnEveryPath(s.body)) out.push({ at: s.at, message: s.cond + if (!moves && !sleepsOnEveryPath(s.body) && !leaves(s.body) && !(s.kind === "do" && s.cond.kind === "const" && s.cond.value === false)) out.push({ at: s.at, message: s.cond ? "This loop's condition never changes inside it, and no path around it sleeps. A program runs until it sleeps or ends, all within one frame of the game, so this loop would never give the frame back and the game would freeze. Add sleep(frames(1)) inside it, or change what it tests." : "A loop without an end needs a sleep() on every path around it. A program runs until it sleeps or ends, all within one frame of the game, so this loop would freeze the game. Add sleep(frames(1)) at the end of its body." }); s.body.forEach(stmt); diff --git a/compiler/hoist.ts b/compiler/hoist.ts index dbba4d0..5f3db50 100644 --- a/compiler/hoist.ts +++ b/compiler/hoist.ts @@ -304,6 +304,8 @@ function planOnce(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunctio if (property) return; const decl = declarationOf(ts, checker, n); if (decl && isGameDecl(decl)) { ok = false; return; } + // A generator is walked by the program's for…of, a copy of its body at each loop: never run when the script is built. + if (decl && (ts.isFunctionDeclaration(decl) || ts.isFunctionExpression(decl)) && decl.asteriskToken) { ok = false; return; } }; const check = (n: TS.Node): void => { if (!ok) return; diff --git a/compiler/structured.ts b/compiler/structured.ts index 7c916d7..4d4f5e5 100644 --- a/compiler/structured.ts +++ b/compiler/structured.ts @@ -115,6 +115,8 @@ interface Ctx { canContinue?: boolean; /** Inside an inlined function: what it returns. `instance`: it returns an instance — which one is settled as its body is walked, and kept here. */ fn?: { kind: Kind | "text" | "void"; instance?: { name: string; made?: Binding } }; + /** Inside the body of a for…of over a generator, walked where a `yield` is: `break` sets this first, and the generator then leaves. */ + breakFlag?: VarDecl; } /** What a variable of a program holds. */ @@ -221,7 +223,7 @@ const SEARCHES = new Set(["some", "every", "find", "findLast", "findIndex", "fin type CallArgument = { init: NumExpr | BoolExpr | UnitExpr | TextExpr; label: string } | { binding: Binding }; /** What a call that is not written as one hands `inline`: a method's instance, and — for `new`, a getter, a setter — the arguments, that nothing comes back, and what runs first in the body. */ -interface MethodCall { self?: Binding; args?: readonly TS.Expression[]; nothing?: boolean; first?: (scope: Scope) => void; /** The call gives an instance: where the one it returns is kept, and the name one it makes is declared under. */ instance?: { name: string; made?: Binding }; /** What runs before the body, inside the call: the calls before this one in a chain (`v.add(w).scale(2)`), so that they run where the expression does. */ before?: Stmt[] } +interface MethodCall { /** A generator, walked by a for…of: a copy of its body at each loop. */ generator?: boolean; self?: Binding; args?: readonly TS.Expression[]; nothing?: boolean; first?: (scope: Scope) => void; /** The call gives an instance: where the one it returns is kept, and the name one it makes is declared under. */ instance?: { name: string; made?: Binding }; /** What runs before the body, inside the call: the calls before this one in a chain (`v.add(w).scale(2)`), so that they run where the expression does. */ before?: Stmt[] } /** A function at one set of arrays passed: inlined where it was first met, called from then on (`fn`), or never to be (`never` says why). */ interface FunctionSite { @@ -257,6 +259,19 @@ export class Structured { /** The body being walked: the program's, or the game function being inlined. */ private body: Body; private inlineDepth = 0; + /** An async function with no await in it (nor a for await): its name or the node to say so at; undefined for any other. */ + private neverAwaits(decl: TS.Node): TS.Node | undefined { + const { ts } = this; + if (!(ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl) || ts.isArrowFunction(decl) || ts.isFunctionExpression(decl)) || decl.asteriskToken || !decl.body) return undefined; + if (!ts.getModifiers(decl)?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return undefined; + let found = false; + const look = (n: TS.Node): void => { if (found || (n !== decl.body && ts.isFunctionLike(n))) return; if (ts.isAwaitExpression(n) || (ts.isForOfStatement(n) && n.awaitModifier)) { found = true; return; } ts.forEachChild(n, look); }; + look(decl.body); + return found ? undefined : (decl.name ?? decl); + } + + /** What a `yield` does in each generator a for…of is walking, innermost last. */ + private yields: ((y: TS.YieldExpression) => void)[] = []; /** The `start` statements so far: the next one's number is one more. */ private taskCount = 0; /** The inline depth at which the next call is a task's (`start(wave(n))`): its parameters are copies of the arguments, never the caller's variables. */ @@ -368,6 +383,11 @@ export class Structured { /* ── Values and bindings ── */ + private stripParens(e: TS.Expression): TS.Expression { + while (this.ts.isParenthesizedExpression(e)) e = e.expression; + return e; + } + /** Strip parentheses, `as`, `satisfies`, `!` — the wrappers that change nothing. */ private unwrap(expr: TS.Expression): TS.Expression { const { ts } = this; @@ -653,6 +673,7 @@ export class Structured { if (ts.isBreakStatement(s) || ts.isContinueStatement(s)) { if (s.label) { this.c.error(s, "Labelled break / continue is not supported."); return; } if (ts.isBreakStatement(s) ? !ctx.canBreak : !ctx.canContinue) { this.c.error(s, `${ts.isBreakStatement(s) ? "break" : "continue"} outside a loop.`); return; } + if (ts.isBreakStatement(s) && ctx.breakFlag) this.emit({ kind: "assignBool", target: ctx.breakFlag.id, value: TRUE, at: this.at(s), label: this.label(s) }, s); this.emit({ kind: ts.isBreakStatement(s) ? "break" : "continue", at: this.at(s), label: this.label(s) }, s); return; } @@ -717,12 +738,21 @@ export class Structured { if (!ts.isIdentifier(d.name)) { // `const { x, y } = mouse(P1)`, `const [a, b] = pair`: every name a variable of its own. if (!d.initializer) { this.c.error(d, "A pattern is taken from something: const { x, y } = p."); continue; } + let given: TS.Expression = d.initializer; + while (ts.isParenthesizedExpression(given)) given = given.expression; + const all = ts.isAwaitExpression(given) ? this.unwrap(given.expression) : undefined; + if (all && ts.isCallExpression(all) && this.promiseCall(all) === "all" && ts.isArrayBindingPattern(d.name)) { this.allValues(d.name, all, d); continue; } const from = this.patternSource(d.initializer, d); if (from) this.bindPattern(d.name, d, from, this.scope); continue; } if (!d.initializer) { this.c.error(d, `Give ${d.name.text} an initial value: let ${d.name.text} = 0 or = false.`); continue; } const init = this.unwrap(d.initializer); + if (ts.isCallExpression(init) && ts.isIdentifier(init.expression)) { + const g = this.gameDeclaration(init.expression); + if (g && ts.isFunctionDeclaration(g) && g.asteriskToken) { this.c.error(init, `${init.expression.text} is a generator: a program walks one with for…of — for (const w of ${init.expression.text}(…)) { … }.`); continue; } + } + if (ts.isAwaitExpression(this.stripParens(d.initializer)) && ts.isCallExpression(init) && this.promiseCall(init) === "all") { this.c.error(init, "Promise.all() gives a value for each item: take them apart where it is awaited — const [a, b] = await Promise.all([…])."); continue; } if (ts.isObjectLiteralExpression(init) && !this.keyedForm(init, this.c.checker.getTypeAtLocation(d.name))) { const record = this.declareRecord(d.name.text, init, this.c.checker.getTypeAtLocation(d.name), d); if (record) this.scope.bind(d, record); @@ -4208,12 +4238,12 @@ export class Structured { private task(expr: TS.Expression, into?: { decl: VarDecl; kind: "number" | "boolean" }) { const { ts } = this; const e = this.unwrap(expr); - const valueless = (what: string) => { if (into) this.c.error(e, `${what} gives no value, and Promise.race gives the value of the branch that ends first: give this one a value with .then(() => …).`); }; + const valueless = (what: string) => { if (into) this.c.error(e, `${what} gives no value, and the value of this item is kept: give it one with .then(() => …).`); }; const keep = (call: Call | undefined, node: TS.Node) => { if (!call) return; this.emit({ kind: "call", call, at: call.at, label: call.label }, node); if (!into) return; - if (!call.result || call.result.kind !== into.kind) { this.c.error(node, `Every branch of a race whose value is kept gives a ${into.kind}.`); return; } + if (!call.result || call.result.kind !== into.kind) { this.c.error(node, `This item gives ${call.result ? `a ${call.result.kind}` : "no value"}, and what is kept from it is a ${into.kind}.`); return; } const value = { kind: "var" as const, id: call.result.decl.id }; this.emit(into.kind === "number" ? { kind: "assign", target: into.decl.id, value, at: call.at, label: call.label } : { kind: "assignBool", target: into.decl.id, value, at: call.at, label: call.label }, node); }; @@ -4238,7 +4268,18 @@ export class Structured { if (ts.isPropertyAccessExpression(e.expression) && e.expression.name.text === "then" && this.isPromise(e.expression.expression)) { if (e.arguments.length !== 1) { this.c.error(e, ".then() takes one function: sleep(seconds(90)).then(() => false)."); return; } const fn = this.unwrap(e.arguments[0]); - if ((ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && fn.parameters.length) { this.c.error(fn, "The function given to .then() takes nothing here. To use what a call gives, keep it first: const v = await f(); then use v."); return; } + if ((ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && fn.parameters.length) { + // `wave(n).then((cleared) => …)`: what the promise gives, in a variable the function's parameter is. + const before = e.expression.expression; + const kind = this.kindOf(this.c.checker.getTypeAtLocation(before)); + if (fn.parameters.length > 1 || (kind !== "number" && kind !== "boolean")) { this.c.error(fn, `The function given to .then() takes what the promise gives${kind ? "" : ", and this one gives nothing"}: wave(n).then((cleared) => …), for a promise of a number or a boolean.`); return; } + const at = this.at(before), label = this.label(before); + const value = this.newVar(`(${fn.parameters[0].name.getText()})`, kind, at, kind === "number" ? this.widthOf(this.c.checker.getTypeAtLocation(before)) : {}); + this.emit({ kind: "declare", decl: value, init: kind === "number" ? num(0) : FALSE, at, label }, before); + this.task(before, { decl: value, kind }); + keep(this.callback(e.arguments[0], "then", [{ kind: "var", v: value }], into?.kind ?? "any", e), e); + return; + } this.task(e.expression.expression); keep(this.callback(e.arguments[0], "then", [], into?.kind ?? "any", e), e); return; @@ -4264,26 +4305,52 @@ export class Structured { * `await Promise.all([…])` / `race` / `any`: each item a branch of its own, side by side (`parallel` in the IR). Nothing in * a program fails, so the first branch to end is the first to succeed, and `any` is a race. */ - private parallel(call: TS.CallExpression, mode: "all" | "race" | "any", into?: { decl: VarDecl; kind: "number" | "boolean" }) { + private parallel(call: TS.CallExpression, mode: "all" | "race" | "any", into?: { decl: VarDecl; kind: "number" | "boolean" }, intos?: ({ decl: VarDecl; kind: "number" | "boolean" } | undefined)[]) { const { ts } = this; const name = `Promise.${mode}`; if (this.inCallback > 0) { this.c.error(call, `${name}() inside a function given to an array method: the method is one loop within the frame.`); return; } const list = call.arguments.length === 1 ? this.unwrap(call.arguments[0]) : undefined; if (!list || !ts.isArrayLiteralExpression(list)) { this.c.error(call, `${name}() takes a list written where it is called: ${name}([wave(1), sleep(seconds(90))]).`); return; } if (!list.elements.length) { this.c.error(list, mode === "all" ? "Promise.all([]) has nothing to wait for." : `${name}([]) never ends.`); return; } + if (intos && intos.length > list.elements.length) { this.c.error(call, `Promise.all() here has ${list.elements.length} item${list.elements.length === 1 ? "" : "s"}, so it gives ${list.elements.length} value${list.elements.length === 1 ? "" : "s"}, not ${intos.length}.`); return; } const branches: Stmt[][] = []; - for (const el of list.elements) { + for (const [i, el] of list.elements.entries()) { if (ts.isSpreadElement(el) || ts.isOmittedExpression(el)) { this.c.error(el, `Each item of ${name}() is written out: what runs side by side is settled when the script is built.`); return; } - branches.push(this.collect(() => this.task(el, into))); + branches.push(this.collect(() => this.task(el, intos ? intos[i] : into))); } this.emit({ kind: "parallel", mode: mode === "all" ? "all" : "race", branches, at: this.at(call), label: this.label(call) }, call); } + /** `const [a, b] = await Promise.all([…])`: each name the value of its branch, which the branch sets as it ends. */ + private allValues(pattern: TS.ArrayBindingPattern, call: TS.CallExpression, d: TS.VariableDeclaration) { + const { ts } = this; + const intos: ({ decl: VarDecl; kind: "number" | "boolean" } | undefined)[] = []; + const from: Binding[] = []; + for (const el of pattern.elements) { + if (ts.isOmittedExpression(el)) { intos.push(undefined); from.push({ kind: "value", value: undefined }); continue; } + if (el.dotDotDotToken || el.initializer || !ts.isIdentifier(el.name)) { this.c.error(el, "Each value of Promise.all() goes to a name of its own: const [a, b] = await Promise.all([…]), with a hole — [, b] — for one you do not keep."); return; } + const type = this.c.checker.getTypeAtLocation(el.name); + const kind = this.kindOf(type); + if (kind !== "number" && kind !== "boolean") { + const none = (type.flags & ts.TypeFlags.Void) !== 0; + this.c.error(el, none ? `${el.name.text} would be void: its item gives no value. Give it one with .then(() => …), or leave a hole for it — [, b].` : `${el.name.text} would be ${this.c.checker.typeToString(type)}: what Promise.all() gives a name here is a number or a boolean.`); + return; + } + const at = this.at(el), label = this.label(el); + const v = this.newVar(el.name.text, kind, at, kind === "number" ? this.widthOf(type) : {}); + this.emit({ kind: "declare", decl: v, init: kind === "number" ? num(0) : FALSE, at, label }, el); + intos.push({ decl: v, kind }); + from.push({ kind: "var", v }); + } + this.parallel(call, "all", undefined, intos); + this.bindPattern(pattern, d, from, this.scope); + } + /** `const first = await Promise.race([…])`: the value of the branch that ends first, in a variable of its own. */ private raceValue(e: TS.CallExpression, kind: "number" | "boolean"): NumExpr | BoolExpr | null { const { ts } = this; const mode = this.promiseCall(e)!; - if (mode === "all") { this.c.error(e, "Promise.all() gives no value yet: await it as a statement, and keep what each branch finds in variables of the program."); return null; } + if (mode === "all") { this.c.error(e, "Promise.all() gives a value for each item: take them apart where it is awaited — const [a, b] = await Promise.all([…])."); return null; } let up: TS.Node = e.parent; while (ts.isParenthesizedExpression(up)) up = up.parent; if (!ts.isAwaitExpression(up)) { this.c.error(e, `Promise.${mode}() gives a promise: await it — const v = await Promise.${mode}([…]).`); return null; } @@ -4417,6 +4484,79 @@ export class Structured { return found; } + /** + * `for (const w of waves(…))`, waves a generator (`function*`, or `async function*` with `for await`): the generator's + * body inlined, and at each `yield` the loop's body, its variable what was yielded — a value the script knows as it + * is, anything else in a variable of the turn. `continue` goes on after the yield; `break` leaves the generator. + */ + private generatorLoop(s: TS.ForOfStatement, decl: TS.VariableDeclaration, ctx: Ctx): boolean { + const { ts } = this; + const e = this.unwrap(s.expression); + if (!ts.isCallExpression(e) || !ts.isIdentifier(e.expression)) return false; + const fn = this.gameDeclaration(e.expression); + if (!fn || !ts.isFunctionDeclaration(fn) || !fn.asteriskToken) return false; + let returns: TS.Node | undefined; + const look = (n: TS.Node): void => { if (returns || ts.isFunctionLike(n)) return; if (ts.isReturnStatement(n)) { returns = n; return; } ts.forEachChild(n, look); }; + look(s.statement); + if (returns) { this.c.error(returns, "return inside a for…of over a generator is not supported yet: break out of the loop, and return after it."); return true; } + const loopBody = this.body, loopScope = this.scope; + const at = this.at(s), label = this.label(s); + const stop = this.newVar("(loop left)", "boolean", at); + this.emit({ kind: "declare", decl: stop, init: FALSE, at, label }, s); + const kind = this.kindOf(this.c.checker.getTypeAtLocation(decl.name)); + const handler = (y: TS.YieldExpression) => { + if (y.asteriskToken) { this.c.error(y, "yield* is not supported yet: walk the other generator with a for…of and yield each of its items."); return; } + if (!y.expression) { this.c.error(y, "yield gives the loop a value here: yield 3, yield { count: 4 }."); return; } + // The value, worked out where the yield is. + const h = this.evaluate(y.expression); + let item: Binding | null = null; + if (h && !isGameValue(h.value)) item = { kind: "value", value: h.value }; + else if (kind) { + const v = this.newVar(ts.isIdentifier(decl.name) ? decl.name.text : "(item)", kind, this.sourceOf(decl.name), kind === "number" ? this.widthOf(this.c.checker.getTypeAtLocation(decl.name)) : {}); + const init = kind === "number" ? this.num(y.expression) : kind === "unit" ? this.unitExpr(y.expression) : this.boolValue(y.expression); + if (!init) return; + this.emit({ kind: "declare", decl: v, init, at: this.at(y), label: this.label(y) }, y); + item = { kind: "var", v }; + } else if (ts.isObjectLiteralExpression(this.unwrap(y.expression))) { + item = this.declareRecord(ts.isIdentifier(decl.name) ? decl.name.text : "(item)", this.unwrap(y.expression) as TS.ObjectLiteralExpression, this.c.checker.getTypeAtLocation(decl.name), y); + } else item = this.bindingOf(y.expression) ?? null; + if (!item) { this.c.error(y.expression, "A generator walked by a program yields numbers, booleans, units, records of them, or values the script knows."); return; } + // The loop's body, where the loop is: its scope, its file, and a yield in it is the generator's around the loop, if any. + const saved = this.enterBody(loopBody); + const inner = this.scope; + const mine = this.yields.pop()!; + const scope = new Scope(loopScope); + this.scope = scope; + let body: Stmt[] = []; + try { + const bound = ts.isIdentifier(decl.name) ? (scope.bind(decl, item), true) : this.bindPattern(decl.name, decl, item, scope); + if (bound) body = this.collect(() => this.block([s.statement], { fn: ctx.fn, canBreak: true, canContinue: true, breakFlag: stop }, new Scope(scope))); + } finally { + this.yields.push(mine); + this.scope = inner; + this.leaveBody(saved); + } + const where = this.at(y), what = this.label(y); + this.emit({ kind: "do", body, cond: FALSE, at: where, label: what, condLabel: what }, y); + this.emit({ kind: "if", cond: { kind: "var", id: stop.id }, then: [{ kind: "return", at: where, label: what }], at: where, label: what }, y); + }; + this.yields.push(handler); + try { + const call = this.inline(e, fn.parameters, fn.body, this.body, fn.name?.text, fn, { generator: true, nothing: true }); + if (call) this.emit({ kind: "call", call, at: call.at, label: call.label }, s); + } finally { + this.yields.pop(); + } + return true; + } + + /** `yield v;` in a generator a for…of walks: the loop's body, there. */ + private yieldStatement(y: TS.YieldExpression) { + const handler = this.yields[this.yields.length - 1]; + if (!handler) { this.c.error(y, "yield is for a generator that a for…of walks: for (const w of waves()) { … }."); return; } + handler(y); + } + /** `t.cancel()`: the task stops where it is. */ private cancelTask(e: TS.CallExpression, receiver: TS.Expression) { if (e.arguments.length) { this.c.error(e, "cancel() takes nothing."); return; } @@ -4429,6 +4569,7 @@ export class Structured { let bare = expr; while (ts.isParenthesizedExpression(bare)) bare = bare.expression; if (ts.isAwaitExpression(bare)) { this.task(bare.expression); return; } + if (ts.isYieldExpression(bare)) { this.yieldStatement(bare); return; } // `void wave(3)`: started, and nothing waits for it — what JavaScript does with a promise nobody keeps. if (ts.isVoidExpression(bare) && this.isPromise(bare.expression)) { this.startTask(bare.expression, bare, TASK_MAX); return; } if (ts.isCallExpression(bare) && this.isLibraryCall(bare, "start")) { this.startCall(bare); return; } @@ -4918,8 +5059,9 @@ export class Structured { private forOfStatement(s: TS.ForOfStatement, ctx: Ctx) { const { ts } = this; - if (s.awaitModifier) { this.c.error(s, "for await is not supported in a program."); return; } const decl = ts.isVariableDeclarationList(s.initializer) && s.initializer.declarations.length === 1 ? s.initializer.declarations[0] : undefined; + if (decl && this.generatorLoop(s, decl, ctx)) return; + if (s.awaitModifier) { this.c.error(s, "for await walks a generator here: for await (const w of waves()) { … }, waves an async function*."); return; } if (decl && this.keyedLoop(s, decl, ctx)) return; if (!decl) { this.c.error(s.initializer, "for…of takes one variable: for (const w of waves) { … }."); return; } if (!ts.isIdentifier(decl.name)) { @@ -5616,7 +5758,7 @@ export class Structured { if (cls.typeParameters?.length) this.c.error(cls.typeParameters[0], "A class of a program takes no type parameters: what each field holds has to be known when the script is built."); for (const m of cls.members) { if (ts.isClassStaticBlockDeclaration(m)) { this.c.error(m, "A static block is not supported in a program; give the static fields their values where they are declared."); continue; } - if (ts.isMethodDeclaration(m) && (m.asteriskToken || ts.getModifiers(m)?.some((x) => x.kind === ts.SyntaxKind.AsyncKeyword))) this.c.error(m, "A generator or an async method is not supported in a class yet; an async function declared in the program can take the instance."); + if (ts.isMethodDeclaration(m) && m.asteriskToken) this.c.error(m, "A generator method is not supported in a class yet; a generator function declared in the program can take the instance."); if (!ts.isPropertyDeclaration(m) || !this.isStatic(m)) continue; const key = this.memberKey(m.name); if (!key) { this.c.error(m.name, "A field's name is written out."); continue; } @@ -5871,7 +6013,7 @@ export class Structured { if (!body) { this.c.error(call, "The function has no body."); return undefined; } // A copy of a body inside a copy of a body, sixteen times over: what happens from here is decided once the arguments are known. const deep = this.inlineDepth >= MAX_INLINE_DEPTH; - if ((ts.isFunctionDeclaration(decl) || ts.isArrowFunction(decl) || ts.isFunctionExpression(decl) || ts.isMethodDeclaration(decl)) && decl.asteriskToken) { this.c.error(decl, "Generators are not supported in a program yet."); return undefined; } + if ((ts.isFunctionDeclaration(decl) || ts.isArrowFunction(decl) || ts.isFunctionExpression(decl) || ts.isMethodDeclaration(decl)) && decl.asteriskToken && !as.generator) { this.c.error(call, `${what} is a generator: a program walks one with for…of — for (const w of ${name ?? "gen"}(…)) { … }.`); return undefined; } const kind: Kind | "text" | "void" = as.nothing ? "void" : this.kindOf(this.c.checker.getTypeAtLocation(call)) ?? (this.isTextType(this.c.checker.getTypeAtLocation(call)) ? "text" : "void"); const line = this.line(call); const out: Call = { ...(name ? { name } : {}), at: this.at(call), label: this.label(call), params: [], body: [] }; @@ -5894,6 +6036,7 @@ export class Structured { // Which instance comes back is settled where the call is: a copy of the body a call. if (as.instance) asCalled("it returns an instance, and which one is settled at each call"); if (copies) asCalled("it runs as a task, a copy of its body for each start()"); + if (as.generator) asCalled("it is a generator, a copy of its body at each for…of"); if (as.before?.length) asCalled("it is called on what another call gives, which runs first and inside it"); // `function len({ x, y }: Point)`: taken apart when the function starts, inside its body — a called function's too. const patterns: (() => void)[] = []; @@ -6073,7 +6216,7 @@ export class Structured { if (because) this.inlinedBecause.set(decl, { why: because, name: name ?? "function", at }); if (deep) { this.c.error(call, (this.walkingBodies.get(decl) ?? 0) > 0 - ? `${what} calls itself, and here it cannot be a function that is called${because ? ` — ${because}` : ""}. A call that is not one is a copy of the function's body, and these copies would have no end.` + ? `${what} calls itself, and here it cannot be a function that is called${because ? ` — ${because}` : ""}. A call that is not one is a copy of the function's body, and these copies would have no end.${because.startsWith("it sleeps") || because.startsWith("it runs as a task") ? " A function that waits cannot call itself yet: write the recursion as a loop — while (n > 0) { …; await sleep(…); n--; }." : ""}` : "Functions nest too deeply: a call that is inlined is a copy of the function's body, and these are sixteen inside one another."); return undefined; } @@ -6081,6 +6224,10 @@ export class Structured { this.walkingBodies.set(decl, (this.walkingBodies.get(decl) ?? 0) + 1); try { out.body = this.walkFunction(body, kind, target, scope, () => { patterns.forEach((take) => take()); as.first?.(scope); }, as.instance); + const idle = this.neverAwaits(decl); + if (idle) { + out.body.unshift({ kind: "remark", short: "never awaits", text: `${what} is async but never awaits: it runs to its end in the frame it is called, as a function that is not async would. Take the async away, or await something in it.`, at: this.sourceOfIn(target, idle) }); + } if (as.before?.length) { // The chain so far, then this call's own arguments, then its body: the order JavaScript works them out in. out.body = [...as.before, ...out.params.map((p): Stmt => ({ kind: "declare", decl: p.decl, init: p.init, at: out.at, label: p.label })), ...out.body]; @@ -6229,6 +6376,10 @@ export class Structured { if (mentions(fn.body, "sleep") || mentions(fn.body, "parallel")) return "it sleeps, and the program wakes up inside it"; if (mentions(fn.body, "start")) return "it starts a task"; if (mentions(fn.body, "edge")) return "rose() / once() remember what they saw, a call each"; + const idle = this.neverAwaits(decl); + if (idle) { + fn.body.unshift({ kind: "remark", short: "never awaits", text: `${name} is async but never awaits: it runs to its end in the frame it is called, as a function that is not async would. Take the async away, or await something in it.`, at: this.sourceOfIn(target, idle) }); + } this.mark(fn, decl); this.functions.push(fn); this.declared.set(fn, decl); diff --git a/probes/game-generators.ts b/probes/game-generators.ts new file mode 100644 index 0000000..8f927cf --- /dev/null +++ b/probes/game-generators.ts @@ -0,0 +1,56 @@ +// TrigScript 4, the rest of B1: a generator walked by for…of, const [a, b] = await Promise.all, .then((value) => …) and an +// async method, run in the lifted game as well as in the simulator; the two have to agree frame for frame. +// npx tsx scripts/test-game.mts probes/game-generators.ts +// A program first runs on frame 2. Built like a probe (Player 1 Terran, Player 2 a Zerg computer, location 1 "Home"). +import { test, expect, program, displayText, setResources, createUnit, sleep, frames, units, P1, P2, type Location } from "trigscript"; + +const HOME = 1 as Location; + +program(async () => { + class Squad { + left: number; + constructor(n: number) { this.left = n; } + async drain() { while (this.left > 0) { this.left--; setResources(P1, "add", 1, "gas"); await sleep(frames(1)); } } + } + async function slow(n: number): Promise { await sleep(frames(n)); return n * 10; } + function* waves(n: number) { + yield { count: 1, unit: units.ZergZergling }; + for (let i = 1; i <= n; i++) yield { count: i + 1, unit: units.ZergZergling }; + } + // Frames 2, 3, 4: one Zergling, then two, then three (a test's frames(n) has run frame n). + for (const w of waves(2)) { createUnit(P2, w.unit, w.count, HOME); await sleep(frames(1)); } + // From frame 5: slow(1) ends on frame 6, slow(3) on frame 8. + const [a, b] = await Promise.all([slow(3), slow(1)]); + setResources(P1, "set", a + b, "ore"); + // From frame 8: slow(2) ends on frame 10 and wins, its 20 handed to .then. + const v = await Promise.race([slow(2).then((k) => k + 1), sleep(frames(10)).then(() => 0)]); + setResources(P1, "set", v, "ore"); + // From frame 10: a gas a frame for three frames. + const s = new Squad(3); + await s.drain(); + displayText("drained"); +}, { owner: P1 }); + +test("a generator: the loop's body at each yield", (sim) => { + sim.frames(2); + expect(sim.count(P2, units.ZergZergling, HOME)).toBe(1); + sim.frames(1); + expect(sim.count(P2, units.ZergZergling, HOME)).toBe(3); + sim.frames(1); + expect(sim.count(P2, units.ZergZergling, HOME)).toBe(6); +}); + +test("Promise.all's values, and .then handed what a promise gives", (sim) => { + sim.frames(7); + expect(sim.resources(P1).ore).toBe(0); + sim.frames(1); + expect(sim.resources(P1).ore).toBe(40); + sim.frames(2); + expect(sim.resources(P1).ore).toBe(21); +}); + +test("an async method, awaited", (sim) => { + sim.frames(14); + expect(sim.resources(P1).gas).toBe(3); + expect(sim).toHavePrinted("drained"); +}); diff --git a/tests/async.test.ts b/tests/async.test.ts index 4b7e811..21725ef 100644 --- a/tests/async.test.ts +++ b/tests/async.test.ts @@ -129,7 +129,7 @@ describe("async functions and await: what is said", () => { expect(messages(`await Promise.all([]);`)).toEqual(["Promise.all([]) has nothing to wait for."]); expect(messages(`const ps = [sleep(frames(1))]; await Promise.all(ps);`)).toContain("Promise.all() takes a list written where it is called: Promise.all([wave(1), sleep(seconds(90))])."); expect(messages(`const v = await Promise.race([sleep(frames(1)), sleep(frames(2)).then(() => 1)]); print(\`\${v}\`);`)).toContain("v would be number | void: 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(() => …)."); - expect(messages(`const v: number = await Promise.all([sleep(frames(1)).then(() => 1)]) as never; print(\`\${v}\`);`)).toEqual(["Promise.all() gives no value yet: await it as a statement, and keep what each branch finds in variables of the program."]); + expect(messages(`const v: number = await Promise.all([sleep(frames(1)).then(() => 1)]) as never; print(\`\${v}\`);`)).toEqual(["Promise.all() gives a value for each item: take them apart where it is awaited — const [a, b] = await Promise.all([…])."]); }); it("await where the game cannot leave and come back", () => { diff --git a/tests/eud-build.test.ts b/tests/eud-build.test.ts index 1a274ca..e5d7425 100644 --- a/tests/eud-build.test.ts +++ b/tests/eud-build.test.ts @@ -333,6 +333,17 @@ const FIXTURES: Record = { await until(() => mine >= 3); setResources(CurrentPlayer, "add", 50, "ore"); }, { owner: AllPlayers });`, + generators: `program(async () => { + class Squad { left: number; constructor(n: number) { this.left = n; } async drain() { while (this.left > 0) { this.left--; await sleep(frames(1)); } } } + async function slow(n: number): Promise { await sleep(frames(n)); return n * 10; } + function* waves(n: number) { yield { count: 1, unit: units.ZergZergling }; for (let i = 1; i <= n; i++) { if (i == 3) continue; yield { count: i + 1, unit: units.ZergZergling }; } } + for (const w of waves(4)) { if (w.count > 4) break; createUnit(P8, w.unit, w.count, locations.Anywhere); await sleep(frames(1)); } + const [a, , c] = await Promise.all([slow(3), sleep(frames(1)), slow(1).then((k) => k > 5)]); + const v = await Promise.race([slow(2).then((k) => k + a), sleep(frames(10)).then(() => 0)]); + if (c) setResources(P1, "set", v, "ore"); + const s = new Squad(3); + await s.drain(); + });`, }; function build(name: string, src: string): { out: number; triggers: number } { diff --git a/tests/tasks.test.ts b/tests/tasks.test.ts index 4b45d7c..1135412 100644 --- a/tests/tasks.test.ts +++ b/tests/tasks.test.ts @@ -122,3 +122,78 @@ describe("tasks", () => { expect(messages(`start(async () => {}, { max: 0 });`)).toEqual(["start()'s options are written out, as { max: 40 }: how many of this start's tasks can run at once, a whole number from 1 to 255."]); }); }); + +describe("the rest of B1", () => { + it("const [a, b] = await Promise.all([…]): each name its branch's value, set as the branch ends", () => { + const r = compile(` + async function slow(n: number): Promise { await sleep(frames(n)); return n * 10; } + const [a, , c] = await Promise.all([slow(3), sleep(frames(1)), until(() => true).then(() => true)]); + print(\`\${a} \${c ? 1 : 0}\`);`); + expect(printed(r, 6)).toEqual(["3:30 1"]); + expect(messages(`const v = await Promise.all([sleep(frames(1))]);`)).toContain("Promise.all() gives a value for each item: take them apart where it is awaited — const [a, b] = await Promise.all([…])."); + expect(messages(`const [a] = await Promise.all([sleep(frames(1))]);`)).toEqual(["a would be void: its item gives no value. Give it one with .then(() => …), or leave a hole for it — [, b]."]); + }); + + it(".then((value) => …) is handed what the promise gives", () => { + const r = compile(` + async function wave(n: number): Promise { await sleep(frames(n)); return n; } + const v = await Promise.race([wave(2).then((k) => k * 100), sleep(frames(5)).then(() => -1)]); + print(\`\${v}\`); + await wave(1).then((k) => { print(\`then \${k}\`); });`); + expect(printed(r, 6)).toEqual(["2:200", "3:then 1"]); + expect(messages(`await sleep(frames(1)).then((x) => x);`)[0]).toContain("this one gives nothing"); + }); + + it("an async method is awaited, and started, as an async function is", () => { + const r = compile(` + class Squad { + left: number; + constructor(n: number) { this.left = n; } + async drain(name: string) { while (this.left > 0) { this.left--; await sleep(frames(1)); } print(\`\${name} empty\`); } + } + const a = new Squad(2); + const b = new Squad(4); + start(b.drain("b")); + await a.drain("a"); + print(\`b has \${b.left}\`);`); + expect(printed(r, 8)).toEqual(["2:a empty", "2:b has 2", "4:b empty"]); + }); + + it("a generator walked by for…of: the loop's body at each yield, values the script knows kept as they are", () => { + const r = compile(` + function* waves(n: number) { + yield { count: 2, unit: units.ZergZergling }; + for (let i = 1; i <= n; i++) yield { count: i * 3, unit: units.ZergHydralisk }; + print("after the last"); + } + for (const w of waves(2)) { + createUnit(P8, w.unit, w.count, locations.Anywhere); + print(\`\${w.count}\`); + await sleep(frames(1)); + } + print("done");`); + expect(printed(r, 6)).toEqual(["0:2", "1:3", "2:6", "3:after the last", "3:done"]); + }); + + it("break leaves the generator, continue goes on after the yield; for await walks an async one", () => { + const r = compile(` + function* count() { let i = 0; while (true) { i++; yield i; } } + for (const k of count()) { if (k % 2 == 0) continue; if (k > 5) break; print(\`\${k}\`); } + async function* slowly() { for (let i = 0; i < 2; i++) { await sleep(frames(2)); yield i; } } + for await (const k of slowly()) print(\`slow \${k}\`); + print("end");`); + expect(printed(r, 6)).toEqual(["0:1", "0:3", "0:5", "2:slow 0", "4:slow 1", "4:end"]); + expect(messages(`function* g() { yield 1; } const it = g();`)[0]).toContain("is a generator: a program walks one with for…of"); + expect(messages(`function* g() { yield 1; } for (const k of g()) { if (k) return; }`)).toEqual(["return inside a for…of over a generator is not supported yet: break out of the loop, and return after it."]); + }); + + it("an async function that never awaits is a hint on its name, once", () => { + const r = compile(` + async function idle(): Promise { return 3; } + async function busy() { await sleep(frames(1)); } + const a = await idle(); const b = await idle(); await busy(); + print(\`\${a + b}\`);`); + expect(r.hints.filter((h) => h.label === "never awaits").map((h) => h.line)).toEqual([2]); + expect(printed(r, 3)).toEqual(["1:6"]); + }); +}); From 9186ae4daed0646dcfcd8e10fadfdf70630f1ec6 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 17:35:02 -0600 Subject: [PATCH 035/238] TrigScript 4: B1's second part as built Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 71 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 723c1ea..db88c7d 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -2,7 +2,7 @@ Written 2026-10-15. Status: Part A built (A0–A5, each with its *As built*), and Remastered's emulator read from a dump (*R0*, 2026-10-16); Part B steered by three showpiece maps -(*Showpieces*, 2026-10-16), B1 started. The question it answers: now that there is a +(*Showpieces*, 2026-10-16), B1 built (both parts, 2026-10-16). The question it answers: now that there is a lifted copy of the game's own code (scm-sim's exact engine) beside eudplib, what does TrigScript become if it uses the game itself as its checker, and how far can the language go towards the TypeScript people actually write. @@ -786,12 +786,62 @@ known when the script is built, so its place and its wait are two cells, like a are. JavaScript does not cancel them, but here a branch is a fixed pair of cells, so a race met again in a loop could not run a second copy beside the first. Stopping is also what the plan's own example means. (Open decisions, below.) -- **Not yet.** Starting a task and leaving it running (`spawn`, or an async call nothing - awaits) needs tasks that are not known when the script is built, which is the heap - frame of the design above. Also not yet: generators, `const [a, b] = await - Promise.all(…)`, async methods of a class, a parameter for `.then`'s function, and - recursion through `await` (it is the inline limit's error today). These are the second - part of B1, together with the hint for an async function that never awaits. +- **Not yet** in the first part: starting a task and leaving it running, generators, + `const [a, b] = await Promise.all(…)`, async methods, a parameter for `.then`'s + function, recursion through `await`, the hint for an async function that never awaits. + All but recursion are the second part, below. + +**As built, second part (2026-10-16, under Node and in the lifted game; branch `b1-tasks`).** + +- **Tasks: `start()`, not `spawn()`.** Scripts already call their own wave functions + `spawn` (the guide's examples do, and so do the tests), so the library's name is + `start`: `start(wave(3))`, `start(async () => { … })`, and `void wave(3)` as JavaScript + leaves a promise nobody keeps. A bare `wave(3);` in an async function stays an error, + which now names both `await` and `start()`. +- **No heap after all.** A task is not a frame on the heap. Each `start` is a body of its + own, inlined once, with `max` slots (16 unless `{ max }` says, up to 255). A slot is a row + of everything the body declares (`VarDecl.task`, `ArrayDecl.task` in IR 16), the same + rows a per-player program has, one level down. In a per-player program they are rows + within each player's. The arguments are worked out at the start, and the parameters + are always copies, never the caller's variables. A start with every slot running starts + nothing, and the game says so once. +- **When a task runs.** It starts in the frame it is started in, once the program has + given the frame back. After that it goes on every frame after the program's turn, by + start and then by slot, and it runs on after the program ends. A task started during + the tasks' turn gets its first turn in the same frame: the slots are gone through + again. Each start keeps a count of its running tasks, so a start with none costs a few + conditions a frame, and the slots after the last running one are not looked at. That + took the probe from about 7,700 conditions and actions a frame to about 1,000. +- **A Task.** `start()` gives a number: + `slot | start << 8 | generation << 16`. `t.running`, `t.cancel()` and `await t` work + through it, and a number of a slot that has been taken again finds nothing. A task + that cancels itself stops at its next wait. +- **What is refused.** A task that reads a variable of the task it was started from is + refused, because which of that task's copies it would find is not settled. So is a task + closing over a variable that the loop around the `start()` changes (JavaScript gives + each turn its own). A function that starts a task is never one called copy. A function + that starts itself, directly or round about, is the inline limit's error, and the + message now says to write it as a loop. +- **Generators** are walked by `for…of` (`for await` for an `async function*`). The + generator's body is inlined, and at each `yield` sits a copy of the loop's body. The + loop variable is what was yielded: a value the script knows stays one, as in an + unrolled loop, and anything else goes in a variable of that turn. `continue` goes on + after the yield, and `break` leaves the generator. A generator kept in a `const` and + stepped with `.next()` is not supported, nor is `yield*` or a `return` in the loop's + body. Each says so. +- **The rest.** `const [a, , c] = await Promise.all([…])` gives each name its item's + value, set as the item ends. `.then((k) => …)` is handed what the promise gives. A + class may have async methods. An async function that never awaits gets a hint on its + name. Unrelated, but met on the way: a loop that has a `break` or a `return` in it is + no longer reported as freezing the game. +- **Recursion through `await`: decided, not built.** An async function that waits keeps + its place where it waits, one place for each copy, so a call of itself would need the + whole chain of places kept and brought back. A loop does the same job, and the error + says so. +- **Checked.** `tests/tasks.test.ts` (15) and three eudplib build fixtures (`started`, + `startedPerPlayer`, `generators`). `probes/game-tasks.ts` (4) and + `probes/game-generators.ts` (3) pass in the lifted game and in the simulator at the + same frames. Every older game probe still passes. ### B2. Events (4.4.0) @@ -1069,10 +1119,11 @@ build, contract 2) ships first, then plugin-scm-sim, then TrigScript. nothing of the game is drawn. A5's "the game beside the code" draws, so it needs the person's own copy, as Test Map does. - **Does `Promise.race` cancel the losers?** JavaScript: no. Proposed: no, as JavaScript, - plus an explicit `task.cancel()` on what `spawn(…)` returns. *Decided otherwise in B1's + plus an explicit `task.cancel()` on what `start(…)` returns (built as `t.cancel()`). *Decided otherwise in B1's first part (2026-10-16), and confirmed by the user:* the losers stop, because a branch is - a fixed pair of cells and a race met again cannot run beside its last run. Once `spawn` - exists, a loser that should go on can be spawned. + a fixed pair of cells and a race met again cannot run beside its last run. A loser that + should go on can be started with `start()` and raced as `await t` (built in B1's second + part). - **What a closure may capture.** Proposed above (B3): `const` by value, `let` only when the closure does not outlive its frame. The alternative is reference-counted environments: closer to JavaScript, more Python, and a new kind of fault. From 31e03985b23970fc2c1042c6e70dcaaca3b245b9 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 18:40:55 -0600 Subject: [PATCH 036/238] TrigScript 4, S2: the UI spike, where a board of text can be drawn Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 16 +++ probes/ui-spike.py | 289 +++++++++++++++++++++++++++++++++++++++++++++ probes/ui-spike.ts | 24 ++++ 3 files changed, 329 insertions(+) create mode 100644 probes/ui-spike.py create mode 100644 probes/ui-spike.ts diff --git a/docs/v4-plan.md b/docs/v4-plan.md index db88c7d..9fb7963 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1041,6 +1041,22 @@ ui.objectives(P1, () => ( and the made-text field writes (built). No new address. - **Probe:** one, for how many characters and colour codes a panel holds before the game cuts it, and whether a rewrite every frame flickers. +- **Where it draws, first** (2026-10-16, started on S2 with a Tetris map as its target): + the objectives panel is read again on every draw, but it is a dialog the player opens, + and in a single-player game opening it may pause the game. A board the player watches + while playing may belong in the text lines instead, which Remastered's emulator lets a + map write (`0x640B60`, 13 lines of 218 bytes; the line pointer at `0x640B58` is kept + beside the game). So the probe asks where before it asks how much: + `probes/ui-spike.py` (hand-written eudplib, built beside `probes/ui-spike.ts`), steps + A–I: how many text lines stay up, which cell characters line up (■ □ █ ▀ ▄ ░ ▓ ㅁ, a + full-width space and more), which colour codes colour a cell, how many cells fit on a + line, whether a line break in one Display Text makes several lines, a board redrawn every + frame and every fourth frame, twenty rows in ten lines with half blocks, the lines' + memory written with no action, a 22-row board in the open objectives with a clock in the + leaderboard's label, and how much of that label shows. Run in the lifted game first: no + address Remastered ends the game on, about 2.4k actions a frame on average and 82k at + most (eudplib's start at frame 2). Copied to + `Maps/TrigScript/trigscript-s2-ui-spike.scx`. Not played yet. ### S3. A hero moved with the keys diff --git a/probes/ui-spike.py b/probes/ui-spike.py new file mode 100644 index 0000000..ba9ea99 --- /dev/null +++ b/probes/ui-spike.py @@ -0,0 +1,289 @@ +""" +[uiSpike] + +The UI spike (TrigScript 4, S2, before any compiler work): where can a script draw a screen of +text that the player watches while playing, and what does it look like? S2's demonstration is +Tetris drawn in the game's text, so every step asks something a Tetris board needs. Hand-written +eudplib, not the compiler's output. + + A 2 the text lines: thirteen numbered lines are printed; which numbers stay on the screen? + B1 12 cell characters, page 1: ten of each in a row between bars; which rows end level? + B2 24 cell characters, page 2 + C 36 the colour codes: which give a colour of their own on a cell? + D 48 how many cells fit on one line before it wraps or is cut? + E 60 one Display Text with line breaks in it: one line or several? + F1 70 a Tetris board in the text lines, redrawn every frame: does it flicker? + F2 82 the same, redrawn every fourth frame + F3 94 twenty rows in ten lines with half blocks, redrawn every frame + G 106 the text lines' memory written straight, no action: does the screen change? when do lines fade? + H 126 a 22-row board in the objectives panel: does it fall while the panel is open? is the game paused? + I 161 the leaderboard's label: how many cells before it is cut? + +The map's texts are written as UTF-8 here (eudplib's own default for a map string is cp949), so +every literal goes through u(). Built beside probes/ui-spike.ts, which says the same. +""" +from eudplib import * +from eudplib.core.mapdata.stringmap import ForceAddString + +tick = EUDVariable() # 0..23, a second's frames +second = EUDVariable() +beat = EUDVariable() # 0..11: a static page is printed again when it is 0 +fall = EUDVariable() # frames since the piece last moved +row = EUDVariable() # the falling piece's top row +redraw = EUDVariable() # F2's frames since the board was last printed + +TEXT_LINES = 0x640B60 # the game's text lines, 218 bytes each (allowed both ways in Remastered's emulator) +LINE = 218 + +SLOT_OBJECTIVES = ForceAddString(" " * 2047) +SLOT_BOARD = ForceAddString(" " * 255) +SLOT_LINE = ForceAddString(" " * 255) + + +def u(s): + return s.encode("utf-8") + + +def show(s): + DoActions(DisplayText(u(s))) + + +def between(a, b): + return EUDIf()([second >= a, second <= b]) + + +def at(n): + return EUDIf()([second == n, tick == 0]) + + +def page(lines): + """Print a page of lines every half second, so it stays up and a reader can compare.""" + if EUDIf()(beat == 0): + for line in lines: + show(line) + EUDEndIf() + + +GREY, WHITE = "\x05", "\x04" +EMPTY10 = GREY + "□" * 10 +PIECE = GREY + "□□□□" + "\x11■■" + GREY + "□□□□" +STACK = [ + "\x08■■\x0E■■■" + GREY + "□" + "\x07■■\x03■■", + "\x10■\x07■■■\x11■■\x0F■■■" + GREY + "□", +] + + +def framed(cells, end=""): + return WHITE + "|" + cells + WHITE + "|" + end + + +def move_piece(every, last): + """The piece's top row goes down one every `every` frames, from 0 to `last` and again.""" + fall.__iadd__(1) + if EUDIf()(fall >= every): + fall << 0 + row.__iadd__(1) + if EUDIf()(row > last): + row << 0 + EUDEndIf() + EUDEndIf() + + +def occupies(i): + """Row i holds the O piece when its top row is i - 1 or i.""" + return [row == 0] if i == 0 else [row >= i - 1, row <= i] + + +def full_board(header): + """Ten lines: an O piece falling through rows 0..7 above a two-row stack.""" + show(header) + for i in range(8): + if EUDIf()(occupies(i)): + show(framed(PIECE)) + if EUDElse()(): + show(framed(EMPTY10)) + EUDEndIf() + for cells in STACK: + show(framed(cells)) + + +HALF_EMPTY = GREY + "░" * 10 +HALF = {"full": "██", "upper": "▀▀", "lower": "▄▄"} +HALF_STACK = [ + "\x08██\x0E███" + GREY + "░" + "\x07██\x03██", + "\x10█\x07███\x11██\x0F███" + GREY + "░", +] + + +def half_board(header): + """Twenty rows in ten lines: line i holds rows 2i (top half) and 2i+1 (bottom half).""" + show(header) + for i in range(8): + cells = {k: framed(GREY + "░░░░\x11" + v + GREY + "░░░░") for k, v in HALF.items()} + if EUDIf()(row == 2 * i): + show(cells["full"]) + if EUDElseIf()(row == 2 * i + 1): + show(cells["lower"]) + if i and EUDElseIf()(row == 2 * i - 1): + show(cells["upper"]) + if EUDElse()(): + show(framed(HALF_EMPTY)) + EUDEndIf() + for cells in HALF_STACK: + show(framed(cells)) + + +def objectives_board(): + """The objectives text: a header and 22 numbered rows, the piece falling through rows 0..19.""" + p = EUDVariable() + p << f_dbstr_print(GetMapStringAddr(SLOT_OBJECTIVES), u("\x07H: a board in the objectives, second "), second, u("\n")) + for i in range(20): + if EUDIf()(occupies(i)): + p << f_dbstr_print(p, u(framed(PIECE, " %d\n" % (i + 1)))) + if EUDElse()(): + p << f_dbstr_print(p, u(framed(EMPTY10, " %d\n" % (i + 1)))) + EUDEndIf() + f_dbstr_print(p, u(framed(STACK[0], " 21\n") + framed(STACK[1], " 22\n") + WHITE + "(23) a row of 20: " + "■" * 20)) + + +GLYPHS_1 = ["■", "□", "█", "▀", "▄", "░", "▓", "ㅁ", " ", "●"] +GLYPHS_2 = ["◆", "★", "O", "▒", "▌", "▐", "■□", " ", "Ⅰ", "■ (codes between)"] +COLOURS = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0E, 0x0F, 0x10, 0x11, + 0x15, 0x16, 0x17, 0x18, 0x19, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F] + + +def glyph_row(g): + if g == "■ (codes between)": + cells = "".join(("\x08" if i % 2 else "\x0E") + "■" for i in range(10)) + elif len(g) == 2 and g != " ": + cells = g * 5 + else: + cells = g * 10 + return framed(cells, " " + WHITE + {" ": "full-width space", " ": "two spaces"}.get(g, g)) + + +def onPluginStart(): + pass + + +def beforeTriggerExec(): + f_setcurpl(P1) + tick.__iadd__(1) + beat.__iadd__(1) + if EUDIf()(beat >= 12): + beat << 0 + EUDEndIf() + if EUDIf()(tick >= 24): + tick << 0 + second.__iadd__(1) + EUDEndIf() + + if between(2, 11): + page(["\x07A: line %d of 13 — which numbers do you see?" % n for n in range(1, 14)]) + EUDEndIf() + + if between(12, 23): + page(["\x07B1: cell characters. Which rows end with their bar under the ■ row's?"] + [glyph_row(g) for g in GLYPHS_1]) + EUDEndIf() + if between(24, 35): + page(["\x07B2: more cell characters. Which rows end level? Which glyphs are not drawn?"] + [glyph_row(g) for g in GLYPHS_2]) + EUDEndIf() + + if between(36, 47): + rows = ["\x07C: the colour codes. Which numbers give a colour of their own?"] + for k in range(0, len(COLOURS), 6): + rows.append("".join("%c■■%s%02X " % (c, WHITE, c) for c in COLOURS[k:k + 6])) + rows.append(WHITE + "all in a row: " + "".join("%c■" % c for c in COLOURS)) + rows.append(WHITE + "half blocks: " + "".join("%c▀▄" % c for c in COLOURS[:11])) + page(rows) + EUDEndIf() + + if between(48, 59): + page(["\x07D: rows of cells. Which is the longest on one line?"] + [framed("■" * n, " " + str(n)) for n in (10, 15, 20, 25, 30, 40, 50, 60)]) + EUDEndIf() + + if between(60, 69): + if EUDIf()([beat == 0, tick <= 11]): + show("\x07E: the next six are ONE Display Text with line breaks. Six lines, or one?") + show("\x04E1: first\nE2: second\nE3: third\nE4: fourth\nE5: fifth\nE6: sixth") + EUDEndIf() + EUDEndIf() + + if at(70): + row << 0 + fall << 0 + EUDEndIf() + if between(70, 81): + move_piece(6, 6) + full_board("\x07F1: a board, redrawn every frame. Does it flicker or tear?") + EUDEndIf() + if between(82, 93): + move_piece(6, 6) + redraw.__iadd__(1) + if EUDIf()(redraw >= 4): + redraw << 0 + full_board("\x07F2: the same board, redrawn every fourth frame. Better, worse, the same?") + EUDEndIf() + EUDEndIf() + if at(94): + row << 0 + fall << 0 + EUDEndIf() + if between(94, 105): + move_piece(3, 14) + half_board("\x07F3: twenty rows in ten lines (half blocks). Does the piece fall half a line at a time?") + EUDEndIf() + + if at(106): + for n in range(1, 12): + show("\x04G: printed at second 106, line %d of 11" % n) + show("\x07G: from now on each line's memory is written every second, no action.") + EUDEndIf() + if between(107, 125): + if EUDIf()(tick == 0): + for n in range(13): + f_dbstr_print(TEXT_LINES + LINE * n, u("\x07G: memory of line %d, written at second " % n), second) + EUDEndIf() + EUDEndIf() + if at(118): + show("\x07G: do you see lines COUNTING? And when did the lines of second 106 fade?") + EUDEndIf() + + if at(126): + row << 0 + fall << 0 + f_dbstr_print(GetMapStringAddr(SLOT_BOARD), u("game clock: "), second) + DoActions([LeaderBoardComputerPlayers(Disable), LeaderBoardKills("Terran Marine", SLOT_BOARD), SetMissionObjectives(SLOT_OBJECTIVES)]) + EUDEndIf() + if between(126, 160): + move_piece(6, 18) + if EUDIf()(fall == 0): + objectives_board() + EUDEndIf() + if EUDIf()(tick == 0): + f_dbstr_print(GetMapStringAddr(SLOT_BOARD), u("game clock: "), second) + EUDEndIf() + if EUDIf()([tick == 0, second <= 156]): + if EUDIf()(second.AtMost(127)): + show("\x07H: open the objectives now (F10, Mission Objectives) and keep them open for half a minute.") + EUDEndIf() + if EUDIf()(second == 135): + show("\x07H: does the orange block fall while they are open? Which row numbers show? Is row 23 cut?") + EUDEndIf() + if EUDIf()(second == 145): + show("\x07H: and the leaderboard's game clock: did it go on counting while they were open?") + EUDEndIf() + EUDEndIf() + EUDEndIf() + + if at(161): + f_dbstr_print(GetMapStringAddr(SLOT_BOARD), u("I:" + "".join("■■■■■■■■■" + str(d % 10) for d in range(1, 8)))) + show("\x07I: the leaderboard's label is now cells with a digit after every ten. The last digit you see?") + EUDEndIf() + if at(173): + show("\x07That is the end of the UI spike. Thank you.") + EUDEndIf() + + +def afterTriggerExec(): + pass diff --git a/probes/ui-spike.ts b/probes/ui-spike.ts new file mode 100644 index 0000000..09720be --- /dev/null +++ b/probes/ui-spike.ts @@ -0,0 +1,24 @@ +// The UI spike (TrigScript 4, S2), played before any compiler work: where a script can draw a screen of text the +// player watches while playing, for S2's Tetris. The script below does next to nothing; what is tried is +// probes/ui-spike.py, hand-written eudplib built into the same map. Host it as a multiplayer game, alone, so that +// opening the objectives does not pause it. What to look for, by the second (each step says so on the screen too): +// A 2 thirteen numbered lines printed: which numbers stay on the screen? +// B1 12 cell characters between bars: which rows end level with the ■ row? +// B2 24 more cell characters; which are not drawn at all? +// C 36 the colour codes on cells: which give a colour of their own? +// D 48 rows of 10 to 60 cells: the longest on one line? +// E 60 one Display Text with line breaks: six lines or one? +// F1 70 a board in the text lines, redrawn every frame: flicker? tearing? +// F2 82 the same, redrawn every fourth frame +// F3 94 twenty rows in ten lines with half blocks +// G 106 the lines' memory written every second with no action: do they count? when do lines fade? +// H 126 open the objectives and keep them open: does the board fall while open? which rows show? clock paused? +// I 161 the leaderboard's label as cells: where is it cut? +// Built with +// EXTRA_PLUGIN=uiSpike=probes/ui-spike.py npx tsx scripts/build-fixture.mts probes/ui-spike.ts fixtures/eud/ui-spike.scx +import { program, displayText, P1 } from "trigscript"; + +program(() => { + sleep(seconds(1)); + displayText("The UI spike: steps A to I, about three minutes. Each says what to look for."); +}, { owner: P1 }); From b301a755849a727c7fe0bd7d16f850cc9527d055 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 19:23:06 -0600 Subject: [PATCH 037/238] TrigScript 4, S2: the UI spike played, the board goes in the text lines Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 9fb7963..bfccf9f 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1056,7 +1056,34 @@ ui.objectives(P1, () => ( leaderboard's label, and how much of that label shows. Run in the lifted game first: no address Remastered ends the game on, about 2.4k actions a frame on average and 82k at most (eudplib's start at frame 2). Copied to - `Maps/TrigScript/trigscript-s2-ui-spike.scx`. Not played yet. + `Maps/TrigScript/trigscript-s2-ui-spike.scx`. + + **Played 2026-10-16** (A–G single player, H again hosted as multiplayer; Remastered at + 2560×1440, the bars measured in the screenshots): + - **The board goes in the text lines.** 11 stay up (a header and ten rows). One Display + Text with line breaks makes as many lines (E), so a board is one action. Redrawn every + frame it neither flickers nor tears, and every fourth frame looks the same (F1, F2). + Sixty cells still fit on one line (D). + - **The lines are drawn from their memory, live** (G): text written into `0x640B60` with + no action appeared and counted. But each line still fades on its own timer; a write + changes the words, not how long the line stays. A Display Text restarts a line's timer. + - **The objectives panel is out for anything live** (H): it pauses a single-player game, + and in multiplayer the open panel is a picture taken when it opened (it said second 128 + at 137 and 146). It shows nine lines; past them the line breaks are lost and the rest + runs together on one line. It is fine for a static text. + - **The leaderboard's label** shows about 23 characters, then "..." (I). Live, as the + strings spike found: a score or a level, not a board. + - **Cells:** ■ □ ● are one width; ㅁ, the full-width space, O and ▒ are another (wider). + █ ▀ ▄ ░ ▓ ▌ ▐ Ⅰ are not in the font and come out as narrow boxes, so half blocks are + dead. Colour codes take no width. + - **Colours:** 02 03 04 07 08 0E 0F 10 11 15 18 19 1B 1C 1D 1E 1F each draw a colour of + their own (16 is 04's white; 01 takes the line's colour). **Grey sticks:** after 05, + every later code on the line is ignored and the rest stays grey, so empty cells must not + be grey. 06 was only seen after 05; a second round tests each code on a line of its own. + + So S2's first surface is `ui.lines(player, () => <…/>)`: the text lines, one Display + Text a frame when the view's inputs changed (and again before the lines fade), ten rows + under a header. The objectives and the label stay as plain made texts. ### S3. A hero moved with the keys From 9afb4c2f5c6e240681198e56a945deaa6f17895f Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 19:24:22 -0600 Subject: [PATCH 038/238] TrigScript 4, S2: the UI spike's second round, a board's colours Co-Authored-By: Claude Opus 5.5 --- probes/ui-spike-2.py | 160 +++++++++++++++++++++++++++++++++++++++++++ probes/ui-spike-2.ts | 18 +++++ 2 files changed, 178 insertions(+) create mode 100644 probes/ui-spike-2.py create mode 100644 probes/ui-spike-2.ts diff --git a/probes/ui-spike-2.py b/probes/ui-spike-2.py new file mode 100644 index 0000000..5d7face --- /dev/null +++ b/probes/ui-spike-2.py @@ -0,0 +1,160 @@ +""" +[uiSpike2] + +The UI spike's second round (TrigScript 4, S2): the colours of a board in the text lines. The first +round found that after colour code 05 (grey) every later code on the line is ignored; this round +asks which other codes do that, tries empty cells in colours that are not grey, draws the board the +way the compiler will (one Display Text with line breaks, made in the game, every frame), and times +how long a line stays up. Hand-written eudplib, not the compiler's output. + + A1 2 one code per line, then red, green and blue cells: which lines stay one colour? + A2 14 the rest of the codes the same way + B1 26 a board with empty cells in 15 (brown), one Display Text a frame + B2 38 empty cells in 18 (dark green) + B3 50 empty cells in 1E (lavender) + B4 62 empty cells in 0E (blue) + C 74 one line printed once: at which second (the leaderboard's clock) does it go? + +The map's texts are written as UTF-8 (eudplib's own default for a map string is cp949). Built beside +probes/ui-spike-2.ts, which says the same. +""" +from eudplib import * +from eudplib.core.mapdata.stringmap import ForceAddString + +tick = EUDVariable() # 0..23, a second's frames +second = EUDVariable() +beat = EUDVariable() # 0..11: a static page is printed again when it is 0 +fall = EUDVariable() # frames since the piece last moved +row = EUDVariable() # the falling piece's top row + +SLOT_SCREEN = ForceAddString(" " * 2047) +SLOT_CLOCK = ForceAddString(" " * 63) +WHITE = "\x04" + + +def u(s): + return s.encode("utf-8") + + +def show(s): + DoActions(DisplayText(u(s))) + + +def between(a, b): + return EUDIf()([second >= a, second <= b]) + + +def at(n): + return EUDIf()([second == n, tick == 0]) + + +def page(lines): + if EUDIf()(beat == 0): + for line in lines: + show(line) + EUDEndIf() + + +CODES = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x0E, 0x0F, 0x10, 0x11, 0x14, + 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F] + + +def code_line(c): + # White label first, then the code under test on two cells, then cells in three other codes and a word. + return WHITE + "%02X: " % c + chr(c) + "■■" + "\x08■\x07■\x0E■" + WHITE + " end" + + +def occupies(i): + """Row i holds the O piece when its top row is i - 1 or i.""" + return [row == 0] if i == 0 else [row >= i - 1, row <= i] + + +def board(header, empty): + """The screen as one text: a header and ten rows, an orange O falling through rows 0..7 above a + two-row stack, empty cells in the code `empty`. Written into a slot, then one Display Text.""" + e = empty + "□" + piece = WHITE + "|" + e * 4 + "\x11■■" + e * 4 + WHITE + "|" + blank = WHITE + "|" + empty + "□" * 10 + WHITE + "|" + stack = [ + WHITE + "|" + "\x08■■\x0E■■■" + e + "\x07■■\x03■■" + WHITE + "|", + WHITE + "|" + "\x10■\x07■■■\x11■■\x0F■■■" + e + WHITE + "|", + ] + p = EUDVariable() + p << f_dbstr_print(GetMapStringAddr(SLOT_SCREEN), u(header)) + for i in range(8): + if EUDIf()(occupies(i)): + p << f_dbstr_print(p, u("\n" + piece)) + if EUDElse()(): + p << f_dbstr_print(p, u("\n" + blank)) + EUDEndIf() + f_dbstr_print(p, u("\n" + stack[0] + "\n" + stack[1])) + DoActions(DisplayText(SLOT_SCREEN)) + + +def move_piece(every, last): + fall.__iadd__(1) + if EUDIf()(fall >= every): + fall << 0 + row.__iadd__(1) + if EUDIf()(row > last): + row << 0 + EUDEndIf() + EUDEndIf() + + +BOARDS = [ + (26, "15", "\x15", "brown"), + (38, "18", "\x18", "dark green"), + (50, "1E", "\x1E", "lavender"), + (62, "0E", "\x0E", "blue"), +] + + +def onPluginStart(): + pass + + +def beforeTriggerExec(): + f_setcurpl(P1) + tick.__iadd__(1) + beat.__iadd__(1) + if EUDIf()(beat >= 12): + beat << 0 + EUDEndIf() + if EUDIf()(tick >= 24): + tick << 0 + second.__iadd__(1) + f_dbstr_print(GetMapStringAddr(SLOT_CLOCK), u("clock: "), second) + EUDEndIf() + + if at(1): + DoActions([LeaderBoardComputerPlayers(Disable), LeaderBoardKills("Terran Marine", SLOT_CLOCK)]) + EUDEndIf() + + if between(2, 13): + page(["\x07A1: a code, then red, green, blue cells and \"end\". Which lines stay ONE colour after the code?"] + [code_line(c) for c in CODES[:10]]) + EUDEndIf() + if between(14, 25): + page(["\x07A2: the rest of the codes. Which lines stay one colour?"] + [code_line(c) for c in CODES[10:]]) + EUDEndIf() + + for start, code, empty, name in BOARDS: + if at(start): + row << 0 + fall << 0 + EUDEndIf() + if between(start, start + 11): + move_piece(6, 6) + board("\x07B: empty cells in %s (%s). Is the piece orange, the stack in colour? Easy to read?" % (code, name), empty) + EUDEndIf() + + if at(74): + show("\x07C: this line is printed once, at clock 74. At which clock does it go?") + EUDEndIf() + if at(100): + show("\x07That is the end of the second round. Thank you.") + EUDEndIf() + + +def afterTriggerExec(): + pass diff --git a/probes/ui-spike-2.ts b/probes/ui-spike-2.ts new file mode 100644 index 0000000..e55b751 --- /dev/null +++ b/probes/ui-spike-2.ts @@ -0,0 +1,18 @@ +// The UI spike's second round (TrigScript 4, S2): the colours of a board in the text lines. The script below does +// next to nothing; what is tried is probes/ui-spike-2.py, hand-written eudplib built into the same map. Single +// player is fine. The leaderboard shows a clock. What to look for, by the second (each step says so on the screen): +// A1 2 one colour code per line, then red, green and blue cells and "end": which lines stay one colour? +// A2 14 the rest of the codes +// B1 26 a board, empty cells in 15 (brown), drawn as one Display Text a frame: piece orange? stack coloured? +// B2 38 empty cells in 18 (dark green) +// B3 50 empty cells in 1E (lavender) +// B4 62 empty cells in 0E (blue) +// C 74 one line printed once: at which clock does it go? +// Built with +// EXTRA_PLUGIN=uiSpike2=probes/ui-spike-2.py npx tsx scripts/build-fixture.mts probes/ui-spike-2.ts fixtures/eud/ui-spike-2.scx +import { program, displayText, P1 } from "trigscript"; + +program(() => { + sleep(seconds(1)); + displayText("The UI spike, second round: steps A to C, about a minute and a half."); +}, { owner: P1 }); From 4c54c5749dc56f2ba8e0f8ae2baf44d224e68f7b Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 19:32:03 -0600 Subject: [PATCH 039/238] TrigScript 4, S2: the second round played, only grey sticks Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index bfccf9f..74f903b 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1081,6 +1081,14 @@ ui.objectives(P1, () => ( every later code on the line is ignored and the rest stays grey, so empty cells must not be grey. 06 was only seen after 05; a second round tests each code on a line of its own. + **Second round played 2026-10-16** (`probes/ui-spike-2.*`): **only 05 sticks**; every + other code lets later codes through, and 06 is red (as 08). A board made in the game and + shown by one Display Text a frame draws right: orange piece, coloured stack. Empty cells: + 15 (brown) nearly vanishes over dirt, 18 (dark green) and 0E (blue) read on black, **1E + (lavender) reads on black and over terrain and is no piece's colour**, so it is the + default for an empty cell. How long a line lasts was not caught (under six seconds, from + G), so a view that has not changed is still shown again once a second. + So S2's first surface is `ui.lines(player, () => <…/>)`: the text lines, one Display Text a frame when the view's inputs changed (and again before the lines fade), ten rows under a header. The objectives and the label stay as plain made texts. From cda1f2d4113b074e28a80ce1d2a8242e2d679116 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 20:15:06 -0600 Subject: [PATCH 040/238] TrigScript 4, S2: JSX texts and ui.lines (IR 17) A .tsx script writes a text the game shows as elements: , , and , coloured by name or by a number of the program's. A JSX text is put together in a buffer of its own (buildText / built), so a board is written in place rather than copied per piece; char is a byte from a number. ui.lines(player, () => ...) is a task that shows the text in the player's lines, built again only when something it reads was written (views.ts) and shown again every 24 frames. Also: a call in a loop's condition that writes what is outside it now counts as changing the condition. Co-Authored-By: Claude Opus 5.5 --- compiler/compiler.ts | 16 +- compiler/declarations.ts | 46 +++- compiler/eud.ts | 16 +- compiler/generated/trigscriptPy.ts | 2 +- compiler/hoist.ts | 25 +- compiler/input.ts | 2 +- compiler/ir.ts | 27 ++- compiler/jsx.ts | 118 +++++++++ compiler/link.ts | 2 +- compiler/numbers.ts | 3 +- compiler/recursion.ts | 4 +- compiler/runtime.ts | 12 + compiler/simulateIr.ts | 23 +- compiler/structured.ts | 372 +++++++++++++++++++++++++++++ compiler/views.ts | 133 +++++++++++ docs/ir.md | 28 ++- python/trigscript.py | 118 ++++++++- scripts/build-fixture.mts | 2 +- scripts/debug-game.mts | 2 +- scripts/game-check.mts | 4 +- scripts/test-game.mts | 2 +- tests/eud-build.test.ts | 22 +- tests/functions.test.ts | 2 +- tests/script-structured.test.ts | 2 +- tests/service.test.ts | 4 +- tests/ui.test.ts | 155 ++++++++++++ 26 files changed, 1094 insertions(+), 48 deletions(-) create mode 100644 compiler/jsx.ts create mode 100644 compiler/views.ts create mode 100644 tests/ui.test.ts diff --git a/compiler/compiler.ts b/compiler/compiler.ts index aaff165..4130920 100644 --- a/compiler/compiler.ts +++ b/compiler/compiler.ts @@ -36,8 +36,11 @@ import { english, type Translate } from "../i18n"; export type { ScriptString } from "./runtime"; export { DEATHS_TABLE_ADDRESS } from "./runtime"; -/** The file a script starts from. */ +/** The file a script starts from: main.ts, or main.tsx for a script that writes JSX in it. */ export const ENTRY_FILE = "main.ts"; +export const ENTRY_TSX = "main.tsx"; +/** The entry of a script's files: main.tsx when there is one, else main.ts. */ +export const entryOf = (files: Iterable): string => ([...files].includes(ENTRY_TSX) ? ENTRY_TSX : ENTRY_FILE); const LIB_FILE = "lib.d.ts"; /** The script's files by path (`main.ts`, `bases.ts`, `ai/waves.ts`). */ @@ -191,7 +194,8 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa const refs: MapReference[] = []; const scripts = new Map(); for (const [path, text] of Object.entries(files)) scripts.set(normalizePath(path), text); - if (!scripts.has(ENTRY_FILE)) { + const entry = entryOf(scripts.keys()); + if (!scripts.has(entry)) { diagnostics.push({ file: ENTRY_FILE, line: 1, column: 1, endLine: 1, endColumn: 1, message: `The script has no ${ENTRY_FILE}; that is the file a build starts from.`, source: "compiler" }); return result(); } @@ -202,12 +206,14 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa const compilerOptions: TS.CompilerOptions = { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS, moduleResolution: ts.ModuleResolutionKind.Bundler, strict: true, types: [], sourceMap: true, noLib: false, + // JSX (in a .tsx file) is a text the game shows: elements are calls of __jsx, which the structured compiler lowers in a program. + jsx: ts.JsxEmit.React, jsxFactory: "__jsx", jsxFragmentFactory: "__fragment", }; const outputs = new Map(); const host: TS.CompilerHost = { getSourceFile: (name) => { const text = texts.get(name); - return text === undefined ? undefined : ts.createSourceFile(name, text, ts.ScriptTarget.ES2022, true); + return text === undefined ? undefined : ts.createSourceFile(name, text, ts.ScriptTarget.ES2022, true, name.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS); }, getDefaultLibFileName: () => LIB_FILE, writeFile: (name, text) => { outputs.set(name, text); }, @@ -314,7 +320,7 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa if (diagnostics.length > planned.size) return result(); const linked = new Map(); for (const name of fileNames) { - const base = name.replace(/\.ts$/, ""); + const base = name.replace(/\.tsx?$/, ""); const js = outputs.get(`${base}.js`); if (js === undefined) { diagnostics.push({ file: name, line: 1, column: 1, endLine: 1, endColumn: 1, message: "The file produced no JavaScript.", source: "compiler" }); continue; } linked.set(name, { js, map: outputs.get(`${base}.js.map`) }); @@ -329,7 +335,7 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa const runtime = { ...createRuntime(names, collector), ...registry.library() }; collector.running = true; // The entry first, so the programs are there; then the test files, which nothing imports. - const failure = runModules(linked, ENTRY_FILE, runtime, MODULE_NAME, { imported: TESTING_NAMES, then: fileNames.filter(isTestFile).sort() }); + const failure = runModules(linked, entry, runtime, MODULE_NAME, { imported: TESTING_NAMES, then: fileNames.filter(isTestFile).sort() }); collector.running = false; if (failure) { const line = failure.line ?? 1; diff --git a/compiler/declarations.ts b/compiler/declarations.ts index 872f349..40b34ad 100644 --- a/compiler/declarations.ts +++ b/compiler/declarations.ts @@ -20,6 +20,7 @@ import { allTables, defaultScriptNames, type NameTable, type ScriptNames } from import { PLAYER_COLORS, TABLE_FIELDS, type TableKind } from "./tables"; import { PLAYER_SLOTS } from "./lower"; import { KEY_NAMES } from "./input"; +import { TEXT_COLORS } from "./jsx"; export const DECLARATIONS_FILE = "trigscript.d.ts"; @@ -187,6 +188,7 @@ ${kw}interface ProgramOptions { } const COLOR_NAMES = Object.keys(PLAYER_COLORS).map((w) => JSON.stringify(w)).join(" | "); +const TEXT_COLOR_TABLE = Object.entries(TEXT_COLORS).map(([w, n]) => `readonly ${w}: ${n}`).join("; "); const COLOR_TABLE = Object.entries(PLAYER_COLORS).map(([w, n]) => `readonly ${w}: PlayerColor<${n}>`).join("; "); function choiceTypes(kw: string): string { @@ -402,6 +404,31 @@ ${kw}function color(player: Player): string; * game's own messages ("Not enough minerals") appear: print(\`Wave \${wave}\`, { to: AllPlayers, position: "center" }). */ ${kw}function print(text: string, options?: { to?: Player; position?: "chat" | "center" }): void; +/** + * The colours a text in the game takes, as their colour codes: for an array that holds a colour a cell, + * \`board[i] = textColors.orange\`, and \`\` shows it (0 is an empty cell). + * grey ends what colour a line can change: everything after it on its line stays grey. + */ +${kw}const textColors: { __TEXT_COLORS__ }; +/** A colour of a text: one of textColors' names, or a colour code (0 for none). */ +${kw}type TextColor = keyof typeof textColors | number; +/** What a program shows on the screen, redrawn by the game as the program's variables change. */ +${kw}const ui: { + /** + * Show a text in the player's text lines, top left, and keep it there: the function is worked out again every + * frame from the program's variables, and shown when it changed. Inside an async program only, as a task: it keeps + * going after the program moves on, and the Task it gives back stops it (\`board.cancel()\`). Eleven lines: one + * player's text lines are this text's only, so give a player one ui.lines at a time. + * + * ui.lines(P1, () => ( + * <> + * Score {score} + * {rows.map((r) => {r.map((c) => )})} + * + * )); + */ + lines(player: Player, view: () => string): Task; +}; /** * What the players do, inside program() only. A key, a click and a typed line are true on the one * frame they arrive, so look for them in a loop that runs every frame: @@ -525,7 +552,7 @@ function body(kw: string, typeKw: string, names: ScriptNames, compact: boolean): return [ types(typeKw).replace("__COLOR_NAMES__", COLOR_NAMES).replace("__KEY_NAMES__", KEY_NAMES.map((k) => JSON.stringify(k)).join(" | ")).replace("__STATS_TYPES__", statsTypes(typeKw)), choiceTypes(typeKw), - functions(kw).replace("__COLOR_TABLE__", COLOR_TABLE), + functions(kw).replace("__COLOR_TABLE__", COLOR_TABLE).replace("__TEXT_COLORS__", TEXT_COLOR_TABLE), "// ── Conditions ──", ...[...CONDITION_IDENTS].map(([ident, def]) => signature(kw, ident, def, "Condition")), "", @@ -544,6 +571,23 @@ function body(kw: string, typeKw: string, names: ScriptNames, compact: boolean): * array of numbers unless it says otherwise (`new Array(12).fill(false)`), as a number is what everything is. */ const ARRAYS = ` +/** JSX is a text the game shows: \`Score {score}\` is a string. */ +declare namespace JSX { + type Element = string; + interface ElementChildrenAttribute { children: {} } + interface IntrinsicElements { + /** A line of its own, in its colour (white unless it says). The lines of a text are put one under another. */ + line: { color?: TextColor; children?: unknown }; + /** What is inside it in its colour; the colour around it comes back after it. */ + span: { color?: TextColor; children?: unknown }; + /** One square: filled in its colour, or empty (lavender) when it has none or 0. A board's cell. */ + cell: { color?: TextColor }; + /** \`width\` squares (10 unless it says), as many of them filled as value is of max. */ + bar: { value: number; max: number; width?: number; color?: TextColor }; + } +} +declare function __jsx(tag: string, props: object | null, ...children: unknown[]): string; +declare const __fragment: string; interface ArrayConstructor { new (arrayLength: number): T[]; (arrayLength: number): T[]; diff --git a/compiler/eud.ts b/compiler/eud.ts index 71a13dd..8927974 100644 --- a/compiler/eud.ts +++ b/compiler/eud.ts @@ -9,7 +9,7 @@ * condition does not mention a variable the body assigns, is an error naming the loop. */ import type { ActionRecord, ConditionRecord } from "../vendor/triggers"; -import { HEAP_CELLS, STACK_DEPTH, bodiesOf, heapCells, stackDepth, isTextExpr, isUnitExpr, mapText, mapTextOperands, mapTextParts, programDeclarations, type At, type BoolExpr, type Call, type FuncDecl, type NumExpr, type Program, type Stmt, type TextExpr, type TextMap, type UnitExpr, type VarDecl } from "./ir"; +import { HEAP_CELLS, STACK_DEPTH, bodiesOf, declarations, eachCall, heapCells, stackDepth, isTextExpr, isUnitExpr, mapText, mapTextOperands, mapTextParts, programDeclarations, type At, type BoolExpr, type Call, type FuncDecl, type NumExpr, type Program, type Stmt, type TextExpr, type TextMap, type UnitExpr, type VarDecl } from "./ir"; import type { LineHint, ScriptString } from "./compiler"; import type { InputPlan } from "./input"; @@ -92,7 +92,7 @@ function expressions(body: Stmt[], visit: (e: NumExpr | BoolExpr) => void, pick? case "return": if (s.value) any(s.value); break; case "action": for (const v of s.variables ?? []) expr(v.expr); if (s.text) text(s.text); break; case "centerLocation": expr(s.x); expr(s.y); break; - case "print": mapTextParts(s.parts, looks); break; + case "print": case "buildText": mapTextParts(s.parts, looks); break; case "call": call(s.call); break; case "block": s.body.forEach(stmt); break; default: break; @@ -341,7 +341,15 @@ function checkSleeps(body: Stmt[], out: ProgramDiagnostic[], functions: Map { switch (s.kind) { case "while": case "for": case "do": { - const moves = s.cond ? [...reads(s.cond)].some((id) => assigned(s.kind === "for" ? [...s.body, ...s.update] : s.body, functions).has(id)) : false; + let moves = s.cond ? [...reads(s.cond)].some((id) => assigned(s.kind === "for" ? [...s.body, ...s.update] : s.body, functions).has(id)) : false; + // A call in the condition that changes what is outside it may change what the condition finds: `while (move(0, 1, 0)) {}`. + if (s.cond && !moves) eachCall(s.cond, (c) => { + const fn = c.fn ? functions.get(c.fn) : undefined; + const own = new Set([...declarations(fn ? fn.body : c.body), ...(fn ? fn.params : c.params.map((p) => p.decl))].map((d) => d.id)); + if (c.result) own.add(c.result.decl.id); + if (fn?.result) own.add(fn.result.decl.id); + if ([...assigned([{ kind: "call", call: c, at: s.at, label: s.label }], functions)].some((id) => !own.has(id))) moves = true; + }); if (!moves && !sleepsOnEveryPath(s.body) && !leaves(s.body) && !(s.kind === "do" && s.cond.kind === "const" && s.cond.value === false)) out.push({ at: s.at, message: s.cond ? "This loop's condition never changes inside it, and no path around it sleeps. A program runs until it sleeps or ends, all within one frame of the game, so this loop would never give the frame back and the game would freeze. Add sleep(frames(1)) inside it, or change what it tests." : "A loop without an end needs a sleep() on every path around it. A program runs until it sleeps or ends, all within one frame of the game, so this loop would freeze the game. Add sleep(frames(1)) at the end of its body." }); @@ -436,7 +444,7 @@ export function serializeIr(programs: Program[], strings: readonly ScriptString[ case "return": return s.value ? { ...s, value: any(s.value) } : s; case "action": return { ...s, record: action(s.record) as unknown as ActionRecord, ...(s.variables ? { variables: s.variables.map((v) => ({ ...v, expr: expr(v.expr) })) } : {}), ...(s.text ? { text: text(s.text) } : {}) }; case "centerLocation": return { ...s, x: expr(s.x), y: expr(s.y) }; - case "print": return { ...s, parts: mapTextParts(s.parts, copies) }; + case "print": case "buildText": return { ...s, parts: mapTextParts(s.parts, copies) }; case "call": return { ...s, call: call(s.call) }; case "block": return { ...s, body: s.body.map(stmt) }; default: return s; diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index 518f96a..3781857 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 16\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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\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\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: EUDArray([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: EUDArray([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\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\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 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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"centerLocation\":\n self.center_location(st)\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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 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 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 = [], []\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\":\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 else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n group, room, first = [], 0, [True]\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 <= T[\"scratch\"] + TEXT_BYTES):\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 << T[\"append\"](pos, value.addr)\n else:\n if EUDIf()(pos <= T[\"scratch\"] + TEXT_BYTES):\n pos << T[\"append\"](pos, value.addr)\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 return pos, temps\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 == \"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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 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 EUDJumpIf(self.again >= 1, sweep)\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 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_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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 17\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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\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 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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 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 EUDJumpIf(self.again >= 1, sweep)\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 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_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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\n"; diff --git a/compiler/hoist.ts b/compiler/hoist.ts index 5f3db50..77dcdeb 100644 --- a/compiler/hoist.ts +++ b/compiler/hoist.ts @@ -108,7 +108,7 @@ function owningDeclaration(ts: typeof TS, decl: TS.Declaration): TS.Node { /** The library calls that must not appear inside a program body: they would run at build time, silently. */ const FORBIDDEN_INSIDE = new Set(["trigger", "program", "hyperTriggers", "game"]); /** The library calls the game answers: never hoisted, the structured compiler lowers them. */ -const GAME_CALLS = new Set(["random", "sleep", "until", "start", "rose", "once", "shared"]); +const GAME_CALLS = new Set(["random", "sleep", "until", "start", "rose", "once", "shared", "lines"]); /** * Calls that read the game — `minerals(P1)`, and a comparing condition without its comparison * (`deaths(P1, unit)`) — or show text a program fills in (`print`). The call is never a @@ -150,7 +150,13 @@ export function isTaskFunction(ts: typeof TS, checker: TS.TypeChecker, node: TS. if (!call.arguments.includes(at as TS.Expression)) return false; if (ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === "then") return true; const lib = libraryCallName(ts, checker, call); - return lib === "until" || (lib === "start" && call.arguments[0] === at); + return lib === "until" || (lib === "start" && call.arguments[0] === at) || (lib === "lines" && call.arguments[1] === at); +} + +/** Whether a node is JSX or has JSX inside it: a text the program puts together, never one worked out when the script is built. */ +export function hasJsx(ts: typeof TS, node: TS.Node): boolean { + if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) return true; + return !!ts.forEachChild(node, (c) => hasJsx(ts, c) || undefined); } export function planProgram(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunction | TS.FunctionExpression, options: PlanOptions = {}): ProgramPlan { @@ -287,6 +293,8 @@ function planOnce(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunctio const hit = memo.get(e); if (hit !== undefined) return hit; let ok = true; + // JSX is the program's own text, put together in the game (`structured.ts#jsx`), whatever is in it. + if (hasJsx(ts, e)) { memo.set(e, false); return false; } // Inside a function value the syntax is the function's own business; only what it refers to matters. const scan = (n: TS.Node): void => { if (!ok) return; @@ -378,6 +386,19 @@ function planOnce(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunctio if (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isNonNullExpression(e) || ts.isSatisfiesExpression(e) || ts.isTypeAssertionExpression(e) || ts.isSpreadElement(e) || ts.isAwaitExpression(e) || ts.isTypeOfExpression(e) || ts.isVoidExpression(e) || ts.isDeleteExpression(e)) { value(e.expression, items); return; } if (ts.isConditionalExpression(e)) { value(e.condition, items); value(e.whenTrue, items); value(e.whenFalse, items); return; } if (ts.isTemplateExpression(e)) { for (const s of e.templateSpans) value(s.expression, items); return; } + // JSX: what is inside its braces, in attributes and among the children, is a value like any other. + if (ts.isJsxElement(e) || ts.isJsxSelfClosingElement(e) || ts.isJsxFragment(e)) { + const attributes = ts.isJsxElement(e) ? e.openingElement.attributes : ts.isJsxSelfClosingElement(e) ? e.attributes : undefined; + for (const a of attributes?.properties ?? []) { + if (ts.isJsxAttribute(a) && a.initializer && ts.isJsxExpression(a.initializer)) value(a.initializer.expression, items); + else if (ts.isJsxSpreadAttribute(a)) value(a.expression, items); + } + for (const c of ts.isJsxSelfClosingElement(e) ? [] : e.children) { + if (ts.isJsxExpression(c)) value(c.expression, items); + else if (!ts.isJsxText(c)) descend(c, items); + } + return; + } if (ts.isArrayLiteralExpression(e)) { for (const el of e.elements) value(el, items); return; } if (ts.isObjectLiteralExpression(e)) { for (const p of e.properties) { diff --git a/compiler/input.ts b/compiler/input.ts index 319ac87..f7608d1 100644 --- a/compiler/input.ts +++ b/compiler/input.ts @@ -258,7 +258,7 @@ export function inputsOf(programs: Program[]): { sources: InputSource[]; mouse: case "switch": expr(s.value); s.cases.forEach((c) => c.body.forEach(stmt)); break; case "return": if (s.value) any(s.value); break; case "action": for (const v of s.variables ?? []) expr(v.expr); if (s.text) text(s.text); break; - case "print": mapTextParts(s.parts, looks); break; + case "print": case "buildText": mapTextParts(s.parts, looks); break; case "call": call(s.call); break; case "block": s.body.forEach(stmt); break; default: break; diff --git a/compiler/ir.ts b/compiler/ir.ts index 1a137ab..a29f704 100644 --- a/compiler/ir.ts +++ b/compiler/ir.ts @@ -10,6 +10,7 @@ import type { ActionRecord, ConditionRecord } from "../vendor/triggers"; import type { InputSource } from "./input"; /** + * 17: texts built a piece at a time — `buildText` writes parts into a buffer (`reset` starts it empty) and `built` is what the buffer holds, which is how a JSX text is put together without copying it at every piece; `char` among the parts, one byte from a number (a colour code). * 16: tasks — `start`: a task that runs beside what started it, a copy of an async function's body with the arguments it was started with; `cancel` and `running` on what `start()` gives back. A task's variables and arrays (`VarDecl.task`, `ArrayDecl.task`) are a row a task, as many rows as tasks of it can run at once. * 15: `parallel` — `await Promise.all([…])` / `Promise.race([…])`: branches that run side by side, each a coroutine of its own inside the program's frame. * 14: a text in the cells of three arrays — `textAt`, `storeText`, `releaseText` — which is what lets a row of an array of records hold one. @@ -26,7 +27,10 @@ import type { InputSource } from "./input"; * 3: reads (`read`), `random(n)` as a number, the bitwise operators and `print` with its text in parts. * 2: a record's text and sound are written out in the JSON (1 had the map's string indices); `cyclesPerSecond` is gone. */ -export const IR_VERSION = 16; +export const IR_VERSION = 17; + +/** The most a buffer of `buildText` holds, in bytes; python/trigscript.py has the same number. Past it the text is cut. */ +export const BUILD_BYTES = 2047; /** Where a node came from; `column` is 1-based like `line`. */ export interface At { file: string; line: number; column: number } @@ -299,9 +303,11 @@ export type TextExpr = /** `s.repeat(n)`; nothing when n is below 1. */ | { kind: "textRepeat"; of: TextExpr; count: NumExpr; at: At; label: string } /** A call inlined here whose result is a text: the value is taken out of the call's result, which then holds none. */ - | { kind: "textCall"; call: Call }; + | { kind: "textCall"; call: Call } + /** What buffer `buffer` holds, as the `buildText` statements before it left it: looked at where it is, never owned. */ + | { kind: "built"; buffer: string }; -const TEXT_KINDS: ReadonlySet = new Set(["text", "textVar", "textOf", "textAt", "template", "textTernary", "textSlice", "textPad", "textRepeat", "textCall"]); +const TEXT_KINDS: ReadonlySet = new Set(["text", "textVar", "textOf", "textAt", "template", "textTernary", "textSlice", "textPad", "textRepeat", "textCall", "built"]); export const isTextExpr = (e: { kind: string }): e is TextExpr => TEXT_KINDS.has(e.kind); /** What a pass that walks or rewrites every expression does with the numbers, the conditions and the calls inside a text; the texts inside it are walked here. */ @@ -310,7 +316,7 @@ export interface TextMap { num: (e: NumExpr) => NumExpr; bool: (e: BoolExpr) => const innerText = (t: TextExpr, f: TextMap): TextExpr => (f.text ? f.text(t) : mapText(t, f)); export function mapTextParts(parts: TextPart[], f: TextMap): TextPart[] { - return parts.map((p) => (p.kind === "number" ? { ...p, expr: f.num(p.expr) } : p.kind === "value" ? { ...p, text: innerText(p.text, f) } : p)); + return parts.map((p) => (p.kind === "number" || p.kind === "char" ? { ...p, expr: f.num(p.expr) } : p.kind === "value" ? { ...p, text: innerText(p.text, f) } : p)); } /** A text with everything it is worked out from handed to `f` — a copy; a pass that only looks gives back what it was handed. */ @@ -397,7 +403,9 @@ export type TextPart = | { kind: "name"; player: number } | { kind: "color"; player: number } /** A text of the program: a variable's, a function's result. */ - | { kind: "value"; text: TextExpr }; + | { kind: "value"; text: TextExpr } + /** One byte, the number's low eight bits: a colour code chosen when the map is played. 0 writes nothing. */ + | { kind: "char"; expr: NumExpr }; /** A field of an action filled in by the program: `bits` 8 is a unit count (done once per unit), 16 a unit type, 32 an amount. */ export interface ActionVariable { field: keyof ActionRecord; bits: 8 | 16 | 32; name: string; expr: NumExpr } @@ -534,6 +542,11 @@ export type Stmt = * force — in the chat area or on the line in the middle of the screen the game's own errors use. */ | { kind: "print"; parts: TextPart[]; to: number; position: "chat" | "center"; at: At; label: string } + /** + * The parts written at the end of buffer `buffer` (`reset`: the buffer emptied first): a text put together a piece at a + * time, read back with `built`. A buffer is one place for the whole game, used up within the frame it is built in. + */ + | { kind: "buildText"; buffer: string; parts: TextPart[]; reset?: boolean; at: At; label: string } | { kind: "call"; call: Call; at: At; label: string } /** A block only for scoping; nothing of its own. */ | { kind: "block"; body: Stmt[]; at: At } @@ -638,7 +651,7 @@ export function declarations(body: Stmt[]): VarDecl[] { case "return": if (s.value) init(s.value); break; case "action": for (const v of s.variables ?? []) expr(v.expr); if (s.text) text(s.text); break; case "centerLocation": expr(s.x); expr(s.y); break; - case "print": parts(s.parts); break; + case "print": case "buildText": parts(s.parts); break; case "call": call(s.call); break; case "block": s.body.forEach(stmt); break; default: break; @@ -651,7 +664,7 @@ export function declarations(body: Stmt[]): VarDecl[] { c.body.forEach(stmt); }; const init = (e: NumExpr | BoolExpr | UnitExpr | TextExpr) => (isTextExpr(e) ? text(e) : isUnitExpr(e) ? unit(e) : isNumExpr(e) ? expr(e) : bool(e)); - const parts = (ps: TextPart[]) => { for (const p of ps) { if (p.kind === "number") expr(p.expr); else if (p.kind === "value") text(p.text); } }; + const parts = (ps: TextPart[]) => { for (const p of ps) { if (p.kind === "number" || p.kind === "char") expr(p.expr); else if (p.kind === "value") text(p.text); } }; const text = (t: TextExpr) => { switch (t.kind) { case "textOf": case "textAt": expr(t.index); break; diff --git a/compiler/jsx.ts b/compiler/jsx.ts new file mode 100644 index 0000000..4cbcd73 --- /dev/null +++ b/compiler/jsx.ts @@ -0,0 +1,118 @@ +/** + * JSX in a script: a text the game shows, written as elements. In a program the structured compiler puts it together in + * the game (`structured.ts#jsx`); outside one, where the script runs when it is built, `jsxAtBuild` makes the same text + * there. The game facts behind it are the UI spike's (TrigScript 4, S2, played 2026-10-16): eleven text lines; ■ □ ● + * one width; a colour code takes no width; after 05 (grey) every later code on its line is ignored. + */ + +/** The colours a text in the game takes, by name: the colour code each one is. Measured in Remastered (the UI spike). */ +export const TEXT_COLORS = Object.freeze({ + white: 0x04, + grey: 0x05, + red: 0x08, + orange: 0x11, + yellow: 0x03, + paleYellow: 0x19, + green: 0x07, + darkGreen: 0x18, + sage: 0x1d, + teal: 0x0f, + cyan: 0x1f, + blue: 0x0e, + lightBlue: 0x1c, + paleBlue: 0x02, + purple: 0x10, + lavender: 0x1e, + brown: 0x15, + cream: 0x1b, +} as const); +export type TextColorName = keyof typeof TEXT_COLORS; + +/** The colour of a line that says none. */ +export const DEFAULT_COLOR = TEXT_COLORS.white; +/** A cell that is filled, in its colour, and one that is empty: the same width (the spike's B1). */ +export const FILLED_CELL = "■"; +export const EMPTY_CELL = "□"; +/** An empty cell's colour: it reads on black and over terrain, and no piece is lavender (the spike's second round). */ +export const EMPTY_COLOR = TEXT_COLORS.lavender; + +/** The elements a text is made of, with the attributes each takes. */ +export const JSX_ELEMENTS: Readonly> = Object.freeze({ + line: ["color"], + span: ["color"], + cell: ["color"], + bar: ["value", "max", "width", "color"], +}); + +/** The colour code for what a script wrote as a colour: a name of TEXT_COLORS, or the code itself (0 for none). */ +export function colorCode(v: unknown): number | null { + if (typeof v === "string") return Object.hasOwn(TEXT_COLORS, v) ? TEXT_COLORS[v as TextColorName] : null; + if (typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 0xff) return v; + return null; +} + +/* ── Outside a program: the same text, made when the script is built ── */ + +/** A colour to be put back after an element that changed it: the element around it fills in its own. */ +const RESTORE = "\u0000"; + +/** What an element gives when the script is built: its text, and whether it is lines (a ``, or holds some). */ +class Built extends String { + readonly lines: boolean; + constructor(text: string, lines: boolean) { super(text); this.lines = lines; } +} + +const flat = (children: unknown[]): unknown[] => children.flatMap((c) => (Array.isArray(c) ? flat(c) : [c])); + +/** The children one after another, a line break before each line but the first thing written — as in the game. */ +function joined(children: unknown[]): { text: string; lines: boolean } { + let text = "", lines = false, any = false; + for (const c of flat(children)) { + if (c === null || c === undefined || typeof c === "boolean") continue; + const isLine = c instanceof Built && c.lines; + if (isLine && any) text += "\n"; + text += String(c); + any = true; + lines ||= isLine; + } + return { text, lines }; +} + +/** The script's JSX when it runs at build time (the factory `__jsx`): the text the game would be shown. */ +export function jsxAtBuild(tag: unknown, props: Record | null, ...children: unknown[]): string { + const p = props ?? {}; + if (tag === JSX_FRAGMENT) { const j = joined(children); return new Built(j.text, j.lines) as unknown as string; } + if (typeof tag !== "string" || !Object.hasOwn(JSX_ELEMENTS, tag)) throw new Error(`<${String(tag)}> is not an element of a text: they are ${Object.keys(JSX_ELEMENTS).map((t) => `<${t}>`).join(", ")}.`); + const code = (fallback: number | null) => { + if (p.color === undefined) return fallback; + const c = colorCode(p.color); + if (c === null) throw new Error(`<${tag}>'s color is one of ${Object.keys(TEXT_COLORS).join(", ")}, or a colour code; got ${JSON.stringify(p.color)}.`); + return c; + }; + const ch = (c: number | null) => (c ? String.fromCharCode(c) : ""); + const inside = (own: number) => joined(children).text.split(RESTORE).join(ch(own)); + switch (tag) { + case "line": { const own = code(DEFAULT_COLOR)!; return new Built(tidy(ch(own) + inside(own)), true) as unknown as string; } + case "span": { const own = code(null); return new Built(own === null ? joined(children).text : ch(own) + inside(own) + RESTORE, false) as unknown as string; } + case "cell": { const own = code(0)!; return new Built(own ? ch(own) + FILLED_CELL + RESTORE : ch(EMPTY_COLOR) + EMPTY_CELL + RESTORE, false) as unknown as string; } + case "bar": { + const value = Number(p.value ?? 0), max = Number(p.max ?? 0), width = Number(p.width ?? 10); + const filled = max > 0 ? Math.max(0, Math.min(width, Math.floor((value * width) / max))) : 0; + return new Built(ch(code(TEXT_COLORS.green)) + FILLED_CELL.repeat(filled) + ch(EMPTY_COLOR) + EMPTY_CELL.repeat(width - filled) + RESTORE, false) as unknown as string; + } + } + throw new Error(`<${tag}> is not an element of a text.`); +} + +/** What `<>…` passes as its tag. */ +export const JSX_FRAGMENT = "__fragment"; + +/** A line's text with what changes nothing taken out: a colour code that another follows, or that ends it. */ +function tidy(s: string): string { + return s.replace(/[\x01-\x08\x0e-\x1f\u0000]+(?=[\x01-\x08\x0e-\x1f]|$)/g, "").replace(/\u0000/g, ""); +} + +/** The text a built value of JSX is, wherever it ends up: what is left to put back is the default colour. */ +export function builtText(v: unknown): string | undefined { + return v instanceof Built ? String(v).split(RESTORE).join(String.fromCharCode(DEFAULT_COLOR)) : undefined; +} diff --git a/compiler/link.ts b/compiler/link.ts index 2ecf027..0868ae0 100644 --- a/compiler/link.ts +++ b/compiler/link.ts @@ -40,7 +40,7 @@ export function resolvePath(from: string, spec: string): string { export function resolveModule(files: ReadonlySet, from: string, spec: string): string | null { if (!spec.startsWith("./") && !spec.startsWith("../")) return null; const path = resolvePath(from, spec); - for (const candidate of [path, `${path}.ts`, path.replace(/\.js$/, ".ts"), `${path}/index.ts`]) if (files.has(candidate)) return candidate; + for (const candidate of [path, `${path}.ts`, `${path}.tsx`, path.replace(/\.js$/, ".ts"), path.replace(/\.js$/, ".tsx"), `${path}/index.ts`, `${path}/index.tsx`]) if (files.has(candidate)) return candidate; return null; } diff --git a/compiler/numbers.ts b/compiler/numbers.ts index 1f7e015..f0b4413 100644 --- a/compiler/numbers.ts +++ b/compiler/numbers.ts @@ -185,6 +185,7 @@ export function typeNumbers(program: Program): ProgramDiagnostic[] { const shown = (parts: TextPart[]) => { for (const p of parts) { if (p.kind === "value") text(p.text); + if (p.kind === "char") { p.expr = num(p.expr)[0]; continue; } if (p.kind !== "number") continue; const [value, t] = num(p.expr); p.expr = value; @@ -286,7 +287,7 @@ export function typeNumbers(program: Program): ProgramDiagnostic[] { case "return": if (s.value && result) s.value = valueFor(result, s.value, s.at); break; case "action": for (const v of s.variables ?? []) v.expr = floor(v.expr, s.at); if (s.text) s.text = text(s.text); break; case "centerLocation": s.x = floor(s.x, s.at); s.y = floor(s.y, s.at); break; - case "print": shown(s.parts); break; + case "print": case "buildText": shown(s.parts); break; case "call": call(s.call); break; case "block": s.body.forEach(stmt); break; default: break; diff --git a/compiler/recursion.ts b/compiler/recursion.ts index 0147e10..a01c8ca 100644 --- a/compiler/recursion.ts +++ b/compiler/recursion.ts @@ -333,8 +333,8 @@ export function settleRecursion(program: Program): ProgramDiagnostic[] { break; } case "centerLocation": [s.x, s.y] = operands([{ e: s.x, kind: "number" }, { e: s.y, kind: "number" }], pre, w) as NumExpr[]; break; - case "print": { - const numbers = s.parts.filter((p) => p.kind === "number"); + case "print": case "buildText": { + const numbers = s.parts.filter((p) => p.kind === "number" || p.kind === "char"); const done = operands(numbers.map((p) => ({ e: p.expr, kind: "number" as const })), pre, w) as NumExpr[]; numbers.forEach((p, i) => { p.expr = done[i]; }); for (const p of s.parts) if (p.kind === "value") p.text = rewriteText(p.text, pre, w); diff --git a/compiler/runtime.ts b/compiler/runtime.ts index 969f1a6..f86d7a4 100644 --- a/compiler/runtime.ts +++ b/compiler/runtime.ts @@ -23,6 +23,7 @@ import { hyperTriggers, negateCondition, PLAYER_SLOTS } from "./lower"; import { allTables, type ScriptNames } from "./names"; import type { RaceId, ReadSource, TableCell, TextPart, UnitFilter } from "./ir"; import { DEAF_KEYS, keyName, KEY_NAMES, MOUSE_BUTTONS, parseChatPattern, type ChatPattern, type InputSource, type MouseButton } from "./input"; +import { JSX_FRAGMENT, TEXT_COLORS, builtText, jsxAtBuild } from "./jsx"; import { PLAYER_COLORS, TABLE_FIELDS, TABLE_SIZE, tableOfBrand, type TableField, type TableKind } from "./tables"; /** `memory(address, …)` reads `deaths` at player `EPD(address)`, unit 0: the deaths table starts here in 1.16.1's memory. */ @@ -280,6 +281,7 @@ export function createRuntime(names: ScriptNames, collector: Collector, options: const argValue = (kind: ArgKind, v: unknown, what: string): number => { switch (kind) { case "text": case "wav": + if (builtText(v) !== undefined) v = builtText(v); if (typeof v === "string") return v === "" ? 0 : collector.localString({ text: v }); if (typeof v === "number") return v === 0 ? 0 : collector.localString({ index: integer(v, what) }); throw new ScriptError(`${what}: expected text, got ${describe(v)}.`); @@ -610,10 +612,19 @@ export function createRuntime(names: ScriptNames, collector: Collector, options: }; rt.colors = Object.freeze({ ...PLAYER_COLORS }); + /* ── JSX: a text as elements ── */ + rt.textColors = TEXT_COLORS; + rt.__jsx = jsxAtBuild; + rt.__fragment = JSX_FRAGMENT; + rt.ui = Object.freeze({ + lines: () => { throw new ScriptError("ui.lines() shows a text in a program's lines, inside program(async () => { … }): ui.lines(P1, () => …)."); }, + }); + /* ── Text a program fills in ── */ rt.name = (player: unknown) => textMark("n", onePlayer(player, "name: player", 12)); rt.color = (player: unknown) => textMark("c", onePlayer(player, "color: player", 12)); rt.print = (text: unknown, options?: unknown): PrintValue => { + if (builtText(text) !== undefined) text = builtText(text); if (typeof text !== "string") throw new ScriptError(`print: expected text, got ${describe(text)}.`); let to: number = PlayerGroup.CurrentPlayer; let position: PrintValue["position"] = "chat"; @@ -696,5 +707,6 @@ export function runtimeNames(names: ScriptNames): string[] { out.push("seconds", "minutes", "frames", "cycles", "sleep", "until", "start", "rose", "once", "shared", "clamp", "u32", "i32"); out.push(...READER_NAMES, "races", "slots", "name", "color", "print"); out.push(...UNIT_CALL_NAMES, "colors", ...INPUT_CALL_NAMES); + out.push("textColors", "ui", "__jsx", "__fragment"); return out; } diff --git a/compiler/simulateIr.ts b/compiler/simulateIr.ts index 339b48a..dd7830c 100644 --- a/compiler/simulateIr.ts +++ b/compiler/simulateIr.ts @@ -36,7 +36,7 @@ */ import { ActionType, Comparison, ConditionType, SetModifier, SWITCH_COUNT, SwitchAction, SwitchState, type ActionRecord, type ConditionRecord } from "../vendor/triggers"; import { emptyAction, ResourceType } from "../vendor/triggers"; -import { HEAP_CELLS, HEAP_SMALLEST, STACK_DEPTH, TEXT_BYTES, TEXT_FIELD_BYTES, eachStart, heapCells, isTextExpr, programDeclarations, stackDepth } from "./ir"; +import { BUILD_BYTES, HEAP_CELLS, HEAP_SMALLEST, STACK_DEPTH, TEXT_BYTES, TEXT_FIELD_BYTES, eachStart, heapCells, isTextExpr, programDeclarations, stackDepth } from "./ir"; import type { TextExpr, ArrayDecl, At, BoolExpr, Call, NumExpr, Program, ReadSource, Stmt, TableCell, TextPart, UnitExpr, UnitFilter, UnitNumField, UnitVerb, VarDecl } from "./ir"; import { cellMax } from "./tables"; import { inputsOf, keyName, matchChat, parseChatPattern, type ChatPattern, type InputSource, type MouseButton } from "./input"; @@ -622,6 +622,8 @@ class ProgramRun { let out = ""; for (const p of parts) { if (p.kind === "number") out += String(p.unsigned ? yield* this.amount(p.expr) : yield* this.num(p.expr)); + // A colour code is a control character in the game; the log keeps the words, as it does for a player's colour. + else if (p.kind === "char") yield* this.num(p.expr); else if (p.kind === "value") { const v = yield* this.text(p.text); used.push(v); out += v.s; } else out += this.sim.partText(p); } @@ -684,6 +686,7 @@ class ProgramRun { this.used(of); return out; } + case "built": return { s: this.sim.buffers.get(e.buffer) ?? "", room: 0, taken: false }; case "textCall": { yield* this.call(e.call); const held = e.call.result ? this.textVars.get(e.call.result.decl.id) : undefined; @@ -1123,6 +1126,20 @@ class ProgramRun { this.sim.print(this, text, s.to, s.at); return "next"; } + case "buildText": { + const used: TextValue[] = []; + const more = yield* this.parts(s.parts, used); + this.used(...used); + const was = s.reset ? "" : this.sim.buffers.get(s.buffer) ?? ""; + let text = was + more; + // Past what a buffer holds the text is cut, as it is in the game, and said once a buffer. + if (bytesOf(text) > BUILD_BYTES) { + if (bytesOf(was) <= BUILD_BYTES) this.sim.faults.push({ cycle: this.sim.cycle, frame: this.sim.frame, program: this.index, at: s.at, message: `This text came to more than the ${BUILD_BYTES.toLocaleString("en-US")} bytes a text put together piece by piece holds: it is cut off there, as it is in the game.` }); + text = cutTo(text, BUILD_BYTES); + } + this.sim.buffers.set(s.buffer, text); + return "next"; + } case "centerLocation": { const x = yield* this.amount(s.x); const y = yield* this.amount(s.y); this.sim.centre(s.location, x, y); return "next"; } case "start": { const pool = this.pools.find((p) => p.start === s); @@ -1235,6 +1252,8 @@ export class ProgramSimulation { /** The arrays that grow inside others, by the number their handle's cell holds. */ readonly inner = new Map(); lastInner = 0; + /** What each buffer of `buildText` holds: one for the whole game, as in the game. */ + readonly buffers = new Map(); readonly heap = { cells: HEAP_CELLS, top: 1, @@ -1475,7 +1494,7 @@ export class ProgramSimulation { this.events.push({ cycle: this.cycle, frame: this.frame, program: run.index, player: run.player, at, action: { ...emptyAction(), type, player: u.owner, unitId: u.type, ...(extra.location ? { location: extra.location } : {}) }, text: extra.text ? `${extra.text}: ${what}` : what }); } - partText(p: Exclude): string { + partText(p: Exclude): string { // A colour is a control character in the game; the log keeps the words. return p.kind === "text" ? p.text : p.kind === "name" ? this.nameOf(this.slotOf(p.player)) : ""; } diff --git a/compiler/structured.ts b/compiler/structured.ts index 4d4f5e5..f340104 100644 --- a/compiler/structured.ts +++ b/compiler/structured.ts @@ -64,6 +64,8 @@ import { declarationOf, libraryCallName } from "./hoist"; 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 } 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"; import { HANDLE_PARTS, Scope, TEXT_PARTS, THIS, UNIT_PARTS, type Binding, type Place, type RowField, type RowShape } from "./scope"; import { ACTIONS_WITH_MODIFIER, LowerError } from "./lower"; import { I32_MAX, I32_MIN, IR_VERSION, TASK_MAX, TEXT_BYTES, U32_MAX, UNIT_FLAGS, UNIT_NUM_FIELDS, UNIT_WRITABLE, declarations, eachCall, eachStart, programDeclarations, textHasId, type ActionVariable, type ArithOp, type ArrayDecl, type At, type BoolExpr, type Call, type CompareOp, type FuncDecl, type NumExpr, type Program, type Stmt, type TextExpr, type TextPart, type UnitExpr, type UnitFlag, type UnitNumField, type UnitVerb, type VarDecl } from "./ir"; @@ -157,6 +159,27 @@ function describe(v: unknown): string { /** The player a trigger is running for, as a player number. */ const CURRENT_PLAYER = 13; +/** A view of ui.lines nothing changed is shown again after this many frames: the game's lines fade in less than six seconds. */ +const REFRESH_FRAMES = 24; + +/** JSX that is a text: an element, one that closes itself, or a fragment. */ +type JsxNode = TS.JsxElement | TS.JsxSelfClosingElement | TS.JsxFragment; +/** A colour a piece of JSX is written in: `key` tells two apart when the script is built, `part` says it in the text. */ +interface JsxColor { key: string; part: TextPart; v?: VarDecl } +/** A JSX text being lowered: its buffer, the parts not yet written, whether something was written (in the game: `started`), the colour showing. */ +interface JsxBuild { buffer: string; pending: TextPart[]; written: "no" | "yes" | "maybe"; started?: VarDecl; shown: string | null; at: At; label: string } + +/** JSX text between elements as React reads it: a line break and the spaces around it go, and lines with nothing else join with a space. */ +function jsxWhitespace(raw: string): string { + const lines = raw.split(/\r\n|\n|\r/); + if (lines.length === 1) return raw; + return lines.map((l, i) => { + let t = l.replace(/\t/g, " "); + if (i > 0) t = t.trimStart(); + if (i < lines.length - 1) t = t.trimEnd(); + return t; + }).filter((t) => t.length).join(" "); +} /** Neighbouring pieces of written text as one. */ function mergeText(parts: TextPart[]): TextPart[] { @@ -274,6 +297,8 @@ export class Structured { private yields: ((y: TS.YieldExpression) => void)[] = []; /** The `start` statements so far: the next one's number is one more. */ private taskCount = 0; + /** The views of `ui.lines` in the program: their flags, and what puts each text together. */ + private readonly views: View[] = []; /** The inline depth at which the next call is a task's (`start(wave(n))`): its parameters are copies of the arguments, never the caller's variables. */ private copyArgsAt = -1; private scope: Scope = new Scope(null); @@ -315,6 +340,7 @@ export class Structured { } this.settleFunctions(program); this.checkTasks(program); + markViewWrites(program, this.views); return { program, nodeOf: (node) => this.nodes.get(node) }; } @@ -3927,6 +3953,7 @@ export class Structured { private text(expr: TS.Expression): TextExpr | null { const { ts } = this; const e = this.unwrap(expr); + if (ts.isJsxElement(e) || ts.isJsxSelfClosingElement(e) || ts.isJsxFragment(e)) return this.jsx(e); const h = this.evaluate(expr); if (h && !isGameValue(h.value)) { const v = h.value; @@ -4573,6 +4600,7 @@ export class Structured { // `void wave(3)`: started, and nothing waits for it — what JavaScript does with a promise nobody keeps. if (ts.isVoidExpression(bare) && this.isPromise(bare.expression)) { this.startTask(bare.expression, bare, TASK_MAX); return; } if (ts.isCallExpression(bare) && this.isLibraryCall(bare, "start")) { this.startCall(bare); return; } + if (ts.isCallExpression(bare) && this.isLibraryCall(bare, "lines")) { this.uiLines(bare); return; } if (ts.isCallExpression(bare) && ts.isPropertyAccessExpression(bare.expression) && bare.expression.name.text === "cancel" && this.isTaskType(this.c.checker.getTypeAtLocation(bare.expression.expression))) { this.cancelTask(bare, bare.expression.expression); return; } // A promise nothing waits for. sleep() in a function that is not async is the language before 4: it pauses there. if (!awaited && ts.isCallExpression(bare) && this.isPromise(bare) && !(this.isLibraryCall(bare, "sleep") && !this.inAsync(bare))) { this.notAwaited(bare); return; } @@ -5298,6 +5326,349 @@ export class Structured { return this.mark({ kind: "call", call }, e); } + /* ── JSX: a text as elements, put together a piece at a time ── */ + + /** + * `…`, `<>…`: the text put together where the expression stands, into a buffer of its own + * (`buildText`), and read where it is used (`built`) — never copied on the way, however many pieces it has. + * A line break goes before every `` but the first thing written; `{…}` is the program's value; `cond && …`, + * `cond ? … : …` and `xs.map((x) => …)` are the program's if and loop. A colour is put back lazily: an element + * that changed it leaves the next piece of its parent to say the parent's again. + */ + private jsx(e: JsxNode): TextExpr | null { + const { ts } = this; + // The buffer is used up within the frame: nothing in the text may wait. + const waits = (n: TS.Node): TS.Node | undefined => (ts.isAwaitExpression(n) || ts.isYieldExpression(n) ? n : ts.isFunctionLike(n) ? undefined : ts.forEachChild(n, waits)); + const waiting = waits(e); + if (waiting) { this.c.error(waiting, "A text is put together within the frame it is shown in: nothing inside it can wait. Await before it, into a variable, and write the variable."); return null; } + const at = this.at(e), label = this.label(e); + const buffer = `jsx L${this.line(e)}#${this.nextId++}`; + const b: JsxBuild = { buffer, pending: [], written: "no", shown: null, at, label }; + if (this.jsxFlowsLines(e)) { + b.started = this.newVar("(something written)", "number", at, { temp: true }); + this.emit({ kind: "declare", decl: b.started, init: num(0), at, label }, e); + } + this.emit({ kind: "buildText", buffer, parts: [], reset: true, at, label }, e); + this.jsxNode(e, b, { key: `c${DEFAULT_COLOR}`, part: { kind: "text", text: String.fromCharCode(DEFAULT_COLOR) } }); + this.jsxFlush(b, e); + return this.mark({ kind: "built", buffer }, e); + } + + /** Whether a JSX text writes something inside a `{…}` — a condition or a loop — so whether a line break is due is known only in the game. */ + private jsxFlowsLines(e: TS.Node): boolean { + const { ts } = this; + const inside = (n: TS.Node): boolean => (ts.isJsxExpression(n) && !!n.expression && !ts.isJsxAttribute(n.parent)) || !!ts.forEachChild(n, (c) => inside(c) || undefined); + return inside(e); + } + + /** The pieces waiting, written as one `buildText`. */ + private jsxFlush(b: JsxBuild, node: TS.Node) { + if (!b.pending.length) return; + this.emit({ kind: "buildText", buffer: b.buffer, parts: b.pending, at: b.at, label: b.label }, node); + b.pending = []; + } + + /** Something visible is written: a line break first when the element is a line and something was written before it. */ + private jsxWrites(b: JsxBuild, node: TS.Node, line: boolean) { + const { at, label } = b; + if (line && b.written === "yes") b.pending.push({ kind: "text", text: "\n" }); + if (line && b.written === "maybe" && b.started) { + this.jsxFlush(b, node); + this.emit({ kind: "if", cond: { kind: "test", expr: varRef(b.started), at, label }, then: [{ kind: "buildText", buffer: b.buffer, parts: [{ kind: "text", text: "\n" }], at, label }], at, label }, node); + } + if (b.started && b.written !== "yes") this.emit({ kind: "assign", target: b.started.id, value: num(1), at, label }, node); + b.written = "yes"; + } + + /** The colour of the element a piece is in, said unless it is the one showing. */ + private jsxColor(b: JsxBuild, color: JsxColor) { + if (b.shown === color.key) return; + b.pending.push(structuredClone(color.part)); + b.shown = color.key; + } + + /** A colour attribute: known when the script is built (a name or a code), or the program's number. Null with a diagnostic. */ + private jsxColorOf(value: TS.Expression | TS.StringLiteral, tag: string): JsxColor | { dynamic: NumExpr } | null { + const h = this.evaluate(value as TS.Expression); + if (h && !isGameValue(h.value)) { + const c = colorCode(h.value); + if (c === null) { this.c.error(value, `<${tag}>'s color is one of ${Object.keys(TEXT_COLORS).join(", ")}, or a colour code from 0 to 255; got ${describe(h.value)}.`); return null; } + return { key: `c${c}`, part: { kind: "text", text: c ? String.fromCharCode(c) : "" } }; + } + const n = this.num(value as TS.Expression); + return n ? { dynamic: n } : null; + } + + /** A colour of the program's held in a variable of its own, so that saying it again reads the same number. */ + private jsxHeldColor(n: NumExpr, node: TS.Node): JsxColor { + const at = this.at(node), label = this.label(node); + const v = this.newVar("(colour)", "number", at, { temp: true }); + this.emit({ kind: "declare", decl: v, init: n, at, label }, node); + return { key: `v${v.id}`, part: { kind: "char", expr: varRef(v) }, v }; + } + + /** An element's attributes by name: a string's text, or the expression in braces. Null with a diagnostic. */ + private jsxAttributes(e: TS.JsxElement | TS.JsxSelfClosingElement, tag: string): Map | null { + const { ts } = this; + const attributes = ts.isJsxElement(e) ? e.openingElement.attributes : e.attributes; + const out = new Map(); + for (const a of attributes.properties) { + if (!ts.isJsxAttribute(a) || !ts.isIdentifier(a.name)) { this.c.error(a, `<${tag}> takes its attributes one by one: ${JSX_ELEMENTS[tag].join(", ")}.`); return null; } + const name = a.name.text; + if (!JSX_ELEMENTS[tag].includes(name)) { this.c.error(a, JSX_ELEMENTS[tag].length ? `<${tag}> takes ${JSX_ELEMENTS[tag].join(", ")}; not ${name}.` : `<${tag}> takes no attributes.`); return null; } + const init = a.initializer; + if (!init) { this.c.error(a, `${name} needs a value: ${name}={…}.`); return null; } + if (ts.isStringLiteral(init)) out.set(name, init); + else if (ts.isJsxExpression(init) && init.expression) out.set(name, init.expression); + else { this.c.error(a, `${name} needs a value: ${name}={…}.`); return null; } + } + return out; + } + + /** An element, a fragment, or text between them, written in colour `color` (its parent's). */ + private jsxNode(e: TS.JsxChild | JsxNode, b: JsxBuild, color: JsxColor) { + const { ts } = this; + if (ts.isJsxText(e)) { + const text = jsxWhitespace(e.text); + if (!text) return; + this.jsxWrites(b, e, false); + this.jsxColor(b, color); + b.pending.push({ kind: "text", text }); + return; + } + if (ts.isJsxExpression(e)) { if (e.expression) this.jsxChild(e.expression, b, color); return; } + if (ts.isJsxFragment(e)) { for (const c of e.children) this.jsxNode(c, b, color); return; } + const opening = ts.isJsxElement(e) ? e.openingElement : e; + const tag = opening.tagName.getText(this.body.sf); + if (!Object.hasOwn(JSX_ELEMENTS, tag)) { + this.c.error(opening.tagName, /^[A-Z]/.test(tag) + ? `<${tag}>: a function of your own is called in braces in a text — {${tag[0].toLowerCase()}${tag.slice(1)}(…)} — and gives back a text. Elements are ${Object.keys(JSX_ELEMENTS).map((t) => `<${t}>`).join(", ")}.` + : `<${tag}> is not an element of a text: they are ${Object.keys(JSX_ELEMENTS).map((t) => `<${t}>`).join(", ")}.`); + return; + } + const attrs = this.jsxAttributes(e as TS.JsxElement | TS.JsxSelfClosingElement, tag); + if (!attrs) return; + const children: readonly TS.JsxChild[] = ts.isJsxElement(e) ? e.children : []; + const own = (fallback: JsxColor | null): JsxColor | null | undefined => { + const v = attrs.get("color"); + if (!v) return fallback; + const c = this.jsxColorOf(v, tag); + if (!c) return undefined; + return "dynamic" in c ? this.jsxHeldColor(c.dynamic, e) : c; + }; + switch (tag) { + case "line": { + const c = own({ key: `c${DEFAULT_COLOR}`, part: { kind: "text", text: String.fromCharCode(DEFAULT_COLOR) } }); + if (!c) return; + this.jsxWrites(b, e, true); + // A line says its colour at its start, whatever was showing at the end of the one before. + b.shown = null; + this.jsxColor(b, c); + for (const child of children) this.jsxNode(child, b, c); + return; + } + case "span": { + const c = own(color); + if (!c) return; + for (const child of children) this.jsxNode(child, b, c); + return; + } + case "cell": { + if (children.length) { this.c.error(e, " is one square and holds nothing: ."); return; } + this.jsxWrites(b, e, false); + const v = attrs.get("color"); + const c = v ? this.jsxColorOf(v, tag) : { key: "c0", part: { kind: "text" as const, text: "" } }; + if (!c) return; + if (!("dynamic" in c)) { + // Known when the script is built: filled in its colour, or empty. + const filled = c.key !== "c0"; + this.jsxColor(b, filled ? c : { key: `c${EMPTY_COLOR}`, part: { kind: "text", text: String.fromCharCode(EMPTY_COLOR) } }); + b.pending.push({ kind: "text", text: filled ? FILLED_CELL : EMPTY_CELL }); + return; + } + // The program's colour: 0 is an empty square in the empty colour, anything else a filled one in it. + const held = this.jsxHeldColor(c.dynamic, e); + const at = this.at(e), label = this.label(e); + const empty: BoolExpr = { kind: "compare", op: "==", left: varRef(held.v!), right: num(0), at, label }; + b.pending.push({ kind: "char", expr: { kind: "ternary", cond: empty, whenTrue: num(EMPTY_COLOR), whenFalse: varRef(held.v!), at, label } }); + b.pending.push({ kind: "value", text: { kind: "textTernary", cond: structuredClone(empty), whenTrue: { kind: "text", text: EMPTY_CELL }, whenFalse: { kind: "text", text: FILLED_CELL }, at, label } }); + b.shown = null; + return; + } + case "bar": { + if (children.length) { this.c.error(e, " holds nothing: ."); return; } + const at = this.at(e), label = this.label(e); + const value = attrs.get("value"), max = attrs.get("max"); + if (!value || !max) { this.c.error(e, " takes value and max: ."); return; } + const w = attrs.get("width"); + const width = w ? this.evaluate(w as TS.Expression)?.value : 10; + if (typeof width !== "number" || !Number.isInteger(width) || width < 1 || width > 60) { this.c.error(w ?? e, "'s width is known when the script is built: a whole number from 1 to 60."); return; } + const c = own({ key: `c${TEXT_COLORS.green}`, part: { kind: "text", text: String.fromCharCode(TEXT_COLORS.green) } }); + const v = this.num(value as TS.Expression), m = this.num(max as TS.Expression); + if (!c || !v || !m) return; + this.jsxWrites(b, e, false); + // How many are filled: value × width / max, from 0 to width; none when max is 0 or less. + const top = this.newVar("(bar's max)", "number", at, { temp: true }); + const n = this.newVar("(filled)", "number", at, { temp: true }); + this.emit({ kind: "declare", decl: top, init: m, at, label }, e); + const share: NumExpr = { kind: "binary", op: "/", left: { kind: "binary", op: "*", left: v, right: num(width), at, label }, right: varRef(top), at, label }; + this.emit({ kind: "declare", decl: n, init: { kind: "ternary", cond: { kind: "compare", op: "<=", left: varRef(top), right: num(0), at, label }, whenTrue: num(0), whenFalse: share, at, label }, at, label }, e); + this.emit({ kind: "if", cond: { kind: "compare", op: ">", left: varRef(n), right: num(width), at, label }, then: [{ kind: "assign", target: n.id, value: num(width), at, label }], at, label }, e); + this.emit({ kind: "if", cond: { kind: "compare", op: "<", left: varRef(n), right: num(0), at, label }, then: [{ kind: "assign", target: n.id, value: num(0), at, label }], at, label }, e); + b.shown = null; + this.jsxColor(b, c); + b.pending.push({ kind: "value", text: { kind: "textRepeat", of: { kind: "text", text: FILLED_CELL }, count: varRef(n), at, label } }); + b.pending.push({ kind: "text", text: String.fromCharCode(EMPTY_COLOR) }); + b.pending.push({ kind: "value", text: { kind: "textRepeat", of: { kind: "text", text: EMPTY_CELL }, count: { kind: "binary", op: "-", left: num(width), right: varRef(n), at, label }, at, label } }); + b.shown = `c${EMPTY_COLOR}`; + return; + } + } + } + + /** What is inside braces among the children: an element, a condition, a loop, or a value of the program written as text. */ + private jsxChild(expr: TS.Expression, b: JsxBuild, color: JsxColor) { + const { ts } = this; + const e = this.unwrap(expr); + if (ts.isJsxElement(e) || ts.isJsxSelfClosingElement(e) || ts.isJsxFragment(e)) { this.jsxNode(e, b, color); return; } + const at = this.at(e), label = this.label(e); + const h = this.evaluate(e); + // React's rule: nothing is written for true, false, null and undefined. + if (h && (h.value === null || h.value === undefined || typeof h.value === "boolean")) return; + if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) { + const known = this.knownCondition(e.left); + if (known !== undefined) { if (known) this.jsxChild(e.right, b, color); return; } + this.jsxBranches(e, b, this.bool(e.left), [() => this.jsxChild(e.right, b, color)], at, label); + return; + } + if (ts.isConditionalExpression(e)) { + const known = this.knownCondition(e.condition); + if (known !== undefined) { this.jsxChild(known ? e.whenTrue : e.whenFalse, b, color); return; } + this.jsxBranches(e, b, this.bool(e.condition), [() => this.jsxChild(e.whenTrue, b, color), () => this.jsxChild(e.whenFalse, b, color)], at, label); + return; + } + if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) && e.expression.name.text === "map" && this.jsxMap(e, b, color)) return; + // A value: its digits, its text. Anything its working out runs comes before what is waiting is written. + const mark = this.out.length; + const parts = this.textOf(e); + if (!parts) return; + if (this.out.length > mark && b.pending.length) { + this.out.splice(mark, 0, this.mark({ kind: "buildText", buffer: b.buffer, parts: b.pending, at: b.at, label: b.label }, e)); + b.pending = []; + } + this.jsxWrites(b, e, false); + this.jsxColor(b, color); + b.pending.push(...parts); + } + + /** `cond && <…/>`, `cond ? <…/> : <…/>`: an if, each side written into the same buffer. */ + private jsxBranches(e: TS.Node, b: JsxBuild, cond: BoolExpr, sides: (() => void)[], at: At, label: string) { + this.jsxFlush(b, e); + const before = { written: b.written, shown: b.shown }; + const after: JsxBuild["written"][] = []; + const bodies = sides.map((side) => this.collect(() => { + b.written = before.written; + b.shown = before.shown; + side(); + this.jsxFlush(b, e); + after.push(b.written); + })); + if (sides.length === 1) after.push(before.written); + b.written = after.every((w) => w === "yes") ? "yes" : after.every((w) => w === "no") ? "no" : "maybe"; + b.shown = null; + this.emit({ kind: "if", cond, then: bodies[0], ...(bodies[1] ? { else: bodies[1] } : {}), at, label }, e); + } + + /** `{xs.map((x, i) => <…/>)}`: the program's loop over the list, the function's element written at each turn. False when it is not a list this can loop over. */ + private jsxMap(e: TS.CallExpression, b: JsxBuild, color: JsxColor): boolean { + const { ts } = this; + const over = this.overOf((e.expression as TS.PropertyAccessExpression).expression); + if (!over) return false; + const fn = this.functionOf(e.arguments[0], "map", e); + if (!fn) return true; + let body: TS.Expression | undefined = ts.isBlock(fn.body) ? undefined : fn.body; + if (ts.isBlock(fn.body) && fn.body.statements.length === 1 && ts.isReturnStatement(fn.body.statements[0])) body = fn.body.statements[0].expression; + if (!body) { this.c.error(fn.body, "In a text, the function given to map gives back what is written for each item, and does nothing else: (x) => ."); return true; } + for (const p of fn.parameters) if (!ts.isIdentifier(p.name)) { this.c.error(p, "In a text, map's function takes the item and its place by name: (x, i) => …."); return true; } + // A list the script has is unrolled, its turns one after another; the program's list is a loop, run any number of times. + const unrolled = over.kind === "values"; + if (!unrolled) { + this.jsxFlush(b, e); + if (b.written === "no") b.written = "maybe"; + b.shown = null; + } + this.loopOver(over, e, (item, index) => { + const scope = new Scope(fn.closure); + const handed = this.handed(over, item, index); + fn.parameters.forEach((p, i) => { if (handed[i]) scope.bind(p, handed[i]); }); + const outer = this.scope; + this.scope = scope; + try { + this.jsxChild(body, b, color); + if (!unrolled) { this.jsxFlush(b, e); b.shown = null; } + } finally { this.scope = outer; } + }); + return true; + } + + /** `ui.lines(P1, () => <…/>)`: a task that shows the function's text in the player's lines, again every frame. */ + private uiLines(call: TS.CallExpression, handle?: VarDecl) { + const { ts } = this; + const at = this.at(call), label = this.label(call); + if (call.arguments.length !== 2) { this.c.error(call, "ui.lines() takes the player and a function that gives the text: ui.lines(P1, () => …)."); return; } + if (!this.inAsync(call)) { this.c.error(call, "ui.lines() runs beside the program as a task: inside program(async () => { … })."); return; } + const h = this.evaluate(call.arguments[0]); + const to = h && typeof h.value === "number" ? h.value : undefined; + const groups: number[] = [CURRENT_PLAYER, PlayerGroup.AllPlayers, PlayerGroup.Force1, PlayerGroup.Force2, PlayerGroup.Force3, PlayerGroup.Force4]; + if (to === undefined || !Number.isInteger(to) || ((to < 0 || to > 7) && !groups.includes(to))) { this.c.error(call.arguments[0], "ui.lines()'s player is known when the script is built: P1 … P8, CurrentPlayer, AllPlayers or a force."); return; } + const fn = this.unwrap(call.arguments[1]); + if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) { this.c.error(fn, "ui.lines() takes a function written where it is used: ui.lines(P1, () => …)."); return; } + if (fn.parameters.length) { this.c.error(fn, "ui.lines()'s function takes nothing: () => …."); return; } + if (fn.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { this.c.error(fn, "ui.lines()'s function gives its text at once, every frame: it is not async."); return; } + let view: TS.Expression | undefined = ts.isBlock(fn.body) ? undefined : fn.body; + if (ts.isBlock(fn.body) && fn.body.statements.length === 1 && ts.isReturnStatement(fn.body.statements[0])) view = fn.body.statements[0].expression; + if (!view) { this.c.error(fn.body, "ui.lines()'s function gives back the text and does nothing else: () => <>…."); return; } + const caught = this.loopCapture(fn, call); + if (caught) { this.c.error(caught, `This text reads ${caught.text}, which the loop around ui.lines() changes; the task would find it as it is when it reads it. Keep it in a variable the program changes instead.`); return; } + const task = ++this.taskCount; + const shown = view; + // Put together again when something it reads was written (`views.ts` sets the flag after every such write), and shown + // again once a second whatever happens, before the game's lines fade — put together then too, in case. + // Declared at the top of the program (`views.ts`): a write before this line already sets it. + const dirty = this.newVar("(redraw)", "number", at, { temp: true }); + let text: TextExpr | null = null; + const build = this.collect(() => { text = this.text(shown); }); + if (!text) return; + const body = this.collect(() => { + const age = this.newVar("(frames since shown)", "number", at, { temp: true }); + this.emit({ kind: "declare", decl: age, init: num(REFRESH_FRAMES), at, label }, call); + const loop = this.collect(() => { + const due: BoolExpr = { kind: "or", items: [{ kind: "test", expr: varRef(dirty), at, label }, { kind: "compare", op: ">=", left: varRef(age), right: num(REFRESH_FRAMES - 1), at, label }] }; + this.emit({ kind: "if", cond: due, then: [ + { kind: "assign", target: dirty.id, value: num(0), at, label }, + { kind: "block", body: build, at }, + { kind: "print", parts: [{ kind: "value", text: text! }], to, position: "chat", at, label }, + { kind: "assign", target: age.id, value: num(0), at, label }, + ], else: [{ kind: "assign", target: age.id, value: { kind: "binary", op: "+", left: varRef(age), right: num(1), at, label }, at, label }], at, label }, call); + this.emit({ kind: "sleep", cycles: 1, at, label }, call); + }); + this.emit({ kind: "while", body: loop, at, label }, call); + }); + this.views.push({ dirty: dirty.id, declare: this.mark({ kind: "declare", decl: dirty, init: num(1), at, label }, call), build }); + const run: Call = this.mark({ name: "ui.lines", at, label, params: [], body }, call); + for (const d of declarations(body)) d.task ??= task; + this.emit({ kind: "start", task, max: 1, call: run, ...(handle ? { handle: handle.id } : {}), at, label }, call); + } + + /** `const board = ui.lines(…)`: its task's number, to stop it with. */ + private uiLinesValue(e: TS.CallExpression): NumExpr { + const at = this.at(e), label = this.label(e); + const t = this.newVar("(task)", "number", at, { temp: true }); + this.emit({ kind: "declare", decl: t, init: num(0), at, label }, e); + this.uiLines(e, t); + return varRef(t); + } + /** `names.join(", ")`: the texts one after another with the separator between, made where the call stands. */ private textsJoin(e: TS.CallExpression, of: Texts): TextExpr | null { const at = this.at(e); @@ -6691,6 +7062,7 @@ export class Structured { if (ofText !== undefined) return ofText; if (this.promiseCall(e)) return this.raceValue(e, "number") as NumExpr | null; if (this.isLibraryCall(e, "start")) return this.startValue(e); + if (this.isLibraryCall(e, "lines")) return this.uiLinesValue(e); const method = this.methodCall(e); if (method !== undefined) return this.numberOf(method, e); if (ts.isIdentifier(e.expression) && (e.expression.text === "parseInt" || e.expression.text === "Number" || e.expression.text === "parseFloat") && !this.gameDeclaration(e.expression)) { diff --git a/compiler/views.ts b/compiler/views.ts new file mode 100644 index 0000000..be31548 --- /dev/null +++ b/compiler/views.ts @@ -0,0 +1,133 @@ +/** + * A view of `ui.lines` is put together again only on a frame when something it reads was written (TrigScript 4, S2). + * The structured compiler lowers the view behind a flag of the program's (`dirty`); this pass finds what the view reads — + * the program's variables and arrays — and sets the flag after every statement that writes one of them, wherever it is: + * the program's body, its tasks, its functions. A view that reads the game itself (units, the players' input, a random + * number, the game's tables) cannot be followed this way and is put together every frame, as it would be without the flag. + */ +import type { ArrayDecl, Program, Stmt } from "./ir"; + +/** A view as the structured compiler leaves it: its flag, and the statements that put its text together. */ +export interface View { dirty: string; declare: Stmt; build: Stmt[] } + +/** What a view reads that only the game knows: never followed. */ +const GAME_READS: ReadonlySet = new Set(["input", "randomInt", "random", "read", "tableRead", "unitField", "cond", "edge", "running", "unitAlive", "unitFlag", "pick", "pop"]); + +/** The statements that hold lists of statements, by the fields that hold them. */ +const LISTS = ["body", "then", "else", "update"] as const; + +/** The array an array's cells are part of: a window (`slice`) is its whole's, and so on down. */ +function rootOf(arrays: Map, id: string): string { + let a = arrays.get(id); + const seen = new Set(); + while (a?.slice && !seen.has(a.id)) { seen.add(a.id); a = arrays.get(a.slice.of) ?? a; if (a.id === id) break; } + return a?.id ?? id; +} + +/** The variables and arrays a view reads (arrays by their whole), or null when it reads the game or calls a function of its own. */ +function readsOf(build: Stmt[], arrays: Map): Set | null { + const reads = new Set(); + const declared = new Set(); + let game = false; + const visit = (o: unknown): void => { + if (game || !o || typeof o !== "object") return; + if (Array.isArray(o)) { o.forEach(visit); return; } + const n = o as Record; + const kind = n.kind; + if (typeof kind === "string") { + if (GAME_READS.has(kind)) { game = true; return; } + if ((kind === "call" || kind === "textCall") && (n.call as { fn?: string } | undefined)?.fn) { game = true; return; } + if ((kind === "var" || kind === "textVar" || kind === "unitVar") && typeof n.id === "string") reads.add(n.id); + if (typeof n.array === "string") reads.add(rootOf(arrays, n.array)); + for (const f of ["addr", "block", "chars"]) if (kind === "textAt" && typeof n[f] === "string") reads.add(rootOf(arrays, n[f] as string)); + if (kind === "declare" && n.decl) declared.add((n.decl as { id: string }).id); + } + for (const v of Object.values(n)) if (v && typeof v === "object") visit(v); + }; + visit(build); + if (game) return null; + for (const d of declared) reads.delete(d); + return reads; +} + +/** What a statement writes itself — not what the lists inside it do, which are walked on their own. */ +function writes(s: Stmt, arrays: Map): string[] { + const array = (id: string) => rootOf(arrays, id); + switch (s.kind) { + case "declare": return [s.decl.id]; + case "assign": case "assignBool": case "assignUnit": case "assignText": return [s.target]; + case "store": case "push": case "pop": case "setLength": case "declareArray": return [array(s.array)]; + case "storeText": return [array(s.addr), array(s.block), array(s.chars)]; + case "releaseText": return [array(s.block)]; + case "textLoop": case "unitLoop": return [s.decl.id]; + default: return []; + } +} + +/** Whether something inside a statement's own expressions (not its lists) takes from an array: `xs.pop()` as a value. */ +function popsInside(s: Stmt, arrays: Map, into: string[]): void { + const visit = (o: unknown, top: boolean): void => { + if (!o || typeof o !== "object") return; + if (Array.isArray(o)) { if (!top) o.forEach((x) => visit(x, false)); return; } + const n = o as Record; + if (n.kind === "pop" && typeof n.array === "string" && !top) into.push(rootOf(arrays, n.array)); + for (const [k, v] of Object.entries(n)) if (v && typeof v === "object" && !(top && (LISTS as readonly string[]).includes(k))) visit(v, false); + }; + visit(s, true); +} + +/** The flag of every view that reads what `s` writes, set after it. */ +function after(s: Stmt, views: { dirty: string; reads: Set }[], arrays: Map): Stmt[] { + const written = writes(s, arrays); + popsInside(s, arrays, written); + // A call that comes back into its function puts saved variables back when it returns. + if (s.kind === "call" && s.call.saves) written.push(...s.call.saves.vars, ...s.call.saves.arrays.map((a) => rootOf(arrays, a))); + if (!written.length) return []; + return views.filter((v) => written.some((id) => v.reads.has(id))).map((v) => ({ kind: "assign", target: v.dirty, value: { kind: "const", value: 1 }, at: s.at, label: "label" in s ? s.label : "" } as Stmt)); +} + +/** Every list of statements in the program, the flag set after each statement that writes what a view reads. */ +export function markViewWrites(program: Program, views: View[]): void { + if (!views.length) return; + // The flags first of all, set: a write anywhere before the ui.lines line already sets one. + program.body.unshift(...views.map((v) => v.declare)); + const arrays = new Map(program.arrays.map((a) => [a.id, a])); + const followed = views.flatMap((v) => { + const reads = readsOf(v.build, arrays); + return reads ? [{ dirty: v.dirty, reads }] : []; + }); + // A view that reads the game is put together every frame: its flag stays set. + for (const v of views) if (!followed.some((f) => f.dirty === v.dirty)) markAlways(v); + if (!followed.length) return; + const own = new Set(views.flatMap((v) => v.build)); + const list = (stmts: Stmt[]): Stmt[] => { + const out: Stmt[] = []; + for (const s of stmts) { + walk(s); + out.push(s); + // The view's own statements write only what they declare: nothing it reads. + if (!own.has(s)) out.push(...after(s, followed, arrays)); + } + return out; + }; + const walk = (o: unknown): void => { + if (!o || typeof o !== "object") return; + if (Array.isArray(o)) { o.forEach(walk); return; } + const n = o as Record; + for (const [k, v] of Object.entries(n)) { + if (Array.isArray(v) && (LISTS as readonly string[]).includes(k) && v.every(isStmt)) n[k] = list(v as Stmt[]); + else if (k === "iterations" || k === "branches") n[k] = (v as Stmt[][]).map((l) => list(l)); + else if (k === "cases") for (const c of v as { body: Stmt[] }[]) c.body = list(c.body); + else if (v && typeof v === "object") walk(v); + } + }; + program.body = list(program.body); + for (const f of program.functions ?? []) f.body = list(f.body); +} + +const isStmt = (x: unknown): boolean => !!x && typeof x === "object" && typeof (x as { kind?: unknown }).kind === "string"; + +/** A view whose flag nothing clears: the lowering clears it before it builds, so set it again first thing in the build. */ +function markAlways(v: View): void { + v.build.unshift({ kind: "assign", target: v.dirty, value: { kind: "const", value: 1 }, at: v.build[0]?.at ?? { file: "", line: 0, column: 0 }, label: "" } as Stmt); +} diff --git a/docs/ir.md b/docs/ir.md index 9d752cf..6d42761 100644 --- a/docs/ir.md +++ b/docs/ir.md @@ -3,7 +3,7 @@ What a `program(() => { … })` body means, written down as data. The compiler's front end (`compiler/structured.ts`) turns the TypeScript into this, `python/trigscript.py` lowers it to eudplib when the map is saved, and `compiler/simulateIr.ts` interprets it for -Simulate and the tests. **Version 16** (15 had no tasks: no `start`, `cancel`, `running`, and no `task` on a declaration; 14 had no `parallel`: nothing ran beside a program's body; 13 had no text in the cells of an array: no `textAt`, `storeText`, `releaseText`; 12 had no texts: a text was the parts of a `print` and nothing else; 11 had no `slice` and no `through`: every array was cells of its own; 10 had no recursion: no `recursive`, no `saves`, and the heap's top set aside for a stack nothing used; 9 had no functions that are called: every `call` carried a body of its own; 8 had no `unitAt` / `unitPart`, so no array could hold a unit; 7 had no arrays that grow; 6 had no arrays; 5 had unsigned numbers only, the two-sided reading of `+` and `−`, no `>>>` and no `unsigned` anywhere; 4 had no input, no `centerLocation`, and one `variable` on an action where 5 has a list; 3 had no units and no tables; 2 had no reads, no `random(n)`, no bitwise operators +Simulate and the tests. **Version 17** (16 had no `buildText`, no `built` and no `char` part: a text was made whole or not at all; 15 had no tasks: no `start`, `cancel`, `running`, and no `task` on a declaration; 14 had no `parallel`: nothing ran beside a program's body; 13 had no text in the cells of an array: no `textAt`, `storeText`, `releaseText`; 12 had no texts: a text was the parts of a `print` and nothing else; 11 had no `slice` and no `through`: every array was cells of its own; 10 had no recursion: no `recursive`, no `saves`, and the heap's top set aside for a stack nothing used; 9 had no functions that are called: every `call` carried a body of its own; 8 had no `unitAt` / `unitPart`, so no array could hold a unit; 7 had no arrays that grow; 6 had no arrays; 5 had unsigned numbers only, the two-sided reading of `+` and `−`, no `>>>` and no `unsigned` anywhere; 4 had no input, no `centerLocation`, and one `variable` on an action where 5 has a list; 3 had no units and no tables; 2 had no reads, no `random(n)`, no bitwise operators and no `print`; 1 had the map's string indices in the records and a `cyclesPerSecond` on the program, for the death-counter backend 3.0 removed). The types are in `compiler/ir.ts`; this is the reference for anyone reading the lowering or writing @@ -184,6 +184,7 @@ frame's `saves`, and a `slice` is never declared. | `parallel` | `mode`, `branches` | `await Promise.all([…])` (`all`) or `await Promise.race([…])` / `Promise.any` (`race`). Each branch is a coroutine of its own with its own place to go on from and its own wait: started here, in order, in this frame; then, every frame the program is at this statement, each branch that has not ended goes on, in order. The program goes on past it in the frame the last branch ends (`all`) or the first (`race`, which leaves the others where they are: they never run again). A branch is never left by `break`, `continue` or `return`. Not inside a called function, a `unitLoop` or a `textLoop`. Since version 15. | | `start` | `task`, `max`, `call`, `handle?` | `start(wave(n))`, `void wave(n)`: a task that runs beside what started it (*Tasks*, below). `call` is the async function's body inlined, its `params` copies of this start's arguments, worked out here; the body runs in the task. `handle` takes the task's number, or 0 when every slot was running. Since version 16. | | `cancel` | `task` (a number) | `t.cancel()`: the task stops where it is; nothing happens for a number that is no running task's. Since version 16. | +| `buildText` | `buffer`, `parts`, `reset?` | The parts written onto the end of buffer `buffer` — emptied first when `reset` — which a `built` reads back (*Texts put together a piece at a time*, below). Since version 17. | | `print` | `parts`, `to`, `position` | Text with values in it. `parts` are `{ kind: "text", text }`, `{ kind: "number", expr, unsigned? }` (its digits, with a minus sign when it is below zero and not `unsigned`), `{ kind: "name", player }` and `{ kind: "color", player }` (the colour code of the player's colour), a player being a slot or 13 for the current player. `to` is who sees it: a slot, 13, All Players (17) or a force (18–21). `position` is `chat` or `center`, the line the game's own errors use. Every number is evaluated before anything is shown. | | `action` | `record`, `variables?` | A trigger action. Each of `variables` names a field of the record that takes an expression's value: `{ field, bits: 8, 16 or 32, name, expr }` — the unit count of `createUnit` and friends is an 8-bit field, a unit type a 16-bit one (the lowering stops it at 228), an amount with a modifier a 32-bit one. Version 4 had one, as `variable`. | | `centerLocation` | `location`, `x`, `y` | Centre a location (1-based) on a point of the map in pixels, its size kept. | @@ -405,6 +406,7 @@ TextExpr = | { kind: "textPad", of, side: "start" | "end", width, with, at, label } | { kind: "textRepeat", of, count, at, label } | { kind: "textCall", call } a call whose result is a text + | { kind: "built", buffer } what a buffer of buildText holds (version 17) ``` The first three, and a `textTernary` between such, *have an id* whatever happens in the @@ -464,6 +466,30 @@ lets a made text into those actions only. A `tableWrite` of a unit type's name t type, on every computer. The places a `textSlice` is given are inside 0 … the length already: the front end counts from the end and clamps. +### Texts put together a piece at a time + +Version 17, for JSX (TrigScript 4, S2). A JSX text has conditions and loops in it, so it +cannot be one `template`; made as templates joined with `+` it would be copied whole at every +piece. Instead it has a **buffer** of its own, named by `buffer` (`jsx L#`): `BUILD_BYTES` +(2 047) bytes and where the writing has got to, one for the whole game. A `buildText` with +`reset` starts it empty; every other `buildText` writes its parts at the end; `built` is the +text there, only looked at — whatever keeps it takes a copy, since the buffer is built again +the next time the same JSX runs. A buffer is used up within the frame it is built in: the +front end never lets an `await` come between a JSX text's first `buildText` and its use. Past +`BUILD_BYTES` the text is cut, never inside a character, and the game says so once in red +where Simulate records a fault. + +Parts gain `{ kind: "char", expr }`: one byte, the number's low eight bits — a colour code +the program chooses (``). 0 writes nothing. Simulate's log drops it, +as it drops a player's colour. + +`ui.lines(player, view)` is a `start` whose body loops: when a flag of the program says the +view is stale, or 24 frames after it was last shown, it builds the view and prints `built`. +The flag is a variable declared first in the program body, set; after every statement anywhere +in the program (its tasks and its functions too) that writes a variable or an array the view +reads, the front end adds `flag = 1` (`views.ts`). A view that reads the game — units, input, +`random`, the game's tables, a function that is called — is built every frame. + ## Units Units (`UnitExpr`): diff --git a/python/trigscript.py b/python/trigscript.py index c2a6ace..161a9ee 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -68,7 +68,7 @@ from eudplib.core.mapdata.stringmap import ForceAddString from eudplib.memio.rwcommon import br1, br2, bw1 -IR_VERSION = 16 +IR_VERSION = 17 FRAMES_PER_SECOND = 24 # Where the game keeps what a read reads (1.16.1 addresses, which Remastered emulates). The player # tables are the ones Magenta's probes 5 and 8 read in the game. @@ -567,6 +567,7 @@ def put(self, name, value): # ── texts ── TEXT_BYTES = 1023 # the most a made text holds; compiler/ir.ts has the same number TEXT_FIELD_BYTES = 255 # the room of a string kept for an action's field or a unit type's name +BUILD_BYTES = 2047 # the most a buffer of buildText holds; compiler/ir.ts has the same number UNIT_MAP_STRING = 0x660260 # units.dat: the id of the name a map gives a unit type, a word each _TEXT = {} _SLOTS = {} @@ -895,6 +896,60 @@ def text_id(text): return EncodeString(text) if text else 0 +_BUFFERS = {} + + +def buffer(name): + """A buffer of buildText: its bytes and where the writing has got to. One for the whole game, used up within the + 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.""" + if name not in _BUFFERS: + _BUFFERS[name] = (Db(BUILD_BYTES + 1 + TEXT_BYTES + 1 + 8), EUDVariable()) + return _BUFFERS[name] + + +_BUILD = {} + + +def builds(): + """What every buffer shares: copying a text onto the end of one, and cutting one that ran past its room.""" + if _BUILD: + return _BUILD + said_cut = EUDVariable(0) # initial: said once a game + + @EUDFunc + def append(pos, src, last): + """The text at src written at pos, ended by a 0; where that 0 is. Never past `last`.""" + br1.seekoffset(src) + bw1.seekoffset(pos) + if EUDWhile()(pos <= last): + b = br1.readbyte() + EUDBreakIf(b == 0) + bw1.writebyte(b) + pos += 1 + EUDEndWhile() + bw1.writebyte(0) + EUDReturn(pos) + + @EUDFunc + def cut(pos, end): + """A buffer whose writing got to `pos`, cut at `end` (never inside a character) when it went past it.""" + if EUDIf()(pos > end): + pos << end + if EUDWhile()(f_bread(pos).ExactlyX(0x80, 0xC0)): + pos -= 1 + EUDEndWhile() + f_bwrite(pos, 0) + if EUDIf()(said_cut == 0): + said_cut << 1 + GetGlobalStringBuffer().print("\x06TrigScript: a text was cut off - one put together piece by piece holds %d bytes." % BUILD_BYTES) + EUDEndIf() + EUDEndIf() + EUDReturn(pos) + + _BUILD.update(append=append, cut=cut) + return _BUILD + + class TextVal: """A text as the lowering holds it: where it is, the block it owns (the int 0: none), and its length in characters - an int, a variable, or None when nobody has counted yet. `taken`: the block is the value's own, so @@ -2127,6 +2182,8 @@ def statement(self, st, ctx): self.action(st) elif k == "print": self.print_(st) + elif k == "buildText": + self.build_text(st) elif k == "centerLocation": self.center_location(st) elif k == "call": @@ -2333,6 +2390,8 @@ def print_(self, st): args.append(PName(self.one_player(part["player"], st))) elif k == "color": args.append(PColor(self.one_player(part["player"], st))) + elif k == "char": + args.append(self.char(part["expr"])) else: raise Fail("trigscript: unknown text part %r%s" % (k, where(st))) if not args: @@ -2430,11 +2489,42 @@ def put(self, storage, e, kind): texts()["release"](storage.cell(storage.block)) storage.write(addr, block, length) + def char(self, e): + """One byte from a number, as something to print: a colour code chosen when the map is played; 0 is nothing.""" + v = self.num(e) + if isinstance(v, int): + return chr(v & 0xFF) if v & 0xFF else "" + one = Db(4) + f_bwrite(one, v & 0xFF) + return ptr2s(one) + def written(self, parts, st): """The parts of a text in the scratch, and where it ends. Every value is worked out first, since working one out may use the scratch itself; then the parts are written a group at a time, a group being what cannot come to more than a text holds, so that checking where the writing has got to between groups keeps it inside.""" T = texts() + items, temps = self.items(parts, st) + pos = fresh(T["scratch"]) + f_bwrite(pos, 0) + self.write_items(items, pos, T["scratch"] + TEXT_BYTES, lambda at, value: T["append"](at, value.addr), True) + return pos, temps + + def build_text(self, st): + """Parts written onto the end of a buffer (emptied first on `reset`): a text put together a piece at a time.""" + data, pos = buffer(st["buffer"]) + B = builds() + items, temps = self.items(st["parts"], st) + if st.get("reset"): + pos << data + f_bwrite(data, 0) + last = data + BUILD_BYTES + TEXT_BYTES + self.write_items(items, pos, data + BUILD_BYTES, lambda at, value: B["append"](at, value.addr, last), False) + pos << B["cut"](pos, data + BUILD_BYTES) + self.used(*temps) + + def items(self, parts, st): + """The parts of a text as what f_dbstr_print takes, each with the most bytes it can come to; a value of the + program as itself, to be copied in. Every value is worked out here, before anything is written.""" items, temps = [], [] for part in parts: k = part["kind"] @@ -2465,11 +2555,19 @@ def written(self, parts, st): v = self.own(v) temps.append(v) items.append((None, TEXT_BYTES, v)) + elif k == "char": + c = self.char(part["expr"]) + if c != "": + items.append(([c], 1, None)) else: raise Fail("trigscript: unknown text part %r%s" % (k, where(st))) - pos = fresh(T["scratch"]) - f_bwrite(pos, 0) - group, room, first = [], 0, [True] + return items, temps + + def write_items(self, items, pos, end, append, first_free): + """Items written at pos, which moves to where the text ends: a group at a time, a group being what cannot come + to more than a text holds, so that checking pos against `end` between groups keeps the writing inside the room + behind it. `first_free`: pos starts at the beginning, so the first group needs no check.""" + group, room, first = [], 0, [first_free] def flush(): if not group: @@ -2477,7 +2575,7 @@ def flush(): if first[0]: pos << f_dbstr_print(pos, *group) else: - if EUDIf()(pos <= T["scratch"] + TEXT_BYTES): + if EUDIf()(pos <= end): pos << f_dbstr_print(pos, *group) EUDEndIf() first[0] = False @@ -2488,10 +2586,10 @@ def flush(): flush() room = 0 if first[0]: - pos << T["append"](pos, value.addr) + pos << append(pos, value) else: - if EUDIf()(pos <= T["scratch"] + TEXT_BYTES): - pos << T["append"](pos, value.addr) + if EUDIf()(pos <= end): + pos << append(pos, value) EUDEndIf() first[0] = False continue @@ -2501,7 +2599,6 @@ def flush(): group.extend(args) room += size flush() - return pos, temps def text(self, e): """A text: where it is, its block, its length (TextVal).""" @@ -2521,6 +2618,9 @@ def text(self, e): return TextVal(addr, fresh(self.array(e["block"], e).get(i)), fresh(self.array(e["chars"], e).get(i)), False) if self.has_id(e): return TextVal(T["address"](as_var(self.text_id_of(e))), 0, None, True) + if k == "built": + # Only looked at: what keeps it takes a copy, since the buffer is built again next time. + return TextVal(fresh(buffer(e["buffer"])[0]), 0, None, False) if k == "template": pos, temps = self.written(e["parts"], e) addr, block, length = T["make"](pos) diff --git a/scripts/build-fixture.mts b/scripts/build-fixture.mts index 7ea9498..7ac6614 100644 --- a/scripts/build-fixture.mts +++ b/scripts/build-fixture.mts @@ -33,7 +33,7 @@ const eudplib = resolve(eudplibArg ?? process.env.EUDPLIB_DIR ?? join(root, ".." if (!existsSync(map)) { console.error(`no map at ${map}`); process.exit(2); } if (!existsSync(join(eudplib, "scripts", "build-map.mts"))) { console.error(`no plugin-eudplib checkout at ${eudplib}`); process.exit(2); } -const r = compileScript(ts, { "main.ts": readFileSync(scriptPath, "utf8") }, defaultScriptNames(), { lib: defaultLib() }); +const r = compileScript(ts, { [scriptPath.endsWith(".tsx") ? "main.tsx" : "main.ts"]: readFileSync(scriptPath, "utf8") }, defaultScriptNames(), { lib: defaultLib() }); if (r.diagnostics.length) { for (const d of r.diagnostics) console.error(`${d.file}:${d.line}:${d.column} ${d.message}`); process.exit(1); } const EDITOR = resolve(process.env.SCMJS_DIR ?? join(root, "..", "scm-js")); const { loadMap, saveMap, readExtras } = await import(join(EDITOR, "src", "formats", "mpq", "scm.ts")); diff --git a/scripts/debug-game.mts b/scripts/debug-game.mts index 33c058f..c9725c6 100644 --- a/scripts/debug-game.mts +++ b/scripts/debug-game.mts @@ -52,7 +52,7 @@ try { const payloadEnd = 0x191943c8 + strings.bytes; const handle = testGame(run.engine, run.lifted, [{ from: strings.gameTable, to: payloadEnd }], { cost: installCost(run.lifted), payloadEnd, eud }) as DebugHost; - const source = { "main.ts": readFileSync(script, "utf8") }; + const source = { [script.endsWith(".tsx") ? "main.tsx" : "main.ts"]: readFileSync(script, "utf8") }; const names = defaultScriptNames(); const locations = readLocations(handle); const compiled = compileScript(ts, source, names, { lib: defaultLib(), tests: {} }); diff --git a/scripts/game-check.mts b/scripts/game-check.mts index 7478b8d..8d9d921 100644 --- a/scripts/game-check.mts +++ b/scripts/game-check.mts @@ -28,8 +28,8 @@ const list = JSON.parse(readFileSync(flag("allowlist") ?? join(root, "data", "al const dir = mkdtempSync(join(tmpdir(), "trigscript-game-")); try { let map = resolve(input); - if (/\.ts$/.test(input)) { - map = join(dir, basename(input, ".ts") + ".scx"); + if (/\.tsx?$/.test(input)) { + map = join(dir, basename(input).replace(/\.tsx?$/, "") + ".scx"); const built = spawnSync("npx", ["tsx", join(root, "scripts", "build-fixture.mts"), resolve(input), map, ...(baseMap ? [resolve(baseMap)] : [])], { cwd: root, encoding: "utf8", maxBuffer: 256 << 20, env: { ...process.env, TRIGGER_MAP: "1" } }); if (built.status !== 0) { console.error(built.stdout.split("\n").slice(-20).join("\n"), built.stderr); process.exit(1); } } diff --git a/scripts/test-game.mts b/scripts/test-game.mts index 270ae59..31cadc8 100644 --- a/scripts/test-game.mts +++ b/scripts/test-game.mts @@ -57,7 +57,7 @@ try { // The tests in the simulator over the map's own world, as the editor runs them; then in the game, the way a host // of the game runs them: the compile's JavaScript handed to dist/gametest.js, loaded from its text. - const source = { "main.ts": readFileSync(script, "utf8") }; + const source = { [script.endsWith(".tsx") ? "main.tsx" : "main.ts"]: readFileSync(script, "utf8") }; const names = defaultScriptNames(); // (The locations from the game once eudplib has put them right: in the built map they are scrambled.) const locations = readLocations(handle); diff --git a/tests/eud-build.test.ts b/tests/eud-build.test.ts index e5d7425..0252e0f 100644 --- a/tests/eud-build.test.ts +++ b/tests/eud-build.test.ts @@ -347,7 +347,8 @@ const FIXTURES: Record = { }; function build(name: string, src: string): { out: number; triggers: number } { - const r = compileScript(ts, { "main.ts": src }, NAMES, { lib: LIB }); + // A fixture that writes JSX is a .tsx file: its name ends in Tsx. + const r = compileScript(ts, { [name.endsWith("Tsx") ? "main.tsx" : "main.ts"]: src }, NAMES, { lib: LIB }); expect(r.diagnostics).toEqual([]); // One program a fixture, but for the numbers probe, which has a second one for every player. expect(r.ir.length).toBe(name === "numbers" || name === "arraysProbe" || name === "functions" || name === "functionsProbe" || name === "callbacksProbe" || name === "insideProbe" || name === "stringsProbe" || name === "classesProbe" || name === "mapProbe" ? 2 : name === "leftoversProbe" ? 1 : name === "recursionProbe" ? 3 : 1); @@ -367,6 +368,23 @@ function build(name: string, src: string): { out: number; triggers: number } { return { out, triggers: m ? Number(m[1]) : 0 }; } +// TrigScript 4, S2: JSX put together in a buffer, a cell of the program's colour, a bar, ui.lines redrawn when what it reads +// changes — and the Tetris that shows it all off, keys and all. +FIXTURES.uiTsx = `program(async () => { + const cells = [0, 8, 17, 0]; + let hp = 40; + const t = ui.lines(P1, () => ( + <> + HP {hp} + {cells.map((c, i) => )}{hp > 50 && high} + + )); + for (let i = 0; i < 10; i++) { hp += 7; cells[3] = i % 2 ? 7 : 0; await sleep(frames(4)); } + t.cancel(); + print(done); + });`; +FIXTURES.tetrisTsx = readFileSync(resolve(import.meta.dirname, "..", "probes", "tetris.tsx"), "utf8"); + // Functions that are called: parameters and results of each kind, an argument that is a call of the same function, a copy an // array, a called function calling another, and — in the second program — cells that are a row a player. FIXTURES.functions = `program(() => { @@ -561,6 +579,6 @@ describe("the IR as the lowering reads it", () => { const ir = JSON.parse(serializeIr(r.ir, r.strings)); const actions = ir.programs[0].body.filter((s: { kind: string }) => s.kind === "action").map((s: { record: { text: unknown; wav: unknown } }) => [s.record.text, s.record.wav]); expect(actions).toEqual([["hello", 0], [0, "sound\\x.wav"]]); - expect(ir.version).toBe(16); + expect(ir.version).toBe(17); }); }); diff --git a/tests/functions.test.ts b/tests/functions.test.ts index e1b291a..725b8af 100644 --- a/tests/functions.test.ts +++ b/tests/functions.test.ts @@ -175,7 +175,7 @@ describe("functions: arrays, and functions inside functions", () => { it("the functions go to the lowering with the program, their text written out", () => { const r = compile("function say(n: number) { displayText(`n`); setDeaths(P1, units.TerranMarine, 'set', n); } let k = 1; say(k); say(k + 1);"); const ir = JSON.parse(serializeIr(r.ir, r.strings)); - expect(ir.version).toBe(16); + expect(ir.version).toBe(17); expect(ir.programs[0].functions).toHaveLength(1); expect(JSON.stringify(ir.programs[0].functions)).toContain('"text":"n"'); }); diff --git a/tests/script-structured.test.ts b/tests/script-structured.test.ts index f2d2f86..7a9bac8 100644 --- a/tests/script-structured.test.ts +++ b/tests/script-structured.test.ts @@ -82,7 +82,7 @@ describe("structured: loops and branches", () => { expect(r.triggers).toEqual([]); expect(r.strings).toEqual([]); expect(r.ir).toHaveLength(1); - expect(r.ir[0]).toMatchObject({ version: 16, owner: 0, owners: [0], perPlayer: false }); + expect(r.ir[0]).toMatchObject({ version: 17, owner: 0, owners: [0], perPlayer: false }); const sim = run(r, 6); expect(sim.events.map((e) => `${e.cycle}:${ActionType.Victory === e.action.type ? "Victory" : e.action.type}`)).toEqual(["2:Victory"]); expect(value(sim, "n")).toBe(6); diff --git a/tests/service.test.ts b/tests/service.test.ts index a176561..710ee4e 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -264,9 +264,9 @@ describe("service: saving applies the script and builds its programs", () => { expect(compiles).toBe(before); expect(Object.keys(input.plugins)).toEqual(["trigscript", "eudTurbo"]); expect(input.plugins.trigscript).toEqual({ ir: "/work/files/trigscript.json" }); - expect(input.sources!.trigscript).toContain("IR_VERSION = 16"); + expect(input.sources!.trigscript).toContain("IR_VERSION = 17"); const ir = JSON.parse(input.files!["trigscript.json"]); - expect(ir.version).toBe(16); + expect(ir.version).toBe(17); expect(ir.programs).toHaveLength(1); expect(JSON.stringify(ir)).toContain('"text":"tick"'); // The library going away takes the contribution with it; coming back, it is made again. diff --git a/tests/ui.test.ts b/tests/ui.test.ts new file mode 100644 index 0000000..cb69acd --- /dev/null +++ b/tests/ui.test.ts @@ -0,0 +1,155 @@ +/** + * TrigScript 4, S2: a text as JSX, and `ui.lines`. A JSX text is put together piece by piece in a buffer of its own + * (`buildText` / `built` in the IR); `ui.lines(P1, () => …)` is a task that shows it in the player's text lines every + * frame. The interpreter here; `eud-build.test.ts` builds the same through eudplib. The simulator's log keeps the words + * of a text and drops its colour codes, as it does for a player's colour. + */ +import ts from "typescript"; +import { describe, expect, it } from "vitest"; +import { compileScript, type CompileResult } from "../compiler/compiler"; +import { jsxAtBuild, JSX_FRAGMENT, builtText } from "../compiler/jsx"; +import { defaultScriptNames } from "../compiler/names"; +import { simulatePrograms } from "../compiler/simulateIr"; +import { ActionType } from "../vendor/triggers"; +import { defaultLib } from "../bundle/lib.mjs"; + +const LIB = defaultLib(); +const NAMES = defaultScriptNames(); +const raw = (source: string): CompileResult => compileScript(ts, { "main.tsx": source }, NAMES, { lib: LIB }); +const compile = (source: string): CompileResult => { const r = raw(source); expect(r.diagnostics.map((d) => `${d.line}: ${d.message}`)).toEqual([]); return r; }; +const messages = (source: string): string[] => raw(source).diagnostics.map((d) => d.message); +const run = (r: CompileResult, frames: number) => simulatePrograms(r.ir, frames, { strings: r.strings }); +/** What was shown, as "frame:text", over that many frames; `codes` keeps the colour codes a text says itself. */ +const printed = (r: CompileResult, frames: number, codes = false): string[] => + run(r, frames).events.filter((e) => e.action.type === ActionType.DisplayText).map((e) => `${e.cycle}:${codes ? e.text : e.text?.replace(/[\x01-\x08\x0e-\x1f]/g, "")}`); +const program = (body: string) => `program(async () => {${body}});`; + +describe("JSX texts", () => { + it("an element is a text: its children, the program's values among them", () => { + const r = compile(program(` + let wave = 3; + print(Wave {wave} of {wave * 2 + 4}); + wave++; + print(Wave {wave});`)); + expect(printed(r, 1)).toEqual(["0:Wave 3 of 10", "0:Wave 4"]); + // Each line says its colour at its start: yellow, and white when it says none. + expect(printed(r, 1, true)).toEqual(["0:\x03Wave 3 of 10", "0:\x04Wave 4"]); + }); + + it("lines are put one under another, and text between elements reads as React reads it", () => { + const r = compile(program(` + const score = 120; + print( + <> + Score {score} + + Lines cleared: + {4} + + + );`)); + expect(printed(r, 1)).toEqual(["0:Score 120\nLines cleared:4"]); + }); + + it("&& and ?: are the program's if; true, false, null and undefined write nothing", () => { + const r = compile(program(` + for (let n = 0; n < 3; n++) { + print(<>{n > 0 && more}{n === 2 ? "two" : not two}{null}{false}); + }`)); + expect(printed(r, 1)).toEqual(["0:not two", "0:more\nnot two", "0:more\ntwo"]); + }); + + it("map over the program's array is a loop; over a list of the script it is unrolled", () => { + const r = compile(program(` + const board = [1, 0, 8, 0]; + board[1] = 17; + print({board.map((c) => )}); + print(<>{[0, 1, 2].map((y) => row {y})}); + const rows = [5, 6]; + rows.push(7); + print(<>{rows.map((r, i) => {i}: {r})});`)); + expect(printed(r, 1)).toEqual(["0:■■■□", "0:row 0\nrow 1\nrow 2", "0:0: 5\n1: 6\n2: 7"]); + }); + + it("a cell known when the script is built, and a bar", () => { + const r = compile(program(` + let hp = 30; + print( ); + hp = 250; + print();`)); + expect(printed(r, 1)).toEqual(["0:■□ ■■■□□□□□□□", "0:■■■■"]); + }); + + it("a JSX text kept in a variable is a copy: the next build does not change it", () => { + const r = compile(program(` + let n = 1; + const first = n is {n}; + n = 2; + const second = n is {n}; + print(first + " / " + second);`)); + expect(printed(r, 1)).toEqual(["0:n is 1 / n is 2"]); + }); +}); + +describe("ui.lines", () => { + it("shows the text every frame, from the program's variables as they are", () => { + const r = compile(program(` + let score = 0; + ui.lines(P1, () => Score {score}); + for (let i = 0; i < 3; i++) { await sleep(frames(1)); score += 10; }`)); + expect(printed(r, 4)).toEqual(["0:Score 0", "1:Score 10", "2:Score 20", "3:Score 30"]); + }); + + it("gives back a Task that stops it", () => { + const r = compile(program(` + const board = ui.lines(P1, () => up); + await sleep(frames(30)); + board.cancel();`)); + expect(printed(r, 60)).toEqual(["0:up", "24:up"]); + }); + + it("is put together again only when something it reads was written, and shown again every 24 frames before it fades", () => { + const r = compile(program(` + const cells = [0, 0, 0]; + let unrelated = 0; + ui.lines(P1, () => {cells.map((c) => )}); + start(async () => { await sleep(frames(3)); cells[1] = textColors.red; }); + for (let i = 0; i < 40; i++) { unrelated++; await sleep(frames(1)); } + cells[2] = textColors.blue;`)); + // The task that writes was started after the view's, so the view has had its turn that frame: it shows the next. + expect(printed(r, 50)).toEqual(["0:□□□", "4:□■□", "28:□■□", "40:□■■"]); + }); + + it("a view that reads the game is put together every frame", () => { + const r = compile(program(` + ui.lines(P1, () => {random(1)});`)); + expect(printed(r, 3)).toEqual(["0:0", "1:0", "2:0"]); + }); + + it("shows to the player it is for", () => { + const r = compile(program(`ui.lines(P3, () => three);`)); + const shown = run(r, 1).events.filter((e) => e.action.type === ActionType.DisplayText); + expect(shown.map((e) => e.action.player)).toEqual([2]); + }); + + it("says what is wrong", () => { + expect(messages(`program(() => { ui.lines(P1, () => x); });`).join("\n")).toMatch(/inside program\(async/); + expect(messages(program(`ui.lines(P1, async () => x);`)).join("\n")).toMatch(/Promise' is not assignable to type 'string'/); + expect(messages(program(`print(
x
);`)).join("\n")).toMatch(/Property 'div' does not exist on type 'JSX.IntrinsicElements'/); + expect(messages(program(`print(x);`)).join("\n")).toMatch(/Property 'size' does not exist/); + expect(messages(program(`async function later() { await sleep(frames(1)); return 2; } print({await later()});`)).join("\n")).toMatch(/nothing inside it can wait/); + expect(messages(program(`print(x);`)).join("\n")).toMatch(/"pink"' is not assignable/); + expect(messages(program(`print(x);`)).join("\n")).toMatch(/color is one of white, grey, .* or a colour code from 0 to 255; got number 300/s); + expect(messages(program(`const xs = [1]; xs.push(2); print({xs.map((x) => { print("a"); return ; })});`)).join("\n")).toMatch(/gives back what is written for each item, and does nothing else/); + }); +}); + +describe("JSX when the script is built", () => { + it("makes the text the game would put together", () => { + const text = builtText(jsxAtBuild(JSX_FRAGMENT, null, + jsxAtBuild("line", { color: "yellow" }, "Score ", 3, [1, 0].map((c) => jsxAtBuild("cell", { color: c ? "red" : 0 }))), + jsxAtBuild("line", null, jsxAtBuild("span", { color: "red" }, "red"), " white"))); + // A colour code that another follows changes nothing and is left out. + expect(text).toBe("\x03Score 3\x08■\x1E□\n\x08red\x04 white"); + }); +}); From 5e7cc4d0244d250d9939ce13715f601d6d1637d8 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 20:15:06 -0600 Subject: [PATCH 041/238] TrigScript 4, S2: Tetris in the text lines, and S2 as built Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 32 ++++++++++ probes/tetris.tsx | 154 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 probes/tetris.tsx diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 74f903b..5a377e3 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1093,6 +1093,38 @@ ui.objectives(P1, () => ( Text a frame when the view's inputs changed (and again before the lines fade), ten rows under a header. The objectives and the label stay as plain made texts. +**As built (2026-10-16, IR 17).** JSX in a `.tsx` file (the entry may be `main.tsx`): the +compiler options say `jsx: react` with the factory `__jsx`, so TypeScript type-checks every +element against `JSX.IntrinsicElements` and a JSX expression is a `string`. The elements are +`` (a line of its own; a line break goes before each one but the first thing +written), `` (its colour comes back after it, lazily: the parent's next piece says +it again), `` (■ in its colour; 0 or none is □ in lavender — a board's cell) and +``. A colour is one of `textColors`' names (the codes the spike +measured) or a number of the program's. `{…}` is the program's value; `cond && …`, `?:` and +`.map((x, i) => …)` are its if and its loop — unrolled over a list the script has, so a board +drawn over `[...Array(10).keys()]` reads its cells at constant indices. Nothing in a text can +wait. + +- **Lowering:** a JSX text is not a template. It has a buffer of its own (`buildText` writes + parts at its end, `reset` empties it; `built` reads it), so a 100-cell board is written in + place, never copied whole per piece. Parts gain `char`, a byte from a number. JSX is never + hoisted (`hoist.ts#hasJsx`); outside a program `__jsx` makes the same text at build time + (`compiler/jsx.ts`). +- **`ui.lines(player, () => …)`** is a task (`start`, one slot) that loops: built and printed + when a flag says the view is stale or 24 frames after it was shown (the lines fade in under + six seconds). `views.ts` declares the flag first in the program, finds what the view reads + and sets the flag after every statement anywhere in the program that writes it. A view that + reads the game is built every frame. It gives back the Task (`board.cancel()`). +- **Measured in the lifted game:** a static 10×10 board costs ~360 actions a frame when nothing + changed, ~63k on a frame it is built and printed. Tetris (`probes/tetris.tsx`): ~80k on a frame + the board changed; a headless run reads the keys as held, so every frame changed there. Nothing + Remastered refuses. +- **Found on the way:** the freeze check called `while (move(0, 1, 0)) {}` endless: a call in a + loop's condition that writes what is outside it now counts as changing it. +- **Not yet:** components (``: call a function in braces instead), `Array.from`, the + per-cell cost (~600 actions: the colour byte and the glyph are copied in; a cell could be one + dword), the editor's workspace knowing `.tsx`, guide and README (wait for 4.0). + ### S3. A hero moved with the keys W A S D move a hero, the mouse aims, a key casts. The map plays as an action game. diff --git a/probes/tetris.tsx b/probes/tetris.tsx new file mode 100644 index 0000000..5ab5d30 --- /dev/null +++ b/probes/tetris.tsx @@ -0,0 +1,154 @@ +// Tetris in StarCraft's text lines (TrigScript 4, S2's showpiece). The screen is JSX: ui.lines() shows it to Player 1 +// every frame, redrawn from the program's variables. A D move, W turns, S drops a row, Space drops all the way. +// The pieces and their four turns are worked out when the script is built, in ordinary JavaScript. +// Host it as a multiplayer game, alone: the keys reach the game the way typed lines do. +// Built with +// npx tsx scripts/build-fixture.mts probes/tetris.tsx fixtures/eud/tetris.scx +import { program, P1, keyPressed, random, sleep, frames, ui, textColors } from "trigscript"; + +const W = 10; +const H = 10; + +// Each piece's four cells in its box, as it first appears. +const PIECES: [number, number][][] = [ + [[0, 1], [1, 1], [2, 1], [3, 1]], // I + [[1, 0], [2, 0], [1, 1], [2, 1]], // O + [[1, 0], [0, 1], [1, 1], [2, 1]], // T + [[1, 0], [2, 0], [0, 1], [1, 1]], // S + [[0, 0], [1, 0], [1, 1], [2, 1]], // Z + [[0, 0], [0, 1], [1, 1], [2, 1]], // J + [[2, 0], [0, 1], [1, 1], [2, 1]], // L +]; +const COLORS = [textColors.cyan, textColors.yellow, textColors.purple, textColors.green, textColors.red, textColors.blue, textColors.orange]; +const BOX = [4, 4, 3, 3, 3, 3, 3]; + +/** The cells of piece p turned r times, one after another as x, y: eight numbers a turn, thirty-two a piece. */ +const SHAPES: number[] = PIECES.flatMap((cells, p) => + [0, 1, 2, 3].flatMap((r) => { + let turned = cells; + // The O stays as it is; the others turn a quarter clockwise in their box. + for (let i = 0; i < r && p !== 1; i++) turned = turned.map(([x, y]) => [BOX[p] - 1 - y, x] as [number, number]); + return turned.flat(); + }), +); +/** Each piece as it first appears, two rows of four: 1 where it has a cell. For the "next" box. */ +const PREVIEW: number[] = PIECES.flatMap((cells) => [0, 1].flatMap((y) => [0, 1, 2, 3].map((x) => (cells.some(([cx, cy]) => cx === x && cy === y) ? 1 : 0)))); +const POINTS = [0, 40, 100, 300, 1200]; +const ROWS = [...Array(H).keys()]; +const COLS = [...Array(W).keys()]; + +program(async () => { + const board: number[] = new Array(W * H).fill(0); + let piece = random(7), next = random(7), turn = 0, px = 3, py = 0; + let score = 0, lines = 0, level = 0, over = false, best = 0; + + ui.lines(P1, () => ( + <> + + TETRIS {score} lines {lines} level {level + 1} + {over && GAME OVER - Space to play again} + + {ROWS.map((y) => ( + + |{COLS.map((x) => )}| + {y === 1 && next} + {(y === 2 || y === 3) && [0, 1, 2, 3].map((x) => )} + {y === 5 && best {best}} + {y === 7 && A D move, W turn} + {y === 8 && S down, Space drop} + + ))} + + )); + + /** Whether piece p, turned r times, fits with its box's corner at (x, y): inside the walls, on empty cells. */ + function fits(p: number, r: number, x: number, y: number): boolean { + const at = (p * 4 + r) * 8; + for (let k = 0; k < 4; k++) { + const cx = x + SHAPES[at + k * 2]; + const cy = y + SHAPES[at + k * 2 + 1]; + if (cx < 0 || cx >= W || cy >= H) return false; + if (cy >= 0 && board[cy * W + cx] !== 0) return false; + } + return true; + } + + /** The falling piece written into the board in a colour (0 takes it out again). */ + function place(color: number) { + const at = (piece * 4 + turn) * 8; + for (let k = 0; k < 4; k++) { + const cy = py + SHAPES[at + k * 2 + 1]; + if (cy >= 0) board[cy * W + px + SHAPES[at + k * 2]] = color; + } + } + + /** The piece moved and turned, if it fits there. */ + function move(dx: number, dy: number, dr: number): boolean { + place(0); + const r = (turn + dr) % 4; + const ok = fits(piece, r, px + dx, py + dy); + if (ok) { px += dx; py += dy; turn = r; } + place(COLORS[piece]); + return ok; + } + + /** Full rows taken out, the rows above them moved down. How many there were. */ + function clearRows(): number { + let cleared = 0; + let y = H - 1; + while (y >= 0) { + let full = true; + for (let x = 0; x < W; x++) if (board[y * W + x] === 0) full = false; + if (full) { + for (let yy = y; yy > 0; yy--) for (let x = 0; x < W; x++) board[yy * W + x] = board[(yy - 1) * W + x]; + for (let x = 0; x < W; x++) board[x] = 0; + cleared++; + } else y--; + } + return cleared; + } + + /** The next piece at the top; the game is over when it does not fit there. */ + function spawn() { + piece = next; + next = random(7); + turn = 0; + px = 3; + py = 0; + if (fits(piece, turn, px, py)) place(COLORS[piece]); + else { over = true; if (score > best) best = score; } + } + + /** The piece stays where it is; full rows go, and they score. */ + function land() { + const n = clearRows(); + lines += n; + score += POINTS[n] * (level + 1); + level = Math.min(9, Math.floor(lines / 5)); + spawn(); + } + + spawn(); + let wait = 0; + while (true) { + if (over) { + if (keyPressed(P1, "Space")) { + board.fill(0); + score = 0; lines = 0; level = 0; over = false; + spawn(); + } + } else { + if (keyPressed(P1, "A")) move(-1, 0, 0); + if (keyPressed(P1, "D")) move(1, 0, 0); + // A turn against a wall tries one step in, each way. + if (keyPressed(P1, "W") && !move(0, 0, 1) && !move(-1, 0, 1)) move(1, 0, 1); + if (keyPressed(P1, "Space")) { while (move(0, 1, 0)) { /* down to the floor */ } land(); wait = 0; } + wait++; + if (keyPressed(P1, "S") || wait >= 24 - level * 2) { + wait = 0; + if (!move(0, 1, 0)) land(); + } + } + await sleep(frames(1)); + } +}, { owner: P1 }); From 60de2864bf1c0225ea023a5c33f5e9de70b5f76f Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 20:25:05 -0600 Subject: [PATCH 042/238] TrigScript: && and || stop at the first item that settles them, in the game too The Python lowering worked every item of an and / or out before testing any, so a call to the right of one that settled it still ran: keyPressed(P1, "W") && !move(0, 0, 1) turned the Tetris piece every frame in the game, never in the simulator. An item after the first that calls, pops or draws a random number is now worked out inside the test of those before it. probes/game-short-circuit.ts fails on the old lowering in the lifted game and passes now. Co-Authored-By: Claude Opus 5.5 --- compiler/generated/trigscriptPy.ts | 2 +- docs/v4-plan.md | 13 +++++++--- probes/game-short-circuit.ts | 27 ++++++++++++++++++++ python/trigscript.py | 41 +++++++++++++++++++++++++++--- 4 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 probes/game-short-circuit.ts diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index 3781857..fea1fb5 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 17\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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\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 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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 == \"and\":\n return EUDAnd(*[self.cond(c) for c in e[\"items\"]])\n if k == \"or\":\n return EUDOr(*[self.cond(c) for c in e[\"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 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 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 EUDJumpIf(self.again >= 1, sweep)\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 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_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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 17\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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\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 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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 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 EUDJumpIf(self.again >= 1, sweep)\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 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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\n"; diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 5a377e3..5501a01 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1116,11 +1116,16 @@ wait. and sets the flag after every statement anywhere in the program that writes it. A view that reads the game is built every frame. It gives back the Task (`board.cancel()`). - **Measured in the lifted game:** a static 10×10 board costs ~360 actions a frame when nothing - changed, ~63k on a frame it is built and printed. Tetris (`probes/tetris.tsx`): ~80k on a frame - the board changed; a headless run reads the keys as held, so every frame changed there. Nothing - Remastered refuses. + changed, ~63k on a frame it is built and printed. Nothing Remastered refuses. - **Found on the way:** the freeze check called `while (move(0, 1, 0)) {}` endless: a call in a - loop's condition that writes what is outside it now counts as changing it. + loop's condition that writes what is outside it now counts as changing it. And, played + (the user held D and the piece "moved in place"): **the Python lowering did not + short-circuit `&&` / `||`** — every item's code ran before any was tested, so + `keyPressed(P1, "W") && !move(0, 0, 1)` turned the piece every frame in the game and never + in the simulator. Fixed (an item after the first that calls, pops or draws a random number is + worked out inside the test of those before it); `probes/game-short-circuit.ts` fails on the + old lowering in the lifted game and passes now. With it fixed, Tetris costs ~645 actions on + a frame nothing moves, ~77k on one the board changes, 7.5k a frame on average. - **Not yet:** components (``: call a function in braces instead), `Array.from`, the per-cell cost (~600 actions: the colour byte and the glyph are copied in; a cell could be one dword), the editor's workspace knowing `.tsx`, guide and README (wait for 4.0). diff --git a/probes/game-short-circuit.ts b/probes/game-short-circuit.ts new file mode 100644 index 0000000..280ae50 --- /dev/null +++ b/probes/game-short-circuit.ts @@ -0,0 +1,27 @@ +// && and || stop at the first item that settles them, in the lifted game as in JavaScript: a call to the right of one +// that settled it never runs. Found in the Tetris showpiece (TrigScript 4, S2), where `pressed && !move(…)` moved the +// piece every frame in the game and never in the simulator. +// npx tsx scripts/test-game.mts probes/game-short-circuit.ts +// A program first runs on frame 2. Built like a probe (Player 1 Terran, Player 2 a Zerg computer). +import { test, expect, program, setResources, sleep, frames, P1 } from "trigscript"; + +program(async () => { + let calls = 0; + let yes = 0; + function bump(answer: boolean): boolean { calls++; return answer; } + let off = false; + let on = true; + await sleep(frames(1)); + if (off && bump(true)) yes += 1; // bump does not run + if (on || bump(true)) yes += 10; // bump does not run + if (on && bump(false) && bump(true)) yes += 100; // the first bump runs, the second does not + if (off || bump(true) || bump(true)) yes += 1000; // the first bump runs, the second does not + setResources(P1, "set", calls, "ore"); + setResources(P1, "set", yes, "gas"); +}, { owner: P1 }); + +test("a call to the right of && or || runs only when what is before it did not settle it", (sim) => { + sim.frames(5); + expect(sim.resources(P1).ore).toBe(2); + expect(sim.resources(P1).gas).toBe(1010); +}); diff --git a/python/trigscript.py b/python/trigscript.py index 161a9ee..72710e6 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -1538,10 +1538,15 @@ def cond(self, e): # One comparison, built once: a comparison between variables writes into its own # condition, and one that is built and dropped is an orphan eudplib refuses. return self.ordered(a, op, b, unsigned) - if k == "and": - return EUDAnd(*[self.cond(c) for c in e["items"]]) - if k == "or": - return EUDOr(*[self.cond(c) for c in e["items"]]) + if k in ("and", "or"): + items = e["items"] + # JavaScript stops at the first item that settles it: an item after it that runs something — a call, a + # pop, a random number — must not run. Each such item is worked out inside the test of the ones before. + if any(has_effect(c) for c in items[1:]): + t = fresh(0) + self.short_circuit(items, k == "and", t) + return t >= 1 + return (EUDAnd if k == "and" else EUDOr)(*[self.cond(c) for c in items]) if k == "not": return EUDNot(self.cond(e["expr"])) if k == "random": @@ -1566,6 +1571,23 @@ def cond(self, e): return as_var(self.text_truth(e)) >= 1 raise Fail("trigscript: unknown condition %r%s" % (k, where(e))) + def short_circuit(self, items, conj, t): + """`a && b && …` (conj) or `a || b || …` into t, each item worked out only when those before it did not settle it.""" + if not items: + if conj: + t << 1 + return + if conj: + if EUDIf()(self.cond(items[0])): + self.short_circuit(items[1:], conj, t) + EUDEndIf() + else: + if EUDIf()(self.cond(items[0])): + t << 1 + if EUDElse()(): + self.short_circuit(items[1:], conj, t) + EUDEndIf() + def truth(self, e): """A boolean expression as 0 / 1.""" if e["kind"] == "const": @@ -3211,6 +3233,17 @@ def each_start(node, visit): each_start(v, visit) +def has_effect(node): + """Whether working out a piece of the IR does something beyond reading: a call, a pop, a random number.""" + if isinstance(node, list): + return any(has_effect(x) for x in node) + if not isinstance(node, dict): + return False + if node.get("kind") in ("pop", "random", "randomInt") or (node.get("kind") in ("call", "textCall") and isinstance(node.get("call"), dict)): + return True + return any(has_effect(v) for v in node.values() if isinstance(v, (dict, list))) + + def has_call(node): """Whether a call is anywhere in a piece of the IR.""" if isinstance(node, list): From b09f04d0b6f3c5f2eac4b8e6a4d8c82b0b4e0212 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 20:35:03 -0600 Subject: [PATCH 043/238] TrigScript: keyPressed() is once a tap, not every frame the key is down MSQC's KeyPress is true on every frame a key is down, and a tap lasts two or three frames: one tap of W turned the Tetris piece two or three times, so it often looked as if it had not turned. The build asks MSQC for KeyDown, the frame the key goes down, and the simulator treats a key down on consecutive frames as one press, as the game does. probes/game-key-tap.ts fails on the old build in the lifted game. Co-Authored-By: Claude Opus 5.5 --- compiler/input.ts | 4 +++- compiler/simulateIr.ts | 7 +++++++ docs/ir.md | 2 +- probes/game-key-tap.ts | 23 +++++++++++++++++++++++ tests/input.test.ts | 2 +- 5 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 probes/game-key-tap.ts diff --git a/compiler/input.ts b/compiler/input.ts index f7608d1..45d3119 100644 --- a/compiler/input.ts +++ b/compiler/input.ts @@ -331,7 +331,9 @@ export function inputPlugins(plan: InputPlan): { before: Record> = {}; if (plan.chats.length) before.chatEvent = { __addr__: INPUT_NAMES.heard, __ptrAddr__: INPUT_NAMES.pointer, __lenAddr__: INPUT_NAMES.length, __patternAddr__: INPUT_NAMES.pattern }; const msqc: Record = { QCUnit: QC_UNIT, QCLoc: plan.qcLocation, QCPlayer: QC_PLAYER, QCDebug: "false" }; - plan.keys.forEach((key, i) => { msqc[`KeyPress(${msqcKey(key)}); NotTyping`] = `${INPUT_NAMES.key(i)}, 1`; }); + // KeyDown, not KeyPress: MSQC's KeyPress is true on every frame the key is down, and a tap is two or three frames — + // one tap of W turned the Tetris piece two or three times (TrigScript 4, S2, played). KeyDown is the frame it goes down. + plan.keys.forEach((key, i) => { msqc[`KeyDown(${msqcKey(key)}); NotTyping`] = `${INPUT_NAMES.key(i)}, 1`; }); plan.buttons.forEach((b, i) => { msqc[`MouseDown(${MSQC_BUTTON[b]})`] = `${INPUT_NAMES.button(i)}, 1`; }); if (plan.mouseBase !== null) msqc.Mouse = plan.mouseBase; if (plan.chats.length) { diff --git a/compiler/simulateIr.ts b/compiler/simulateIr.ts index dd7830c..370ce12 100644 --- a/compiler/simulateIr.ts +++ b/compiler/simulateIr.ts @@ -1288,6 +1288,8 @@ export class ProgramSimulation { /** What the players did, for the next frame and for the one running: "key:0:F2", "click:0:left", and a typed line as the pattern it matched with its values. */ private queued: { events: Set; lines: Map } = { events: new Set(), lines: new Map() }; private current = this.queued; + /** The keys and buttons down on the last frame. */ + private down = new Set(); /** Where each player's mouse is, in map pixels. */ readonly mice = new Map(); cycle = 0; @@ -1549,6 +1551,11 @@ export class ProgramSimulation { // What was done since the last frame is what this one finds, and only this one. this.current = this.queued; this.queued = { events: new Set(), lines: new Map() }; + // A key or a button down on this frame and the last is held, not pressed again: the game reports the frame it goes + // down (MSQC's KeyDown / MouseDown), however many frames a tap lasts. + const down = new Set([...this.current.events].filter((e) => e.startsWith("key:") || e.startsWith("click:"))); + for (const e of down) if (this.down.has(e)) this.current.events.delete(e); + this.down = down; for (const run of this.runs) { this.world.current = run.player; try { run.tick(); } catch (err) { if (err instanceof Halt) throw new Error(err.message); throw err; } diff --git a/docs/ir.md b/docs/ir.md index 6d42761..77d2927 100644 --- a/docs/ir.md +++ b/docs/ir.md @@ -597,7 +597,7 @@ The build's plugin sections, in the order they run (`buildPlugins`): **chatEvent messages of its own — it only finds the line the local player typed and leaves its address and length; **trigscript**, which before the triggers matches that line against the patterns on the computer it was typed on, into a number for the pattern and up to three values; -**MSQC**, which sends those, the keys (`KeyPress(K); NotTyping`) and the clicks +**MSQC**, which sends those, the keys (`KeyDown(K); NotTyping`: the frame a key goes down; `KeyPress` is every frame it is down, and a tap is two or three) and the clicks (`MouseDown(B)`) to every computer as the player they came from, and keeps each human's mouse in a location; **eudTurbo**. Every cell is an `EUDArray(12)` or an `EUDVariable` the lowering registers by name (`tsin_key0`, `tsin_chat_in`, …), which is how the other two diff --git a/probes/game-key-tap.ts b/probes/game-key-tap.ts new file mode 100644 index 0000000..6d40fc6 --- /dev/null +++ b/probes/game-key-tap.ts @@ -0,0 +1,23 @@ +// keyPressed() is true on the frame a key goes down, once a tap, however many frames the key stays down. Found in the +// Tetris showpiece (TrigScript 4, S2): MSQC's KeyPress is every frame the key is down, and one tap of W turned the piece +// two or three times; the build now asks MSQC for KeyDown. +// npx tsx scripts/test-game.mts probes/game-key-tap.ts +// A program first runs on frame 2. Built like a probe (Player 1 Terran, Player 2 a Zerg computer). +import { test, expect, program, setResources, keyPressed, sleep, frames, P1 } from "trigscript"; + +program(async () => { + let presses = 0; + while (true) { + if (keyPressed(P1, "W")) { presses++; setResources(P1, "set", presses, "ore"); } + await sleep(frames(1)); + } +}, { owner: P1 }); + +test("a key held down three frames is one press; let go and pressed again, another", (sim) => { + sim.frames(4); + for (let i = 0; i < 3; i++) sim.press("W").frames(1); + sim.frames(4); + expect(sim.resources(P1).ore).toBe(1); + sim.press("W").frames(5); + expect(sim.resources(P1).ore).toBe(2); +}); diff --git a/tests/input.test.ts b/tests/input.test.ts index f673031..f821689 100644 --- a/tests/input.test.ts +++ b/tests/input.test.ts @@ -142,7 +142,7 @@ describe("input: the plan and the build's plugins", () => { expect(r.input).toEqual({ keys: ["A", "Escape"], buttons: ["left"], chats: [], qcLocation: 62, mouseBase: null }); const plugins = buildPlugins(r.input, "/ir.json"); expect(Object.keys(plugins)).toEqual(["trigscript", "MSQC", "eudTurbo"]); - expect(plugins.MSQC).toEqual({ QCUnit: 58, QCLoc: 62, QCPlayer: 11, QCDebug: "false", "KeyPress(A); NotTyping": "tsin_key0, 1", "KeyPress(ESC); NotTyping": "tsin_key1, 1", "MouseDown(L)": "tsin_button0, 1" }); + expect(plugins.MSQC).toEqual({ QCUnit: 58, QCLoc: 62, QCPlayer: 11, QCDebug: "false", "KeyDown(A); NotTyping": "tsin_key0, 1", "KeyDown(ESC); NotTyping": "tsin_key1, 1", "MouseDown(L)": "tsin_button0, 1" }); }); it("the mouse: eight locations in a row, clear of the map's own and of Anywhere", () => { From bb6b80738818d8aee305da252fcb7b1d449b24aa Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 20:47:30 -0600 Subject: [PATCH 044/238] TrigScript: keyHeld(), and the held-key probe keyHeld(player, key) is true on every frame a key is down: MSQC's KeyPress, sent each such frame into cells of its own (tsin_held). In the lifted game a hold of n frames is read on n - 1 of them (the first two frames' messages arrive together); probes/game-key-held.ts checks it there and in the simulator. probes/key-held.tsx draws each key's last 32 frames as a strip, to play in Remastered: whether a held key reaches the game the whole time, and stops when it is let go. Co-Authored-By: Claude Opus 5.5 --- compiler/declarations.ts | 5 +++ compiler/generated/trigscriptPy.ts | 2 +- compiler/input.ts | 15 ++++++--- compiler/runtime.ts | 12 ++++--- compiler/simulateIr.ts | 6 ++-- compiler/structured.ts | 2 +- probes/game-key-held.ts | 26 +++++++++++++++ probes/key-held.tsx | 52 ++++++++++++++++++++++++++++++ python/trigscript.py | 4 +++ tests/input.test.ts | 14 ++++++++ 10 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 probes/game-key-held.ts create mode 100644 probes/key-held.tsx diff --git a/compiler/declarations.ts b/compiler/declarations.ts index 40b34ad..d6ca76e 100644 --- a/compiler/declarations.ts +++ b/compiler/declarations.ts @@ -440,6 +440,11 @@ ${kw}const ui: { */ /** True on the frame a player's press of a key arrives. Not while the player is typing a message. */ ${kw}function keyPressed(player: Player, key: Key): boolean; +/** + * True on every frame a key is down — for moving while it is held. It reaches every player's computer + * in step, a few frames after it happens, as a press does. Not while the player is typing a message. + */ +${kw}function keyHeld(player: Player, key: Key): boolean; /** True on the frame a player's press of a mouse button arrives ("left" when none is named). */ ${kw}function clicked(player: Player, button?: "left" | "right" | "middle"): boolean; /** Where a player's mouse is on the map, in pixels (32 to a tile): \`const at = mouse(CurrentPlayer);\` keeps the place as it is now. */ diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index fea1fb5..786420a 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 17\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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\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 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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 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 EUDJumpIf(self.again >= 1, sweep)\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 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 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 op == \">\":\n return av > b\n return av >= b\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.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.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 17\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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\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 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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 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 EUDJumpIf(self.again >= 1, sweep)\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 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 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 op == \">\":\n return av > b\n return av >= b\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 self.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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_cells, 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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\n"; diff --git a/compiler/input.ts b/compiler/input.ts index 45d3119..eeae381 100644 --- a/compiler/input.ts +++ b/compiler/input.ts @@ -167,8 +167,8 @@ export function matchChat(p: ChatPattern, line: string, unitByName: (lower: stri /** One thing a program reads from the players; `player` is a slot, or 13 for the current player. */ export type InputSource = - /** 1 on the frame the key's press arrives. */ - | { source: "key"; key: string; player: number } + /** 1 on the frame the key's press arrives; `held`, on every frame it is down. */ + | { source: "key"; key: string; player: number; held?: boolean } /** 1 on the frame the button's press arrives. */ | { source: "click"; button: MouseButton; player: number } /** Where the player's mouse is on the map, in pixels. */ @@ -179,6 +179,8 @@ export type InputSource = /** Everything the input plugins are set up from, written into the IR file beside the programs. */ export interface InputPlan { keys: string[]; + /** The keys a program asks are held, each read on every frame it is down (MSQC's KeyPress). */ + held?: string[]; buttons: MouseButton[]; chats: ChatPattern[]; /** Unit names a `{…:unit}` capture knows, lower case, with the type each means; only when one is used. */ @@ -277,10 +279,12 @@ export function inputPlan(programs: Program[], locations: NameTable, units: Name const { sources, mouse } = inputsOf(programs); if (sources.length === 0 && !mouse) return null; const keys: string[] = []; + const held: string[] = []; const buttons: MouseButton[] = []; const chats: ChatPattern[] = []; for (const s of sources) { - if (s.source === "key" && !keys.includes(s.key)) keys.push(s.key); + if (s.source === "key" && s.held) { if (!held.includes(s.key)) held.push(s.key); } + else if (s.source === "key" && !keys.includes(s.key)) keys.push(s.key); else if (s.source === "click" && !buttons.includes(s.button)) buttons.push(s.button); else if (s.source === "chat" && !chats.some((c) => c.pattern === s.pattern)) chats.push(parseChatPattern(s.pattern)); } @@ -299,7 +303,7 @@ export function inputPlan(programs: Program[], locations: NameTable, units: Name } if (mouseBase === null) throw new Error("Reading the mouse needs eight free locations in a row among the map's first 63, one per player, besides one more for the plugin that carries input; this map has no such run."); } - const plan: InputPlan = { keys, buttons, chats, qcLocation, mouseBase }; + const plan: InputPlan = { keys, ...(held.length ? { held } : {}), buttons, chats, qcLocation, mouseBase }; if (chats.some((c) => c.captures.some((x) => x.kind === "unit"))) { const seen = new Set(); plan.unitNames = []; @@ -314,6 +318,7 @@ export function inputPlan(programs: Program[], locations: NameTable, units: Name /** The names the lowering registers its cells under, which is how MSQC's and chatEvent's settings reach them. */ export const INPUT_NAMES = { key: (i: number) => `tsin_key${i}`, + held: (i: number) => `tsin_held${i}`, button: (i: number) => `tsin_button${i}`, chatLocal: "tsin_chat", chatIn: "tsin_chat_in", @@ -334,6 +339,8 @@ export function inputPlugins(plan: InputPlan): { before: Record { msqc[`KeyDown(${msqcKey(key)}); NotTyping`] = `${INPUT_NAMES.key(i)}, 1`; }); + // keyHeld(): every frame the key is down on the player's computer, sent each such frame. + plan.held?.forEach((key, i) => { msqc[`KeyPress(${msqcKey(key)}); NotTyping`] = `${INPUT_NAMES.held(i)}, 1`; }); plan.buttons.forEach((b, i) => { msqc[`MouseDown(${MSQC_BUTTON[b]})`] = `${INPUT_NAMES.button(i)}, 1`; }); if (plan.mouseBase !== null) msqc.Mouse = plan.mouseBase; if (plan.chats.length) { diff --git a/compiler/runtime.ts b/compiler/runtime.ts index f86d7a4..120915c 100644 --- a/compiler/runtime.ts +++ b/compiler/runtime.ts @@ -148,7 +148,7 @@ export const READER_NAMES = ["minerals", "gas", "resources", "countUnits", "kill /** The library's functions about units on the map and the game's tables: the game's too, never computed when the script is built. */ export const UNIT_CALL_NAMES = ["unitsAt", "unitsOf", "allUnits", "first", "nearest", "randomUnit", "underMouse", "stats"] as const; /** The library's functions about what the players do, and the one that moves a location to a point: the game's as well. */ -export const INPUT_CALL_NAMES = ["keyPressed", "clicked", "mouse", "chatted", "centerLocation"] as const; +export const INPUT_CALL_NAMES = ["keyPressed", "keyHeld", "clicked", "mouse", "chatted", "centerLocation"] as const; /** * What the transformer turns `program(() => { … })` — and the arrow of `game(…)` — into: @@ -552,13 +552,15 @@ export function createRuntime(names: ScriptNames, collector: Collector, options: if (collector.running) throw new ScriptError(`${what.split(":")[0]}() asks what a player does while the game runs: use it inside program().`); return onePlayer(v, what, 8); }; - rt.keyPressed = (player: unknown, key: unknown) => { + const keyRead = (what: "keyPressed" | "keyHeld") => (player: unknown, key: unknown) => { const name = typeof key === "string" ? keyName(key) : null; const deaf = typeof key === "string" ? DEAF_KEYS[key.trim().toUpperCase()] : undefined; - if (deaf) throw new ScriptError(`keyPressed: ${deaf}.`); - if (!name) throw new ScriptError(`keyPressed: the key is one of ${KEY_NAMES.slice(0, 3).map((k) => JSON.stringify(k)).join(", ")} … "F1" … "Space", "Enter", "Escape", "Left" …; got ${describe(key)}.`); - return inputValue(`keyPressed(…, ${JSON.stringify(name)})`, { source: "key", key: name, player: human(player, "keyPressed: player") }, true); + if (deaf) throw new ScriptError(`${what}: ${deaf}.`); + if (!name) throw new ScriptError(`${what}: the key is one of ${KEY_NAMES.slice(0, 3).map((k) => JSON.stringify(k)).join(", ")} … "F1" … "Space", "Enter", "Escape", "Left" …; got ${describe(key)}.`); + return inputValue(`${what}(…, ${JSON.stringify(name)})`, { source: "key", key: name, player: human(player, `${what}: player`), ...(what === "keyHeld" ? { held: true } : {}) }, true); }; + rt.keyPressed = keyRead("keyPressed"); + rt.keyHeld = keyRead("keyHeld"); rt.clicked = (player: unknown, button: unknown = "left") => { const b = typeof button === "string" ? button.trim().toLowerCase() : ""; if (!(MOUSE_BUTTONS as readonly string[]).includes(b)) throw new ScriptError(`clicked: the button is "left", "right" or "middle", got ${describe(button)}.`); diff --git a/compiler/simulateIr.ts b/compiler/simulateIr.ts index 370ce12..ef508ac 100644 --- a/compiler/simulateIr.ts +++ b/compiler/simulateIr.ts @@ -1288,8 +1288,9 @@ export class ProgramSimulation { /** What the players did, for the next frame and for the one running: "key:0:F2", "click:0:left", and a typed line as the pattern it matched with its values. */ private queued: { events: Set; lines: Map } = { events: new Set(), lines: new Map() }; private current = this.queued; - /** The keys and buttons down on the last frame. */ + /** The keys and buttons down on the last frame, and on this one (what keyHeld() finds). */ private down = new Set(); + private downNow = new Set(); /** Where each player's mouse is, in map pixels. */ readonly mice = new Map(); cycle = 0; @@ -1434,7 +1435,7 @@ export class ProgramSimulation { input(i: InputSource): number { const p = this.slotOf(i.player); switch (i.source) { - case "key": return this.current.events.has(`key:${p}:${i.key}`) ? 1 : 0; + case "key": return (i.held ? this.downNow : this.current.events).has(`key:${p}:${i.key}`) ? 1 : 0; case "click": return this.current.events.has(`click:${p}:${i.button}`) ? 1 : 0; case "mouse": return this.mice.get(p)?.[i.axis] ?? 0; case "chat": { @@ -1556,6 +1557,7 @@ export class ProgramSimulation { const down = new Set([...this.current.events].filter((e) => e.startsWith("key:") || e.startsWith("click:"))); for (const e of down) if (this.down.has(e)) this.current.events.delete(e); this.down = down; + this.downNow = down; for (const run of this.runs) { this.world.current = run.player; try { run.tick(); } catch (err) { if (err instanceof Halt) throw new Error(err.message); throw err; } diff --git a/compiler/structured.ts b/compiler/structured.ts index f340104..bb6b648 100644 --- a/compiler/structured.ts +++ b/compiler/structured.ts @@ -4854,7 +4854,7 @@ export class Structured { if (this.isLibraryCall(e, "shared")) { this.c.error(e, "shared() goes on a declaration: let total = shared(0)."); return; } if (this.isLibraryCall(e, "print")) { this.printStatement(e); return; } if (this.isLibraryCall(e, "centerLocation")) { this.centerLocation(e); return; } - if (this.isLibraryCall(e, "keyPressed") || this.isLibraryCall(e, "clicked") || this.isLibraryCall(e, "mouse") || this.isLibraryCall(e, "chatted")) { this.c.error(e, "This asks what a player did and does nothing on its own: test it in an if, or keep it in a const."); return; } + if (this.isLibraryCall(e, "keyPressed") || this.isLibraryCall(e, "keyHeld") || this.isLibraryCall(e, "clicked") || this.isLibraryCall(e, "mouse") || this.isLibraryCall(e, "chatted")) { this.c.error(e, "This asks what a player did and does nothing on its own: test it in an if, or keep it in a const."); return; } const callee = this.evaluate(e.expression)?.value; if (isGameFunction(callee)) { const call = this.gameCall(e, callee); if (call) this.emit({ kind: "call", call, at: call.at, label: call.label }, e); return; } if (isReader(callee) || (isBuilder(callee) && callee.kind === "condition" && READ_ARITY.get(callee.ident) === e.arguments.length)) { this.c.error(e, "This reads a value and does nothing on its own: assign it to a variable, or compare it in an if."); return; } diff --git a/probes/game-key-held.ts b/probes/game-key-held.ts new file mode 100644 index 0000000..6e7d158 --- /dev/null +++ b/probes/game-key-held.ts @@ -0,0 +1,26 @@ +// keyHeld() is true on every frame a key is down, and a press is still one, in the lifted game as in the simulator. +// npx tsx scripts/test-game.mts probes/game-key-held.ts +// A program first runs on frame 2. Built like a probe (Player 1 Terran, Player 2 a Zerg computer). +import { test, expect, program, setResources, keyHeld, keyPressed, sleep, frames, P1 } from "trigscript"; + +program(async () => { + let held = 0, presses = 0; + while (true) { + if (keyHeld(P1, "D")) held++; + if (keyPressed(P1, "D")) presses++; + setResources(P1, "set", held, "ore"); + setResources(P1, "set", presses, "gas"); + await sleep(frames(1)); + } +}, { owner: P1 }); + +// In the lifted game the first two frames' messages arrive together (MSQC), so a hold of n frames is read on n − 1 of +// them there — 1 → 1, 3 → 2, 10 → 9 — and on n in the simulator. Either way it is held from the second frame to the end. +test("a key down ten frames is held on (nearly) every one of them and pressed once", (sim) => { + sim.frames(4); + for (let i = 0; i < 10; i++) sim.press("D").frames(1); + sim.frames(8); + expect(sim.resources(P1).ore).toBeGreaterThanOrEqual(9); + expect(sim.resources(P1).ore).toBeLessThanOrEqual(10); + expect(sim.resources(P1).gas).toBe(1); +}); diff --git a/probes/key-held.tsx b/probes/key-held.tsx new file mode 100644 index 0000000..e3a165e --- /dev/null +++ b/probes/key-held.tsx @@ -0,0 +1,52 @@ +// The held-key probe (TrigScript 4, S2 → S3): does a key held down reach the game on every frame it is down, and stop +// when it is let go? MSQC's KeyPress is the key's state on the player's own computer, sent each frame it is down; the +// tap bug found in Tetris says it is true for every frame of a tap. Magenta probe 7 read "one event a press, no key-up". +// This draws each key's last 32 frames as a strip, newest on the right: ■ held, □ not. +// Host it as a multiplayer game, alone. +// npx tsx scripts/build-fixture.mts probes/key-held.tsx fixtures/eud/key-held.scx +import { program, P1, keyHeld, keyPressed, sleep, frames, ui, textColors } from "trigscript"; + +const KEYS = ["W", "A", "S", "D", "Space"] as const; +const COLORS = [textColors.cyan, textColors.green, textColors.yellow, textColors.orange, textColors.purple]; +const SPAN = 32; +const PLACES = [...Array(SPAN).keys()]; + +program(async () => { + const strip: number[] = new Array(KEYS.length * SPAN).fill(0); + const now: number[] = new Array(KEYS.length).fill(0); + const longest: number[] = new Array(KEYS.length).fill(0); + const taps: number[] = new Array(KEYS.length).fill(0); + const releases: number[] = new Array(KEYS.length).fill(0); + let head = 0; + let frame = 0; + + ui.lines(P1, () => ( + <> + KEY HELD PROBE frame {frame} + {KEYS.map((name, k) => ( + + {name.padEnd(5)} {PLACES.map((i) => )} + now {now[k]} longest {longest[k]} taps {taps[k]} let go {releases[k]} + + ))} + 1 tap each key: a short mark, taps +1 + 2 hold W 3 seconds: a solid strip, longest near 72 + 3 let go: does the strip end at once? let go +1 + 4 Enter, type wasd, Enter: nothing should mark + 5 hold A and D together: both strips? + + )); + + while (true) { + frame++; + head = (head + 1) % SPAN; + KEYS.forEach((name, k) => { + const held = keyHeld(P1, name); + strip[k * SPAN + head] = held ? COLORS[k] : 0; + if (held) { now[k]++; if (now[k] > longest[k]) longest[k] = now[k]; } + else { if (now[k] > 0) releases[k]++; now[k] = 0; } + if (keyPressed(P1, name)) taps[k]++; + }); + await sleep(frames(1)); + } +}, { owner: P1 }); diff --git a/python/trigscript.py b/python/trigscript.py index 72710e6..9f1e92d 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -3373,6 +3373,7 @@ class Input: def __init__(self, plan): self.plan = plan or {} self.keys = list(self.plan.get("keys", [])) + self.held = list(self.plan.get("held", [])) self.buttons = list(self.plan.get("buttons", [])) self.chats = list(self.plan.get("chats", [])) self.mouse_base = self.plan.get("mouseBase") @@ -3381,6 +3382,7 @@ def __init__(self, plan): if plan and not self.humans: raise Fail("trigscript: a program reads keys, clicks, the mouse or chat, and the map has no human player to give any") self.key_cells = [self.register("tsin_key%d" % i, EUDArray(12)) for i in range(len(self.keys))] + self.held_cells = [self.register("tsin_held%d" % i, EUDArray(12)) for i in range(len(self.held))] self.button_cells = [self.register("tsin_button%d" % i, EUDArray(12)) for i in range(len(self.buttons))] self.captures = max([len(c["captures"]) for c in self.chats] or [0]) if self.chats: @@ -3405,6 +3407,8 @@ def register(name, cell): def read(self, i, low, node): source = i.get("source") p = low.one_player(i["player"], node) + if source == "key" and i.get("held"): + return self.cell(self.held_cells, self.held, i["key"], node)[p] if source == "key": return self.cell(self.key_cells, self.keys, i["key"], node)[p] if source == "click": diff --git a/tests/input.test.ts b/tests/input.test.ts index f821689..394fe4b 100644 --- a/tests/input.test.ts +++ b/tests/input.test.ts @@ -145,6 +145,12 @@ describe("input: the plan and the build's plugins", () => { expect(plugins.MSQC).toEqual({ QCUnit: 58, QCLoc: 62, QCPlayer: 11, QCDebug: "false", "KeyDown(A); NotTyping": "tsin_key0, 1", "KeyDown(ESC); NotTyping": "tsin_key1, 1", "MouseDown(L)": "tsin_button0, 1" }); }); + it("keyHeld: MSQC's KeyPress, every frame the key is down, into cells of its own", () => { + const r = compile(`while (true) { if (keyHeld(P1, "D") || keyPressed(P1, "D")) displayText("x"); sleep(frames(1)); }`); + expect(r.input).toEqual({ keys: ["D"], held: ["D"], buttons: [], chats: [], qcLocation: 62, mouseBase: null }); + expect(buildPlugins(r.input, "/ir.json").MSQC).toEqual({ QCUnit: 58, QCLoc: 62, QCPlayer: 11, QCDebug: "false", "KeyDown(D); NotTyping": "tsin_key0, 1", "KeyPress(D); NotTyping": "tsin_held0, 1" }); + }); + it("the mouse: eight locations in a row, clear of the map's own and of Anywhere", () => { const names = scriptNames({ locations: [{ index: 62, name: "Top" }, { index: 58, name: "Gate" }] }); const r = compileFile(`program(() => { while (true) { const u = underMouse(P1); if (u) u.kill(); sleep(frames(1)); } });`, names); @@ -187,6 +193,14 @@ describe("input: the interpreter", () => { expect([sim.value("n"), sim.value("v")]).toEqual([2, 10]); }); + it("a key down on frames in a row is one press, and held on each of them", () => { + const sim = simulate(loop(`if (keyPressed(CurrentPlayer, "D")) n += 1; if (keyHeld(CurrentPlayer, "D")) v += 1;`)); + for (let i = 0; i < 4; i++) sim.press("D").step(); + sim.step(); + sim.press("D").step(); + expect([sim.value("n"), sim.value("v")]).toEqual([2, 5]); + }); + it("another player's key is not this player's", () => { const sim = simulate(loop(`if (keyPressed(P2, "A")) n += 1; if (keyPressed(P1, "A")) v += 1;`)); sim.press("A", 1).step(); From d9ab17afa3b48e7ac7f71e04b98a6370c396804a Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 20:51:21 -0600 Subject: [PATCH 045/238] TrigScript 4: held keys played, keyHeld() works in Remastered Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 5501a01..c6c1427 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1130,6 +1130,13 @@ wait. per-cell cost (~600 actions: the colour byte and the glyph are copied in; a cell could be one dword), the editor's workspace knowing `.tsx`, guide and README (wait for 4.0). +**Held keys, played 2026-10-16** (`probes/key-held.tsx`, Remastered): a key held down reaches +the game on every frame it is down — the strip stays solid — and a tap is two or three frames +of it, counted as one press by `keyPressed` (MSQC's KeyDown since `b09f04d`). So `keyHeld()` +(MSQC's KeyPress, sent each frame) works today, for every player, through the same path as a +press: S3's movement needs no `Local` and no `sync()`. Magenta probe 7's "one event a press, +no key-up" was its own setup; R0's `GetKeyState` reading agrees with what was played. + ### S3. A hero moved with the keys W A S D move a hero, the mouse aims, a key casts. The map plays as an action game. From 45c90d8e733a52bb015022f196b7a88f4c5b46cc Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 21:10:39 -0600 Subject: [PATCH 046/238] TrigScript 4: the unit-pixel probe; Tetris slides when held; centerLocation never hoisted probes/unit-pixels.ts asks whether a board can be drawn with units: owners as colours, vision, which kinds make a clean pixel, a pixel moved by removing and making it, and how many a frame before the game slows. Run in the lifted game first: Overlords land on their cells on any ground; buildings and small items only where the ground takes them; Player 2's (a computer's) is moved by its AI; large air units push each other apart. The probe maps gain a 64 x 64 location, Pixel. Tetris: held A or D slides the piece after a moment, held S keeps dropping (keyHeld). centerLocation() with every argument known at build time was computed when the script was built, and threw: it is a call of the game's now. Co-Authored-By: Claude Opus 5.5 --- compiler/hoist.ts | 2 +- probes/tetris.tsx | 25 ++++++++---- probes/unit-pixels.ts | 86 +++++++++++++++++++++++++++++++++++++++ scripts/build-fixture.mts | 5 +++ tests/input.test.ts | 6 +++ 5 files changed, 116 insertions(+), 8 deletions(-) create mode 100644 probes/unit-pixels.ts diff --git a/compiler/hoist.ts b/compiler/hoist.ts index 77dcdeb..114f518 100644 --- a/compiler/hoist.ts +++ b/compiler/hoist.ts @@ -108,7 +108,7 @@ function owningDeclaration(ts: typeof TS, decl: TS.Declaration): TS.Node { /** The library calls that must not appear inside a program body: they would run at build time, silently. */ const FORBIDDEN_INSIDE = new Set(["trigger", "program", "hyperTriggers", "game"]); /** The library calls the game answers: never hoisted, the structured compiler lowers them. */ -const GAME_CALLS = new Set(["random", "sleep", "until", "start", "rose", "once", "shared", "lines"]); +const GAME_CALLS = new Set(["random", "sleep", "until", "start", "rose", "once", "shared", "lines", "centerLocation"]); /** * Calls that read the game — `minerals(P1)`, and a comparing condition without its comparison * (`deaths(P1, unit)`) — or show text a program fills in (`print`). The call is never a diff --git a/probes/tetris.tsx b/probes/tetris.tsx index 5ab5d30..5cc7b7e 100644 --- a/probes/tetris.tsx +++ b/probes/tetris.tsx @@ -1,10 +1,11 @@ // Tetris in StarCraft's text lines (TrigScript 4, S2's showpiece). The screen is JSX: ui.lines() shows it to Player 1 -// every frame, redrawn from the program's variables. A D move, W turns, S drops a row, Space drops all the way. +// every frame, redrawn from the program's variables. A D move (hold to slide), W turns, S drops (hold to keep +// dropping), Space drops all the way. // The pieces and their four turns are worked out when the script is built, in ordinary JavaScript. // Host it as a multiplayer game, alone: the keys reach the game the way typed lines do. // Built with // npx tsx scripts/build-fixture.mts probes/tetris.tsx fixtures/eud/tetris.scx -import { program, P1, keyPressed, random, sleep, frames, ui, textColors } from "trigscript"; +import { program, P1, keyPressed, keyHeld, random, sleep, frames, ui, textColors } from "trigscript"; const W = 10; const H = 10; @@ -34,6 +35,10 @@ const SHAPES: number[] = PIECES.flatMap((cells, p) => /** Each piece as it first appears, two rows of four: 1 where it has a cell. For the "next" box. */ const PREVIEW: number[] = PIECES.flatMap((cells) => [0, 1].flatMap((y) => [0, 1, 2, 3].map((x) => (cells.some(([cx, cy]) => cx === x && cy === y) ? 1 : 0)))); const POINTS = [0, 40, 100, 300, 1200]; +/** Held, A or D slides after this many frames, then a column every SLIDE frames; S held drops a row every DROP frames. */ +const DELAY = 8; +const SLIDE = 2; +const DROP = 2; const ROWS = [...Array(H).keys()]; const COLS = [...Array(W).keys()]; @@ -54,8 +59,8 @@ program(async () => { {y === 1 && next} {(y === 2 || y === 3) && [0, 1, 2, 3].map((x) => )} {y === 5 && best {best}} - {y === 7 && A D move, W turn} - {y === 8 && S down, Space drop} + {y === 7 && A D move (hold), W turn} + {y === 8 && S down (hold), Space drop} ))} @@ -130,6 +135,8 @@ program(async () => { spawn(); let wait = 0; + // How long A, D and S have been held, in frames. + let left = 0, right = 0, down = 0; while (true) { if (over) { if (keyPressed(P1, "Space")) { @@ -138,13 +145,17 @@ program(async () => { spawn(); } } else { - if (keyPressed(P1, "A")) move(-1, 0, 0); - if (keyPressed(P1, "D")) move(1, 0, 0); + // A press moves at once; held, the piece slides after a moment, as Tetris does. + left = keyHeld(P1, "A") ? left + 1 : 0; + right = keyHeld(P1, "D") ? right + 1 : 0; + down = keyHeld(P1, "S") ? down + 1 : 0; + if (keyPressed(P1, "A") || (left > DELAY && left % SLIDE === 0)) move(-1, 0, 0); + if (keyPressed(P1, "D") || (right > DELAY && right % SLIDE === 0)) move(1, 0, 0); // A turn against a wall tries one step in, each way. if (keyPressed(P1, "W") && !move(0, 0, 1) && !move(-1, 0, 1)) move(1, 0, 1); if (keyPressed(P1, "Space")) { while (move(0, 1, 0)) { /* down to the floor */ } land(); wait = 0; } wait++; - if (keyPressed(P1, "S") || wait >= 24 - level * 2) { + if (keyPressed(P1, "S") || (down > 0 && down % DROP === 0) || wait >= 24 - level * 2) { wait = 0; if (!move(0, 1, 0)) land(); } diff --git a/probes/unit-pixels.ts b/probes/unit-pixels.ts new file mode 100644 index 0000000..68c7c8f --- /dev/null +++ b/probes/unit-pixels.ts @@ -0,0 +1,86 @@ +// The unit-pixel probe (TrigScript 4, S2 → a Tetris drawn with units): can a script draw on the minimap and the map +// with units for pixels? A unit's position can never be written (Magenta probe 6), but a location can be centred on any +// point and a unit made or taken away there; Create Unit puts it inside the location, so the location ("Pixel") is one +// cell, 64 × 64. A pixel's colour is its owner's. +// +// Run in the lifted game first: an Overlord lands on its cell on any ground (168 of 168), a building only where the +// ground takes one — (2)Binary Burghs is a patchwork near the base — and small items lose a few cells too. The lifted +// game never makes a Map Revealer (a gap of its own; Remastered does), so step B is the game's to answer. +// +// What to look for, by the second (each step says so in the middle of the screen too): +// A 2 a row of Overlords, one for each player P1 … P8 and P12: which colours show on the minimap without vision? +// (Player 2 is a computer and may move its own; Player 12's was not made in the lifted game.) +// B 12 Map Revealers of yours over the board: which show now, on the minimap and on the screen? +// C 22 rows of other kinds, one for each of P1 … P8: Science Vessel, Pylon, Psi Emitter, Data Disk, Flag. +// Which make a clean square on the minimap? Which look good on the screen? Any missing, or not on its cell? +// (In the lifted game: Vessels zigzag, Pylons and small items miss cells where the ground will not take them.) +// D 40 two pixels fall down the right side, an Overlord and a Psi Emitter: smooth? Anything left behind? +// E 58 10, 20, 40 then 80 Overlords made and taken away every frame, to the right: when does the game slow or stutter? +// Host it as a multiplayer game, alone, or single player: nothing here reads the keys. +// npx tsx scripts/build-fixture.mts probes/unit-pixels.ts fixtures/eud/unit-pixels.scx +// npx tsx scripts/test-game.mts probes/unit-pixels.ts (where each kind landed, in the lifted game) +import { test, expect, program, print, createUnit, removeUnitAt, centerLocation, centerView, sleep, seconds, frames, units, players, P1, P2, P3, P4, P5, P6, P7, P8, type Location } from "trigscript"; + +const PIXEL = 3 as Location; +/** The board's corner, beside Player 1's base on (2)Binary Burghs, and its cells. */ +const OX = 672, OY = 2144, CELL = 64; +const OWNERS = [P1, P2, P3, P4, P5, P6, P7, P8]; +const KINDS = [units.TerranScienceVessel, units.ProtossPylon, units.PsiEmitter, units.DataDisk, units.Flag]; + +program(async () => { + /** The pixel location on the middle of cell (x, y). */ + function at(x: number, y: number) { centerLocation(PIXEL, OX + x * CELL + CELL / 2, OY + y * CELL + CELL / 2); } + + await sleep(seconds(1)); + at(5, 4); + centerView(PIXEL); + print("A: a row of nine Overlords, one for each player (P1 to P8, then P12). On the minimap: which colours show?", { position: "center" }); + OWNERS.forEach((p, i) => { at(i, 0); createUnit(p, units.ZergOverlord, 1, PIXEL); }); + at(8, 0); + createUnit(players.P12, units.ZergOverlord, 1, PIXEL); + + await sleep(seconds(10)); + print("B: now with Map Revealers of yours over the board. Which colours show on the minimap now? On the screen?", { position: "center" }); + for (let y = 0; y < 14; y += 3) for (let x = 0; x < 24; x += 3) { at(x, y); createUnit(P1, units.MapRevealer, 1, PIXEL); } + + await sleep(seconds(10)); + print("C: a row of each kind, one for each player: Science Vessel, Pylon, Psi Emitter, Data Disk, Flag. Clean squares? Missing? Off their cells?", { position: "center" }); + KINDS.forEach((kind, r) => OWNERS.forEach((p, i) => { at(i, r + 1); createUnit(p, kind, 1, PIXEL); })); + + await sleep(seconds(18)); + print("D: two pixels fall down the right side of the board, an Overlord and a Psi Emitter. Smooth? Anything left behind?", { position: "center" }); + for (let y = 0; y < 12; y++) { + at(9, y); createUnit(P3, units.ZergOverlord, 1, PIXEL); + at(10, y); createUnit(P5, units.PsiEmitter, 1, PIXEL); + await sleep(frames(8)); + at(9, y); removeUnitAt(P3, units.ZergOverlord, 1, PIXEL); + at(10, y); removeUnitAt(P5, units.PsiEmitter, 1, PIXEL); + } + + await sleep(seconds(1)); + // E: N Overlords to the right of the board, made on one frame and taken away on the next, for six seconds each. + for (const n of [10, 20, 40, 80]) { + print(`E: ${n} Overlords made and taken away every frame, to the right. Does the game slow down or stutter?`, { position: "center" }); + for (let f = 0; f < 72; f++) { + for (let k = 0; k < n; k++) { + at(12 + k % 10, Math.floor(k / 10)); + if (f % 2 === 0) createUnit(P4, units.ZergOverlord, 1, PIXEL); + else removeUnitAt(P4, units.ZergOverlord, 1, PIXEL); + } + await sleep(frames(1)); + } + } + print("That is the end of the unit-pixel probe. Thank you.", { position: "center" }); +}, { owner: P1 }); + +// What the lifted game showed besides: Player 2's Overlord was moved off its cell (a computer: its AI moves its units), +// Player 12's was never made, Pylons landed on 4 of 8 cells and Psi Emitters on 7 (the ground), and Science Vessels +// zigzagged — every other one pushed 56 pixels down, since air units that overlap push each other apart. Overlords, 64 +// apart, stay put. So a board's pixels are Overlords of Player 1 and the empty slots, P3 … P8: seven colours, as many as +// Tetris has pieces. +test("Overlords land on their cells, for Player 1 and the empty slots", (sim) => { + sim.frames(24 * 23); + const mine = [0, 2, 3, 4, 5, 6, 7]; + const onCell = sim.units({ type: units.ZergOverlord }).filter((u) => mine.includes(u.owner) && (u.x - OX - CELL / 2) % CELL === 0 && (u.y - OY - CELL / 2) % CELL === 0); + expect(onCell.length).toBe(7); +}); diff --git a/scripts/build-fixture.mts b/scripts/build-fixture.mts index 7ac6614..55acc8f 100644 --- a/scripts/build-fixture.mts +++ b/scripts/build-fixture.mts @@ -80,6 +80,11 @@ if (home && scn.locations?.[0] && !scn.locations[0].nameIndex && scn.locations[0 const awayLeft = left + 384 + 256 + 128 <= w ? left + 384 + 256 : Math.max(0, left - 256 - 128); scn.locations[1] = { ...scn.locations[1], left: awayLeft, top, right: awayLeft + 128, bottom: top + 192, nameIndex: internString(scn, "Away") }; } + // Location 3, "Pixel": 64 × 64 — one cell of a board drawn with units, room for a 2 × 2 building, which Create Unit puts + // inside the location or not at all — for a probe to centre on a point and make or take away a unit there. + if (scn.locations[2] && !scn.locations[2].nameIndex && scn.locations[2].right === 0) { + scn.locations[2] = { ...scn.locations[2], left: 0, top: 0, right: 64, bottom: 64, nameIndex: internString(scn, "Pixel") }; + } markDirty(scn, "MRGN"); } markDirty(scn, "TRIG", "UNIT"); diff --git a/tests/input.test.ts b/tests/input.test.ts index 394fe4b..3c464d4 100644 --- a/tests/input.test.ts +++ b/tests/input.test.ts @@ -107,6 +107,12 @@ describe("input: the front end", () => { expect(action).toContain(`"field":"modifier","bits":8`); }); + it("centerLocation with every argument known when the script is built is still the program's, in a function too", () => { + const r = compileFile(`program(() => { function at(x: number, y: number) { centerLocation(3 as Location, x * 64, y * 64); } centerLocation(3 as Location, 4, 5); at(1, 2); });`); + expect(r.diagnostics).toEqual([]); + expect(serializeIr(r.ir, r.strings, r.input).match(/"kind":"centerLocation"/g)).toHaveLength(2); + }); + it("says what is wrong", () => { expect(errors(`keyPressed(P1, "F2");`)[0]).toMatch(/does nothing on its own/); expect(errors(`if (keyPressed(players.Force1, "F2")) displayText("x");`)[0]).toMatch(/expected one player/); From 96d7730df9ad494d43f8c2b149ddb32f679eaa77 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 21:25:10 -0600 Subject: [PATCH 047/238] TrigScript: keyHeld() rides out a missing or late message A held key is a message a frame, carried by MSQC as a game command, and a command can go missing for a frame (the lifted game loses the second of every hold) or arrive with the next frame's (a multiplayer game carries commands in turns). Tetris's slide, which counts held frames in a row, never got going. What arrives is now banked and paid out a frame at a time, and a key stays held until two frames pass with no message; the simulator has the same grace. The game tests run a main.tsx too. Co-Authored-By: Claude Opus 5.5 --- compiler/gameTests.ts | 4 +-- compiler/generated/trigscriptPy.ts | 2 +- compiler/simulateIr.ts | 8 +++++- probes/game-key-held.ts | 9 +++---- python/trigscript.py | 39 +++++++++++++++++++++++++++++- tests/input.test.ts | 7 ++++-- 6 files changed, 57 insertions(+), 12 deletions(-) diff --git a/compiler/gameTests.ts b/compiler/gameTests.ts index 327481f..fc30af3 100644 --- a/compiler/gameTests.ts +++ b/compiler/gameTests.ts @@ -7,7 +7,7 @@ * * Bundled on its own as `dist/gametest.js`, which the plugin hands to the host as text. */ -import { ENTRY_FILE } from "./compiler"; +import { ENTRY_FILE, entryOf } from "./compiler"; import { MODULE_NAME } from "./api"; import { GameSim, readLocations, type GameHandle } from "./gameSim"; import type { TriggerMap, VariableMap } from "./gameVariables"; @@ -43,7 +43,7 @@ function register(payload: GameTestPayload) { registry.where = () => where(new Error()) ?? { file: ENTRY_FILE, line: 1 }; const runtime = { ...createRuntime(payload.names, collector), ...registry.library() }; collector.running = true; - const failure = runModules(linked, ENTRY_FILE, runtime, MODULE_NAME, { imported: TESTING_NAMES, then: [...linked.keys()].filter(isTestFile).sort() }); + const failure = runModules(linked, entryOf(linked.keys()), runtime, MODULE_NAME, { imported: TESTING_NAMES, then: [...linked.keys()].filter(isTestFile).sort() }); collector.running = false; return { linked, registry, where, error: failure ? `${failure.file ?? ENTRY_FILE}:${failure.line ?? 1}: ${failure.message}` : null }; } diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index 786420a..bc61c84 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 17\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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\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 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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 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 EUDJumpIf(self.again >= 1, sweep)\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 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 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 op == \">\":\n return av > b\n return av >= b\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 self.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 @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_cells, 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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 17\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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\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 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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 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 EUDJumpIf(self.again >= 1, sweep)\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 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 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 op == \">\":\n return av > b\n return av >= b\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 self.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 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 in zip(self.held_cells, self.held_bank, self.held_now, self.held_quiet):\n for p in self.humans:\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\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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 if INPUT.held:\n INPUT.frame()\n for prog in PROGRAMS:\n prog.run()\n"; diff --git a/compiler/simulateIr.ts b/compiler/simulateIr.ts index ef508ac..c03e4ea 100644 --- a/compiler/simulateIr.ts +++ b/compiler/simulateIr.ts @@ -50,6 +50,8 @@ export const FRAMES_PER_SECOND = 24; /** What Elapsed Time and the countdown count: a game second is sixteen frames (the slice 2 probe: 21 after 336 frames), not the twenty-four of a second at Fastest. */ export const FRAMES_PER_GAME_SECOND = 16; const U32 = 0x1_0000_0000; +/** Frames with no message before a held key counts as let go; python/trigscript.py has the same number (Input.HELD_GRACE). */ +const HELD_GRACE = 2; export interface ProgramEvent { /** 0-based run of the programs. */ @@ -1291,6 +1293,7 @@ export class ProgramSimulation { /** The keys and buttons down on the last frame, and on this one (what keyHeld() finds). */ private down = new Set(); private downNow = new Set(); + private readonly lastDown = new Map(); /** Where each player's mouse is, in map pixels. */ readonly mice = new Map(); cycle = 0; @@ -1557,7 +1560,10 @@ export class ProgramSimulation { const down = new Set([...this.current.events].filter((e) => e.startsWith("key:") || e.startsWith("click:"))); for (const e of down) if (this.down.has(e)) this.current.events.delete(e); this.down = down; - this.downNow = down; + // keyHeld(): down now, or let go no more than HELD_GRACE frames ago — as the game's lowering has it, which covers a + // message that went missing on its way between the players' computers. + for (const e of down) this.lastDown.set(e, this.frame); + this.downNow = new Set([...this.lastDown].filter(([, f]) => this.frame - f <= HELD_GRACE).map(([e]) => e)); for (const run of this.runs) { this.world.current = run.player; try { run.tick(); } catch (err) { if (err instanceof Halt) throw new Error(err.message); throw err; } diff --git a/probes/game-key-held.ts b/probes/game-key-held.ts index 6e7d158..e6afed2 100644 --- a/probes/game-key-held.ts +++ b/probes/game-key-held.ts @@ -14,13 +14,12 @@ program(async () => { } }, { owner: P1 }); -// In the lifted game the first two frames' messages arrive together (MSQC), so a hold of n frames is read on n − 1 of -// them there — 1 → 1, 3 → 2, 10 → 9 — and on n in the simulator. Either way it is held from the second frame to the end. -test("a key down ten frames is held on (nearly) every one of them and pressed once", (sim) => { +// A held key is still held for two frames after the last message, in the game and the simulator alike: a message can go +// missing for a frame (the lifted game loses the second of every hold) and a multiplayer game carries commands in turns. +test("a key down ten frames is held on those ten and two more, and pressed once", (sim) => { sim.frames(4); for (let i = 0; i < 10; i++) sim.press("D").frames(1); sim.frames(8); - expect(sim.resources(P1).ore).toBeGreaterThanOrEqual(9); - expect(sim.resources(P1).ore).toBeLessThanOrEqual(10); + expect(sim.resources(P1).ore).toBe(12); expect(sim.resources(P1).gas).toBe(1); }); diff --git a/python/trigscript.py b/python/trigscript.py index 9f1e92d..b527abe 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -3383,6 +3383,14 @@ def __init__(self, plan): raise Fail("trigscript: a program reads keys, clicks, the mouse or chat, and the map has no human player to give any") self.key_cells = [self.register("tsin_key%d" % i, EUDArray(12)) for i in range(len(self.keys))] self.held_cells = [self.register("tsin_held%d" % i, EUDArray(12)) for i in range(len(self.held))] + # A held key is a message each frame it is down, but a multiplayer game carries commands in turns: two frames' + # messages may arrive on one frame and none on the next. What arrives is banked and paid out a frame at a time, + # so keyHeld() is true on as many frames as the key was down, however the turns grouped them. + self.held_bank = [EUDArray(12) for _ in self.held] + self.held_now = [EUDArray(12) for _ in self.held] + # And a message can go missing for a frame (seen in the lifted game, the second frame of every hold): a key is + # still held until HELD_GRACE frames have passed with no message, which delays a release by as much. + self.held_quiet = [EUDArray(12) for _ in self.held] self.button_cells = [self.register("tsin_button%d" % i, EUDArray(12)) for i in range(len(self.buttons))] self.captures = max([len(c["captures"]) for c in self.chats] or [0]) if self.chats: @@ -3398,6 +3406,33 @@ def __init__(self, plan): self.capture_in = [self.register("tsin_capture%d_in" % i, EUDArray(12)) for i in range(self.captures)] self.unit_table = None + HELD_BANK_MOST = 8 # a burst after a lag spike pays out at most this many frames + HELD_GRACE = 2 # frames with no message before a held key counts as let go + + def frame(self): + """Once a frame, before the programs: this frame's held messages banked, one frame of each paid out, and a key + with nothing left to pay still held for HELD_GRACE quiet frames.""" + for cells, bank, now, quiet in zip(self.held_cells, self.held_bank, self.held_now, self.held_quiet): + for p in self.humans: + b = fresh(bank[p] + cells[p]) + if EUDIf()(b >= self.HELD_BANK_MOST + 1): + b << self.HELD_BANK_MOST + EUDEndIf() + if EUDIf()(b >= 1): + now[p] = 1 + bank[p] = b - 1 + quiet[p] = 0 + if EUDElse()(): + bank[p] = 0 + q = fresh(quiet[p] + 1) + quiet[p] = q + if EUDIf()([now[p] >= 1, q <= self.HELD_GRACE]): + now[p] = 1 + if EUDElse()(): + now[p] = 0 + EUDEndIf() + EUDEndIf() + @staticmethod def register(name, cell): EUDRegisterObjectToNamespace(name, cell) @@ -3408,7 +3443,7 @@ def read(self, i, low, node): source = i.get("source") p = low.one_player(i["player"], node) if source == "key" and i.get("held"): - return self.cell(self.held_cells, self.held, i["key"], node)[p] + return self.cell(self.held_now, self.held, i["key"], node)[p] if source == "key": return self.cell(self.key_cells, self.keys, i["key"], node)[p] if source == "click": @@ -3769,5 +3804,7 @@ def beforeTriggerExec(): def afterTriggerExec(): + if INPUT.held: + INPUT.frame() for prog in PROGRAMS: prog.run() diff --git a/tests/input.test.ts b/tests/input.test.ts index 3c464d4..960c0a5 100644 --- a/tests/input.test.ts +++ b/tests/input.test.ts @@ -199,12 +199,15 @@ describe("input: the interpreter", () => { expect([sim.value("n"), sim.value("v")]).toEqual([2, 10]); }); - it("a key down on frames in a row is one press, and held on each of them", () => { + it("a key down on frames in a row is one press, and held on each of them and two frames after", () => { const sim = simulate(loop(`if (keyPressed(CurrentPlayer, "D")) n += 1; if (keyHeld(CurrentPlayer, "D")) v += 1;`)); for (let i = 0; i < 4; i++) sim.press("D").step(); sim.step(); sim.press("D").step(); - expect([sim.value("n"), sim.value("v")]).toEqual([2, 5]); + // Let go for one frame: still held (the grace covers a message lost on the way), and pressed again after it. + expect([sim.value("n"), sim.value("v")]).toEqual([2, 6]); + for (let i = 0; i < 4; i++) sim.step(); + expect(sim.value("v")).toBe(8); }); it("another player's key is not this player's", () => { From 659ab2783dc167c25268cddd34e20e7e9a159947 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 21:35:10 -0600 Subject: [PATCH 048/238] TrigScript: with a held key in the game, a press is worked out from it MSQC's carrier has one order, and a multiplayer game may bring two frames' commands at once: the later replaces the earlier. A tap sent "went down" then "is down", and when the two arrived together the press was lost - in Tetris, a tap of A or D now and then did nothing, in multiplayer only. With any held key in the game every key is sent as held, and a press is held now and not before, worked out where it arrives; the simulator does the same. Without held keys, KeyDown as before: one command a tap. Co-Authored-By: Claude Opus 5.5 --- compiler/generated/trigscriptPy.ts | 2 +- compiler/input.ts | 8 ++++++-- compiler/simulateIr.ts | 6 ++++++ python/trigscript.py | 12 +++++++++++- tests/input.test.ts | 13 +++++++++---- 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index bc61c84..7171e8d 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 17\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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\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 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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 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 EUDJumpIf(self.again >= 1, sweep)\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 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 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 op == \">\":\n return av > b\n return av >= b\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 self.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\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 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 in zip(self.held_cells, self.held_bank, self.held_now, self.held_quiet):\n for p in self.humans:\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\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\":\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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 if INPUT.held:\n INPUT.frame()\n for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 17\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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\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 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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 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 EUDJumpIf(self.again >= 1, sweep)\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 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 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 op == \">\":\n return av > b\n return av >= b\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 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 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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 if INPUT.held:\n INPUT.frame()\n for prog in PROGRAMS:\n prog.run()\n"; diff --git a/compiler/input.ts b/compiler/input.ts index eeae381..3311a4f 100644 --- a/compiler/input.ts +++ b/compiler/input.ts @@ -303,6 +303,10 @@ export function inputPlan(programs: Program[], locations: NameTable, units: Name } if (mouseBase === null) throw new Error("Reading the mouse needs eight free locations in a row among the map's first 63, one per player, besides one more for the plugin that carries input; this map has no such run."); } + // With a held key in the game, keys go every frame they are down, and a multiplayer game may bring two frames' commands + // at once — the later replaces the earlier, since MSQC's carrier has one order. A press of its own ("went down") would + // be lost that way, so every key is sent as held and a press is worked out where it arrives: held now, not before. + if (held.length) for (const k of keys) if (!held.includes(k)) held.push(k); const plan: InputPlan = { keys, ...(held.length ? { held } : {}), buttons, chats, qcLocation, mouseBase }; if (chats.some((c) => c.captures.some((x) => x.kind === "unit"))) { const seen = new Set(); @@ -338,8 +342,8 @@ export function inputPlugins(plan: InputPlan): { before: Record = { QCUnit: QC_UNIT, QCLoc: plan.qcLocation, QCPlayer: QC_PLAYER, QCDebug: "false" }; // KeyDown, not KeyPress: MSQC's KeyPress is true on every frame the key is down, and a tap is two or three frames — // one tap of W turned the Tetris piece two or three times (TrigScript 4, S2, played). KeyDown is the frame it goes down. - plan.keys.forEach((key, i) => { msqc[`KeyDown(${msqcKey(key)}); NotTyping`] = `${INPUT_NAMES.key(i)}, 1`; }); - // keyHeld(): every frame the key is down on the player's computer, sent each such frame. + // Without a held key, a press is MSQC's KeyDown: one command a tap. With one, every key goes as held (see inputPlan). + if (!plan.held?.length) plan.keys.forEach((key, i) => { msqc[`KeyDown(${msqcKey(key)}); NotTyping`] = `${INPUT_NAMES.key(i)}, 1`; }); plan.held?.forEach((key, i) => { msqc[`KeyPress(${msqcKey(key)}); NotTyping`] = `${INPUT_NAMES.held(i)}, 1`; }); plan.buttons.forEach((b, i) => { msqc[`MouseDown(${MSQC_BUTTON[b]})`] = `${INPUT_NAMES.button(i)}, 1`; }); if (plan.mouseBase !== null) msqc.Mouse = plan.mouseBase; diff --git a/compiler/simulateIr.ts b/compiler/simulateIr.ts index c03e4ea..7c5486b 100644 --- a/compiler/simulateIr.ts +++ b/compiler/simulateIr.ts @@ -1294,6 +1294,8 @@ export class ProgramSimulation { private down = new Set(); private downNow = new Set(); private readonly lastDown = new Map(); + /** Whether a program reads a held key: then a press is worked out from the held keys, as the game does. */ + private heldKeys = false; /** Where each player's mouse is, in map pixels. */ readonly mice = new Map(); cycle = 0; @@ -1334,6 +1336,7 @@ export class ProgramSimulation { }; this.unitByName = options.unitByName ?? (() => undefined); for (const s of inputsOf(programs).sources) if (s.source === "chat" && !this.chats.some((c) => c.pattern === s.pattern)) this.chats.push(parseChatPattern(s.pattern)); + this.heldKeys = inputsOf(programs).sources.some((s) => s.source === "key" && s.held); this.current = { events: new Set(), lines: new Map() }; // A program runs once a frame for each of its owners who is in the game, each run with its own variables; one that // is not per player runs as the first of them. Without the map's player settings there is the one player and one run. @@ -1563,7 +1566,10 @@ export class ProgramSimulation { // keyHeld(): down now, or let go no more than HELD_GRACE frames ago — as the game's lowering has it, which covers a // message that went missing on its way between the players' computers. for (const e of down) this.lastDown.set(e, this.frame); + const was = this.downNow; this.downNow = new Set([...this.lastDown].filter(([, f]) => this.frame - f <= HELD_GRACE).map(([e]) => e)); + // With a held key in the game every key goes as held, and a press is held now and not before (compiler/input.ts). + if (this.heldKeys) for (const e of [...this.current.events]) if (e.startsWith("key:") && (!this.downNow.has(e) || was.has(e))) this.current.events.delete(e); for (const run of this.runs) { this.world.current = run.player; try { run.tick(); } catch (err) { if (err instanceof Halt) throw new Error(err.message); throw err; } diff --git a/python/trigscript.py b/python/trigscript.py index b527abe..83f76c8 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -3391,6 +3391,8 @@ def __init__(self, plan): # And a message can go missing for a frame (seen in the lifted game, the second frame of every hold): a key is # still held until HELD_GRACE frames have passed with no message, which delays a release by as much. self.held_quiet = [EUDArray(12) for _ in self.held] + # With held keys every key is sent as held (compiler/input.ts#inputPlan): a press is held now and not before. + self.held_pressed = [EUDArray(12) for _ in self.held] self.button_cells = [self.register("tsin_button%d" % i, EUDArray(12)) for i in range(len(self.buttons))] self.captures = max([len(c["captures"]) for c in self.chats] or [0]) if self.chats: @@ -3412,8 +3414,9 @@ def __init__(self, plan): def frame(self): """Once a frame, before the programs: this frame's held messages banked, one frame of each paid out, and a key with nothing left to pay still held for HELD_GRACE quiet frames.""" - for cells, bank, now, quiet in zip(self.held_cells, self.held_bank, self.held_now, self.held_quiet): + for cells, bank, now, quiet, pressed in zip(self.held_cells, self.held_bank, self.held_now, self.held_quiet, self.held_pressed): for p in self.humans: + was = fresh(now[p]) b = fresh(bank[p] + cells[p]) if EUDIf()(b >= self.HELD_BANK_MOST + 1): b << self.HELD_BANK_MOST @@ -3432,6 +3435,11 @@ def frame(self): now[p] = 0 EUDEndIf() EUDEndIf() + if EUDIf()([now[p] >= 1, was == 0]): + pressed[p] = 1 + if EUDElse()(): + pressed[p] = 0 + EUDEndIf() @staticmethod def register(name, cell): @@ -3444,6 +3452,8 @@ def read(self, i, low, node): p = low.one_player(i["player"], node) if source == "key" and i.get("held"): return self.cell(self.held_now, self.held, i["key"], node)[p] + if source == "key" and self.held: + return self.cell(self.held_pressed, self.held, i["key"], node)[p] if source == "key": return self.cell(self.key_cells, self.keys, i["key"], node)[p] if source == "click": diff --git a/tests/input.test.ts b/tests/input.test.ts index 960c0a5..0f7cab6 100644 --- a/tests/input.test.ts +++ b/tests/input.test.ts @@ -154,7 +154,12 @@ describe("input: the plan and the build's plugins", () => { it("keyHeld: MSQC's KeyPress, every frame the key is down, into cells of its own", () => { const r = compile(`while (true) { if (keyHeld(P1, "D") || keyPressed(P1, "D")) displayText("x"); sleep(frames(1)); }`); expect(r.input).toEqual({ keys: ["D"], held: ["D"], buttons: [], chats: [], qcLocation: 62, mouseBase: null }); - expect(buildPlugins(r.input, "/ir.json").MSQC).toEqual({ QCUnit: 58, QCLoc: 62, QCPlayer: 11, QCDebug: "false", "KeyDown(D); NotTyping": "tsin_key0, 1", "KeyPress(D); NotTyping": "tsin_held0, 1" }); + // No KeyDown with a held key in the game: two frames' commands may arrive at once, the later replacing the earlier, and + // a press of its own would be lost; a press is worked out from the held key where it arrives. + expect(buildPlugins(r.input, "/ir.json").MSQC).toEqual({ QCUnit: 58, QCLoc: 62, QCPlayer: 11, QCDebug: "false", "KeyPress(D); NotTyping": "tsin_held0, 1" }); + const both = compile(`while (true) { if (keyHeld(P1, "D") || keyPressed(P1, "W")) displayText("x"); sleep(frames(1)); }`); + expect(both.input?.held).toEqual(["D", "W"]); + expect(Object.keys(buildPlugins(both.input, "/ir.json").MSQC)).toEqual(["QCUnit", "QCLoc", "QCPlayer", "QCDebug", "KeyPress(D); NotTyping", "KeyPress(W); NotTyping"]); }); it("the mouse: eight locations in a row, clear of the map's own and of Anywhere", () => { @@ -199,13 +204,13 @@ describe("input: the interpreter", () => { expect([sim.value("n"), sim.value("v")]).toEqual([2, 10]); }); - it("a key down on frames in a row is one press, and held on each of them and two frames after", () => { + it("a key down on frames in a row is one press, and held on each of them and two frames after; a gap within those is no new press", () => { const sim = simulate(loop(`if (keyPressed(CurrentPlayer, "D")) n += 1; if (keyHeld(CurrentPlayer, "D")) v += 1;`)); for (let i = 0; i < 4; i++) sim.press("D").step(); sim.step(); sim.press("D").step(); - // Let go for one frame: still held (the grace covers a message lost on the way), and pressed again after it. - expect([sim.value("n"), sim.value("v")]).toEqual([2, 6]); + // Let go for one frame: still held (the grace covers a message lost on the way), so not pressed again either. + expect([sim.value("n"), sim.value("v")]).toEqual([1, 6]); for (let i = 0; i < 4; i++) sim.step(); expect(sim.value("v")).toBe(8); }); From 36071ab9050801e5591a0f360c7728c1155e6d80 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 21:42:08 -0600 Subject: [PATCH 049/238] TrigScript 4: pixels made of units, played (steps A-D) Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index c6c1427..41a401d 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1137,6 +1137,22 @@ of it, counted as one press by `keyPressed` (MSQC's KeyDown since `b09f04d`). So press: S3's movement needs no `Local` and no `sync()`. Magenta probe 7's "one event a press, no key-up" was its own setup; R0's `GetKeyState` reading agrees with what was played. +**Pixels made of units, played 2026-10-16** (`probes/unit-pixels.ts`, steps A–D; E not yet +reported). A board can be drawn with units on the map and the minimap: +- **Colours:** nine Overlords, P1 … P8 and P12, show nine colours on the minimap once the board + is in sight (your own units are green there). Player 2 is a computer and its AI moved its + Overlord a cell, so a board's owners are P1 and the empty slots P3 … P8 (P12 too: Remastered + made it, the lifted game did not). +- **Vision:** Map Revealers of Player 1's lift the fog over the board. The lifted game never + makes a Map Revealer — a gap in scm-sim, not the game. +- **The minimap merges neighbours:** units 64 pixels apart and of one owner join into one block, + so a piece reads as a solid shape. +- **Overlords** sit on their cells on any ground. Science Vessels are wider than a cell and + zigzag; Pylons, Flags and small items miss cells where the ground will not take them, and + **Remastered prints "WARNING: Unit unplaceable! (Protoss Pylon) (1024,2304)" in the text lines + for every Create Unit that fails** — so a unit board must never ask for a cell it cannot have, + or it scribbles over a text board beside it. + ### S3. A hero moved with the keys W A S D move a hero, the mouse aims, a key casts. The map plays as an action game. From 3405d3a06f096f0bf462856cd53f3b7b0e1cd3aa Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 21:52:45 -0600 Subject: [PATCH 050/238] TrigScript 4, S2: Tetris on the minimap too - the board drawn with Overlords The game writes one board and two screens show it: the text lines (ui.lines, JSX) and a board of Overlords beside the base, one a cell in its piece's colour, which the minimap shows as solid blocks. probes/lib/unit-screen.ts draws any board that way and knows nothing of Tetris: only the cells that changed are taken away and made again. The probe scripts now take the files a script imports (scripts/script-files). In the lifted game the Overlords follow the board piece by piece; it makes no unit of Player 12's (the Z piece's colour), which Remastered does. Co-Authored-By: Claude Opus 5.5 --- probes/lib/unit-screen.ts | 37 +++++++++++++++++++++++++++++++++++++ probes/tetris.tsx | 25 ++++++++++++++++++++++--- scripts/build-fixture.mts | 3 ++- scripts/debug-game.mts | 3 ++- scripts/script-files.mts | 24 ++++++++++++++++++++++++ scripts/test-game.mts | 3 ++- 6 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 probes/lib/unit-screen.ts create mode 100644 scripts/script-files.mts diff --git a/probes/lib/unit-screen.ts b/probes/lib/unit-screen.ts new file mode 100644 index 0000000..0c0083f --- /dev/null +++ b/probes/lib/unit-screen.ts @@ -0,0 +1,37 @@ +// A board drawn with units, on the map and the minimap (TrigScript 4, S2): a cell of the board is an Overlord on the map, +// its owner's colour the cell's colour. Nothing moves a unit: the screen keeps what it has drawn and, given the board, +// takes away and makes only the cells that changed — a location centred on the cell, Remove Unit and Create Unit there. +// +// What the unit-pixel probe found, played in Remastered (2026-10-16): Overlords sit on their cells on any ground; a +// computer's AI moves its own units, so the owners are the empty slots; neighbours of one owner join into one block on +// the minimap; 80 made and taken away a frame did not slow the game; a Create Unit that fails prints a warning in the +// text lines, which an Overlord never does. The map needs a 64 × 64 location for the cells (the probe maps' "Pixel"). +// +// A game() function, since a function from another file reaches a program that way; a class would read better, and a +// class declared outside a program is not the program's yet. +import { game, centerLocation, createUnit, removeUnitAt, units, players, P3, P4, P5, P6, P7, P8, type Location, type Player } from "trigscript"; + +/** Where a board is drawn: its corner on the map, its width in cells, a cell's size, and the location moved to each cell. */ +export interface UnitBoard { x: number; y: number; width: number; cell: number; at: Location } +/** What a board's cell holds — a colour each piece has — and the player whose Overlord shows each. */ +export interface Palette { colors: readonly number[]; owners: readonly Player[] } + +/** The empty slots and Player 12: units no computer moves. */ +export const OWNERS: readonly Player[] = [P3, P8, P4, P6, players.P12, P7, P5]; + +/** + * The board on the map. `shown` is what each cell shows now — the board as it was the last time — and is brought up to + * date: each cell that changed is taken away and made again in its colour (0 is an empty cell, nothing on the map). + */ +export const drawUnits = game((board: number[], shown: number[], where: UnitBoard, palette: Palette) => { + for (let i = 0; i < shown.length; i++) { + const now = board[i]; + const was = shown[i]; + if (now !== was) { + centerLocation(where.at, where.x + (i % where.width) * where.cell + where.cell / 2, where.y + Math.floor(i / where.width) * where.cell + where.cell / 2); + palette.colors.forEach((color, k) => { if (was === color) removeUnitAt(palette.owners[k], units.ZergOverlord, 1, where.at); }); + palette.colors.forEach((color, k) => { if (now === color) createUnit(palette.owners[k], units.ZergOverlord, 1, where.at); }); + shown[i] = now; + } + } +}); diff --git a/probes/tetris.tsx b/probes/tetris.tsx index 5cc7b7e..d96e9f8 100644 --- a/probes/tetris.tsx +++ b/probes/tetris.tsx @@ -1,11 +1,14 @@ -// Tetris in StarCraft's text lines (TrigScript 4, S2's showpiece). The screen is JSX: ui.lines() shows it to Player 1 -// every frame, redrawn from the program's variables. A D move (hold to slide), W turns, S drops (hold to keep +// Tetris in StarCraft's text lines and on its minimap (TrigScript 4, S2's showpiece). The game writes one board; two +// screens show it. The text lines are JSX: ui.lines() shows them to Player 1, redrawn when the board changes. The other +// is a board of Overlords on the map beside your base, one a cell in its piece's colour, which the minimap shows as solid +// blocks: probes/lib/unit-screen.ts, a function that knows nothing of Tetris. A D move (hold to slide), W turns, S drops (hold to keep // dropping), Space drops all the way. // The pieces and their four turns are worked out when the script is built, in ordinary JavaScript. // Host it as a multiplayer game, alone: the keys reach the game the way typed lines do. // Built with // npx tsx scripts/build-fixture.mts probes/tetris.tsx fixtures/eud/tetris.scx -import { program, P1, keyPressed, keyHeld, random, sleep, frames, ui, textColors } from "trigscript"; +import { program, P1, keyPressed, keyHeld, random, sleep, frames, ui, textColors, createUnit, centerLocation, centerView, units, type Location } from "trigscript"; +import { drawUnits, OWNERS } from "./lib/unit-screen"; const W = 10; const H = 10; @@ -40,10 +43,22 @@ const DELAY = 8; const SLIDE = 2; const DROP = 2; const ROWS = [...Array(H).keys()]; +/** The board of Overlords: its corner on the map, beside Player 1's base on (2)Binary Burghs, a cell 64 pixels. */ +const MAP_X = 672, MAP_Y = 2144, CELL = 64; +const PIXEL = 3 as Location; +const ON_MAP = { x: MAP_X, y: MAP_Y, width: W, cell: CELL, at: PIXEL }; const COLS = [...Array(W).keys()]; program(async () => { const board: number[] = new Array(W * H).fill(0); + /** What the board of Overlords shows now. */ + let shown: number[] = new Array(W * H).fill(0); + /** Whether the board was written since the Overlords were last drawn. */ + let changed = true; + // Sight over the board, for the minimap; and the camera on it, the text lines beside. + for (let y = 1; y < H; y += 3) for (let x = 1; x < W; x += 3) { centerLocation(PIXEL, MAP_X + x * CELL, MAP_Y + y * CELL); createUnit(P1, units.MapRevealer, 1, PIXEL); } + centerLocation(PIXEL, MAP_X + (W * CELL) / 2, MAP_Y + (H * CELL) / 2); + centerView(PIXEL); let piece = random(7), next = random(7), turn = 0, px = 3, py = 0; let score = 0, lines = 0, level = 0, over = false, best = 0; @@ -85,6 +100,7 @@ program(async () => { const cy = py + SHAPES[at + k * 2 + 1]; if (cy >= 0) board[cy * W + px + SHAPES[at + k * 2]] = color; } + changed = true; } /** The piece moved and turned, if it fits there. */ @@ -108,6 +124,7 @@ program(async () => { for (let yy = y; yy > 0; yy--) for (let x = 0; x < W; x++) board[yy * W + x] = board[(yy - 1) * W + x]; for (let x = 0; x < W; x++) board[x] = 0; cleared++; + changed = true; } else y--; } return cleared; @@ -141,6 +158,7 @@ program(async () => { if (over) { if (keyPressed(P1, "Space")) { board.fill(0); + changed = true; score = 0; lines = 0; level = 0; over = false; spawn(); } @@ -160,6 +178,7 @@ program(async () => { if (!move(0, 1, 0)) land(); } } + if (changed) { drawUnits(board, shown, ON_MAP, { colors: COLORS, owners: OWNERS }); changed = false; } await sleep(frames(1)); } }, { owner: P1 }); diff --git a/scripts/build-fixture.mts b/scripts/build-fixture.mts index 55acc8f..4d8d5d7 100644 --- a/scripts/build-fixture.mts +++ b/scripts/build-fixture.mts @@ -24,6 +24,7 @@ import { readTriggerMap, readVariableMap } from "../compiler/gameVariables"; import { resolveStrings } from "../script"; import { defaultScriptNames } from "../compiler/names"; import { defaultLib } from "../bundle/lib.mjs"; +import { scriptFiles } from "./script-files.mts"; const [scriptPath, outPath, mapArg, eudplibArg] = process.argv.slice(2); if (!scriptPath || !outPath) { console.error("usage: build-fixture [map.scx] [plugin-eudplib dir]"); process.exit(2); } @@ -33,7 +34,7 @@ const eudplib = resolve(eudplibArg ?? process.env.EUDPLIB_DIR ?? join(root, ".." if (!existsSync(map)) { console.error(`no map at ${map}`); process.exit(2); } if (!existsSync(join(eudplib, "scripts", "build-map.mts"))) { console.error(`no plugin-eudplib checkout at ${eudplib}`); process.exit(2); } -const r = compileScript(ts, { [scriptPath.endsWith(".tsx") ? "main.tsx" : "main.ts"]: readFileSync(scriptPath, "utf8") }, defaultScriptNames(), { lib: defaultLib() }); +const r = compileScript(ts, scriptFiles(scriptPath), defaultScriptNames(), { lib: defaultLib() }); if (r.diagnostics.length) { for (const d of r.diagnostics) console.error(`${d.file}:${d.line}:${d.column} ${d.message}`); process.exit(1); } const EDITOR = resolve(process.env.SCMJS_DIR ?? join(root, "..", "scm-js")); const { loadMap, saveMap, readExtras } = await import(join(EDITOR, "src", "formats", "mpq", "scm.ts")); diff --git a/scripts/debug-game.mts b/scripts/debug-game.mts index c9725c6..10dbe55 100644 --- a/scripts/debug-game.mts +++ b/scripts/debug-game.mts @@ -21,6 +21,7 @@ import { defaultScriptNames } from "../compiler/names"; import { STEP_EUDPLIB, STEP_TEST, STEP_UNSEEN, type DebugHost, type DebugRecording } from "../compiler/gameDebug"; import { programVariables, type VariableMap } from "../compiler/gameVariables"; import { defaultLib } from "../bundle/lib.mjs"; +import { scriptFiles } from "./script-files.mts"; const root = resolve(import.meta.dirname, ".."); const [script, only, baseMap] = process.argv.slice(2); @@ -52,7 +53,7 @@ try { const payloadEnd = 0x191943c8 + strings.bytes; const handle = testGame(run.engine, run.lifted, [{ from: strings.gameTable, to: payloadEnd }], { cost: installCost(run.lifted), payloadEnd, eud }) as DebugHost; - const source = { [script.endsWith(".tsx") ? "main.tsx" : "main.ts"]: readFileSync(script, "utf8") }; + const source = scriptFiles(script); const names = defaultScriptNames(); const locations = readLocations(handle); const compiled = compileScript(ts, source, names, { lib: defaultLib(), tests: {} }); diff --git a/scripts/script-files.mts b/scripts/script-files.mts new file mode 100644 index 0000000..ab9b3bd --- /dev/null +++ b/scripts/script-files.mts @@ -0,0 +1,24 @@ +/** + * A script on disk as the compiler takes it: the file itself as main.ts (or main.tsx), and every file it imports by a + * relative path, followed from file to file, under the path it has from the script's folder. For the probes, whose + * libraries sit beside them (probes/lib/…). + */ +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join, relative, resolve } from "node:path"; + +export function scriptFiles(path: string): Record { + const root = dirname(resolve(path)); + const files: Record = {}; + const visit = (file: string, as: string) => { + if (as in files) return; + const text = readFileSync(file, "utf8"); + files[as] = text; + for (const m of text.matchAll(/\bfrom\s+["'](\.{1,2}\/[^"']+)["']/g)) { + const base = resolve(dirname(file), m[1]); + const found = [base, `${base}.ts`, `${base}.tsx`].find((f) => existsSync(f) && /\.tsx?$/.test(f)); + if (found) visit(found, relative(root, found).split("\\").join("/")); + } + }; + visit(resolve(path), path.endsWith(".tsx") ? "main.tsx" : "main.ts"); + return files; +} diff --git a/scripts/test-game.mts b/scripts/test-game.mts index 31cadc8..d2c1456 100644 --- a/scripts/test-game.mts +++ b/scripts/test-game.mts @@ -20,6 +20,7 @@ import { mergeCopies } from "../compiler/gameCopies"; import { defaultScriptNames } from "../compiler/names"; import type { TestReport, TestWorld } from "../compiler/testing"; import { defaultLib } from "../bundle/lib.mjs"; +import { scriptFiles } from "./script-files.mts"; const root = resolve(import.meta.dirname, ".."); const [script, baseMap] = process.argv.slice(2); @@ -57,7 +58,7 @@ try { // The tests in the simulator over the map's own world, as the editor runs them; then in the game, the way a host // of the game runs them: the compile's JavaScript handed to dist/gametest.js, loaded from its text. - const source = { [script.endsWith(".tsx") ? "main.tsx" : "main.ts"]: readFileSync(script, "utf8") }; + const source = scriptFiles(script); const names = defaultScriptNames(); // (The locations from the game once eudplib has put them right: in the built map they are scrambled.) const locations = readLocations(handle); From d48820fc8f18fc41422a87c6d271ce9aa283a32f Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 22:00:35 -0600 Subject: [PATCH 051/238] Tests: the Tetris build fixture takes the library it imports Co-Authored-By: Claude Opus 5.5 --- tests/eud-build.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/eud-build.test.ts b/tests/eud-build.test.ts index 0252e0f..9b93bb7 100644 --- a/tests/eud-build.test.ts +++ b/tests/eud-build.test.ts @@ -348,7 +348,7 @@ const FIXTURES: Record = { function build(name: string, src: string): { out: number; triggers: number } { // A fixture that writes JSX is a .tsx file: its name ends in Tsx. - const r = compileScript(ts, { [name.endsWith("Tsx") ? "main.tsx" : "main.ts"]: src }, NAMES, { lib: LIB }); + const r = compileScript(ts, { [name.endsWith("Tsx") ? "main.tsx" : "main.ts"]: src, ...(FIXTURE_FILES[name] ?? {}) }, NAMES, { lib: LIB }); expect(r.diagnostics).toEqual([]); // One program a fixture, but for the numbers probe, which has a second one for every player. expect(r.ir.length).toBe(name === "numbers" || name === "arraysProbe" || name === "functions" || name === "functionsProbe" || name === "callbacksProbe" || name === "insideProbe" || name === "stringsProbe" || name === "classesProbe" || name === "mapProbe" ? 2 : name === "leftoversProbe" ? 1 : name === "recursionProbe" ? 3 : 1); @@ -384,6 +384,10 @@ FIXTURES.uiTsx = `program(async () => { print(done); });`; FIXTURES.tetrisTsx = readFileSync(resolve(import.meta.dirname, "..", "probes", "tetris.tsx"), "utf8"); +/** The files a fixture imports, beside it. */ +const FIXTURE_FILES: Record> = { + tetrisTsx: { "lib/unit-screen.ts": readFileSync(resolve(import.meta.dirname, "..", "probes", "lib", "unit-screen.ts"), "utf8") }, +}; // Functions that are called: parameters and results of each kind, an argument that is a call of the same function, a copy an // array, a called function calling another, and — in the second program — cells that are a row a player. From c3a8d86033ad735c05d5d6ad8dab1e3f21cf2104 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 22:11:14 -0600 Subject: [PATCH 052/238] TrigScript 4, S2: Tetris's Overlords in the pieces' own colours, away from the camera Players 2 to 8 own the Overlords, a player a piece, each given its piece's colour as the text lines draw it (measured from screenshots) - Remastered's own colours for a map's players. Player 2 is no computer on this map, so nothing moves them. The board is in the top left corner, away from the camera, so the minimap's view box is not over it. A probe states such players in a line of its own (// probe-players: {...}), which the probe-map build applies. Co-Authored-By: Claude Opus 5.5 --- probes/tetris.tsx | 22 +++++++++++++--------- scripts/build-fixture.mts | 4 ++++ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/probes/tetris.tsx b/probes/tetris.tsx index d96e9f8..d7e8664 100644 --- a/probes/tetris.tsx +++ b/probes/tetris.tsx @@ -1,14 +1,17 @@ // Tetris in StarCraft's text lines and on its minimap (TrigScript 4, S2's showpiece). The game writes one board; two // screens show it. The text lines are JSX: ui.lines() shows them to Player 1, redrawn when the board changes. The other -// is a board of Overlords on the map beside your base, one a cell in its piece's colour, which the minimap shows as solid -// blocks: probes/lib/unit-screen.ts, a function that knows nothing of Tetris. A D move (hold to slide), W turns, S drops (hold to keep +// is a board of Overlords in the top left corner of the map, one a cell in its piece's colour, which the minimap shows as +// solid blocks: probes/lib/unit-screen.ts, a function that knows nothing of Tetris. Players 2 to 8 own the Overlords, a +// player a piece, each given its piece's colour as the text lines draw it (measured from screenshots); Player 2 is no +// computer here, so nothing moves them. A D move (hold to slide), W turns, S drops (hold to keep // dropping), Space drops all the way. // The pieces and their four turns are worked out when the script is built, in ordinary JavaScript. // Host it as a multiplayer game, alone: the keys reach the game the way typed lines do. +// probe-players: {"1":{"type":0,"rgb":[0,255,255]},"2":{"rgb":[255,255,73]},"3":{"rgb":[165,77,189]},"4":{"rgb":[19,255,29]},"5":{"rgb":[255,5,5]},"6":{"rgb":[15,87,247]},"7":{"rgb":[255,169,24]}} // Built with // npx tsx scripts/build-fixture.mts probes/tetris.tsx fixtures/eud/tetris.scx -import { program, P1, keyPressed, keyHeld, random, sleep, frames, ui, textColors, createUnit, centerLocation, centerView, units, type Location } from "trigscript"; -import { drawUnits, OWNERS } from "./lib/unit-screen"; +import { program, P1, P2, P3, P4, P5, P6, P7, P8, keyPressed, keyHeld, random, sleep, frames, ui, textColors, createUnit, centerLocation, units, type Location } from "trigscript"; +import { drawUnits } from "./lib/unit-screen"; const W = 10; const H = 10; @@ -24,6 +27,8 @@ const PIECES: [number, number][][] = [ [[2, 0], [0, 1], [1, 1], [2, 1]], // L ]; const COLORS = [textColors.cyan, textColors.yellow, textColors.purple, textColors.green, textColors.red, textColors.blue, textColors.orange]; +/** Whose Overlords show each piece's colour: the player given that colour (probe-players above). */ +const OWNERS = [P2, P3, P4, P5, P6, P7, P8]; const BOX = [4, 4, 3, 3, 3, 3, 3]; /** The cells of piece p turned r times, one after another as x, y: eight numbers a turn, thirty-two a piece. */ @@ -43,8 +48,9 @@ const DELAY = 8; const SLIDE = 2; const DROP = 2; const ROWS = [...Array(H).keys()]; -/** The board of Overlords: its corner on the map, beside Player 1's base on (2)Binary Burghs, a cell 64 pixels. */ -const MAP_X = 672, MAP_Y = 2144, CELL = 64; +/** The board of Overlords: its corner on the map, the top left of (2)Binary Burghs — away from the camera, which stays at + * the base, so the minimap's view box is never over it — and a cell, 64 pixels. */ +const MAP_X = 128, MAP_Y = 128, CELL = 64; const PIXEL = 3 as Location; const ON_MAP = { x: MAP_X, y: MAP_Y, width: W, cell: CELL, at: PIXEL }; const COLS = [...Array(W).keys()]; @@ -55,10 +61,8 @@ program(async () => { let shown: number[] = new Array(W * H).fill(0); /** Whether the board was written since the Overlords were last drawn. */ let changed = true; - // Sight over the board, for the minimap; and the camera on it, the text lines beside. + // Sight over the board, for the minimap. for (let y = 1; y < H; y += 3) for (let x = 1; x < W; x += 3) { centerLocation(PIXEL, MAP_X + x * CELL, MAP_Y + y * CELL); createUnit(P1, units.MapRevealer, 1, PIXEL); } - centerLocation(PIXEL, MAP_X + (W * CELL) / 2, MAP_Y + (H * CELL) / 2); - centerView(PIXEL); let piece = random(7), next = random(7), turn = 0, px = 3, py = 0; let score = 0, lines = 0, level = 0, over = false, best = 0; diff --git a/scripts/build-fixture.mts b/scripts/build-fixture.mts index 4d8d5d7..41f210e 100644 --- a/scripts/build-fixture.mts +++ b/scripts/build-fixture.mts @@ -49,6 +49,10 @@ patchPlayer(scn, 0, { type: 6, race: 1, force: 0 }); patchPlayer(scn, 1, { type: 5, race: 0, force: 1 }); // PEOPLE=0,1: those slots open to a person each (Terran), for a test of several players (scripts/test-game.mts). for (const slot of (process.env.PEOPLE ?? "").split(",").filter(Boolean).map(Number)) patchPlayer(scn, slot, { type: 6, race: 1, force: slot === 0 ? 0 : 1 }); +// A probe that needs its players set otherwise says so in a line of its own: `// probe-players: {"1":{"type":0,"rgb":[0,255,255]}}`, +// by 0-based slot — a slot type (0 inactive, 5 computer, 6 human) and a Remastered colour of its own. +const playersLine = /^\/\/ probe-players: (.+)$/m.exec(readFileSync(scriptPath, "utf8")); +if (playersLine) for (const [slot, patch] of Object.entries(JSON.parse(playersLine[1]) as Record)) patchPlayer(scn, Number(slot), patch); // The base map is a melee map: its stock triggers defeat whoever commands no buildings, and in // Use Map Settings nobody is handed melee units — both players lose at once and the game // ends in a draw a second in. The probe's own script is the only source of triggers it should have, From 68ce0f44fde5062a4616fb82f6e14cc9f55fd0da Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 22:18:58 -0600 Subject: [PATCH 053/238] TrigScript 4, S2: Tetris keeps its computer; the S piece is yours Remastered will not start a game without a computer, single player or multiplayer, and a computer moves its own units and sends them to fight (in the lifted game its Overlords wandered and its Scourges destroyed a P3 Overlord). Player 2 stays the computer and owns no cell. The green S piece is Player 1's - your units are green on the minimap anyway - and P3 to P8 have the other six, each in its piece's colour. Co-Authored-By: Claude Opus 5.5 --- probes/tetris.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/probes/tetris.tsx b/probes/tetris.tsx index d7e8664..298a615 100644 --- a/probes/tetris.tsx +++ b/probes/tetris.tsx @@ -1,16 +1,17 @@ // Tetris in StarCraft's text lines and on its minimap (TrigScript 4, S2's showpiece). The game writes one board; two // screens show it. The text lines are JSX: ui.lines() shows them to Player 1, redrawn when the board changes. The other // is a board of Overlords in the top left corner of the map, one a cell in its piece's colour, which the minimap shows as -// solid blocks: probes/lib/unit-screen.ts, a function that knows nothing of Tetris. Players 2 to 8 own the Overlords, a -// player a piece, each given its piece's colour as the text lines draw it (measured from screenshots); Player 2 is no -// computer here, so nothing moves them. A D move (hold to slide), W turns, S drops (hold to keep +// solid blocks: probes/lib/unit-screen.ts, a function that knows nothing of Tetris. A player a piece owns the Overlords, +// each given its piece's colour as the text lines draw it (measured from screenshots). Remastered will not start a game +// without a computer, and a computer moves its own units and sends them to fight, so Player 2 stays a computer and owns +// none: the S piece is yours — your units are green on the minimap anyway — and P3 … P8 have the other six. A D move (hold to slide), W turns, S drops (hold to keep // dropping), Space drops all the way. // The pieces and their four turns are worked out when the script is built, in ordinary JavaScript. // Host it as a multiplayer game, alone: the keys reach the game the way typed lines do. -// probe-players: {"1":{"type":0,"rgb":[0,255,255]},"2":{"rgb":[255,255,73]},"3":{"rgb":[165,77,189]},"4":{"rgb":[19,255,29]},"5":{"rgb":[255,5,5]},"6":{"rgb":[15,87,247]},"7":{"rgb":[255,169,24]}} +// probe-players: {"0":{"rgb":[19,255,29]},"2":{"rgb":[0,255,255]},"3":{"rgb":[255,255,73]},"4":{"rgb":[165,77,189]},"5":{"rgb":[255,5,5]},"6":{"rgb":[15,87,247]},"7":{"rgb":[255,169,24]}} // Built with // npx tsx scripts/build-fixture.mts probes/tetris.tsx fixtures/eud/tetris.scx -import { program, P1, P2, P3, P4, P5, P6, P7, P8, keyPressed, keyHeld, random, sleep, frames, ui, textColors, createUnit, centerLocation, units, type Location } from "trigscript"; +import { program, P1, P3, P4, P5, P6, P7, P8, keyPressed, keyHeld, random, sleep, frames, ui, textColors, createUnit, centerLocation, units, type Location } from "trigscript"; import { drawUnits } from "./lib/unit-screen"; const W = 10; @@ -28,7 +29,7 @@ const PIECES: [number, number][][] = [ ]; const COLORS = [textColors.cyan, textColors.yellow, textColors.purple, textColors.green, textColors.red, textColors.blue, textColors.orange]; /** Whose Overlords show each piece's colour: the player given that colour (probe-players above). */ -const OWNERS = [P2, P3, P4, P5, P6, P7, P8]; +const OWNERS = [P3, P4, P5, P1, P6, P7, P8]; const BOX = [4, 4, 3, 3, 3, 3, 3]; /** The cells of piece p turned r times, one after another as x, y: eight numbers a turn, thirty-two a piece. */ From ac84290c863d0b9556a4587476ffbdfead241703 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 22:36:19 -0600 Subject: [PATCH 054/238] TrigScript 4: a class from any file is the program's, no game() needed A class declared at the top of any of the script's files is the program's wherever a program makes an instance of it, and an ordinary class when the script is built. It is planned as a game function is (hoist.ts#planClass); the emitter adds __class(C, ...) after it, handing the run what its bodies compute at build time in its own file; in a program a script class is never hoisted, and the lowering declares the classes a program names (and theirs, and those of the game() functions it calls) before its first statement. Its errors are reported only where a program uses it. Co-Authored-By: Claude Opus 5.5 --- compiler/compiler.ts | 34 ++++++++++++++++-- compiler/declarations.ts | 2 ++ compiler/hoist.ts | 75 ++++++++++++++++++++++++++++++++++------ compiler/runtime.ts | 8 ++++- compiler/structured.ts | 57 +++++++++++++++++++++++++----- docs/v4-plan.md | 23 ++++++++++++ tests/classes.test.ts | 58 +++++++++++++++++++++++++++++-- 7 files changed, 232 insertions(+), 25 deletions(-) diff --git a/compiler/compiler.ts b/compiler/compiler.ts index 4130920..e1bded7 100644 --- a/compiler/compiler.ts +++ b/compiler/compiler.ts @@ -20,7 +20,7 @@ import type { TriggerRecord } from "../vendor/triggers"; import { MODULE_NAME } from "./api"; import { isTestFile, listTests, runTests, TestRegistry, TESTING_NAMES, type TestReport, type TestRunOptions } from "./testing"; import { DECLARATIONS_FILE, generateDeclarations } from "./declarations"; -import { libraryCallName, libraryName, planProgram, transformer, type ProgramPlan } from "./hoist"; +import { isScriptClass, libraryCallName, libraryName, planClass, planProgram, transformer, type ProgramPlan } from "./hoist"; import { locate, resolveModule, runModules, type LinkedFile } from "./link"; import { checkProgram } from "./eud"; import { typeNumbers } from "./numbers"; @@ -274,12 +274,21 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa /* ── Plan the programs and the game functions ── */ const plans = new Map(); + /** The classes at the top of the script's files, each planned as a game function is (`planClass`). */ + const classPlans = new Map(); const byPosition = new Map(); const fileIndex = (sf: TS.SourceFile) => fileNames.indexOf(sf.fileName); const buildTime: SourceRange[] = []; for (const name of fileNames) { const sf = program.getSourceFile(name)!; const visit = (node: TS.Node) => { + // Its errors wait for a program that makes an instance of it (`classBody` below); a class only ever used when the + // script is built has none. + if (isScriptClass(ts, node) && !isTestFile(name)) { + const plan = planClass(ts, checker, node); + classPlans.set(node, plan); + byPosition.set(`${fileIndex(sf)}:${node.getStart(sf)}`, { plan, sf, name: node.name!.text }); + } if (ts.isCallExpression(node)) { const lib = libraryCallName(ts, checker, node); const arrow = node.arguments[0]; @@ -313,7 +322,7 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa for (const d of diagnostics) planned.add(`${d.file}:${d.line}`); /* ── Emit ── */ - const emitted = program.emit(undefined, undefined, undefined, false, { before: [transformer(ts, checker, { fileIndex, planFor: (arrow) => plans.get(arrow) })] }); + const emitted = program.emit(undefined, undefined, undefined, false, { before: [transformer(ts, checker, { fileIndex, planFor: (arrow) => plans.get(arrow), planForClass: (cls) => classPlans.get(cls) })] }); for (const d of emitted.diagnostics) { if (d.category === ts.DiagnosticCategory.Error) diagnostics.push({ file: ENTRY_FILE, line: 1, column: 1, endLine: 1, endColumn: 1, message: ts.flattenDiagnosticMessageText(d.messageText, "\n"), source: "typescript" }); } @@ -358,6 +367,25 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa return b; }; const resolve = (fn: GameFunctionValue): Body | undefined => bodyOf(fn.descriptor) ?? undefined; + // A class of the script's own, when a program makes an instance of it: its plan, with what the run made of its bodies' + // build-time parts. Its plan's errors are reported now, once. + const classBodies = new Map(); + const classBody = (cls: TS.ClassDeclaration): Body | undefined => { + let b = classBodies.get(cls); + if (b === undefined) { + const sf = cls.getSourceFile(); + const key = `${fileIndex(sf)}:${cls.getStart(sf)}`; + const found = byPosition.get(key); + const ran = collector.classes.get(key); + b = null; + if (found && ran) { + try { b = newBody(found.plan, found.sf, ran.hoisted(), found.name); } catch { b = null; } + for (const e of found.plan.errors) nodeError(e.node, e.message); + } + classBodies.set(cls, b); + } + return b ?? undefined; + }; const triggers: TriggerRecord[] = []; const sources: (TriggerSource | null)[] = []; @@ -378,7 +406,7 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa if (!body) { diagnostics.push({ file, line: at.line, column: 1, endLine: at.line, endColumn: 2, message: "program(): the body could not be found again.", source: "compiler" }); continue; } const owners = entry.options.owners; const owner = owners.find((o) => o < PLAYER_SLOTS) ?? 0; - const emitted = new Structured({ ts, checker, body, owner, owners, perPlayer: entry.options.perPlayer, strings: collector.strings, error: (node, message, source) => nodeError(node, message, source), resolve }).run(); + const emitted = new Structured({ ts, checker, body, owner, owners, perPlayer: entry.options.perPlayer, strings: collector.strings, error: (node, message, source) => nodeError(node, message, source), resolve, classBody }).run(); if (entry.options.name) emitted.program.name = entry.options.name; // What every number is read as, written into the operations that care, before anything looks at the program. const mixes = typeNumbers(emitted.program); diff --git a/compiler/declarations.ts b/compiler/declarations.ts index d6ca76e..2c267ee 100644 --- a/compiler/declarations.ts +++ b/compiler/declarations.ts @@ -592,6 +592,8 @@ declare namespace JSX { } } declare function __jsx(tag: string, props: object | null, ...children: unknown[]): string; +/** What the build adds after each class at the top of a file, for a program that makes an instance of it. Not for scripts. */ +declare function __class(cls: unknown, descriptor: unknown): void; declare const __fragment: string; interface ArrayConstructor { new (arrayLength: number): T[]; diff --git a/compiler/hoist.ts b/compiler/hoist.ts index 114f518..4fb55e5 100644 --- a/compiler/hoist.ts +++ b/compiler/hoist.ts @@ -42,7 +42,10 @@ export type PlanItem = | { kind: "block"; items: PlanItem[] }; export interface ProgramPlan { - arrow: TS.ArrowFunction | TS.FunctionExpression; + /** The program's or the game function's arrow; unset for a class's plan (`planClass`). */ + arrow?: TS.ArrowFunction | TS.FunctionExpression; + /** What the plan covers: the arrow's body, or the class. */ + root: TS.Node; body: TS.Block; /** A `game()` function with an expression body (`game((a: number) => a + 1)`): what it returns. */ expression?: TS.Expression; @@ -170,11 +173,42 @@ export function planProgram(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.Ar } } -function planOnce(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunction | TS.FunctionExpression, options: PlanOptions, forced: Set): ProgramPlan { - const plan: ProgramPlan = { arrow, body: ts.isBlock(arrow.body) ? arrow.body : undefined as unknown as TS.Block, hoisted: [], index: new Map(), game: new Set(), consts: new Map(), constList: [], tree: [], errors: [], grows: new Set() }; +/** + * A class declared at the top of a script's file, planned as a game function is: it is ordinary JavaScript when the script + * is built, and the program's own wherever a program makes an instance of it — its parameters and its variables the + * game's, what its fields' first values and its bodies compute when the script is built hoisted. Its errors are reported + * only where a program uses it. + */ +export function planClass(ts: typeof TS, checker: TS.TypeChecker, cls: TS.ClassDeclaration): ProgramPlan { + const forced = new Set(); + for (;;) { + const before = forced.size; + const plan = planOnce(ts, checker, cls, { parameters: true }, forced); + if (forced.size === before) return plan; + } +} + +/** Whether a class is one of the script's own, declared at the top of one of its files: a program may make instances of it. */ +export function isScriptClass(ts: typeof TS, decl: TS.Node): decl is TS.ClassDeclaration { + return ts.isClassDeclaration(decl) && !!decl.name && ts.isSourceFile(decl.parent) && !decl.parent.isDeclarationFile && !decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword); +} + +function planOnce(ts: typeof TS, checker: TS.TypeChecker, of: TS.ArrowFunction | TS.FunctionExpression | TS.ClassDeclaration, options: PlanOptions, forced: Set): ProgramPlan { + if (ts.isClassDeclaration(of)) return planClassOnce(ts, checker, of, forced); + const arrow = of; + const plan: ProgramPlan = { arrow, root: arrow.body, body: ts.isBlock(arrow.body) ? arrow.body : undefined as unknown as TS.Block, hoisted: [], index: new Map(), game: new Set(), consts: new Map(), constList: [], tree: [], errors: [], grows: new Set() }; + return planWithin(ts, checker, plan, arrow, options, forced); +} + +function planClassOnce(ts: typeof TS, checker: TS.TypeChecker, cls: TS.ClassDeclaration, forced: Set): ProgramPlan { + const plan: ProgramPlan = { root: cls, body: ts.factory.createBlock([]), hoisted: [], index: new Map(), game: new Set(), consts: new Map(), constList: [], tree: [], errors: [], grows: new Set() }; + return planWithin(ts, checker, plan, undefined, { parameters: true }, forced); +} + +function planWithin(ts: typeof TS, checker: TS.TypeChecker, plan: ProgramPlan, arrow: TS.ArrowFunction | TS.FunctionExpression | undefined, options: PlanOptions, forced: Set): ProgramPlan { const error = (node: TS.Node, message: string) => plan.errors.push({ node, message }); const what = options.parameters ? "game()" : "program()"; - if (!ts.isBlock(arrow.body)) { + if (arrow && !ts.isBlock(arrow.body)) { if (!options.parameters) { error(arrow.body, "program() takes an arrow with a block body: program(() => { … })."); plan.body = ts.factory.createBlock([]); @@ -183,12 +217,12 @@ function planOnce(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunctio plan.expression = arrow.body; plan.body = ts.factory.createBlock([]); } - if (arrow.parameters.length && !options.parameters) error(arrow.parameters[0], "The program's arrow takes no parameters."); - if (arrow.asteriskToken) error(arrow, `A ${what} body cannot be a generator.`); + if (arrow && arrow.parameters.length && !options.parameters) error(arrow.parameters[0], "The program's arrow takes no parameters."); + if (arrow?.asteriskToken) error(arrow, `A ${what} body cannot be a generator.`); /* ── Bindings ── */ const declare = (node: TS.Node) => plan.game.add(node); - for (const p of arrow.parameters) { + for (const p of arrow?.parameters ?? []) { if (!options.parameters) break; if (!ts.isIdentifier(p.name)) error(p, "Destructured parameters are not supported in a game function."); else if (p.dotDotDotToken) error(p, "Rest parameters are not supported in a game function."); @@ -218,7 +252,7 @@ function planOnce(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunctio } ts.forEachChild(node, collect); }; - collect(arrow.body); + collect(plan.root); // A `const` list the body stores into — `const hp = [0, 0, 0]; hp[i] = 5`, which TypeScript allows — is an array of the // program like a `let` one; a list that is only read stays a value of the script, which a program may still look up in. const ARRAY_WRITERS = new Set(["push", "pop", "fill"]); @@ -265,8 +299,8 @@ function planOnce(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunctio } } }; - const inside = (decl: TS.Node) => { for (let n: TS.Node | undefined = decl; n; n = n.parent) if (n === arrow.body) return true; return false; }; - written(arrow.body); + const inside = (decl: TS.Node) => { for (let n: TS.Node | undefined = decl; n; n = n.parent) if (n === plan.root) return true; return false; }; + written(plan.root); for (const decl of forced) declare(decl); /* ── Hoistability ── */ @@ -312,6 +346,8 @@ function planOnce(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunctio if (property) return; const decl = declarationOf(ts, checker, n); if (decl && isGameDecl(decl)) { ok = false; return; } + // A class of the script's own: inside a program, `new C()` is an instance of the program's, whatever its arguments. + if (decl && isScriptClass(ts, decl)) { ok = false; return; } // A generator is walked by the program's for…of, a copy of its body at each loop: never run when the script is built. if (decl && (ts.isFunctionDeclaration(decl) || ts.isFunctionExpression(decl)) && decl.asteriskToken) { ok = false; return; } }; @@ -490,6 +526,7 @@ function planOnce(ts: typeof TS, checker: TS.TypeChecker, arrow: TS.ArrowFunctio return items; }; if (plan.expression) { const items: PlanItem[] = []; value(plan.expression, items); plan.tree = items; return plan; } + if (ts.isClassDeclaration(plan.root)) { plan.tree = classItems(plan.root); return plan; } plan.tree = block(plan.body.statements); return plan; } @@ -602,6 +639,8 @@ export interface TransformContext { fileIndex: (sf: TS.SourceFile) => number; /** The plan for a program's arrow, made before the transform. */ planFor: (arrow: TS.ArrowFunction | TS.FunctionExpression) => ProgramPlan | undefined; + /** The plan for a class of the script's own (`planClass`), made before the transform. */ + planForClass?: (cls: TS.ClassDeclaration) => ProgramPlan | undefined; } /** @@ -619,7 +658,7 @@ export function transformer(ts: typeof TS, checker: TS.TypeChecker, ctx: Transfo while (out.length < upTo) out.push(f.createIdentifier("undefined")); return out; }; - const visit = (node: TS.Node): TS.Node => { + const visit = (node: TS.Node): TS.VisitResult => { if (ts.isCallExpression(node)) { const lib = libraryCallName(ts, checker, node); if (lib === "trigger" && node.arguments.length >= 3 && node.arguments.length <= 4) { @@ -641,6 +680,20 @@ export function transformer(ts: typeof TS, checker: TS.TypeChecker, ctx: Transfo } } } + // `class Squad { … }` at the top of a file: the class as it is, and after it what its bodies compute when the script is + // built, handed to the run (`__class`), for a program that makes an instance of it. + if (ts.isClassDeclaration(node) && node.name && ts.isSourceFile(node.parent)) { + const plan = ctx.planForClass?.(node); + const visited = ts.visitEachChild(node, visit, context); + if (!plan) return visited; + const descriptor = f.createObjectLiteralExpression([ + f.createPropertyAssignment("__trigscript", f.createStringLiteral("class")), + f.createPropertyAssignment("at", at(node)), + f.createPropertyAssignment("pos", f.createNumericLiteral(node.getStart(sf))), + f.createPropertyAssignment("hoisted", hoistedFunction(ts, checker, plan, context)), + ], true); + return [visited, f.createExpressionStatement(f.createCallExpression(f.createIdentifier("__class"), undefined, [f.createIdentifier(node.name.text), descriptor]))]; + } return ts.visitEachChild(node, visit, context); }; return ts.visitNode(sf, visit) as TS.SourceFile; diff --git a/compiler/runtime.ts b/compiler/runtime.ts index 120915c..389a176 100644 --- a/compiler/runtime.ts +++ b/compiler/runtime.ts @@ -24,6 +24,7 @@ import { allTables, type ScriptNames } from "./names"; import type { RaceId, ReadSource, TableCell, TextPart, UnitFilter } from "./ir"; import { DEAF_KEYS, keyName, KEY_NAMES, MOUSE_BUTTONS, parseChatPattern, type ChatPattern, type InputSource, type MouseButton } from "./input"; import { JSX_FRAGMENT, TEXT_COLORS, builtText, jsxAtBuild } from "./jsx"; +import type { HoistedThunks } from "./hoist"; import { PLAYER_COLORS, TABLE_FIELDS, TABLE_SIZE, tableOfBrand, type TableField, type TableKind } from "./tables"; /** `memory(address, …)` reads `deaths` at player `EPD(address)`, unit 0: the deaths table starts here in 1.16.1's memory. */ @@ -199,6 +200,8 @@ export class ScriptError extends Error { /** What a run of the script produces, in execution order. */ export class Collector { readonly entries: Entry[] = []; + /** The classes at the top of the script's files, by where each is (`file:position`): what their bodies compute when the script is built. */ + readonly classes = new Map HoistedThunks }>(); readonly strings: ScriptString[] = []; /** The script emitted hyper triggers: the trigger loop runs twelve times a second, not once in two. */ hyper = false; @@ -615,6 +618,9 @@ export function createRuntime(names: ScriptNames, collector: Collector, options: rt.colors = Object.freeze({ ...PLAYER_COLORS }); /* ── JSX: a text as elements ── */ + // After each class at the top of a file (hoist.ts#transformer): what its bodies compute when the script is built, for a + // program that makes an instance of it. + rt.__class = (_cls: unknown, d: { at: [number, number]; pos: number; hoisted: () => HoistedThunks }) => { collector.classes.set(`${d.at[0]}:${d.pos}`, { hoisted: d.hoisted }); }; rt.textColors = TEXT_COLORS; rt.__jsx = jsxAtBuild; rt.__fragment = JSX_FRAGMENT; @@ -709,6 +715,6 @@ export function runtimeNames(names: ScriptNames): string[] { out.push("seconds", "minutes", "frames", "cycles", "sleep", "until", "start", "rose", "once", "shared", "clamp", "u32", "i32"); out.push(...READER_NAMES, "races", "slots", "name", "color", "print"); out.push(...UNIT_CALL_NAMES, "colors", ...INPUT_CALL_NAMES); - out.push("textColors", "ui", "__jsx", "__fragment"); + out.push("textColors", "ui", "__jsx", "__fragment", "__class"); return out; } diff --git a/compiler/structured.ts b/compiler/structured.ts index bb6b648..ac507c7 100644 --- a/compiler/structured.ts +++ b/compiler/structured.ts @@ -60,7 +60,7 @@ import type * as TS from "typescript"; import { ActionType, PlayerGroup } from "../vendor/triggers"; import type { ActionRecord } from "../vendor/triggers"; import type { HoistedThunks, ProgramPlan } from "./hoist"; -import { declarationOf, libraryCallName } from "./hoist"; +import { declarationOf, isScriptClass, libraryCallName } from "./hoist"; 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 } from "./runtime"; import { cellMax } from "./tables"; @@ -100,6 +100,8 @@ export interface StructuredContext { error(node: TS.Node, message: string, source?: "compiler" | "script"): void; /** The body of a `game()` function, from the value the run made for it; undefined when the compiler cannot place it. */ resolve(fn: GameFunctionValue): Body | undefined; + /** The body of a class at the top of one of the script's files (`hoist.ts#planClass`); undefined when there is none. */ + classBody?(cls: TS.ClassDeclaration): Body | undefined; } /** A hoisted expression threw when it was evaluated: reported at the expression, as the script's own error. */ @@ -333,6 +335,7 @@ export class Structured { this.nodes.set(program, this.body.plan.body); this.out = program.body; try { + this.declareScriptClasses(this.body.plan.root); this.block(statements, {}, this.topScope); } catch (err) { if (!(err instanceof LowerError)) throw err; @@ -1167,7 +1170,7 @@ export class Structured { if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isElementAccessExpression(n.left) && mine(n.left.expression)) take(n.right); ts.forEachChild(n, walk); }; - walk(this.body.plan.arrow.body); + walk(this.body.plan.root); const init = d.initializer && this.unwrap(d.initializer); if (init && ts.isArrayLiteralExpression(init)) init.elements.forEach(take); if (made.size > 1) { this.c.error(d, `${d.name.getText(this.body.sf)} would hold ${[...made].map((c) => this.className(c)).join(" and ")}: an array of instances holds one class, since a row has the fields of one. Keep an array for each.`); return null; } @@ -2000,7 +2003,7 @@ export class Structured { else if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(n.left) && n.left.name.text === "length" && rowOfDecl(n.left.expression)) found = true; else ts.forEachChild(n, walk); }; - walk(this.body.plan.arrow.body); + walk(this.body.plan.root); return found; } @@ -2014,7 +2017,7 @@ export class Structured { } ts.forEachChild(n, walk); }; - walk(this.body.plan.arrow.body); + walk(this.body.plan.root); return out; } @@ -6061,20 +6064,57 @@ export class Structured { /** The constructors being walked, innermost last: whose `super(…)` a call of it is, the instance, and the name its fields are declared under. */ private readonly constructing: { cls: TS.ClassDeclaration; self: Instance; name: string }[] = []; - /** The class an expression names, when it is one declared in the body. */ + /** The class an expression names, when it is one declared in the body or one of the script's own (`scriptClass`). */ private classOf(expr: TS.Expression): TS.ClassDeclaration | undefined { const { ts } = this; const e = this.unwrap(expr); if (!ts.isIdentifier(e)) return undefined; const decl = declarationOf(ts, this.c.checker, e); - return decl && ts.isClassDeclaration(decl) && this.body.plan.game.has(decl) ? decl : undefined; + return decl && ts.isClassDeclaration(decl) && (this.body.plan.game.has(decl) || this.scriptClass(decl)) ? decl : undefined; } - /** The class a type is an instance of, when it is one declared in the body. */ + /** The class a type is an instance of, when it is one declared in the body or one of the script's own. */ private classOfType(type: TS.Type): TS.ClassDeclaration | undefined { const { ts } = this; const decl = type.getSymbol()?.valueDeclaration; - return decl && ts.isClassDeclaration(decl) && this.body.plan.game.has(decl) ? decl : undefined; + return decl && ts.isClassDeclaration(decl) && (this.body.plan.game.has(decl) || this.scriptClass(decl)) ? decl : undefined; + } + + /** + * A class declared at the top of one of the script's files: the program's wherever a program makes an instance of it, + * its bodies walked in their own file with what they compute when the script is built (`hoist.ts#planClass`). Known + * once `run` has declared it — its static fields — before the program's first statement. + */ + private scriptClass(decl: TS.ClassDeclaration): boolean { + return this.classBodies.has(decl) && isScriptClass(this.ts, decl); + } + + /** The script's classes a program uses — the classes it names, and theirs in turn, what they extend first — declared before its first statement. */ + private declareScriptClasses(root: TS.Node) { + const { ts } = this; + const seen = new Set(); + const visit = (n: TS.Node) => { + if (ts.isIdentifier(n)) { + const decl = declarationOf(ts, this.c.checker, n); + if (decl && isScriptClass(ts, decl) && !seen.has(decl)) use(decl); + // A game() function the program calls, wherever it is written: the classes it names too. + const init = decl && ts.isVariableDeclaration(decl) ? decl.initializer : undefined; + if (init && ts.isCallExpression(init) && !followed.has(init) && libraryCallName(ts, this.c.checker, init) === "game") { followed.add(init); init.arguments.forEach(visit); } + } + ts.forEachChild(n, visit); + }; + const followed = new Set(); + const use = (cls: TS.ClassDeclaration) => { + seen.add(cls); + const body = this.c.classBody?.(cls); + if (!body) return; + // What it extends, and the classes its bodies name, first: a static field's first value may make one. + visit(cls); + if (this.classBodies.has(cls)) return; + const saved = this.enterBody(body); + try { this.declareClass(cls); } finally { this.leaveBody(saved); } + }; + visit(root); } /** What a class extends: a class of the body, null when it extends nothing, undefined — with a diagnostic — when it extends anything else. */ @@ -6351,6 +6391,7 @@ export class Structured { const target = this.c.resolve(fn); if (!target) { this.c.error(call, "This game function's body could not be found again."); return undefined; } const arrow = target.plan.arrow; + if (!arrow) { this.c.error(call, "This game function's body could not be found again."); return undefined; } return this.inline(call, arrow.parameters, target.plan.expression ?? (arrow.body as TS.Block), target, target.name, arrow); } diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 41a401d..630a358 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -843,6 +843,29 @@ known when the script is built, so its place and its wait are two cells, like a `probes/game-generators.ts` (3) pass in the lifted game and in the simulator at the same frames. Every older game probe still passes. +### Classes from any file (built 2026-10-16, between B1 and B2) + +Asked for while S2's Tetris grew a library (`probes/lib/unit-screen.ts`): a class declared at +the top of any of the script's files is the program's wherever a program makes an instance of +it, with no `game()` around it, and stays an ordinary class when the script is built +(`const AT_BUILD = new V(21).double()` beside `new V(n)` in a program). + +- **Planned as a game function is** (`hoist.ts#planClass`): its parameters and variables are + the game's, and what its field values and bodies compute when the script is built is + hoisted. The emitter adds `__class(C, { at, pos, hoisted })` after each such class, so the + run hands its own file's values — its imports, its constants — to the lowering. +- **In a program**, a script class is never hoisted (`new C(1)` is an instance, whatever its + arguments). Before the program's first statement the lowering declares every script class + the program names, the ones they name and extend, and those of the `game()` functions it + calls — their static fields — then walks their bodies in their own files. +- **Errors wait for use:** a class only ever used when the script is built reports nothing. +- **The rules of a program's class still hold:** an array field has its first value where it + is declared, a field holds no build-time object, and an action's player and location are + known when the script is built — so the unit screen takes them with each `draw(board, look)`. +- Checked: same file, another file with its own imports, `extends` across files with a static + field, a class used at build time and in a program, errors only where used; Tetris's + `UnitScreen` follows the board in the lifted game as the function did. + ### B2. Events (4.4.0) ```ts diff --git a/tests/classes.test.ts b/tests/classes.test.ts index 099fdb2..43b5a4b 100644 --- a/tests/classes.test.ts +++ b/tests/classes.test.ts @@ -204,8 +204,8 @@ describe("a method is a function", () => { }); describe("what is said when a class cannot be the program's", () => { - it("a class declared outside the program is the script's", () => { - expect(messages("let n = 1; const c = new Outside(n);", "class Outside { constructor(public n: number) {} }").length).toBeGreaterThan(0); + it("a class declared outside the program is the program's too, since TrigScript 4", () => { + expect(messages("let n = 1; const c = new Outside(n); print(`${c.n}`);", "class Outside { constructor(public n: number) {} }")).toEqual([]); }); it("a method that is not there", () => { expect(messages("class A { n = 0; } const a = new A(); (a as any).gone();").join("\n")).toMatch(/has no method gone/); @@ -232,3 +232,57 @@ describe("the probe", () => { expect(sim.events.some((e) => e.text === "done")).toBe(true); }); }); + +describe("a class declared outside the program (TrigScript 4)", () => { + const files = (main: string, more: Record = {}): CompileResult => compileScript(ts, { "main.ts": main, ...more }, NAMES, { lib: LIB }); + const ok = (r: CompileResult) => { expect(r.diagnostics.map((d) => `${d.file}:${d.line}: ${d.message}`)).toEqual([]); return r; }; + const printed = (r: CompileResult, frames = 1) => simulatePrograms(r.ir, frames, { strings: r.strings }).events.map((e) => e.text); + + it("is the program's where a program makes an instance of it, in the same file", () => { + const r = ok(files(`${COUNTER}\nprogram(() => { let k = 2; const c = new Counter(k); c.bump(); c.bump(); c.twice = 10; c.bump(); print(\`\${c.n} \${c.twice}\`); });`)); + expect(printed(r)).toEqual(["7 14"]); + }); + + it("comes from another file, with what its bodies use from its own file's imports", () => { + const r = ok(files( + `import { Squad } from "./squad";\nprogram(() => { const s = new Squad(1); s.add(3); s.add(4); s.spawn(); print(\`\${s.size} \${s.total}\`); });`, + { "squad.ts": `import { createUnit, units, P2, type Location } from "trigscript"; + const HOME = 1 as Location; + const BONUS = [10, 20, 30].reduce((a, b) => a + b, 0); + export class Squad { + tag: number; counts: number[] = []; total = BONUS; + constructor(tag: number) { this.tag = tag; } + add(n: number) { this.counts.push(n); this.total += n; } + get size() { return this.counts.length; } + spawn() { for (const n of this.counts) createUnit(P2, units.ZergZergling, n, HOME); } + }` }, + )); + const sim = simulatePrograms(r.ir, 1, { strings: r.strings }); + expect(sim.events.filter((e) => e.action.type === 9).map((e) => e.text)).toEqual(["2 67"]); + // BONUS was worked out in squad.ts when the script was built: 60, then 3 and 4 added in the game. + expect(sim.events.filter((e) => e.action.type === 44)).toHaveLength(2); + }); + + it("extends another class of the script's, across files, and keeps static fields as the program's", () => { + const r = ok(files( + `import { Hero } from "./hero";\nprogram(() => { const a = new Hero(5); const b = new Hero(7); a.hurt(2); print(\`\${a.hp} \${b.hp} \${Hero.made} \${a.name()}\`); });`, + { + "unit.ts": `export class Thing { hp: number; static made = 0; constructor(hp: number) { this.hp = hp; Thing.made++; } hurt(n: number) { this.hp -= n; } name() { return "thing"; } }`, + "hero.ts": `import { Thing } from "./unit";\nexport class Hero extends Thing { constructor(hp: number) { super(hp * 10); } name() { return "hero"; } }`, + }, + )); + expect(printed(r)).toEqual(["48 70 2 hero"]); + }); + + it("is still an ordinary class when the script is built, beside the program's instances", () => { + const r = ok(files(`class V { x: number; constructor(x: number) { this.x = x; } double() { return this.x * 2; } }\nconst AT_BUILD = new V(21).double();\nprogram(() => { let n = 1; const v = new V(n); n = v.double(); print(\`\${AT_BUILD} \${n}\`); });`)); + expect(printed(r)).toEqual(["42 2"]); + }); + + it("says what is wrong only where a program uses it", () => { + const unused = files(`class Tool { f = () => 1; run(n: number) { return n; } }\nconst t = new Tool();\nprogram(() => { print("x"); });`); + expect(unused.diagnostics).toEqual([]); + const used = files(`class Tool { f = () => 1; run(n: number) { return n; } }\nprogram(() => { let k = 1; const t = new Tool(); k = t.run(k); print(\`\${k}\`); });`); + expect(used.diagnostics.length).toBeGreaterThan(0); + }); +}); From 13c71330929cacc04b788308bbdf4ca0d9dfbee2 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 22:36:19 -0600 Subject: [PATCH 055/238] TrigScript 4, S2: the unit screen is a class Co-Authored-By: Claude Opus 5.5 --- probes/lib/unit-screen.ts | 65 +++++++++++++++++++++++---------------- probes/tetris.tsx | 12 ++++---- 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/probes/lib/unit-screen.ts b/probes/lib/unit-screen.ts index 0c0083f..0d410f1 100644 --- a/probes/lib/unit-screen.ts +++ b/probes/lib/unit-screen.ts @@ -3,35 +3,48 @@ // takes away and makes only the cells that changed — a location centred on the cell, Remove Unit and Create Unit there. // // What the unit-pixel probe found, played in Remastered (2026-10-16): Overlords sit on their cells on any ground; a -// computer's AI moves its own units, so the owners are the empty slots; neighbours of one owner join into one block on -// the minimap; 80 made and taken away a frame did not slow the game; a Create Unit that fails prints a warning in the -// text lines, which an Overlord never does. The map needs a 64 × 64 location for the cells (the probe maps' "Pixel"). +// computer moves its own units (its idle order), so the owners are you and the empty slots; neighbours of one owner join +// into one block on the minimap; 80 made and taken away a frame did not slow the game; a Create Unit that fails prints +// a warning in the text lines, which an Overlord never does. The map needs a 64 × 64 location for the cells (the probe +// maps' "Pixel"). // -// A game() function, since a function from another file reaches a program that way; a class would read better, and a -// class declared outside a program is not the program's yet. -import { game, centerLocation, createUnit, removeUnitAt, units, players, P3, P4, P5, P6, P7, P8, type Location, type Player } from "trigscript"; - -/** Where a board is drawn: its corner on the map, its width in cells, a cell's size, and the location moved to each cell. */ -export interface UnitBoard { x: number; y: number; width: number; cell: number; at: Location } -/** What a board's cell holds — a colour each piece has — and the player whose Overlord shows each. */ -export interface Palette { colors: readonly number[]; owners: readonly Player[] } - -/** The empty slots and Player 12: units no computer moves. */ -export const OWNERS: readonly Player[] = [P3, P8, P4, P6, players.P12, P7, P5]; +// An ordinary class: declared outside any program, it is the program's wherever a program makes one (TrigScript 4). +import { centerLocation, createUnit, removeUnitAt, units, type Location, type Player } from "trigscript"; /** - * The board on the map. `shown` is what each cell shows now — the board as it was the last time — and is brought up to - * date: each cell that changed is taken away and made again in its colour (0 is an empty cell, nothing on the map). + * How the cells look, known when the script is built: each colour a cell can hold, the player whose Overlord shows it, + * and the location moved to each cell — an action's player and location are, so they come with each draw, not as fields. */ -export const drawUnits = game((board: number[], shown: number[], where: UnitBoard, palette: Palette) => { - for (let i = 0; i < shown.length; i++) { - const now = board[i]; - const was = shown[i]; - if (now !== was) { - centerLocation(where.at, where.x + (i % where.width) * where.cell + where.cell / 2, where.y + Math.floor(i / where.width) * where.cell + where.cell / 2); - palette.colors.forEach((color, k) => { if (was === color) removeUnitAt(palette.owners[k], units.ZergOverlord, 1, where.at); }); - palette.colors.forEach((color, k) => { if (now === color) createUnit(palette.owners[k], units.ZergOverlord, 1, where.at); }); - shown[i] = now; +export interface Look { colors: readonly number[]; owners: readonly Player[]; at: Location } + +export class UnitScreen { + /** What each cell shows on the map now: the board as it was the last time it was drawn. */ + shown: number[] = []; + x: number; + y: number; + width: number; + cell: number; + + /** A board `width` × `height` cells, its corner at (x, y) on the map, a cell `cell` pixels. */ + constructor(x: number, y: number, width: number, height: number, cell: number) { + this.x = x; + this.y = y; + this.width = width; + this.cell = cell; + for (let i = 0; i < width * height; i++) this.shown.push(0); + } + + /** The board on the map: each cell that changed since the last time, taken away and made again in its colour (0 is empty). */ + draw(board: number[], look: Look) { + for (let i = 0; i < this.shown.length; i++) { + const now = board[i]; + const was = this.shown[i]; + if (now !== was) { + centerLocation(look.at, this.x + (i % this.width) * this.cell + this.cell / 2, this.y + Math.floor(i / this.width) * this.cell + this.cell / 2); + look.colors.forEach((color, k) => { if (was === color) removeUnitAt(look.owners[k], units.ZergOverlord, 1, look.at); }); + look.colors.forEach((color, k) => { if (now === color) createUnit(look.owners[k], units.ZergOverlord, 1, look.at); }); + this.shown[i] = now; + } } } -}); +} diff --git a/probes/tetris.tsx b/probes/tetris.tsx index 298a615..c772446 100644 --- a/probes/tetris.tsx +++ b/probes/tetris.tsx @@ -1,7 +1,7 @@ // Tetris in StarCraft's text lines and on its minimap (TrigScript 4, S2's showpiece). The game writes one board; two // screens show it. The text lines are JSX: ui.lines() shows them to Player 1, redrawn when the board changes. The other // is a board of Overlords in the top left corner of the map, one a cell in its piece's colour, which the minimap shows as -// solid blocks: probes/lib/unit-screen.ts, a function that knows nothing of Tetris. A player a piece owns the Overlords, +// solid blocks: probes/lib/unit-screen.ts, a class that knows nothing of Tetris. A player a piece owns the Overlords, // each given its piece's colour as the text lines draw it (measured from screenshots). Remastered will not start a game // without a computer, and a computer moves its own units and sends them to fight, so Player 2 stays a computer and owns // none: the S piece is yours — your units are green on the minimap anyway — and P3 … P8 have the other six. A D move (hold to slide), W turns, S drops (hold to keep @@ -12,7 +12,7 @@ // Built with // npx tsx scripts/build-fixture.mts probes/tetris.tsx fixtures/eud/tetris.scx import { program, P1, P3, P4, P5, P6, P7, P8, keyPressed, keyHeld, random, sleep, frames, ui, textColors, createUnit, centerLocation, units, type Location } from "trigscript"; -import { drawUnits } from "./lib/unit-screen"; +import { UnitScreen } from "./lib/unit-screen"; const W = 10; const H = 10; @@ -53,13 +53,13 @@ const ROWS = [...Array(H).keys()]; * the base, so the minimap's view box is never over it — and a cell, 64 pixels. */ const MAP_X = 128, MAP_Y = 128, CELL = 64; const PIXEL = 3 as Location; -const ON_MAP = { x: MAP_X, y: MAP_Y, width: W, cell: CELL, at: PIXEL }; +/** How the Overlord board looks: a piece's colour, the player whose Overlords show it, the location moved to each cell. */ +const LOOK = { colors: COLORS, owners: OWNERS, at: PIXEL }; const COLS = [...Array(W).keys()]; program(async () => { const board: number[] = new Array(W * H).fill(0); - /** What the board of Overlords shows now. */ - let shown: number[] = new Array(W * H).fill(0); + const screen = new UnitScreen(MAP_X, MAP_Y, W, H, CELL); /** Whether the board was written since the Overlords were last drawn. */ let changed = true; // Sight over the board, for the minimap. @@ -183,7 +183,7 @@ program(async () => { if (!move(0, 1, 0)) land(); } } - if (changed) { drawUnits(board, shown, ON_MAP, { colors: COLORS, owners: OWNERS }); changed = false; } + if (changed) { screen.draw(board, LOOK); changed = false; } await sleep(frames(1)); } }, { owner: P1 }); From fb8e5bb2654288e6c4114e68a6c788a90d6209fe Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 16 Oct 2026 23:41:13 -0600 Subject: [PATCH 056/238] =?UTF-8?q?TrigScript=204,=20B2:=20events=20?= =?UTF-8?q?=E2=80=94=20on.created,=20on.died,=20on.chat,=20on.key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each on.… is a listener task (IR 18: start.listen) inside an async program that hands on what happened from the next frame: a handler that is not async is inlined, an async one is started each time. Units come from a pass once a frame that walks the game's visible and dying lists from their heads to where the last pass left them, with a slot table of its own; nothing of the game is written (unitLoop.source). on.chat and on.key are unrolled over the players, so the player is known in the handler. Idle, a listener costs about 46 conditions and actions a frame (2 for each more); a unit made about 100, a death about 80. The build-time evaluator now takes object literals and a record's known fields. Co-Authored-By: Claude Opus 5.5 --- compiler/declarations.ts | 40 +++++ compiler/eud.ts | 12 +- compiler/generated/trigscriptPy.ts | 2 +- compiler/hoist.ts | 12 +- compiler/ir.ts | 16 +- compiler/runtime.ts | 9 +- compiler/simulateIr.ts | 36 +++- compiler/structured.ts | 259 +++++++++++++++++++++++++++-- compiler/world.ts | 7 +- docs/ir.md | 32 +++- docs/v4-plan.md | 57 +++++++ probes/events.ts | 81 +++++++++ probes/game-events.ts | 77 +++++++++ python/trigscript.py | 201 +++++++++++++++++++++- scripts/test-game.mts | 1 + tests/eud-build.test.ts | 18 +- tests/events.test.ts | 105 ++++++++++++ tests/functions.test.ts | 2 +- tests/script-structured.test.ts | 2 +- tests/service.test.ts | 4 +- 20 files changed, 935 insertions(+), 38 deletions(-) create mode 100644 probes/events.ts create mode 100644 probes/game-events.ts create mode 100644 tests/events.test.ts diff --git a/compiler/declarations.ts b/compiler/declarations.ts index 2c267ee..dac61cc 100644 --- a/compiler/declarations.ts +++ b/compiler/declarations.ts @@ -429,6 +429,46 @@ ${kw}const ui: { */ lines(player: Player, view: () => string): Task; }; +/** A unit as it was when it died, which is what on.died() hands its function: the unit itself is gone. */ +${kw}interface DeadUnit { + readonly type: UnitType; + readonly owner: Player; + /** Where it died, in pixels (32 a tile). */ + readonly x: number; + readonly y: number; +} +/** How many of an event's handlers can be waiting at once, when the handler is async (16 unless said, up to 255). */ +${kw}interface EventOptions { max?: number } +/** + * What happens in the game, as it happens: on.created(…), on.died(…), on.chat(…), on.key(…) each runs its function for + * every time it happens from the next frame on, inside an async program only. Each is a task (it keeps listening after + * the program moves on, and the Task it gives back stops it). A function that is not async runs within the frame, once + * for each time; an async one is started as a task each time, so it can wait: \`max\` of them at once. + */ +${kw}const on: { + /** + * A unit comes onto the map — made by an action or a program, trained, hatched, landed, placed by a test — and the + * function is handed it in the next frame. A unit in a transport or a hangar comes on when it first comes out. + * \`on.created(units.ZergZergling, (u) => { u.energy = 50; })\` + */ + created(handler: (unit: Unit) => void | Promise, options?: EventOptions): Task; + created(filter: UnitType | UnitFilter, handler: (unit: Unit) => void | Promise, options?: EventOptions): Task; + /** + * A unit dies or is removed, and the function is handed what it was and where, in the next frame — once for every + * unit on.created() would have been handed. The filter is of the unit as it died. + * \`on.died({ owner: P8 }, async (u) => { await sleep(seconds(2)); createUnit(P8, u.type, 1, locations.Spawn); })\` + */ + died(handler: (unit: DeadUnit) => void | Promise, options?: EventOptions): Task; + died(filter: UnitType | UnitFilter, handler: (unit: DeadUnit) => void | Promise, options?: EventOptions): Task; + /** + * A player types a line that fits the pattern, which is chatted()'s: \`on.chat("-give {n}", (p, { n }) => setResources(p, "add", n, "ore"))\`. + * Every player P1 … P8 unless \`from\` names one (CurrentPlayer in a program for several players); \`p\` is known + * when the script is built, so an action can be for them. + */ + chat(pattern: P, handler: (player: Player, values: ChatValues

) => void | Promise, options?: EventOptions & { from?: Player }): Task; + /** A player presses a key, as keyPressed() sees it: \`on.key("F2", (p) => print("Help", { to: p }))\`. Every player P1 … P8 unless \`from\` names one. */ + key(key: Key, handler: (player: Player) => void | Promise, options?: EventOptions & { from?: Player }): Task; +}; /** * What the players do, inside program() only. A key, a click and a typed line are true on the one * frame they arrive, so look for them in a loop that runs every frame: diff --git a/compiler/eud.ts b/compiler/eud.ts index 8927974..f1bbaf2 100644 --- a/compiler/eud.ts +++ b/compiler/eud.ts @@ -101,8 +101,8 @@ function expressions(body: Stmt[], visit: (e: NumExpr | BoolExpr) => void, pick? body.forEach(stmt); } -/** Every statement of a list, nested ones and calls' bodies (as statements) included. */ -function statements(body: Stmt[], visit: (s: Stmt) => void) { +/** Every statement of a list, nested ones and calls' bodies (as statements) included; a started task's body unless `tasks` is false. */ +function statements(body: Stmt[], visit: (s: Stmt) => void, tasks = true) { const stmt = (s: Stmt) => { visit(s); switch (s.kind) { @@ -111,7 +111,7 @@ function statements(body: Stmt[], visit: (s: Stmt) => void) { case "for": s.body.forEach(stmt); s.update.forEach(stmt); break; case "unrolled": s.iterations.forEach((i) => i.forEach(stmt)); break; case "parallel": s.branches.forEach((b) => b.forEach(stmt)); break; - case "start": s.call.body.forEach(stmt); break; + case "start": if (tasks) s.call.body.forEach(stmt); break; case "switch": s.cases.forEach((c) => c.body.forEach(stmt)); break; case "call": s.call.body.forEach(stmt); break; case "block": s.body.forEach(stmt); break; @@ -128,7 +128,7 @@ function checkUnitLoops(body: Stmt[], out: ProgramDiagnostic[]) { statements(s.body, (inner) => { if (inner.kind === "parallel") out.push({ at: inner.at, message: "await inside a loop over units: the loop looks at the game's units as they are in one frame and cannot be left half way. Collect the units into an array first, and await in a loop over that array." }); if (inner.kind === "sleep") out.push({ at: inner.at, message: "sleep() inside a loop over units: the loop looks at the game's units as they are in one frame and cannot be left half way. To do something to one unit at a time, find it again after each sleep: while (true) { const u = first(…); if (!u) break; u.kill(); sleep(seconds(1)); }" }); - }); + }, false); }); } @@ -139,7 +139,7 @@ function checkTextLoops(body: Stmt[], out: ProgramDiagnostic[]) { statements(s.body, (inner) => { if (inner.kind === "parallel") out.push({ at: inner.at, message: "await inside a for…of over a text: the loop walks the text once, within the frame. A loop over its places can wait between turns: for (let i = 0; i < s.length; i++) { … }" }); if (inner.kind === "sleep") out.push({ at: inner.at, message: "sleep() inside a for…of over a text: the loop walks the text once, within the frame. A loop over its places can sleep between turns: for (let i = 0; i < s.length; i++) { const ch = s[i]; … sleep(frames(2)); }" }); - }); + }, false); }); } @@ -157,7 +157,7 @@ function scans(program: Program, out: LineHint[]) { for (const body of bodiesOf(program)) statements(body, (s) => { if (s.kind === "for" && s.sorts) hint(s.at, "sorts in the frame", `${s.sorts} is sorted where it stands, within the frame: each item is taken in hand and the ones that go after it moved up. Nearly in order already, that is a pass over the list; in no order it is the length squared — a few dozen items are nothing, a few hundred every frame will be felt.`); }); // A call that stands in an expression (`unitsOf(P1).some(…)` in an if) has statements of its own, which the walk over statements does not reach. const everywhere = (body: Stmt[], visit: (s: Stmt) => void) => { statements(body, visit); expressions(body, (e) => { if (e.kind === "call") statements(e.call.body, visit); }); }; - for (const body of bodiesOf(program)) everywhere(body, (s) => { if (s.kind === "unitLoop") hint(s.at, "scans units", `Looks at ${SLOTS} every time the line runs${each}, and runs the body for the ones that match. Fine once or a few times a second; inside a loop that runs every frame, ask whether it needs to.`); }); + for (const body of bodiesOf(program)) everywhere(body, (s) => { if (s.kind === "unitLoop" && !s.source) hint(s.at, "scans units", `Looks at ${SLOTS} every time the line runs${each}, and runs the body for the ones that match. Fine once or a few times a second; inside a loop that runs every frame, ask whether it needs to.`); }); for (const body of bodiesOf(program)) expressions(body, () => {}, (u) => hint(u.at, u.by === "random" ? "scans units ×2" : "scans units", u.by === "random" ? `Looks at ${SLOTS} twice every time the line runs${each}: once to count the units that match, once to take the one drawn.` : `Looks at ${SLOTS} every time the line runs${each}. Keep the unit in a variable when several lines need it.`)); diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index 7171e8d..dcb6ecf 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 17\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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\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 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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 self.scan(st.get(\"filter\", {}), st, body)\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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 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 EUDJumpIf(self.again >= 1, sweep)\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 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 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 op == \">\":\n return av > b\n return av >= b\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 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 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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":

, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\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 if INPUT.held:\n INPUT.frame()\n for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 18\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# 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 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# 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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 is walked from its head to the record that was behind the head at the pass before, the units linked in since\n lying in between; when that record has left the list the walk goes to the end, which costs more and misses nothing.\n Nothing of the game is written.\"\"\"\n\n def __init__(self, created, died):\n self.created, self.died = created, died\n self.seen = SlotTable()\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 = {h: [EUDVariable(0) for _ in range(5)] for h in (VISIBLE_HEAD, DYING_HEAD)} # initial: no pass before\n\n def cell(self, epd):\n return epd + (EPD(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 run(self):\n self.made_n << 0\n self.gone_n << 0\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(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\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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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, count = (EVENTS.made, EVENTS.made_n) if source == \"created\" else (EVENTS.gone, EVENTS.gone_n)\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 == \"created\":\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 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 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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 (EVENTS.made_n if source == \"created\" else EVENTS.gone_n) >= 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 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 op == \">\":\n return av > b\n return av >= b\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 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 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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\n_DIED = uses_source(IR.get(\"programs\", []), \"died\")\nEVENTS = Events(uses_source(IR.get(\"programs\", []), \"created\"), _DIED) if _DIED or uses_source(IR.get(\"programs\", []), \"created\") 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 if INPUT.held:\n INPUT.frame()\n if EVENTS is not None:\n EVENTS.run()\n for prog in PROGRAMS:\n prog.run()\n"; diff --git a/compiler/hoist.ts b/compiler/hoist.ts index 4fb55e5..6517e1f 100644 --- a/compiler/hoist.ts +++ b/compiler/hoist.ts @@ -101,6 +101,12 @@ export function libraryCallName(ts: typeof TS, checker: TS.TypeChecker, call: TS return id ? libraryName(ts, checker, id) : null; } +/** Whether a call is one of `on`'s — `on.died(…)` — with `on` the library's own object. */ +export function isOnCall(ts: typeof TS, checker: TS.TypeChecker, call: TS.CallExpression): boolean { + const callee = call.expression; + return ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) && libraryName(ts, checker, callee.expression) === "on"; +} + /** Climb from a binding element to the declaration that owns it. */ function owningDeclaration(ts: typeof TS, decl: TS.Declaration): TS.Node { let d: TS.Node = decl; @@ -108,10 +114,13 @@ function owningDeclaration(ts: typeof TS, decl: TS.Declaration): TS.Node { return d; } +/** What `on.…` listens for (TrigScript 4, B2): `on.created(filter, handler)` and the rest. */ +export const ON_EVENTS = ["created", "died", "chat", "key"] as const; + /** The library calls that must not appear inside a program body: they would run at build time, silently. */ const FORBIDDEN_INSIDE = new Set(["trigger", "program", "hyperTriggers", "game"]); /** The library calls the game answers: never hoisted, the structured compiler lowers them. */ -const GAME_CALLS = new Set(["random", "sleep", "until", "start", "rose", "once", "shared", "lines", "centerLocation"]); +const GAME_CALLS = new Set(["random", "sleep", "until", "start", "rose", "once", "shared", "lines", "centerLocation", ...ON_EVENTS]); /** * Calls that read the game — `minerals(P1)`, and a comparing condition without its comparison * (`deaths(P1, unit)`) — or show text a program fills in (`print`). The call is never a @@ -153,6 +162,7 @@ export function isTaskFunction(ts: typeof TS, checker: TS.TypeChecker, node: TS. if (!call.arguments.includes(at as TS.Expression)) return false; if (ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === "then") return true; const lib = libraryCallName(ts, checker, call); + if (lib && (ON_EVENTS as readonly string[]).includes(lib) && isOnCall(ts, checker, call)) return call.arguments.includes(at as TS.Expression); return lib === "until" || (lib === "start" && call.arguments[0] === at) || (lib === "lines" && call.arguments[1] === at); } diff --git a/compiler/ir.ts b/compiler/ir.ts index a29f704..9c495bf 100644 --- a/compiler/ir.ts +++ b/compiler/ir.ts @@ -10,6 +10,7 @@ import type { ActionRecord, ConditionRecord } from "../vendor/triggers"; import type { InputSource } from "./input"; /** + * 18: events — `unitLoop.source`: the units that came onto the map (`created`) or died (`died`) since the frame before, as the frame's events pass found them, where a loop without one goes through the unit table. * 17: texts built a piece at a time — `buildText` writes parts into a buffer (`reset` starts it empty) and `built` is what the buffer holds, which is how a JSX text is put together without copying it at every piece; `char` among the parts, one byte from a number (a colour code). * 16: tasks — `start`: a task that runs beside what started it, a copy of an async function's body with the arguments it was started with; `cancel` and `running` on what `start()` gives back. A task's variables and arrays (`VarDecl.task`, `ArrayDecl.task`) are a row a task, as many rows as tasks of it can run at once. * 15: `parallel` — `await Promise.all([…])` / `Promise.race([…])`: branches that run side by side, each a coroutine of its own inside the program's frame. @@ -27,7 +28,7 @@ import type { InputSource } from "./input"; * 3: reads (`read`), `random(n)` as a number, the bitwise operators and `print` with its text in parts. * 2: a record's text and sound are written out in the JSON (1 had the map's string indices); `cyclesPerSecond` is gone. */ -export const IR_VERSION = 17; +export const IR_VERSION = 18; /** The most a buffer of `buildText` holds, in bytes; python/trigscript.py has the same number. Past it the text is cut. */ export const BUILD_BYTES = 2047; @@ -483,8 +484,12 @@ export type Stmt = /** * `for (const u of unitsAt(…))`: the body once for every unit the filter matches, in the order of * the game's unit table, all within the frame — no `sleep` inside. `decl` is the unit of the turn. + * `source` (`on.created` / `on.died`): instead of the table, the units of this frame's events pass — the ones that + * came onto the map since the pass of the frame before (`created`; one that has gone since is passed over), or that + * died or were removed since then (`died`; the unit of the turn is the dead one, its fields read as it died, and + * nothing done to it does anything). */ - | { kind: "unitLoop"; decl: VarDecl; filter: UnitFilter; body: Stmt[]; at: At; label: string } + | { kind: "unitLoop"; decl: VarDecl; filter: UnitFilter; source?: "created" | "died"; body: Stmt[]; at: At; label: string } /** `u.hp = 40`, `u.invincible = true`: nothing happens when the unit is none or gone. */ | { kind: "unitWrite"; unit: UnitExpr; field: UnitNumField | UnitFlag; value: NumExpr | BoolExpr; at: At; label: string } /** `u.kill()`, `u.order("move", there)`: nothing happens when the unit is none or gone. */ @@ -524,7 +529,12 @@ export type Stmt = * the variable that takes a number for the task — `slot | task << 8 | generation << 16`, the generation one more at * every start in the slot — or 0 when nothing started. */ - | { kind: "start"; task: number; max: number; call: Call; handle?: string; at: At; label: string } + /** + * `listen` (on.created, on.died, on.chat, on.key): the task is a listener, whose body waits a frame at the top of a loop + * that hands on what happened, so a turn when nothing did does nothing. Once it has begun, it is given a turn only in + * a frame the events pass found units of `source`, or one of `inputs` arrived. A lowering may also give it every turn. + */ + | { kind: "start"; task: number; max: number; call: Call; handle?: string; listen?: { source?: "created" | "died"; inputs?: InputSource[] }; at: At; label: string } /** `t.cancel()`: the task stops where it is. Nothing happens when it has ended, or the number is no task's. */ | { kind: "cancel"; task: NumExpr; at: At; label: string } /** A trigger action; each of `variables` names a field that takes an expression's value instead of the record's. */ diff --git a/compiler/runtime.ts b/compiler/runtime.ts index 389a176..16b3a62 100644 --- a/compiler/runtime.ts +++ b/compiler/runtime.ts @@ -627,6 +627,13 @@ export function createRuntime(names: ScriptNames, collector: Collector, options: rt.ui = Object.freeze({ lines: () => { throw new ScriptError("ui.lines() shows a text in a program's lines, inside program(async () => { … }): ui.lines(P1, () => …)."); }, }); + const listens = (what: string, example: string) => () => { throw new ScriptError(`on.${what}() listens while the game runs, inside program(async () => { … }): ${example}.`); }; + rt.on = Object.freeze({ + created: listens("created", "on.created(units.ZergZergling, (u) => { … })"), + died: listens("died", "on.died({ owner: P8 }, (u) => { … })"), + chat: listens("chat", "on.chat(\"-give {n}\", (p, { n }) => { … })"), + key: listens("key", "on.key(\"F2\", (p) => { … })"), + }); /* ── Text a program fills in ── */ rt.name = (player: unknown) => textMark("n", onePlayer(player, "name: player", 12)); @@ -715,6 +722,6 @@ export function runtimeNames(names: ScriptNames): string[] { out.push("seconds", "minutes", "frames", "cycles", "sleep", "until", "start", "rose", "once", "shared", "clamp", "u32", "i32"); out.push(...READER_NAMES, "races", "slots", "name", "color", "print"); out.push(...UNIT_CALL_NAMES, "colors", ...INPUT_CALL_NAMES); - out.push("textColors", "ui", "__jsx", "__fragment", "__class"); + out.push("textColors", "ui", "on", "__jsx", "__fragment", "__class"); return out; } diff --git a/compiler/simulateIr.ts b/compiler/simulateIr.ts index 7c5486b..d93ae2a 100644 --- a/compiler/simulateIr.ts +++ b/compiler/simulateIr.ts @@ -216,6 +216,8 @@ class ProgramRun { readonly unsigned = new Set(); /** Every latch's state; a task has its own, swapped in while it runs. */ latches = new Map(); + /** The units of on.died() turns: read as they died, though they are gone. */ + readonly asDied = new Set(); /** The program's arrays: their cells, as a variable's value is kept. */ readonly arrays: Map = new (class extends Map { run!: ProgramRun; @@ -497,6 +499,8 @@ class ProgramRun { /** The unit, when there is one and it is still on the map. */ private *living(e: UnitExpr): Gen { const u = yield* this.unit(e); + // The unit of an on.died() turn is read as it died. + if (u && e.kind === "unitVar" && this.asDied.has(e.id)) return u; return u?.alive ? u : null; } @@ -945,9 +949,12 @@ class ProgramRun { } case "unitLoop": { this.declare(s.decl); - // The units as they are when the loop starts, in table order; one that dies on the way is passed over. - for (const u of this.sim.matching(s.filter)) { - if (!u.alive) continue; + // The units as they are when the loop starts, in table order; one that dies on the way is passed over. Those of + // the frame's events pass (`on.created` / `on.died`): the ones that came on, and the ones that died, read as they died. + const dead = s.source === "died"; + if (dead) this.asDied.add(s.decl.id); + for (const u of s.source ? this.sim.eventUnits(s.source, s.filter) : this.sim.matching(s.filter)) { + if (!dead && !u.alive) continue; this.unitVars.set(s.decl.id, u); const flow = yield* this.block(s.body, ctx); if (flow === "break") break; @@ -1553,8 +1560,31 @@ export class ProgramSimulation { } } + /** + * The frame's events pass (on.created / on.died), as the game's lowering has it: a unit is seen the first time a pass + * finds it on the map, and its death is found by the first pass after it, once. A unit made and gone between two + * passes is neither. Each list is the latest first, as the game's lists keep them. + */ + private eventsFrom = 0; + private readonly seen = new Set(); + private events_: { created: SimUnit[]; died: SimUnit[] } = { created: [], died: [] }; + private eventsPass(): void { + const created: SimUnit[] = [], died: SimUnit[] = []; + for (const u of this.seen) if (!u.alive) { died.push(u); this.seen.delete(u); } + const all = this.game.units; + for (; this.eventsFrom < all.length; this.eventsFrom++) { const u = all[this.eventsFrom]; if (u.alive) { created.push(u); this.seen.add(u); } } + this.events_ = { created: created.reverse(), died: died.reverse() }; + } + + /** What this frame's events pass found that a filter matches: the units that came on and are still here, or the ones that died, as they died. */ + eventUnits(source: "created" | "died", f: UnitFilter): SimUnit[] { + const owners = f.owner === undefined ? undefined : this.slotsOf(f.owner); + return this.events_[source].filter((u) => (source === "died" || u.alive) && this.game.fits(u, owners, f.type, f.at)); + } + /** One frame: every program in order, from where it left off. */ step(): void { + this.eventsPass(); // What was done since the last frame is what this one finds, and only this one. this.current = this.queued; this.queued = { events: new Set(), lines: new Map() }; diff --git a/compiler/structured.ts b/compiler/structured.ts index ac507c7..2dac1a0 100644 --- a/compiler/structured.ts +++ b/compiler/structured.ts @@ -60,7 +60,8 @@ import type * as TS from "typescript"; import { ActionType, PlayerGroup } from "../vendor/triggers"; import type { ActionRecord } from "../vendor/triggers"; import type { HoistedThunks, ProgramPlan } from "./hoist"; -import { declarationOf, isScriptClass, libraryCallName } from "./hoist"; +import { declarationOf, isOnCall, isScriptClass, libraryCallName } from "./hoist"; +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 } from "./runtime"; import { cellMax } from "./tables"; @@ -517,6 +518,11 @@ export class Structured { if (ts.isNumericLiteral(e)) return { value: Number(e.text) }; if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)) return { value: e.text }; if (ts.isPropertyAccessExpression(e)) { + // A field of a record of the program that is known when the script is built: what on.died({ owner: P8 }, …) says of `u.owner`. + if (ts.isIdentifier(e.expression)) { + const r = this.bindingOf(e.expression); + if (r?.kind === "record") { const f = r.fields.get(e.name.text); return f?.kind === "value" ? { value: f.value } : undefined; } + } const obj = sub(e.expression); if (!obj || obj.value === null || obj.value === undefined) return undefined; return { value: (obj.value as Record)[e.name.text] }; @@ -556,6 +562,16 @@ export class Structured { } return { value: out }; } + // `{ to: p }` where p is known when the script is built, as a handler's player is (on.chat): an object of values. + if (ts.isObjectLiteralExpression(e)) { + const out: Record = {}; + for (const p of e.properties) { + if (ts.isPropertyAssignment(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) { const v = sub(p.initializer); if (!v || isGameValue(v.value)) return undefined; out[p.name.text] = v.value; continue; } + if (ts.isShorthandPropertyAssignment(p)) { const v = sub(p.name); if (!v || isGameValue(v.value)) return undefined; out[p.name.text] = v.value; continue; } + return undefined; + } + return { value: out }; + } if (ts.isArrayLiteralExpression(e)) { const out: unknown[] = []; for (const el of e.elements) { @@ -4448,18 +4464,7 @@ export class Structured { if (!call) { if (stmts.length) refuse(); return; } } else { refuse(); return; } if (!call) return; - // What the body declares is the task's own: one of each a slot. A start inside it marked its own task's first. - for (const d of [...call.params.map((p) => p.decl), ...(call.result ? [call.result.decl] : []), ...declarations(call.body)]) d.task ??= task; - const arrays = new Set(); - const visit = (root: unknown): void => { - if (Array.isArray(root)) { root.forEach(visit); return; } - if (!root || typeof root !== "object") return; - const o = root as Record; - if (o.kind === "declareArray" && typeof o.array === "string") arrays.add(o.array); - Object.values(o).forEach((v) => { if (v && typeof v === "object") visit(v); }); - }; - visit(call.body); - for (const a of this.arrays) if (arrays.has(a.id)) a.task ??= task; + this.claimTask(call, task); this.emit({ kind: "start", task, max, call, ...(handle ? { handle: handle.id } : {}), at: this.at(node), label: this.label(node) }, node); } @@ -4604,6 +4609,7 @@ export class Structured { if (ts.isVoidExpression(bare) && this.isPromise(bare.expression)) { this.startTask(bare.expression, bare, TASK_MAX); return; } if (ts.isCallExpression(bare) && this.isLibraryCall(bare, "start")) { this.startCall(bare); return; } if (ts.isCallExpression(bare) && this.isLibraryCall(bare, "lines")) { this.uiLines(bare); return; } + if (ts.isCallExpression(bare) && isOnCall(ts, this.c.checker, bare)) { this.onEvent(bare); return; } if (ts.isCallExpression(bare) && ts.isPropertyAccessExpression(bare.expression) && bare.expression.name.text === "cancel" && this.isTaskType(this.c.checker.getTypeAtLocation(bare.expression.expression))) { this.cancelTask(bare, bare.expression.expression); return; } // A promise nothing waits for. sleep() in a function that is not async is the language before 4: it pauses there. if (!awaited && ts.isCallExpression(bare) && this.isPromise(bare) && !(this.isLibraryCall(bare, "sleep") && !this.inAsync(bare))) { this.notAwaited(bare); return; } @@ -5672,6 +5678,232 @@ export class Structured { return varRef(t); } + /** + * `on.created(…)`, `on.died(…)`, `on.chat(…)`, `on.key(…)` (TrigScript 4, B2): a task that, from the next frame on, runs + * the handler for every time it happens — a unit the frame's events pass found, a line typed, a key pressed. A handler + * that is not async is inlined in the task's loop; an async one is a start of its own each time, its parameters copies. + */ + private onEvent(call: TS.CallExpression, handle?: VarDecl) { + const { ts } = this; + const what = (call.expression as TS.PropertyAccessExpression).name.text as "created" | "died" | "chat" | "key"; + const at = this.at(call), label = this.label(call); + const named = `on.${what}()`; + if (!this.inAsync(call)) { this.c.error(call, `${named} listens beside the program as a task: inside program(async () => { … }).`); return; } + const args = [...call.arguments]; + const isFn = (a: TS.Expression) => { const u = this.unwrap(a); return ts.isArrowFunction(u) || ts.isFunctionExpression(u); }; + let filter: { type?: number; owner?: number; at?: number } = {}; + let fnArg: TS.Expression | undefined, optArg: TS.Expression | undefined, word = ""; + if (what === "created" || what === "died") { + const f = args.findIndex(isFn); + if (f !== 0 && f !== 1) { this.c.error(call, `${named} takes what to look for, if you like, and a function written where it is used: on.${what}(units.ZergZergling, (u) => { … }).`); return; } + if (f === 1) { const got = this.eventFilter(args[0], named); if (!got) return; filter = got; } + fnArg = args[f]; optArg = args[f + 1]; + if (args.length > f + 2) { this.c.error(args[f + 2], `${named} takes the filter, the function and the options.`); return; } + } else { + const h = args[0] && this.evaluate(args[0]); + if (!h || typeof h.value !== "string") { this.c.error(args[0] ?? call, what === "chat" ? `on.chat() takes the pattern first, as chatted() does: on.chat("-give {n}", (p, { n }) => { … }).` : `on.key() takes the key first, as keyPressed() does: on.key("F2", (p) => { … }).`); return; } + word = h.value; + fnArg = args[1]; optArg = args[2]; + if (!fnArg || !isFn(fnArg)) { this.c.error(args[1] ?? call, `${named} takes a function written where it is used, after the ${what === "chat" ? "pattern" : "key"}.`); return; } + if (args.length > 3) { this.c.error(args[3], `${named} takes the ${what === "chat" ? "pattern" : "key"}, the function and the options.`); return; } + } + // The options: how many async handlers at once, and whose lines or keys. + let max = TASK_MAX; + let from: number | undefined; + if (optArg) { + const h = this.evaluate(optArg); + const o = h?.value as Record | undefined; + if (!o || typeof o !== "object" || Array.isArray(o)) { this.c.error(optArg, `${named}'s options are written out, as { max: 4 }.`); return; } + for (const [k, v] of Object.entries(o)) { + if (v === undefined) continue; + if (k === "max") { + if (typeof v !== "number" || !Number.isInteger(v) || v < 1 || v > 255) { this.c.error(optArg, `${named}'s max is how many of its handlers can be waiting at once: a whole number from 1 to 255.`); return; } + max = v; + } else if (k === "from" && (what === "chat" || what === "key")) { + if (typeof v !== "number" || !Number.isInteger(v) || ((v < 0 || v > 7) && v !== CURRENT_PLAYER)) { this.c.error(optArg, `${named}'s from is one player who can type and press keys: P1 … P8, or CurrentPlayer.`); return; } + from = v; + } else { this.c.error(optArg, `${named} has no option "${k}"; it has max${what === "chat" || what === "key" ? " and from" : ""}.`); return; } + } + } + const fn = this.unwrap(fnArg) as TS.ArrowFunction | TS.FunctionExpression; + const waits = !!fn.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); + if (!waits && optArg && max !== TASK_MAX) { this.c.error(optArg, `max is for an async handler: how many can be waiting at once. This one runs within the frame.`); return; } + const caught = this.loopCapture(fn, call); + if (caught) { this.c.error(caught, `This handler reads ${caught.text}, which the loop around ${named} changes; the task would find it as it is when it reads it. Keep it in a variable the program changes instead.`); return; } + const task = ++this.taskCount; + const heard: InputSource[] = []; + const name = (i: number, or: string) => { const p = fn.parameters[i]; return p && ts.isIdentifier(p.name) ? p.name.text : or; }; + // The handler, handed `bound`: inlined, or started — its parameters copies of what it is handed, in the started task's cells. + const run = (bound: Binding[]) => { + if (!waits) { + const c = this.callback(fnArg, named, bound, "void", call); + if (c) this.emit({ kind: "call", call: c, at, label }, call); + return; + } + const params: Call["params"] = []; + const copy = (b: Binding): Binding => { + if (b.kind === "var") { const v = this.newVar(b.v.name, b.v.kind, b.v.at, { ...(b.v.bits ? { bits: b.v.bits } : {}), ...(b.v.unsigned ? { unsigned: true } : {}) }); params.push({ decl: v, init: refOf(b.v), label }); return { kind: "var", v }; } + if (b.kind === "record") return { ...b, fields: new Map([...b.fields].map(([k, f]) => [k, copy(f)])) }; + return b; + }; + const mine = bound.map(copy); + const inner = ++this.taskCount; + const c = this.callback(fnArg, named, mine, "void", call, true); + if (!c) return; + c.params.unshift(...params); + this.claimTask(c, inner); + this.emit({ kind: "start", task: inner, max, call: c, at, label }, call); + }; + const body = this.collect(() => { + const loop = this.collect(() => { + // What happened since the frame before: the first frame of the task, the frame it was started in, looks at nothing. + this.emit({ kind: "sleep", cycles: 1, at, label }, call); + if (what === "created") { + const v = this.newVar(name(0, "(unit)"), "unit", at, { temp: true }); + const turn = this.collect(() => run([{ kind: "var", v }])); + this.emit({ kind: "unitLoop", decl: v, filter: { ...filter }, source: "created", body: turn, at, label }, call); + } else if (what === "died") { + const v = this.newVar("(dead unit)", "unit", at, { temp: true }); + const turn = this.collect(() => { + // What it was and where, taken as it died; what the filter says for certain is known when the script is built. + const fields = new Map(); + const known: Record = { type: filter.type !== undefined && filter.type < 228 ? filter.type : undefined, owner: filter.owner !== undefined && filter.owner !== CURRENT_PLAYER ? filter.owner : undefined }; + // Only what the handler reads is read from the dead unit: each field is a read of the game. + const wanted = this.fieldsRead(fn, 0); + for (const f of ["type", "owner", "x", "y"] as const) { + const k = known[f]; + if (k !== undefined) { fields.set(f, { kind: "value", value: k }); continue; } + if (wanted && !wanted.has(f)) { fields.set(f, { kind: "value", value: 0 }); continue; } + const fv = this.newVar(`${name(0, "(dead unit)")}.${f}`, "number", at, { temp: true }); + this.emit({ kind: "declare", decl: fv, init: { kind: "unitField", unit: unitRef(v), field: f, at, label }, at, label }, call); + fields.set(f, { kind: "var", v: fv }); + } + run([{ kind: "record", fields }]); + }); + this.emit({ kind: "unitLoop", decl: v, filter: { ...filter }, source: "died", body: turn, at, label }, call); + } else { + const players = from !== undefined ? [from] : this.c.perPlayer ? [CURRENT_PLAYER] : [0, 1, 2, 3, 4, 5, 6, 7]; + let parsed: ChatPattern | undefined; + let key = ""; + if (what === "chat") { + try { parsed = parseChatPattern(word); } catch (err) { this.c.error(args[0], `on.chat(): ${err instanceof Error ? err.message : String(err)}`); return; } + } else { + const deaf = DEAF_KEYS[word.trim().toUpperCase()]; + const k = keyName(word); + if (deaf || !k) { this.c.error(args[0], deaf ? `on.key(): ${deaf}.` : `on.key(): the key is one of ${KEY_NAMES.slice(0, 3).map((x) => JSON.stringify(x)).join(", ")} … "F1" … "Space", "Enter", "Escape", "Left" …; got ${JSON.stringify(word)}.`); return; } + key = k; + } + for (const p of players) { + const input = (source: InputSource): NumExpr => ({ kind: "input", input: source, at, label }); + const source: InputSource = what === "chat" ? { source: "chat", pattern: word, capture: null, player: p } : { source: "key", key, player: p }; + heard.push(source); + const happened = input(source); + const turn = this.collect(() => { + const bound: Binding[] = [{ kind: "value", value: p }]; + if (parsed) { + const fields = new Map(); + parsed.captures.forEach((c, i) => { + const fv = this.newVar(`${name(1, "(typed)")}.${c.name}`, "number", at, { temp: true }); + this.emit({ kind: "declare", decl: fv, init: input({ source: "chat", pattern: word, capture: i, player: p }), at, label }, call); + fields.set(c.name, { kind: "var", v: fv }); + }); + bound.push({ kind: "record", fields }); + } + run(bound); + }); + this.emit({ kind: "if", cond: { kind: "test", expr: happened, at, label }, then: turn, at, label }, call); + } + } + }); + this.emit({ kind: "while", body: loop, at, label }, call); + }); + const listener: Call = this.mark({ name: named, at, label, params: [], body }, call); + this.claimTask(listener, task); + this.emit({ kind: "start", task, max: 1, call: listener, ...(handle ? { handle: handle.id } : {}), listen: what === "created" || what === "died" ? { source: what } : { inputs: heard }, at, label }, call); + } + + /** + * The fields of a function's parameter its body reads — `u.x`, `const { type } = u` — or null when the parameter is used + * some other way (handed on whole), and then every field is wanted. + */ + private fieldsRead(fn: TS.ArrowFunction | TS.FunctionExpression, index: number): Set | null { + const { ts } = this; + const p = fn.parameters[index]; + if (!p) return new Set(); + const out = new Set(); + if (ts.isObjectBindingPattern(p.name)) { + for (const el of p.name.elements) { + if (el.dotDotDotToken) return null; + const key = el.propertyName ?? el.name; + if (!ts.isIdentifier(key)) return null; + out.add(key.text); + } + return out; + } + if (!ts.isIdentifier(p.name)) return null; + const symbol = this.c.checker.getSymbolAtLocation(p.name); + let whole = false; + const walk = (n: TS.Node): void => { + if (whole) return; + if (ts.isIdentifier(n) && n !== p.name && this.c.checker.getSymbolAtLocation(n) === symbol) { + const parent = n.parent; + if (ts.isPropertyAccessExpression(parent) && parent.expression === n) out.add(parent.name.text); + else whole = true; + return; + } + ts.forEachChild(n, walk); + }; + walk(fn.body); + return whole ? null : out; + } + + /** `const deaths = on.died(…)`: its task's number, to stop it with. */ + private onEventValue(e: TS.CallExpression): NumExpr { + const at = this.at(e), label = this.label(e); + const t = this.newVar("(task)", "number", at, { temp: true }); + this.emit({ kind: "declare", decl: t, init: num(0), at, label }, e); + this.onEvent(e, t); + return varRef(t); + } + + /** What on.created() / on.died() looks for: a unit type, or a filter as unitsOf() takes one; known when the script is built. */ + private eventFilter(arg: TS.Expression, named: string): { type?: number; owner?: number; at?: number } | null { + const h = this.evaluate(arg); + if (!h) { this.notConstant(arg, `What ${named} looks for`); return null; } + const v = h.value; + const out: { type?: number; owner?: number; at?: number } = {}; + const fail = () => { this.c.error(arg, `${named} looks for a unit type — units.ZergZergling — or a filter such as { type: units.TerranMarine, owner: P1, at: locations.Base }.`); return null; }; + if (typeof v === "number") out.type = v; + else if (v && typeof v === "object" && !Array.isArray(v) && !isGameValue(v)) { + for (const [k, x] of Object.entries(v as Record)) { + if (x === undefined || x === null) continue; + if ((k !== "type" && k !== "owner" && k !== "at") || typeof x !== "number" || !Number.isInteger(x)) return fail(); + out[k] = x; + } + } else return fail(); + if (out.type === 229) delete out.type; + if (out.type !== undefined && (out.type < 0 || out.type > 232)) return fail(); + if (out.owner !== undefined && (out.owner < 0 || (out.owner > 11 && out.owner !== CURRENT_PLAYER))) { this.c.error(arg, `${named}'s owner is one player: P1 … P12, or CurrentPlayer.`); return null; } + if (out.at !== undefined && (out.at === 0 || out.at === 64)) delete out.at; + if (out.at !== undefined && (out.at < 0 || out.at > 255)) return fail(); + return out; + } + + /** What a task's body declares is the task's own: a cell of each a slot. A task started inside it marked its own first. */ + private claimTask(c: Call, task: number) { + for (const d of [...c.params.map((p) => p.decl), ...(c.result ? [c.result.decl] : []), ...declarations(c.body)]) d.task ??= task; + const arrays = new Set(); + const visit = (root: unknown): void => { + if (Array.isArray(root)) { root.forEach(visit); return; } + if (!root || typeof root !== "object") return; + const o = root as Record; + if (o.kind === "declareArray" && typeof o.array === "string") arrays.add(o.array); + Object.values(o).forEach((x) => { if (x && typeof x === "object") visit(x); }); + }; + visit(c.body); + for (const a of this.arrays) if (arrays.has(a.id)) a.task ??= task; + } + /** `names.join(", ")`: the texts one after another with the separator between, made where the call stands. */ private textsJoin(e: TS.CallExpression, of: Texts): TextExpr | null { const at = this.at(e); @@ -7104,6 +7336,7 @@ export class Structured { if (this.promiseCall(e)) return this.raceValue(e, "number") as NumExpr | null; if (this.isLibraryCall(e, "start")) return this.startValue(e); if (this.isLibraryCall(e, "lines")) return this.uiLinesValue(e); + if (isOnCall(this.ts, this.c.checker, e)) return this.onEventValue(e); const method = this.methodCall(e); if (method !== undefined) return this.numberOf(method, e); if (ts.isIdentifier(e.expression) && (e.expression.text === "parseInt" || e.expression.text === "Number" || e.expression.text === "parseFloat") && !this.gameDeclaration(e.expression)) { diff --git a/compiler/world.ts b/compiler/world.ts index 1cfecb6..4dc3626 100644 --- a/compiler/world.ts +++ b/compiler/world.ts @@ -221,7 +221,12 @@ export class World { /** The living units of these owners (any, when undefined), of a type or a class, inside a location — in table order. */ matching(owners: readonly number[] | undefined, type?: number, location?: number): SimUnit[] { - return this.living().filter((u) => (!owners || owners.includes(u.owner)) && this.typed(u, type) && this.inside(u, location)); + return this.living().filter((u) => this.fits(u, owners, type, location)); + } + + /** Whether a unit, living or as it died, is of these owners (any, when undefined), of a type or a class, inside a location. */ + fits(u: SimUnit, owners: readonly number[] | undefined, type?: number, location?: number): boolean { + return (!owners || owners.includes(u.owner)) && this.typed(u, type) && this.inside(u, location); } centreOf(location: number): { x: number; y: number } { diff --git a/docs/ir.md b/docs/ir.md index 77d2927..b9c6e38 100644 --- a/docs/ir.md +++ b/docs/ir.md @@ -3,7 +3,7 @@ What a `program(() => { … })` body means, written down as data. The compiler's front end (`compiler/structured.ts`) turns the TypeScript into this, `python/trigscript.py` lowers it to eudplib when the map is saved, and `compiler/simulateIr.ts` interprets it for -Simulate and the tests. **Version 17** (16 had no `buildText`, no `built` and no `char` part: a text was made whole or not at all; 15 had no tasks: no `start`, `cancel`, `running`, and no `task` on a declaration; 14 had no `parallel`: nothing ran beside a program's body; 13 had no text in the cells of an array: no `textAt`, `storeText`, `releaseText`; 12 had no texts: a text was the parts of a `print` and nothing else; 11 had no `slice` and no `through`: every array was cells of its own; 10 had no recursion: no `recursive`, no `saves`, and the heap's top set aside for a stack nothing used; 9 had no functions that are called: every `call` carried a body of its own; 8 had no `unitAt` / `unitPart`, so no array could hold a unit; 7 had no arrays that grow; 6 had no arrays; 5 had unsigned numbers only, the two-sided reading of `+` and `−`, no `>>>` and no `unsigned` anywhere; 4 had no input, no `centerLocation`, and one `variable` on an action where 5 has a list; 3 had no units and no tables; 2 had no reads, no `random(n)`, no bitwise operators +Simulate and the tests. **Version 18** (17 had no events: no `source` on a `unitLoop` and no `listen` on a `start`; 16 had no `buildText`, no `built` and no `char` part: a text was made whole or not at all; 15 had no tasks: no `start`, `cancel`, `running`, and no `task` on a declaration; 14 had no `parallel`: nothing ran beside a program's body; 13 had no text in the cells of an array: no `textAt`, `storeText`, `releaseText`; 12 had no texts: a text was the parts of a `print` and nothing else; 11 had no `slice` and no `through`: every array was cells of its own; 10 had no recursion: no `recursive`, no `saves`, and the heap's top set aside for a stack nothing used; 9 had no functions that are called: every `call` carried a body of its own; 8 had no `unitAt` / `unitPart`, so no array could hold a unit; 7 had no arrays that grow; 6 had no arrays; 5 had unsigned numbers only, the two-sided reading of `+` and `−`, no `>>>` and no `unsigned` anywhere; 4 had no input, no `centerLocation`, and one `variable` on an action where 5 has a list; 3 had no units and no tables; 2 had no reads, no `random(n)`, no bitwise operators and no `print`; 1 had the map's string indices in the records and a `cyclesPerSecond` on the program, for the death-counter backend 3.0 removed). The types are in `compiler/ir.ts`; this is the reference for anyone reading the lowering or writing @@ -168,7 +168,7 @@ frame's `saves`, and a `slice` is never declared. | `store` | `array`, `index`, `value` | `hp[i] = value`; `hp[i] += v` arrives as a store of `hp[i] + v`. The value is evaluated, then the index; nothing is stored past either end. | | `assignBool` | `target`, `value` | `target = value` for a boolean. | | `assignUnit` | `target`, `value` | `target = value` for a unit variable. | -| `unitLoop` | `decl`, `filter`, `body` | `for (const u of unitsAt(…))`: the body once for every unit the filter matches, in the order of the game's unit table, within the frame (the compiler refuses a `sleep` inside). `decl` is the unit of the turn; `break` and `continue` are the loop's. | +| `unitLoop` | `decl`, `filter`, `source?`, `body` | `for (const u of unitsAt(…))`: the body once for every unit the filter matches, in the order of the game's unit table, within the frame (the compiler refuses a `sleep` inside). `decl` is the unit of the turn; `break` and `continue` are the loop's. With `source` (`on.created` / `on.died`, since version 18) the units are this frame's events pass's instead (*Events*, below). | | `unitWrite` | `unit`, `field`, `value` | `u.hp = 40`, `u.invincible = true`. Nothing happens when the unit is none or gone. Hit points at 0 kill. | | `unitDo` | `unit`, `verb` | `{ do: "kill" }`, `{ do: "remove" }`, `{ do: "give", to }`, `{ do: "order", order, target }` (a location), `{ do: "damage" \| "heal", amount, percent }`, `{ do: "locate", location }`. Nothing happens when the unit is none or gone; an amount is evaluated either way. | | `tableWrite` | `cell`, `value`, `scaled?`, `boolean?` | A cell of the game's tables. `value` is a number expression, a boolean one (`boolean`), or `{ kind: "text", text }` for a name. `scaled`: the number is already what the cell stores. | @@ -182,7 +182,7 @@ frame's `saves`, and a `slice` is never declared. | `return` | `value?` | Inside a `call` body: writes the result and leaves the call. | | `sleep` | `ms?`, `cycles?` | Park the program: a duration in milliseconds, or in frames (`cycles`). `cycles: 1` goes on in the very next frame. Inside a `parallel` branch it parks that branch alone. | | `parallel` | `mode`, `branches` | `await Promise.all([…])` (`all`) or `await Promise.race([…])` / `Promise.any` (`race`). Each branch is a coroutine of its own with its own place to go on from and its own wait: started here, in order, in this frame; then, every frame the program is at this statement, each branch that has not ended goes on, in order. The program goes on past it in the frame the last branch ends (`all`) or the first (`race`, which leaves the others where they are: they never run again). A branch is never left by `break`, `continue` or `return`. Not inside a called function, a `unitLoop` or a `textLoop`. Since version 15. | -| `start` | `task`, `max`, `call`, `handle?` | `start(wave(n))`, `void wave(n)`: a task that runs beside what started it (*Tasks*, below). `call` is the async function's body inlined, its `params` copies of this start's arguments, worked out here; the body runs in the task. `handle` takes the task's number, or 0 when every slot was running. Since version 16. | +| `start` | `task`, `max`, `call`, `handle?`, `listen?` | `start(wave(n))`, `void wave(n)`: a task that runs beside what started it (*Tasks*, below). `call` is the async function's body inlined, its `params` copies of this start's arguments, worked out here; the body runs in the task. `handle` takes the task's number, or 0 when every slot was running. Since version 16. `listen` (`{ source }` or `{ inputs }`, since version 18): a listener's start (*Events*, below). | | `cancel` | `task` (a number) | `t.cancel()`: the task stops where it is; nothing happens for a number that is no running task's. Since version 16. | | `buildText` | `buffer`, `parts`, `reset?` | The parts written onto the end of buffer `buffer` — emptied first when `reset` — which a `built` reads back (*Texts put together a piece at a time*, below). Since version 17. | | `print` | `parts`, `to`, `position` | Text with values in it. `parts` are `{ kind: "text", text }`, `{ kind: "number", expr, unsigned? }` (its digits, with a minus sign when it is below zero and not `unsigned`), `{ kind: "name", player }` and `{ kind: "color", player }` (the colour code of the player's colour), a player being a slot or 13 for the current player. `to` is who sees it: a slot, 13, All Players (17) or a force (18–21). `position` is `chat` or `center`, the line the game's own errors use. Every number is evaluated before anything is shown. | @@ -192,6 +192,32 @@ frame's `saves`, and a `slice` is never declared. | `block` | `body` | Scoping only. | | `remark` | `text`, `short?` | A word for the editor about the line: a loop unrolled when the script was applied. | +### Events + +`on.created`, `on.died`, `on.chat` and `on.key` are each a `start` with `max` 1 and `listen`, whose body is +`while (true) { sleep 1; … }`: the first turn, in the frame it is started in, only waits, so a listener hands +on what happens from the next frame. What it hands on is a `unitLoop` with a `source`, or for a typed line or +a key an `if` on the `input` for each player it listens to, unrolled (the player is a value of the script in +each copy). A handler that is not async is inlined there; an async one is a `start` of its own each time, its +`params` copies of what it is handed. Once a listener has begun, the lowering may skip its turn in a frame +nothing it listens for happened (`listen.source`: the pass found none of those; `listen.inputs`: none +arrived), since such a turn does nothing; the simulator gives it every turn. + +The **events pass** runs once a frame before the programs, when the IR has a `unitLoop` with a `source`. It +walks two of the game's lists from their heads along "next" (1.16.1's addresses: the visible units at +0x628430 and the dying ones at 0x62842C; each links a record in just behind its head, so the records linked +in since the pass before lie between the head and the record that was behind it then). A cell for each unit +slot holds the uniqueness byte of the unit last seen in it, and once that unit's death has been handed on, +one more: a unit on the visible list whose cell is not its byte came on (`created`); a unit on the dying +list whose cell is its byte has died or been removed (`died`), and is handed on once. A unit made and gone +between two passes, a turret, a scanner sweep and a unit that never left its transport are neither. When the +head and the record behind it are both as the pass before left them, a walk is passed by; when that record +has left the list, the walk goes to the end. At most 512 of each a frame; the rest come in the next. + +A `created` turn is a unit still on the map (one gone since the pass is passed over); a `died` turn is the +dying record, whose fields read as the unit was when it died — its slot is not given to another unit before +the next frame's dying pass — and the front end only reads its fields. Each list is the latest first. + ### Tasks Each `start` statement is a body of its own — a loop unrolled when the script is built has one diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 630a358..acb3168 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -885,6 +885,63 @@ on.chat("-give {n}", (p, { n }) => …); hint says so. - A handler is a task (B1), so it may await. +**As built, first part (2026-10-16, under Node and in the lifted game; branch `b2-events`).** `on.created`, +`on.died`, `on.chat` and `on.key`; `on.order` and who killed a unit are the second part. + +```ts +on.created(units.ZergZergling, (u) => { u.energy = 50; }); +on.died({ owner: P8 }, async (u) => { await sleep(seconds(2)); createUnit(P8, u.type, 1, locations.Spawn); }); +on.chat("-give {n}", (p, { n }) => setResources(p, "add", n, "ore")); +on.key("F2", (p) => print("Help", { to: p }), { from: P1 }); +``` + +- **A listener is a task.** Each `on.…` is a `start` of one slot whose body is `while (true) { sleep 1; … }`, + inside an async program, as `ui.lines` is; it gives a Task, so `cancel()` stops it. The first turn only waits: + a listener hands on what happens from the next frame, as a JavaScript listener added now hears nothing from + before. A handler that is not async is inlined in the loop; an async one is a `start` of its own each time + (`{ max }`, 16 unless said), its parameters copies of what it was handed. +- **Where the units come from: the frame's events pass**, once a frame before the programs. It walks two of the + game's lists from their heads, the visible units (0x628430) and the dying ones (0x62842C). The reader's spec + `scm-sim docs/specs/unit-lists.md` says every list links a record in just behind its head, so what came in + since the pass before lies between the head and the record that was behind it then: the walk is as long as + what happened, and a frame where nothing did costs three conditions a list. A table of a cell for each unit slot, + 336 bytes apart as the slots are (eudplib's EUDLoopNewUnit keeps its marks that way), holds the uniqueness byte of + the unit seen in the slot, and one more once its death has been handed on. Nothing of the game is written. +- **What counts.** A unit that comes onto the visible list for the first time is made: made by an action or a + program, trained (when it comes out, not when it is queued), placed by a test; a unit in a transport is made when + it first comes out, and unloading it again is nothing new. A unit on the dying list that was seen is dead, + killed or removed (both go through the dying list; Remove Unit releases the sprite at once), handed on once. So + every unit `on.created` hands on gets one `on.died`, and a turret, a scanner sweep, a unit made and gone between + two passes, or one that never left its transport is neither. A unit that turns into another — an egg hatching, a + Drone becoming a building, a Siege Tank sieging — is the same unit: it was made once, as what it was then. +- **What a handler is handed.** `on.created`: the Unit. `on.died`: a `DeadUnit` record, `{ type, owner, x, y }`, + read from the dying record as the unit died (the slot is not given to another unit before the next dying pass), + and only the fields the handler reads; what the filter says for certain is a value of the script, so + `on.died({ owner: P8 }, (u) => createUnit(u.owner, …))` has its player known. `on.chat` / `on.key`: the player, + a value of the script in each of eight unrolled copies (P1 … P8, or `{ from }`, or CurrentPlayer in a program for + several players), so an action can be for them; `on.chat` also the pattern's values, as `chatted()` gives them. +- **What it costs** (lifted game, Binary Burghs): a frame where nothing happened, about 46 conditions and actions for + the pass and a listener, 2 more for each other listener; about 100 for each unit made and 80 for each death. Two + things made it so: the task runner passes a listener's whole start by when it has nothing to hand on and no task + that has not had its first turn (`start.listen` in IR 18; a skipped turn would have done nothing), and the pass + passes a walk by when the list's head and the record behind it are as it left them. Before either, two idle + listeners cost about 1,240 a frame. `on.chat` and `on.key` for all eight players idle at about 1,290, where the + same polled with `chatted()` / `keyPressed()` in a loop costs about 1,730. +- **Simulator.** The same pass over its world's units: a unit is seen at the first pass it is alive at, its death + found by the first pass after, each list latest first. +- **Found on the way:** the build-time evaluator had no object literals, so `print(t, { to: p })` with `p` known + failed; and a record's known field (`u.owner` above) was not a value. Both are fixed. The lifted game's + `sim.printed()` folds a text shown twice in one frame (its text-line reader); the probe counts the actions. +- **Checked.** `tests/events.test.ts` (7), eudplib fixtures `events` and `eventsPerPlayer`, `probes/game-events.ts` + (4) and `probes/events.ts` (3) in the lifted game and the simulator, at the same frames; every older game probe + still passes. **Probe for Remastered:** `Maps/TrigScript/trigscript-b2-events.scx` with its answer sheet: whether + Remastered's emulator gives the dying list as 1.16.1 has it (R0 says 0x62842C reads; nobody has walked it), a + trained SCV, unloading, 200 made and killed in a frame, and chat and a key in a multiplayer game. Not played yet. +- **Not yet:** `on.order` (each unit's order against the last frame's: a scan, or a pass over the visible list), + `u.killedBy` (the killer's kill count rises a frame before; or the unit's +0x90, last attacking player, from the + dying record — both to check), a `removed` flag on a DeadUnit, a morph event, listeners at the top of a script + without a program around them. + ### B3. Function values (4.5.0) ```ts diff --git a/probes/events.ts b/probes/events.ts new file mode 100644 index 0000000..b52ff1b --- /dev/null +++ b/probes/events.ts @@ -0,0 +1,81 @@ +// The events probe (TrigScript 4, B2): does Remastered hand on what on.created / on.died / on.chat / on.key listen for? +// The events pass walks two of the game's lists of units from their heads — the visible units (0x628430) and the dying +// ones (0x62842C) — and reads each record's links and uniqueness byte; R0's dump says all of it reads, and eudplib's own +// EUDLoopNewUnit walks the first, but nobody has walked the dying list in Remastered. The lifted game passes every step. +// +// What to look for, by the second (each step says so in the middle of the screen too): +// A 3 three Marines made by the program beside your base: "made 0" three times (0 is the Marine's number). +// B 7 the three die: "died 0 at x, y" three times, with places near your base; +// then two seconds later "back" three times and three new Marines, each "made 0" again. +// C 15 two Firebats made and taken off the map without dying: "made 32" twice, then "died 32" twice. +// D 20 your turn: train an SCV at your Command Center. "made 7" when it comes out, not when you queue it. +// Then load and unload anything you like (a Bunker, a Dropship): nothing new should be said. +// E 35 200 Zerglings of yours in one frame, then all killed in one frame: "E: made 200, died 200". Does the game +// slow or stutter at either? +// F 45 in a multiplayer game only: type "-give 100" (Enter, the line, Enter): +100 minerals. Press F2: "help". +// Host it as a multiplayer game, alone, for F; the rest also works single player. +// npx tsx scripts/build-fixture.mts probes/events.ts fixtures/eud/events.scx +// npx tsx scripts/test-game.mts probes/events.ts (A to E in the lifted game) +import { test, expect, type Sim, program, print, createUnit, setResources, sleep, seconds, frames, unitsOf, on, units, P1, type Location } from "trigscript"; + +const HOME = 1 as Location; + +program(async () => { + let made = 0, died = 0; + on.created((u) => { made++; if (u.type != units.ZergZergling) print(`made ${u.type}`); }); + on.died((u) => { died++; if (u.type != units.ZergZergling) print(`died ${u.type} at ${u.x}, ${u.y}`); }); + on.died(units.TerranMarine, async () => { await sleep(seconds(2)); createUnit(P1, units.TerranMarine, 1, HOME); print("back"); }); + on.chat("-give {n}", (p, { n }) => { setResources(p, "add", n, "ore"); print(`gave ${n}`, { to: p }); }); + on.key("F2", (p) => print("help", { to: p })); + + await sleep(seconds(3)); + print("A: three Marines made by the program. Expect \"made 0\" three times.", { position: "center" }); + createUnit(P1, units.TerranMarine, 3, HOME); + + await sleep(seconds(4)); + print("B: they die. Expect \"died 0 at x, y\" three times, then \"back\" and \"made 0\" three times.", { position: "center" }); + for (const u of unitsOf(P1, { type: units.TerranMarine, at: HOME })) u.kill(); + + await sleep(seconds(8)); + print("C: two Firebats made, then taken off the map. Expect \"made 32\" twice, then \"died 32\" twice.", { position: "center" }); + createUnit(P1, units.TerranFirebat, 2, HOME); + await sleep(frames(12)); + for (const u of unitsOf(P1, { type: units.TerranFirebat })) u.remove(); + + await sleep(seconds(5)); + print("D: train an SCV now. Expect \"made 7\" when it comes out. Loading and unloading should say nothing.", { position: "center" }); + + await sleep(seconds(15)); + print("E: 200 Zerglings in one frame, then all killed in one frame. Does the game slow?", { position: "center" }); + const before = made, gone = died; + for (let i = 0; i < 8; i++) createUnit(P1, units.ZergZergling, 25, HOME); + await sleep(seconds(3)); + for (const u of unitsOf(P1, { type: units.ZergZergling })) u.kill(); + await sleep(seconds(1)); + print(`E: made ${made - before}, died ${died - gone}`); + + await sleep(seconds(6)); + print("F: in multiplayer, type -give 100, and press F2.", { position: "center" }); +}, { owner: P1, name: "events" }); + +/** How many times a text was shown. (The game's own lines fold the same text shown twice in a frame, so the actions are counted.) */ +const said = (sim: Sim, text: string | RegExp) => sim.events.filter((e) => { const t = e.text.replace(/^Display Text Message — /, ""); return e.text !== t && (typeof text === "string" ? t === text : text.test(t)); }).length; + +test("A and B: made, died where, back", (sim) => { + sim.seconds(9); + expect(said(sim, /^died 0 at \d+, \d+$/)).toBe(3); + sim.seconds(3); + expect(said(sim, "back")).toBe(3); + expect(said(sim, "made 0")).toBe(6); +}); + +test("C: removed is died too", (sim) => { + sim.seconds(19); + expect(said(sim, "made 32")).toBe(2); + expect(said(sim, /^died 32 at/)).toBe(2); +}); + +test("E: two hundred in a frame, each way", (sim) => { + sim.seconds(44); + expect(said(sim, "E: made 200, died 200")).toBe(1); +}); diff --git a/probes/game-events.ts b/probes/game-events.ts new file mode 100644 index 0000000..dfd7a21 --- /dev/null +++ b/probes/game-events.ts @@ -0,0 +1,77 @@ +// TrigScript 4, B2: events — on.created, on.died, a handler that waits, cancel() — run in the lifted game as well as in the +// simulator; the two have to agree frame for frame. +// npx tsx scripts/test-game.mts probes/game-events.ts +// A program first runs on frame 2. Built like a probe (Player 1 Terran, Player 2 a Zerg computer, location 1 "Home"). +import { test, expect, program, print, createUnit, sleep, frames, unitsOf, on, units, CurrentPlayer, P1, P2, type Location } from "trigscript"; + +const HOME = 1 as Location; + +program(async () => { + let made = 0, marines = 0, gone = 0, removed = 0, back = 0; + on.created({ owner: P1 }, () => { made++; }); + on.created(units.TerranMarine, (u) => { marines++; u.hp = 20; }); + const died = on.died({ owner: P1, type: units.TerranMarine }, async (u) => { + gone++; + print(`gone at ${u.x}`); + await sleep(frames(4)); + createUnit(u.owner, u.type, 1, HOME); + back++; + }); + on.died(units.TerranFirebat, () => { removed++; }); + // Frame 4: three Marines and two Firebats; the handlers see them on frame 5. + await sleep(frames(2)); + createUnit(P1, units.TerranMarine, 3, HOME); + createUnit(P1, units.TerranFirebat, 2, HOME); + // Frame 8: the Marines die, the Firebats are removed. Each death is handed on once. + await sleep(frames(4)); + for (const u of unitsOf(P1, { type: units.TerranMarine, at: HOME })) u.kill(); + for (const u of unitsOf(P1, { type: units.TerranFirebat })) u.remove(); + // Frame 20: no more listening for deaths; a Marine killed now comes back no more. + await sleep(frames(12)); + died.cancel(); + for (const u of unitsOf(P1, { type: units.TerranMarine, at: HOME })) u.kill(); +}, { owner: P1, name: "events" }); + +// A program for two players: each has its own listener, and CurrentPlayer in the filter is that player. +program(async () => { + let mine = 0; + on.created({ owner: CurrentPlayer, type: units.ZergOverlord }, () => { mine++; }); + await sleep(frames(2)); + createUnit(CurrentPlayer, units.ZergOverlord, 2, HOME); +}, { owner: [P1, P2], name: "each" }); + +test("a listener for each player: each sees its own", (sim) => { + sim.frames(5); + expect(sim.program("each", P1).mine).toBe(2); + expect(sim.program("each", P2).mine).toBe(2); +}); + +test("on.created sees what was made, in the next frame, filtered", (sim) => { + sim.frames(4); + expect(sim.program("events").made).toBe(0); + sim.frames(1); + // Five of this program's, and the two Overlords the program below makes for Player 1. + expect(sim.program("events").made).toBe(7); + expect(sim.program("events").marines).toBe(3); + expect(sim.units({ owner: P1, type: units.TerranMarine, at: HOME }).map((u) => u.hp)).toEqual([20, 20, 20]); +}); + +test("on.died: each death once, the unit as it died, a handler that waits", (sim) => { + sim.frames(9); + expect(sim.program("events").gone).toBe(3); + expect(sim.program("events").removed).toBe(2); + expect(sim).toHavePrinted(/^gone at \d+$/); + sim.frames(4); + expect(sim.program("events").back).toBe(3); + expect(sim.count(P1, units.TerranMarine, HOME)).toBe(3); + // The three that came back are new, and are seen as made in the frame after. + expect(sim.program("events").marines).toBe(3); + sim.frames(1); + expect(sim.program("events").marines).toBe(6); +}); + +test("cancel() stops listening", (sim) => { + sim.frames(30); + expect(sim.program("events").gone).toBe(3); + expect(sim.count(P1, units.TerranMarine, HOME)).toBe(0); +}); diff --git a/python/trigscript.py b/python/trigscript.py index 83f76c8..7c504f3 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -68,7 +68,7 @@ from eudplib.core.mapdata.stringmap import ForceAddString from eudplib.memio.rwcommon import br1, br2, bw1 -IR_VERSION = 17 +IR_VERSION = 18 FRAMES_PER_SECOND = 24 # Where the game keeps what a read reads (1.16.1 addresses, which Remastered emulates). The player # tables are the ones Magenta's probes 5 and 8 read in the game. @@ -95,6 +95,11 @@ # flingy.dat, by flingy id: movement control (byte), top speed (dword), acceleration (word), halt distance (dword). FLINGY_CONTROL, FLINGY_SPEED, FLINGY_ACCELERATION, FLINGY_HALT = 0x6C9858, 0x6C9EF8, 0x6C9C78, 0x6C9930 MINIMAP_COLOR_OFFSET = 0x581DD6 - 0x581D76 +# The heads of the game's lists of units (1.16.1, which Remastered emulates): the visible units and the dying ones. Each +# links a record in just behind its head, so behind the head a list runs latest first (scm-sim docs/specs/unit-lists.md). +VISIBLE_HEAD, DYING_HEAD = 0x628430, 0x62842C +# How many units one frame's events pass hands on of each kind; past that the rest are handed on in the next frame. +EVENT_CAP = 512 # The location table, and the one an order borrows for the length of one action (put back after). MRGN = 0x58DC60 SCRATCH_LOCATION = 255 @@ -1151,6 +1156,9 @@ def __init__(self, lowering, st): # turn can test it every time round: the slots after the last running one are not looked at. self.live = RowCell(Rows(lowering, True, None) if lowering.per_player else None, 0) self.count = self.live.store if not lowering.per_player else EUDVariable(0) # initial: the pool's own state + # A listener's start (on.created, ...): set when one of its tasks is started and has not had its first turn, so + # that without it and with nothing heard the tasks' turn passes the whole start by. + self.fresh = RowCell(Rows(lowering, True, None) if lowering.per_player else None, 0) if st.get("listen") is not None else None def add(self, n): """One more task running (1) or one fewer (-1).""" @@ -1169,6 +1177,116 @@ def at(self, cell, slot): return cell.store[self.base + slot] +class SlotTable(EUDObject): + """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 + its cell, as eudplib's EUDLoopNewUnit keeps its marks. The space between the cells is left for anything else.""" + + def GetDataSize(self): + return 4 + UNIT_SIZE * (UNIT_SLOTS - 1) + + def WritePayload(self, buf): + buf.WriteDword(0) + for _ in range(UNIT_SLOTS - 1): + buf.WriteSpace(UNIT_SIZE - 4) + buf.WriteDword(0) + + +class Events: + """The frame's events pass (on.created / on.died), before the programs' turns: the units that came onto the map since + 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 + 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 + 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 + list is walked from its head to the record that was behind the head at the pass before, the units linked in since + lying in between; when that record has left the list the walk goes to the end, which costs more and misses nothing. + Nothing of the game is written.""" + + def __init__(self, created, died): + self.created, self.died = created, died + self.seen = SlotTable() + # Each a unit's pointer, which f_cunitepdread_epd reads back cheaply with its EPD: within the frame a slot is not + # given to another unit (that takes a dying pass), so its uniqueness byte need not be kept. + self.made = EUDArray(EVENT_CAP) if created else None + self.made_n = EUDVariable(0) # initial: the pass's own state, set at its start + self.gone = EUDArray(EVENT_CAP) if died else None + self.gone_n = EUDVariable(0) # initial: the pass's own state, set at its start + # For each list, as the pass before left it: the record behind the head (pointer, EPD, uniqueness byte) and the + # head (pointer, EPD). Nothing is linked in but behind the head, so with both the same nothing came in since. + self.bounds = {h: [EUDVariable(0) for _ in range(5)] for h in (VISIBLE_HEAD, DYING_HEAD)} # initial: no pass before + + def cell(self, epd): + return epd + (EPD(self.seen) - EPD(UNIT_TABLE)) + + def walk(self, head, step): + """`step(ptr, epd, uid, cell, stop)` for each record from the head to the one behind the head at the pass before.""" + bptr, bepd, buid, hptr, hepd = self.bounds[head] + top, done, stop = Forward(), Forward(), Forward() + # Nothing linked in since the pass before: the same head, the same record behind it (or none). + walk = Forward() + EUDJumpIfNot(Memory(head, Exactly, hptr), walk) + EUDJumpIf(hptr == 0, stop) + if EUDIf()(bptr == 0): + EUDJumpIf(MemoryEPD(hepd + 1, Exactly, 0), stop) + if EUDElse()(): + EUDJumpIf([MemoryEPD(hepd + 1, Exactly, bptr), MemoryXEPD(bepd + OFF_UID, Exactly, buid, 0xFF00)], stop) + EUDEndIf() + walk << NextTrigger() + ptr, epd = f_cunitepdread_epd(EPD(head)) + first = fresh(1) + top << NextTrigger() + EUDJumpIf(ptr == 0, done) + uid = f_maskread_epd(epd + OFF_UID, 0xFF00) + EUDJumpIf([first == 0, ptr == bptr, uid == buid], done) + first << 0 + step(ptr, epd, uid, self.cell(epd), stop) + f_cunitepdread_epd(epd + 1, ret=[ptr, epd]) + EUDJump(top) + done << NextTrigger() + # What is behind the head now is where the next pass stops. + f_cunitepdread_epd(EPD(head), ret=[hptr, hepd]) + bptr << 0 + if EUDIf()(hptr >= 1): + f_cunitepdread_epd(hepd + 1, ret=[bptr, bepd]) + if EUDIf()(bptr >= 1): + buid << f_maskread_epd(bepd + OFF_UID, 0xFF00) + EUDEndIf() + EUDEndIf() + stop << NextTrigger() + + def run(self): + self.made_n << 0 + self.gone_n << 0 + + def came(ptr, epd, uid, cell, stop): + if EUDIfNot()(MemoryEPD(cell, Exactly, uid)): + EUDJumpIf(self.made_n >= EVENT_CAP, stop) + f_dwwrite_epd(cell, uid) + if self.created: + self.made[self.made_n] = ptr + self.made_n += 1 + EUDEndIf() + self.walk(VISIBLE_HEAD, came) + if self.died: + def went(ptr, epd, uid, cell, stop): + if EUDIf()(MemoryEPD(cell, Exactly, uid)): + EUDJumpIf(self.gone_n >= EVENT_CAP, stop) + f_dwwrite_epd(cell, uid + 1) + self.gone[self.gone_n] = ptr + self.gone_n += 1 + EUDEndIf() + self.walk(DYING_HEAD, went) + + +def uses_source(node, which): + """Whether a piece of the IR has a loop over the events pass's units of that kind.""" + if isinstance(node, list): + return any(uses_source(x, which) for x in node) + if isinstance(node, dict): + if node.get("kind") == "unitLoop" and node.get("source") == which: + return True + return any(uses_source(v, which) for v in node.values() if isinstance(v, (list, dict))) + return False + + class Lowering: def __init__(self, program): self.p = program @@ -1866,8 +1984,55 @@ def unit_loop(self, st, ctx): def body(ref, next_, exit_): self.vars[st["decl"]["id"]] = LoopUnit(ref) self.straight(st["body"], dict(ctx, **{"break": exit_, "continue": next_})) + if st.get("source"): + self.event_loop(st["source"], st.get("filter", {}), st, body) + return self.scan(st.get("filter", {}), st, body) + def event_loop(self, source, flt, node, body): + """The units of this frame's events pass, in the order it found them: `body(ref, next_, exit_)` for each the + filter matches. One that came on and has gone since is passed over; one that died is the dying record, read as + it died (its slot is not given to another unit before the next frame's dying pass).""" + if EVENTS is None: + raise Fail("trigscript: a loop over the events pass's units, and no pass%s" % where(node)) + cells, count = (EVENTS.made, EVENTS.made_n) if source == "created" else (EVENTS.gone, EVENTS.gone_n) + i = fresh(0) + head, next_, exit_ = Forward(), Forward(), Forward() + head << NextTrigger() + EUDJumpIf(i >= count, exit_) + ptr, epd = f_cunitepdread_epd(EPD(cells) + i) + if source == "created": + # Gone since the pass: killed or removed by a program before this turn (its slot is still its own). + EUDJumpIfNot([MemoryEPD(epd + OFF_SPRITE, AtLeast, 1), MemoryXEPD(epd + OFF_OWNER_ORDER, AtLeast, 0x100, 0xFF00)], next_) + match = [] + owner = flt.get("owner") + if owner is not None: + who = self.current() if owner == CURRENT_PLAYER else int(owner) + match.append(MemoryXEPD(epd + OFF_OWNER_ORDER, Exactly, who, 0xFF)) + kind = flt.get("type") + if kind is not None and kind < 228: + match.append(MemoryXEPD(epd + OFF_TYPE, Exactly, int(kind), 0xFFFF)) + elif kind is not None and kind not in GROUP_BITS: + raise Fail("trigscript: unknown unit type %r%s" % (kind, where(node))) + if flt.get("at"): + left, top, right, bottom = self.location_bounds(flt["at"]) + for v in (left, top): + if EUDIf()(v >= 0x80000000): + v << 0 + EUDEndIf() + match += [MemoryXEPD(epd + OFF_POS, AtLeast, left, 0xFFFF), MemoryXEPD(epd + OFF_POS, AtMost, right, 0xFFFF), + MemoryXEPD(epd + OFF_POS, AtLeast, f_bitlshift(top, 16), 0xFFFF0000), MemoryXEPD(epd + OFF_POS, AtMost, f_bitlshift(bottom, 16), 0xFFFF0000)] + if match: + EUDJumpIfNot(match, next_) + if kind in GROUP_BITS: + group = fresh(f_bread(UNITS_GROUP + f_maskread_epd(epd + OFF_TYPE, 0xFFFF))) + EUDJumpIf((group & GROUP_BITS[kind]) == 0, next_) + body(UnitRef(ptr, epd, None), next_, exit_) + next_ << NextTrigger() + i += 1 + EUDJump(head) + exit_ << NextTrigger() + def unit_write(self, st): ref = self.unit(st["unit"]) field = st["field"] @@ -3038,6 +3203,13 @@ def run_tasks(self): sweep << NextTrigger() self.again << 0 for pool in self.pools.values(): + passed = Forward() + if pool.fresh is not None: + go = Forward() + EUDJumpIf(as_var(pool.fresh.get()) >= 1, go) + EUDJumpIfNot(self.heard(pool.st["listen"]), passed) + go << NextTrigger() + pool.fresh.set(0) i, seen = EUDVariable(), EUDVariable() i << 0 seen << 0 @@ -3050,6 +3222,14 @@ def run_tasks(self): seen += 1 EUDJumpIf(as_var(pool.ran.get()) == self.tick, skip) pool.ran.set(self.tick) + listen = pool.st.get("listen") + if listen is not None: + # A listener (on.created, on.chat, ...) that has begun waits at the top of its loop, where a turn + # with nothing to hand on does nothing: it is not given one. + go = Forward() + EUDJumpIf(as_var(pool.co.get_state()) == 0, go) + EUDJumpIfNot(self.heard(listen), skip) + go << NextTrigger() outer = self.pool self.pool = pool end = Forward() @@ -3064,8 +3244,21 @@ def run_tasks(self): skip << NextTrigger() i += 1 EUDEndWhile() + passed << NextTrigger() EUDJumpIf(self.again >= 1, sweep) + def heard(self, listen): + """Whether a listener has anything to hand on this frame: units the events pass found, or one of its inputs.""" + source = listen.get("source") + if source is not None: + return (EVENTS.made_n if source == "created" else EVENTS.gone_n) >= 1 + found = fresh(0) + for inp in listen.get("inputs", []): + if EUDIf()(as_var(INPUT.read(inp, self, listen)) >= 1): + found << 1 + EUDEndIf() + return found >= 1 + def start(self, st): """A task into a free slot of its start: the arguments worked out as things are now, then the slot taken - its generation one more, its parameters set, its place the body's start - and the number the task is known by @@ -3125,6 +3318,8 @@ def start(self, st): pool.co.set_wait(0) pool.ran.set(self.tick - 1) pool.add(1) + if pool.fresh is not None: + pool.fresh.set(1) self.again << 1 if st.get("handle"): self.var(st["handle"], st).set(pool.slot + pool.task * 256 + f_bitlshift(fresh(gen), 16)) @@ -3798,6 +3993,8 @@ def at(o): INPUT = Input(IR.get("input")) +_DIED = uses_source(IR.get("programs", []), "died") +EVENTS = Events(uses_source(IR.get("programs", []), "created"), _DIED) if _DIED or uses_source(IR.get("programs", []), "created") else None PROGRAMS = [Lowering(p) for p in IR.get("programs", [])] @@ -3816,5 +4013,7 @@ def beforeTriggerExec(): def afterTriggerExec(): if INPUT.held: INPUT.frame() + if EVENTS is not None: + EVENTS.run() for prog in PROGRAMS: prog.run() diff --git a/scripts/test-game.mts b/scripts/test-game.mts index d2c1456..3f4a1af 100644 --- a/scripts/test-game.mts +++ b/scripts/test-game.mts @@ -90,6 +90,7 @@ try { const ran = r.cost.perFrame.filter((n) => n > 0), avg = ran.length ? Math.round(ran.reduce((a, b) => a + b, 0) / ran.length) : 0; console.log(` cost: ${avg} conditions and actions a frame on average, ${r.cost.peak} at the most (frame ${r.cost.peakFrame})`); for (const l of r.cost.lines.slice(0, 3)) console.log(` line ${l.line}: ${l.peak} at the most (frame ${l.peakFrame}), ran in ${l.frames} frames`); + if (process.env.COST_FRAMES) console.log(` frames from ${r.cost.from}: ${r.cost.perFrame.join(" ")}\n other: ${r.cost.other.join(" ")}`); } } // A test of several players: the map built again with their slots open to people, then a copy of the game for diff --git a/tests/eud-build.test.ts b/tests/eud-build.test.ts index 9b93bb7..9369eb9 100644 --- a/tests/eud-build.test.ts +++ b/tests/eud-build.test.ts @@ -325,6 +325,22 @@ const FIXTURES: Record = { f.cancel(); victory(); });`, + events: `program(async () => { + let made = 0; + on.created(units.ZergZergling, (u) => { u.energy = 50; made++; }); + on.created({ owner: P1, at: locations.Anywhere }, async (u) => { await sleep(frames(2)); u.heal(5); }, { max: 4 }); + const deaths = on.died({ owner: P8 }, async (u) => { await sleep(seconds(2)); createUnit(P8, u.type, 1, locations.Anywhere); print(\`back at \${u.x}, \${u.y}\`); }); + on.died(units.Men, (u) => { if (u.owner == 0) setResources(P1, "add", 1, "ore"); }); + on.chat("-give {n}", (p, { n }) => { setResources(p, "add", n, "ore"); }); + on.key("F2", (p) => print("help", { to: p }), { from: P1 }); + await until(() => made >= 10); + deaths.cancel(); + });`, + eventsPerPlayer: `program(async () => { + let mine = 0; + on.created({ owner: CurrentPlayer }, () => { mine++; }); + on.chat("-me", () => { print(\`\${mine}\`); }); + }, { owner: AllPlayers });`, startedPerPlayer: `program(async () => { let mine = 0; async function add(k: number) { await sleep(frames(k)); mine += k; } @@ -583,6 +599,6 @@ describe("the IR as the lowering reads it", () => { const ir = JSON.parse(serializeIr(r.ir, r.strings)); const actions = ir.programs[0].body.filter((s: { kind: string }) => s.kind === "action").map((s: { record: { text: unknown; wav: unknown } }) => [s.record.text, s.record.wav]); expect(actions).toEqual([["hello", 0], [0, "sound\\x.wav"]]); - expect(ir.version).toBe(17); + expect(ir.version).toBe(18); }); }); diff --git a/tests/events.test.ts b/tests/events.test.ts new file mode 100644 index 0000000..9d798df --- /dev/null +++ b/tests/events.test.ts @@ -0,0 +1,105 @@ +/** + * TrigScript 4, slice B2: events. `on.created`, `on.died`, `on.chat` and `on.key` are each a task that, from the next + * frame on, runs its handler for every time the thing happens: the units come from the frame's events pass (a loop with + * a `source` in the IR), the lines and keys from what chatted() and keyPressed() read. The interpreter here; + * `eud-build.test.ts` builds the same through eudplib, and `probes/game-events.ts` runs it in the game. + */ +import ts from "typescript"; +import { describe, expect, it } from "vitest"; +import { compileScript, type CompileResult } from "../compiler/compiler"; +import { defaultScriptNames } from "../compiler/names"; +import { ProgramSimulation, type SimUnitInit } from "../compiler/simulateIr"; +import { ActionType } from "../vendor/triggers"; +import { defaultLib } from "../bundle/lib.mjs"; +import type { Stmt } from "../compiler/ir"; + +const LIB = defaultLib(); +const NAMES = defaultScriptNames(); +const MARINE = 0, ZERGLING = 37; +const raw = (body: string, options = ""): CompileResult => compileScript(ts, { "main.ts": `const HOME = 1 as Location;\nprogram(async () => {${body}}${options ? `, ${options}` : ""});` }, NAMES, { lib: LIB }); +const compile = (body: string, options = ""): CompileResult => { const r = raw(body, options); expect(r.diagnostics.map((d) => d.message)).toEqual([]); return r; }; +const messages = (body: string): string[] => raw(body).diagnostics.map((d) => d.message); +const WORLD: SimUnitInit[] = [ + { type: MARINE, owner: 0, x: 100, y: 100, hp: 40 }, + { type: ZERGLING, owner: 7, x: 120, y: 110, hp: 35 }, +]; +const LOCATIONS = { 1: { left: 0, top: 0, right: 256, bottom: 256 } }; +const sim = (r: CompileResult) => new ProgramSimulation(r.ir, { strings: r.strings, units: WORLD, locations: LOCATIONS, players: [0, 1], unitStats: () => ({ hp: 40 }) }); +/** What was shown, as "frame:text". */ +const printed = (s: ProgramSimulation): string[] => s.events.filter((e) => e.action.type === ActionType.DisplayText).map((e) => `${e.cycle}:${e.text}`); + +describe("events: the front end", () => { + it("on.created is a task whose loop goes through the units the frame's pass found", () => { + const r = compile(`on.created(units.ZergZergling, (u) => { u.energy = 50; });`); + const start = r.ir[0].body[0] as Extract; + expect(start).toMatchObject({ kind: "start", max: 1, call: { name: "on.created()" } }); + expect(start.call.body[0]).toMatchObject({ kind: "while", body: [{ kind: "sleep", cycles: 1 }, { kind: "unitLoop", source: "created", filter: { type: ZERGLING } }] }); + }); + + it("what is wrong is said", () => { + expect(messages(`on.created((u) => {});`)).toEqual([]); + expect(raw(`});\nprogram(() => { on.died((u) => {}); `).diagnostics.map((d) => d.message)).toContain("on.died() listens beside the program as a task: inside program(async () => { … })."); + expect(messages(`on.created(units.ZergZergling);`)[0]).toContain("is not assignable"); + expect(messages(`on.key("F6", (p) => {});`)[0]).toContain("F6"); + expect(messages(`on.created((u) => {}, { max: 4 });`)[0]).toContain("max is for an async handler"); + expect(messages(`on.chat("-x", (p) => {}, { from: P9 });`)[0]).toContain("from is one player"); + }); +}); + +describe("events: in the simulator", () => { + it("on.created runs in the frame after a unit is made, for the ones the filter matches; the map's own units are not new", () => { + const r = compile(` + on.created(units.ZergZergling, (u) => { print(\`made \${u.owner} \${u.hp}\`); }); + await sleep(frames(2)); + createUnit(P8, units.ZergZergling, 2, HOME); + createUnit(P1, units.TerranMarine, 1, HOME);`); + const s = sim(r).run(5); + expect(printed(s)).toEqual(["3:made 7 40", "3:made 7 40"]); + }); + + it("on.died hands what the unit was and where, as it died, to a handler that may wait", () => { + const r = compile(` + on.died({ owner: P8 }, async (u) => { print(\`gone \${u.type} at \${u.x}\`); await sleep(frames(3)); createUnit(u.owner, u.type, 1, HOME); print("back"); }); + await sleep(frames(1)); + for (const u of unitsOf(P8)) u.kill();`); + const s = sim(r).run(8); + expect(printed(s)).toEqual([`2:gone ${ZERGLING} at 120`, "5:back"]); + expect(s.game.living().filter((u) => u.owner === 7)).toHaveLength(1); + }); + + it("a unit made and gone between two passes is neither made nor dead; each death is handed once", () => { + const r = compile(` + let made = 0, died = 0; + on.created((u) => { made++; }); + on.died((u) => { died++; }); + await sleep(frames(1)); + createUnit(P1, units.TerranMarine, 3, HOME); + for (const u of unitsOf(P1, { type: units.TerranMarine })) u.remove(); + await sleep(frames(1)); + createUnit(P1, units.TerranMarine, 1, HOME); + await sleep(frames(2)); + for (const u of unitsOf(P1)) u.kill(); + await sleep(frames(3)); + print(\`\${made} \${died}\`);`); + expect(printed(sim(r).run(10))).toEqual(["7:1 2"]); + }); + + it("cancel() stops listening", () => { + const r = compile(` + const t = on.created((u) => { print("seen"); }); + await sleep(frames(1)); + t.cancel(); + createUnit(P1, units.TerranMarine, 1, HOME);`); + expect(printed(sim(r).run(4))).toEqual([]); + }); + + it("on.chat and on.key: every player, the player known in the handler", () => { + const r = compile(` + on.chat("-give {n}", (p, { n }) => { setResources(p, "add", n, "ore"); print(\`gave \${n}\`, { to: p }); }); + on.key("F2", (p) => print("help", { to: p }), { from: P2 });`); + const s = sim(r); + s.run(2).type("-give 25", 1).press("F2", 1).press("F2", 0).run(1); + expect(printed(s)).toEqual(["2:gave 25", "2:help"]); + expect(s.events.filter((e) => e.action.type === ActionType.SetResources).map((e) => e.action.player)).toEqual([1]); + }); +}); diff --git a/tests/functions.test.ts b/tests/functions.test.ts index 725b8af..0e2231a 100644 --- a/tests/functions.test.ts +++ b/tests/functions.test.ts @@ -175,7 +175,7 @@ describe("functions: arrays, and functions inside functions", () => { it("the functions go to the lowering with the program, their text written out", () => { const r = compile("function say(n: number) { displayText(`n`); setDeaths(P1, units.TerranMarine, 'set', n); } let k = 1; say(k); say(k + 1);"); const ir = JSON.parse(serializeIr(r.ir, r.strings)); - expect(ir.version).toBe(17); + expect(ir.version).toBe(18); expect(ir.programs[0].functions).toHaveLength(1); expect(JSON.stringify(ir.programs[0].functions)).toContain('"text":"n"'); }); diff --git a/tests/script-structured.test.ts b/tests/script-structured.test.ts index 7a9bac8..5ed6e72 100644 --- a/tests/script-structured.test.ts +++ b/tests/script-structured.test.ts @@ -82,7 +82,7 @@ describe("structured: loops and branches", () => { expect(r.triggers).toEqual([]); expect(r.strings).toEqual([]); expect(r.ir).toHaveLength(1); - expect(r.ir[0]).toMatchObject({ version: 17, owner: 0, owners: [0], perPlayer: false }); + expect(r.ir[0]).toMatchObject({ version: 18, owner: 0, owners: [0], perPlayer: false }); const sim = run(r, 6); expect(sim.events.map((e) => `${e.cycle}:${ActionType.Victory === e.action.type ? "Victory" : e.action.type}`)).toEqual(["2:Victory"]); expect(value(sim, "n")).toBe(6); diff --git a/tests/service.test.ts b/tests/service.test.ts index 710ee4e..a202618 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -264,9 +264,9 @@ describe("service: saving applies the script and builds its programs", () => { expect(compiles).toBe(before); expect(Object.keys(input.plugins)).toEqual(["trigscript", "eudTurbo"]); expect(input.plugins.trigscript).toEqual({ ir: "/work/files/trigscript.json" }); - expect(input.sources!.trigscript).toContain("IR_VERSION = 17"); + expect(input.sources!.trigscript).toContain("IR_VERSION = 18"); const ir = JSON.parse(input.files!["trigscript.json"]); - expect(ir.version).toBe(17); + expect(ir.version).toBe(18); expect(ir.programs).toHaveLength(1); expect(JSON.stringify(ir)).toContain('"text":"tick"'); // The library going away takes the contribution with it; coming back, it is made again. From 65a6a6bb0ba708c867e5707f62bbf5579522d058 Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 07:41:01 -0600 Subject: [PATCH 057/238] TrigScript 4, B2: walk the dying list both ways Ghidra on Remastered 1.23.10: its kill routine links a dying unit in behind the other end of the list from the one its EUD layer maps to 1.16.1's 0x62842C, so a walk along "next" from there sees one death a frame. The events pass now walks the dying list whole, after and then before the record at 0x62842C, which is right in both games; the cell still hands each death on once. Co-Authored-By: Claude Opus 5.5 --- compiler/generated/trigscriptPy.ts | 2 +- docs/ir.md | 15 ++++++----- docs/v4-plan.md | 19 +++++++++++--- probes/events.ts | 7 +++--- python/trigscript.py | 40 ++++++++++++++++++++++++++---- 5 files changed, 65 insertions(+), 18 deletions(-) diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index dcb6ecf..b1714fb 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 18\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# 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 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# 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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 is walked from its head to the record that was behind the head at the pass before, the units linked in since\n lying in between; when that record has left the list the walk goes to the end, which costs more and misses nothing.\n Nothing of the game is written.\"\"\"\n\n def __init__(self, created, died):\n self.created, self.died = created, died\n self.seen = SlotTable()\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 = {h: [EUDVariable(0) for _ in range(5)] for h in (VISIBLE_HEAD, DYING_HEAD)} # initial: no pass before\n\n def cell(self, epd):\n return epd + (EPD(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 run(self):\n self.made_n << 0\n self.gone_n << 0\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(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\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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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, count = (EVENTS.made, EVENTS.made_n) if source == \"created\" else (EVENTS.gone, EVENTS.gone_n)\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 == \"created\":\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 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 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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 (EVENTS.made_n if source == \"created\" else EVENTS.gone_n) >= 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 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 op == \">\":\n return av > b\n return av >= b\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 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 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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\n_DIED = uses_source(IR.get(\"programs\", []), \"died\")\nEVENTS = Events(uses_source(IR.get(\"programs\", []), \"created\"), _DIED) if _DIED or uses_source(IR.get(\"programs\", []), \"created\") 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 if INPUT.held:\n INPUT.frame()\n if EVENTS is not None:\n EVENTS.run()\n for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 18\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# 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 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# 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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):\n self.created, self.died = created, died\n self.seen = SlotTable()\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):\n return epd + (EPD(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 run(self):\n self.made_n << 0\n self.gone_n << 0\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\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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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, count = (EVENTS.made, EVENTS.made_n) if source == \"created\" else (EVENTS.gone, EVENTS.gone_n)\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 == \"created\":\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 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 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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 (EVENTS.made_n if source == \"created\" else EVENTS.gone_n) >= 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 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 op == \">\":\n return av > b\n return av >= b\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 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 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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\n_DIED = uses_source(IR.get(\"programs\", []), \"died\")\nEVENTS = Events(uses_source(IR.get(\"programs\", []), \"created\"), _DIED) if _DIED or uses_source(IR.get(\"programs\", []), \"created\") 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 if INPUT.held:\n INPUT.frame()\n if EVENTS is not None:\n EVENTS.run()\n for prog in PROGRAMS:\n prog.run()\n"; diff --git a/docs/ir.md b/docs/ir.md index b9c6e38..a99faa9 100644 --- a/docs/ir.md +++ b/docs/ir.md @@ -204,19 +204,22 @@ nothing it listens for happened (`listen.source`: the pass found none of those; arrived), since such a turn does nothing; the simulator gives it every turn. The **events pass** runs once a frame before the programs, when the IR has a `unitLoop` with a `source`. It -walks two of the game's lists from their heads along "next" (1.16.1's addresses: the visible units at -0x628430 and the dying ones at 0x62842C; each links a record in just behind its head, so the records linked -in since the pass before lie between the head and the record that was behind it then). A cell for each unit +walks two of the game's lists (1.16.1's addresses: the visible units at 0x628430 and the dying ones at +0x62842C). The visible list is walked from its head along "next": it links a record in just behind its head, +so the records linked in since the pass before lie between the head and the record that was behind it then. +The dying list is walked whole, along "next" and then "previous" from the record its address gives, since +Remastered's EUD layer gives the list's other end there. A cell for each unit slot holds the uniqueness byte of the unit last seen in it, and once that unit's death has been handed on, one more: a unit on the visible list whose cell is not its byte came on (`created`); a unit on the dying list whose cell is its byte has died or been removed (`died`), and is handed on once. A unit made and gone between two passes, a turret, a scanner sweep and a unit that never left its transport are neither. When the -head and the record behind it are both as the pass before left them, a walk is passed by; when that record -has left the list, the walk goes to the end. At most 512 of each a frame; the rest come in the next. +head and the record behind it are both as the pass before left them, the visible walk is passed by; when that +record has left the list, the walk goes to the end. At most 512 of each a frame; the rest come in the next. A `created` turn is a unit still on the map (one gone since the pass is passed over); a `died` turn is the dying record, whose fields read as the unit was when it died — its slot is not given to another unit before -the next frame's dying pass — and the front end only reads its fields. Each list is the latest first. +the next frame's dying pass — and the front end only reads its fields. The visible list is the latest first; +the order of a frame's deaths is the dying list's, which is not the same in the two games. ### Tasks diff --git a/docs/v4-plan.md b/docs/v4-plan.md index acb3168..045c711 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -907,6 +907,19 @@ on.key("F2", (p) => print("Help", { to: p }), { from: P1 }); what happened, and a frame where nothing did costs three conditions a list. A table of a cell for each unit slot, 336 bytes apart as the slots are (eudplib's EUDLoopNewUnit keeps its marks that way), holds the uniqueness byte of the unit seen in the slot, and one more once its death has been handed on. Nothing of the game is written. +- **Remastered's dying list runs the other way (Ghidra, 2026-10-17).** Its kill routine unlinks the unit from the + visible list (img+0xe77fc8 head, +0xe78078 tail) and links it into the dying list with the same link-in routine + (img+0x1956a0: the record goes behind the first pointer it is given), passing img+0xe78080 as the end it links + behind and img+0xe77fd0 as the other; the free step unlinks it the same way round. The EUD emulator maps 1.16.1's + 0x62842C to img+0xe77fd0 (traced under Unicorn: the region handler 0x2e1e50 reads that global), so a map reading + the dying list's "head" gets Remastered's far end, whose "next" is 0, and 1.16.1's 0x59CC98 (where Remastered's + linking end would be) is not readable. A walk along "next" from 0x62842C would see one death a frame. The visible + list maps the right way (0x628430 → img+0xe77fc8, linked behind it); unit +0x00 / +0x04 map to Remastered's + +0x00 / +0x08, previous and next, the same as 1.16.1. (The hidden and sweep lists are mapped the other way round + too: 0x628428 and 0x628434, tails in 1.16.1, give Remastered's linking ends.) So the dying list is walked whole, + both ways from the record at 0x62842C, every frame it is not empty; the cell says which records are new deaths. + A record stays on it until its sprite is gone: 14 frames for a Zergling, and 200 of them cost about 21k a frame + for those frames. The order a frame's deaths are handed on in is the list's, so it differs between the games. - **What counts.** A unit that comes onto the visible list for the first time is made: made by an action or a program, trained (when it comes out, not when it is queued), placed by a test; a unit in a transport is made when it first comes out, and unloading it again is nothing new. A unit on the dying list that was seen is dead, @@ -924,7 +937,8 @@ on.key("F2", (p) => print("Help", { to: p }), { from: P1 }); the pass and a listener, 2 more for each other listener; about 100 for each unit made and 80 for each death. Two things made it so: the task runner passes a listener's whole start by when it has nothing to hand on and no task that has not had its first turn (`start.listen` in IR 18; a skipped turn would have done nothing), and the pass - passes a walk by when the list's head and the record behind it are as it left them. Before either, two idle + passes the visible list's walk by when its head and the record behind it are as it left them (the dying list costs + three conditions when empty, about 100 a record otherwise). Before either, two idle listeners cost about 1,240 a frame. `on.chat` and `on.key` for all eight players idle at about 1,290, where the same polled with `chatted()` / `keyPressed()` in a loop costs about 1,730. - **Simulator.** The same pass over its world's units: a unit is seen at the first pass it is alive at, its death @@ -935,8 +949,7 @@ on.key("F2", (p) => print("Help", { to: p }), { from: P1 }); - **Checked.** `tests/events.test.ts` (7), eudplib fixtures `events` and `eventsPerPlayer`, `probes/game-events.ts` (4) and `probes/events.ts` (3) in the lifted game and the simulator, at the same frames; every older game probe still passes. **Probe for Remastered:** `Maps/TrigScript/trigscript-b2-events.scx` with its answer sheet: whether - Remastered's emulator gives the dying list as 1.16.1 has it (R0 says 0x62842C reads; nobody has walked it), a - trained SCV, unloading, 200 made and killed in a frame, and chat and a key in a multiplayer game. Not played yet. + every death reaches `on.died` in Remastered (rebuilt 2026-10-17 with the walk both ways), a trained SCV, unloading, 200 made and killed in a frame, and chat and a key in a multiplayer game. Not played yet. - **Not yet:** `on.order` (each unit's order against the last frame's: a scan, or a pass over the visible list), `u.killedBy` (the killer's kill count rises a frame before; or the unit's +0x90, last attacking player, from the dying record — both to check), a `removed` flag on a DeadUnit, a morph event, listeners at the top of a script diff --git a/probes/events.ts b/probes/events.ts index b52ff1b..970da10 100644 --- a/probes/events.ts +++ b/probes/events.ts @@ -1,7 +1,8 @@ // The events probe (TrigScript 4, B2): does Remastered hand on what on.created / on.died / on.chat / on.key listen for? -// The events pass walks two of the game's lists of units from their heads — the visible units (0x628430) and the dying -// ones (0x62842C) — and reads each record's links and uniqueness byte; R0's dump says all of it reads, and eudplib's own -// EUDLoopNewUnit walks the first, but nobody has walked the dying list in Remastered. The lifted game passes every step. +// The events pass walks two of the game's lists of units — the visible units (0x628430) from their head, and the dying +// ones (0x62842C) whole, both ways — and reads each record's links and uniqueness byte. Ghidra (2026-10-17): Remastered +// links its dying list the other way round and hands a map that list's far end at 0x62842C, so a walk along "next" alone +// would see one death a frame there. This checks every death arrives. The lifted game passes every step. // // What to look for, by the second (each step says so in the middle of the screen too): // A 3 three Marines made by the program beside your base: "made 0" three times (0 is the Marine's number). diff --git a/python/trigscript.py b/python/trigscript.py index 7c504f3..7b97b3e 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -1196,9 +1196,12 @@ class Events: 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 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 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 - list is walked from its head to the record that was behind the head at the pass before, the units linked in since - lying in between; when that record has left the list the walk goes to the end, which costs more and misses nothing. - Nothing of the game is written.""" + list on the map is walked from its head to the record that was behind the head at the pass before, the units linked + in since lying in between; when that record has left the list the walk goes to the end, which costs more and misses + nothing. The dying list is walked whole, both ways from the record its address gives: in 1.16.1 that is the list's + head, but Remastered links its dying list the other way round and hands an EUD map its tail there (its own code + links a dying unit in behind the other end, which no map can read), so only a walk both ways sees every record in + both. A record stays on it only until its sprite is gone. Nothing of the game is written.""" def __init__(self, created, died): self.created, self.died = created, died @@ -1211,7 +1214,7 @@ def __init__(self, created, died): self.gone_n = EUDVariable(0) # initial: the pass's own state, set at its start # For each list, as the pass before left it: the record behind the head (pointer, EPD, uniqueness byte) and the # head (pointer, EPD). Nothing is linked in but behind the head, so with both the same nothing came in since. - self.bounds = {h: [EUDVariable(0) for _ in range(5)] for h in (VISIBLE_HEAD, DYING_HEAD)} # initial: no pass before + self.bounds = {VISIBLE_HEAD: [EUDVariable(0) for _ in range(5)]} # initial: no pass before def cell(self, epd): return epd + (EPD(self.seen) - EPD(UNIT_TABLE)) @@ -1252,6 +1255,33 @@ def walk(self, head, step): EUDEndIf() stop << NextTrigger() + def walk_all(self, head, step): + """`step(ptr, epd, uid, cell, stop)` for every record of a list: the one at its address, those after it (+4), then + those before it (+0).""" + top, done, stop = Forward(), Forward(), Forward() + EUDJumpIf(Memory(head, Exactly, 0), stop) + ptr, epd = f_cunitepdread_epd(EPD(head)) + sepd = EUDVariable() + sepd << epd + link = fresh(1) + top << NextTrigger() + EUDJumpIf(ptr == 0, done) + uid = f_maskread_epd(epd + OFF_UID, 0xFF00) + step(ptr, epd, uid, self.cell(epd), stop) + if EUDIf()(link == 1): + f_cunitepdread_epd(epd + 1, ret=[ptr, epd]) + if EUDElse()(): + f_cunitepdread_epd(epd, ret=[ptr, epd]) + EUDEndIf() + EUDJump(top) + done << NextTrigger() + # The way after it is done: now the way before it, from the record before the first. + EUDJumpIf(link == 0, stop) + link << 0 + f_cunitepdread_epd(sepd, ret=[ptr, epd]) + EUDJump(top) + stop << NextTrigger() + def run(self): self.made_n << 0 self.gone_n << 0 @@ -1273,7 +1303,7 @@ def went(ptr, epd, uid, cell, stop): self.gone[self.gone_n] = ptr self.gone_n += 1 EUDEndIf() - self.walk(DYING_HEAD, went) + self.walk_all(DYING_HEAD, went) def uses_source(node, which): From 47c9c9aaf702db5c308c7cdf3bbab19b3e057a3a Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 08:00:52 -0600 Subject: [PATCH 058/238] TrigScript 4, B2: the events probe gives minerals for its SCV; on.chat and on.key in the lifted game Co-Authored-By: Claude Opus 5.5 --- probes/events.ts | 3 ++- probes/game-events-input.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 probes/game-events-input.ts diff --git a/probes/events.ts b/probes/events.ts index 970da10..a32a921 100644 --- a/probes/events.ts +++ b/probes/events.ts @@ -44,7 +44,8 @@ program(async () => { for (const u of unitsOf(P1, { type: units.TerranFirebat })) u.remove(); await sleep(seconds(5)); - print("D: train an SCV now. Expect \"made 7\" when it comes out. Loading and unloading should say nothing.", { position: "center" }); + setResources(P1, "add", 100, "ore"); + print("D: train an SCV now (you have 100 more minerals). Expect \"made 7\" when it comes out. Loading and unloading should say nothing.", { position: "center" }); await sleep(seconds(15)); print("E: 200 Zerglings in one frame, then all killed in one frame. Does the game slow?", { position: "center" }); diff --git a/probes/game-events-input.ts b/probes/game-events-input.ts new file mode 100644 index 0000000..86e483d --- /dev/null +++ b/probes/game-events-input.ts @@ -0,0 +1,19 @@ +// TrigScript 4, B2: on.chat and on.key in the lifted game, with what the handler prints to the player. +// npx tsx scripts/test-game.mts probes/game-events-input.ts +import { test, expect, program, print, setResources, on, P1 } from "trigscript"; + +program(async () => { + let gave = 0, helped = 0; + on.chat("-give {n}", (p, { n }) => { gave += n; setResources(p, "add", n, "ore"); print(`gave ${n}`, { to: p }); }); + on.key("F2", (p) => { helped++; print("help", { to: p }); }); +}, { owner: P1, name: "input" }); + +test("a typed line and a key reach their handlers, and what they print shows", (sim) => { + sim.frames(4); + sim.type("-give 100").seconds(1); + expect(sim.program("input").gave).toBe(100); + expect(sim).toHavePrinted("gave 100"); + sim.press("F2").seconds(1); + expect(sim.program("input").helped).toBe(1); + expect(sim).toHavePrinted("help"); +}); From df38a74e09d8a75c805a7c4b8bf836f7ec4f03a3 Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 08:15:34 -0600 Subject: [PATCH 059/238] TrigScript 4: F2 to F5 are keys Remastered keeps, as F6 is Played 2026-10-17: on.key heard Q, F7 and F9 but not F2, F3, F4 or F5, and keyPressed did not hear F2 either (F2 to F4 move the camera). They leave the Key type with a message saying why; the examples and tests use F7. The events probe passed every other step in Remastered, recorded in the plan. Co-Authored-By: Claude Opus 5.5 --- README.md | 9 +++++---- compiler/declarations.ts | 6 +++--- compiler/input.ts | 9 +++++++-- compiler/runtime.ts | 2 +- compiler/simulateIr.ts | 2 +- docs/v4-plan.md | 7 +++++++ probes/events.ts | 8 ++++---- probes/game-events-input.ts | 4 ++-- tests/eud-build.test.ts | 4 ++-- tests/events.test.ts | 4 ++-- tests/input.test.ts | 27 +++++++++++++++------------ tests/testing.test.ts | 4 ++-- 12 files changed, 51 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 9c22151..b7e44ee 100644 --- a/README.md +++ b/README.md @@ -277,7 +277,7 @@ import { test, expect, program, keyPressed, chatted, setResources, print, sleep, program(() => { let shop = false; while (true) { - if (keyPressed(CurrentPlayer, "F2")) { shop = !shop; print(shop ? "Shop open" : "Shop closed"); } + if (keyPressed(CurrentPlayer, "F7")) { shop = !shop; print(shop ? "Shop open" : "Shop closed"); } const give = chatted(CurrentPlayer, "-give {n}"); if (give && shop) setResources(CurrentPlayer, "add", give.n, "ore"); sleep(frames(1)); @@ -287,7 +287,7 @@ program(() => { test("-give pays only while the shop is open", (sim) => { sim.type("-give 100").frames(2); expect(sim.resources(0 as Player).ore).toBe(0); - sim.press("F2").frames(1); + sim.press("F7").frames(1); expect(sim.program("shop").shop).toBe(true); sim.type("-give 100").frames(2); expect(sim.resources(0 as Player).ore).toBe(100); @@ -1292,8 +1292,9 @@ program(() => { `keyPressed(player, key)` is true when the press arrives — once per press, not while the key is held (the game reports no held key), and not while the player is typing a message. -Keys are the letters and digits, `"F1"` … `"F12"` without `"F6"` (the game keeps it to -itself and reports no press of it — played and seen — so it is an error to ask), `"Space"`, `"Enter"`, `"Escape"`, +Keys are the letters and digits, `"F1"`, `"F7"` … `"F12"` (the game keeps F2 to F6 to +itself and reports no press of them — played and seen; F2 to F4 move the camera — so it is +an error to ask), `"Space"`, `"Enter"`, `"Escape"`, `"Tab"`, `"Shift"`, `"Ctrl"`, `"Alt"`, the arrows (`"Left"` …), `"Backspace"`, `"Delete"`, `"Insert"`, `"Home"`, `"End"`, `"PageUp"`, `"PageDown"` and `"Numpad0"` … `"Numpad9"`; the game's own use of a key still happens. `clicked(player, "left" | "right" | "middle")` is diff --git a/compiler/declarations.ts b/compiler/declarations.ts index dac61cc..1ece333 100644 --- a/compiler/declarations.ts +++ b/compiler/declarations.ts @@ -167,7 +167,7 @@ ${kw}interface UnitFilter { /** Inside this location. */ at?: Location; } -/** A key keyPressed() knows. F6 is not among them: the game reports no press of it. */ +/** A key keyPressed() knows. F2 to F6 are not among them: StarCraft: Remastered reports no press of them. */ ${kw}type Key = __KEY_NAMES__; /** What a chatted() pattern's captures are read as: {n} a number, {unit:unit} a unit type, {kind:ore|gas} the place of the word in its list. */ ${kw}type ChatCapture = C extends \`\${infer N}:unit\` ? { readonly [K in N]: UnitType } : C extends \`\${infer N}:\${string}\` ? { readonly [K in N]: number } : { readonly [K in C]: number }; @@ -466,13 +466,13 @@ ${kw}const on: { * when the script is built, so an action can be for them. */ chat(pattern: P, handler: (player: Player, values: ChatValues

) => void | Promise, options?: EventOptions & { from?: Player }): Task; - /** A player presses a key, as keyPressed() sees it: \`on.key("F2", (p) => print("Help", { to: p }))\`. Every player P1 … P8 unless \`from\` names one. */ + /** A player presses a key, as keyPressed() sees it: \`on.key("F7", (p) => print("Help", { to: p }))\`. Every player P1 … P8 unless \`from\` names one. */ key(key: Key, handler: (player: Player) => void | Promise, options?: EventOptions & { from?: Player }): Task; }; /** * What the players do, inside program() only. A key, a click and a typed line are true on the one * frame they arrive, so look for them in a loop that runs every frame: - * \`while (true) { if (keyPressed(CurrentPlayer, "F2")) …; sleep(frames(1)); }\`. They reach every + * \`while (true) { if (keyPressed(CurrentPlayer, "F7")) …; sleep(frames(1)); }\`. They reach every * player's computer in step, a few frames after they happen. The player is one of P1 … P8 or * CurrentPlayer — in a program with \`{ owner: AllPlayers }\`, each player's own keys. * The map gives up a little for it: one free location among the first 63 (nine when the mouse is read), diff --git a/compiler/input.ts b/compiler/input.ts index 3311a4f..d628e24 100644 --- a/compiler/input.ts +++ b/compiler/input.ts @@ -33,9 +33,14 @@ const DIGITS = "0123456789".split(""); /** * Keys the game never reports a press of, with why: the slice 4 probe pressed F6 in two builds — once * first in MSQC's settings, once third — and nothing arrived either time, while F7, F8, a digit and - * the letters beside it did. Left out of the list, so writing one is an error and not a silent key. + * the letters beside it did. The B2 keys probe (2026-10-17) found F2 to F5 the same, through on.key and + * keyPressed alike, while Q, F7 and F9 answered: F2–F4 are the game's camera keys. Left out of the list, + * so writing one is an error and not a silent key. F1, F10, F11 and F12 have not been played. */ -export const DEAF_KEYS: Readonly> = { F6: "StarCraft: Remastered keeps F6 to itself and reports no press of it (played and seen); F7 and F8 work" }; +const kept = (key: string, what: string) => `StarCraft: Remastered keeps ${key} to itself${what} and reports no press of it (played and seen); F7, F8 and F9 work`; +export const DEAF_KEYS: Readonly> = { + F2: kept("F2", " (a camera key)"), F3: kept("F3", " (a camera key)"), F4: kept("F4", " (a camera key)"), F5: kept("F5", ""), F6: kept("F6", ""), +}; /** Every key name, as the declarations list them. */ export const KEY_NAMES: readonly string[] = [ ...LETTERS, ...DIGITS, ...Array.from({ length: 12 }, (_, i) => `F${i + 1}`).filter((k) => !(k in DEAF_KEYS)), ...Object.keys(NAMED_KEYS), ...DIGITS.map((d) => `Numpad${d}`), diff --git a/compiler/runtime.ts b/compiler/runtime.ts index 16b3a62..c7855b7 100644 --- a/compiler/runtime.ts +++ b/compiler/runtime.ts @@ -632,7 +632,7 @@ export function createRuntime(names: ScriptNames, collector: Collector, options: created: listens("created", "on.created(units.ZergZergling, (u) => { … })"), died: listens("died", "on.died({ owner: P8 }, (u) => { … })"), chat: listens("chat", "on.chat(\"-give {n}\", (p, { n }) => { … })"), - key: listens("key", "on.key(\"F2\", (p) => { … })"), + key: listens("key", "on.key(\"F7\", (p) => { … })"), }); /* ── Text a program fills in ── */ diff --git a/compiler/simulateIr.ts b/compiler/simulateIr.ts index d93ae2a..45fe4e3 100644 --- a/compiler/simulateIr.ts +++ b/compiler/simulateIr.ts @@ -1294,7 +1294,7 @@ export class ProgramSimulation { /** The typed-line patterns of all the programs, in the order the game tries them: the first that matches a line is the one that line is. */ private readonly chats: ChatPattern[] = []; private readonly unitByName: (lower: string) => number | undefined; - /** What the players did, for the next frame and for the one running: "key:0:F2", "click:0:left", and a typed line as the pattern it matched with its values. */ + /** What the players did, for the next frame and for the one running: "key:0:F7", "click:0:left", and a typed line as the pattern it matched with its values. */ private queued: { events: Set; lines: Map } = { events: new Set(), lines: new Map() }; private current = this.queued; /** The keys and buttons down on the last frame, and on this one (what keyHeld() finds). */ diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 045c711..3547b2f 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -950,6 +950,13 @@ on.key("F2", (p) => print("Help", { to: p }), { from: P1 }); (4) and `probes/events.ts` (3) in the lifted game and the simulator, at the same frames; every older game probe still passes. **Probe for Remastered:** `Maps/TrigScript/trigscript-b2-events.scx` with its answer sheet: whether every death reaches `on.died` in Remastered (rebuilt 2026-10-17 with the walk both ways), a trained SCV, unloading, 200 made and killed in a frame, and chat and a key in a multiplayer game. Not played yet. +- **Played in Remastered, 2026-10-17** (multiplayer, alone): every step passes but the key. A: three "made 0". + B: three "died 0 at x, y" near the base, then three "back" and three new Marines, each "made 0". C: two Firebats + made and removed, "died 32" twice. D: "made 7" when the SCV came out, not when queued (loading not tried: nothing + to load). E: "E: made 200, died 200", no stutter. F: `-give 100` gave 100 and printed "gave 100" to the player. + F2 moved the camera (the game's own key) and said nothing; a second probe (Q, F2–F5, F7, F9, each through + `on.key`, and F2 through `keyPressed`) heard only Q, F7 and F9. So F2–F5 join F6 as keys Remastered keeps: they + are not a `Key`, and the examples use F7. F1, F10–F12 not played. - **Not yet:** `on.order` (each unit's order against the last frame's: a scan, or a pass over the visible list), `u.killedBy` (the killer's kill count rises a frame before; or the unit's +0x90, last attacking player, from the dying record — both to check), a `removed` flag on a DeadUnit, a morph event, listeners at the top of a script diff --git a/probes/events.ts b/probes/events.ts index a32a921..156758b 100644 --- a/probes/events.ts +++ b/probes/events.ts @@ -13,7 +13,7 @@ // Then load and unload anything you like (a Bunker, a Dropship): nothing new should be said. // E 35 200 Zerglings of yours in one frame, then all killed in one frame: "E: made 200, died 200". Does the game // slow or stutter at either? -// F 45 in a multiplayer game only: type "-give 100" (Enter, the line, Enter): +100 minerals. Press F2: "help". +// F 45 in a multiplayer game only: type "-give 100" (Enter, the line, Enter): +100 minerals. Press F7: "help". // Host it as a multiplayer game, alone, for F; the rest also works single player. // npx tsx scripts/build-fixture.mts probes/events.ts fixtures/eud/events.scx // npx tsx scripts/test-game.mts probes/events.ts (A to E in the lifted game) @@ -27,7 +27,7 @@ program(async () => { on.died((u) => { died++; if (u.type != units.ZergZergling) print(`died ${u.type} at ${u.x}, ${u.y}`); }); on.died(units.TerranMarine, async () => { await sleep(seconds(2)); createUnit(P1, units.TerranMarine, 1, HOME); print("back"); }); on.chat("-give {n}", (p, { n }) => { setResources(p, "add", n, "ore"); print(`gave ${n}`, { to: p }); }); - on.key("F2", (p) => print("help", { to: p })); + on.key("F7", (p) => print("help", { to: p })); await sleep(seconds(3)); print("A: three Marines made by the program. Expect \"made 0\" three times.", { position: "center" }); @@ -48,7 +48,7 @@ program(async () => { print("D: train an SCV now (you have 100 more minerals). Expect \"made 7\" when it comes out. Loading and unloading should say nothing.", { position: "center" }); await sleep(seconds(15)); - print("E: 200 Zerglings in one frame, then all killed in one frame. Does the game slow?", { position: "center" }); + print("E: 200 Zerglings made in one frame; three seconds later all killed in one frame. Does the game slow?", { position: "center" }); const before = made, gone = died; for (let i = 0; i < 8; i++) createUnit(P1, units.ZergZergling, 25, HOME); await sleep(seconds(3)); @@ -57,7 +57,7 @@ program(async () => { print(`E: made ${made - before}, died ${died - gone}`); await sleep(seconds(6)); - print("F: in multiplayer, type -give 100, and press F2.", { position: "center" }); + print("F: in multiplayer, type -give 100, and press F7.", { position: "center" }); }, { owner: P1, name: "events" }); /** How many times a text was shown. (The game's own lines fold the same text shown twice in a frame, so the actions are counted.) */ diff --git a/probes/game-events-input.ts b/probes/game-events-input.ts index 86e483d..7991eb9 100644 --- a/probes/game-events-input.ts +++ b/probes/game-events-input.ts @@ -5,7 +5,7 @@ import { test, expect, program, print, setResources, on, P1 } from "trigscript"; program(async () => { let gave = 0, helped = 0; on.chat("-give {n}", (p, { n }) => { gave += n; setResources(p, "add", n, "ore"); print(`gave ${n}`, { to: p }); }); - on.key("F2", (p) => { helped++; print("help", { to: p }); }); + on.key("F7", (p) => { helped++; print("help", { to: p }); }); }, { owner: P1, name: "input" }); test("a typed line and a key reach their handlers, and what they print shows", (sim) => { @@ -13,7 +13,7 @@ test("a typed line and a key reach their handlers, and what they print shows", ( sim.type("-give 100").seconds(1); expect(sim.program("input").gave).toBe(100); expect(sim).toHavePrinted("gave 100"); - sim.press("F2").seconds(1); + sim.press("F7").seconds(1); expect(sim.program("input").helped).toBe(1); expect(sim).toHavePrinted("help"); }); diff --git a/tests/eud-build.test.ts b/tests/eud-build.test.ts index 9369eb9..e682b58 100644 --- a/tests/eud-build.test.ts +++ b/tests/eud-build.test.ts @@ -176,7 +176,7 @@ const FIXTURES: Record = { input: `program(() => { let gold = 0; while (true) { - if (keyPressed(CurrentPlayer, "F2") || keyPressed(CurrentPlayer, "Space")) gold += 1; + if (keyPressed(CurrentPlayer, "F7") || keyPressed(CurrentPlayer, "Space")) gold += 1; if (clicked(CurrentPlayer) || clicked(CurrentPlayer, "right")) { const at = mouse(CurrentPlayer); centerLocation(1, at.x + 8, at.y); createUnit(CurrentPlayer, units.TerranMarine, 1, 1); } const m = chatted(CurrentPlayer, "-spawn {n} {unit:unit}"); if (m) createUnit(CurrentPlayer, m.unit, m.n, 1); @@ -332,7 +332,7 @@ const FIXTURES: Record = { const deaths = on.died({ owner: P8 }, async (u) => { await sleep(seconds(2)); createUnit(P8, u.type, 1, locations.Anywhere); print(\`back at \${u.x}, \${u.y}\`); }); on.died(units.Men, (u) => { if (u.owner == 0) setResources(P1, "add", 1, "ore"); }); on.chat("-give {n}", (p, { n }) => { setResources(p, "add", n, "ore"); }); - on.key("F2", (p) => print("help", { to: p }), { from: P1 }); + on.key("F7", (p) => print("help", { to: p }), { from: P1 }); await until(() => made >= 10); deaths.cancel(); });`, diff --git a/tests/events.test.ts b/tests/events.test.ts index 9d798df..4a45a41 100644 --- a/tests/events.test.ts +++ b/tests/events.test.ts @@ -96,9 +96,9 @@ describe("events: in the simulator", () => { it("on.chat and on.key: every player, the player known in the handler", () => { const r = compile(` on.chat("-give {n}", (p, { n }) => { setResources(p, "add", n, "ore"); print(\`gave \${n}\`, { to: p }); }); - on.key("F2", (p) => print("help", { to: p }), { from: P2 });`); + on.key("F7", (p) => print("help", { to: p }), { from: P2 });`); const s = sim(r); - s.run(2).type("-give 25", 1).press("F2", 1).press("F2", 0).run(1); + s.run(2).type("-give 25", 1).press("F7", 1).press("F7", 0).run(1); expect(printed(s)).toEqual(["2:gave 25", "2:help"]); expect(s.events.filter((e) => e.action.type === ActionType.SetResources).map((e) => e.action.player)).toEqual([1]); }); diff --git a/tests/input.test.ts b/tests/input.test.ts index 0f7cab6..43116c4 100644 --- a/tests/input.test.ts +++ b/tests/input.test.ts @@ -75,13 +75,13 @@ describe("chat patterns", () => { describe("input: the front end", () => { it("a key, a click, the mouse and a typed line arrive as IR", () => { const r = compile(`while (true) { - if (keyPressed(P1, "F2") && !clicked(P2, "right")) { const at = mouse(P1); centerLocation(3 as Location, at.x, at.y + 16); } + if (keyPressed(P1, "F7") && !clicked(P2, "right")) { const at = mouse(P1); centerLocation(3 as Location, at.x, at.y + 16); } const m = chatted(CurrentPlayer, "-give {n}"); if (m) setResources(CurrentPlayer, "add", m.n, "ore"); sleep(frames(1)); }`); const ir = JSON.stringify(r.ir); - expect(ir).toContain(`"input":{"source":"key","key":"F2","player":0}`); + expect(ir).toContain(`"input":{"source":"key","key":"F7","player":0}`); expect(ir).toContain(`"input":{"source":"click","button":"right","player":1}`); expect(ir).toContain(`"input":{"source":"mouse","axis":"x","player":0}`); expect(ir).toContain(`"input":{"source":"chat","pattern":"-give {n}","capture":null,"player":13}`); @@ -98,6 +98,9 @@ describe("input: the front end", () => { // The game reports no press of F6 (the probe, twice): not a Key, and said plainly to a script that gets past the types. expect(errors(`if (keyPressed(P1, "F6")) displayText("x");`)[0]).toMatch(/not assignable to parameter of type 'Key'/); expect(errors(`if (keyPressed(P1, "f6" as Key)) displayText("x");`)[0]).toMatch(/keeps F6 to itself/); + // F2 to F5 the same (the B2 keys probe: neither on.key nor keyPressed heard them; F2–F4 move the camera). + for (const k of ["F2", "F3", "F4", "F5"]) expect(errors(`if (keyPressed(P1, "${k}")) displayText("x");`)[0]).toMatch(/not assignable to parameter of type 'Key'/); + expect(errors(`if (keyPressed(P1, "f2" as Key)) displayText("x");`)[0]).toMatch(/keeps F2 to itself \(a camera key\)/); }); it("an action takes a unit type and a count from a typed line at once", () => { @@ -114,23 +117,23 @@ describe("input: the front end", () => { }); it("says what is wrong", () => { - expect(errors(`keyPressed(P1, "F2");`)[0]).toMatch(/does nothing on its own/); - expect(errors(`if (keyPressed(players.Force1, "F2")) displayText("x");`)[0]).toMatch(/expected one player/); + expect(errors(`keyPressed(P1, "F7");`)[0]).toMatch(/does nothing on its own/); + expect(errors(`if (keyPressed(players.Force1, "F7")) displayText("x");`)[0]).toMatch(/expected one player/); expect(errors(`if (chatted(P1, "{n}")) displayText("x");`)[0]).toMatch(/starts with its own word/); expect(errors(`let x = mouse(P1) + 1;`)[0]).toBeDefined(); expect(errors(`let n = 0; centerLocation(locations.Anywhere, n, n);`)[0]).toMatch(/not Anywhere/); expect(errors(`const u = underMouse(P1, { within: 0 });`)[0]).toMatch(/distance in pixels/); // What the players did does not change within a frame: a loop waiting for it has to sleep. - expect(errors(`while (!keyPressed(P1, "F2")) { displayText("waiting"); }`)[0]).toMatch(/never changes inside it/); - expect(errors(`while (!keyPressed(P1, "F2")) { sleep(frames(1)); }`)).toEqual([]); + expect(errors(`while (!keyPressed(P1, "F7")) { displayText("waiting"); }`)[0]).toMatch(/never changes inside it/); + expect(errors(`while (!keyPressed(P1, "F7")) { sleep(frames(1)); }`)).toEqual([]); }); it("outside a program the functions say where they belong", () => { // Outside, the call would be an object and \`if (keyPressed(…))\` always true: refused at once instead. - expect(compileFile(`if (keyPressed(P1, "F2")) trigger(P1, [always()], [displayText("x")]);`).diagnostics[0].message).toMatch(/keyPressed\(\) asks what a player does while the game runs: use it inside program\(\)/); + expect(compileFile(`if (keyPressed(P1, "F7")) trigger(P1, [always()], [displayText("x")]);`).diagnostics[0].message).toMatch(/keyPressed\(\) asks what a player does while the game runs: use it inside program\(\)/); expect(compileFile(`const m = chatted(P1, "-help");`).diagnostics[0].message).toMatch(/inside program\(\)/); // A helper of the script's own, called from a program, is the program's. - expect(compileFile(`const fire = () => keyPressed(CurrentPlayer, "F2"); program(() => { while (true) { if (fire()) displayText("x"); sleep(frames(1)); } });`).diagnostics).toEqual([]); + expect(compileFile(`const fire = () => keyPressed(CurrentPlayer, "F7"); program(() => { while (true) { if (fire()) displayText("x"); sleep(frames(1)); } });`).diagnostics).toEqual([]); expect(compileFile(`centerLocation(1 as Location, 0, 0);`).diagnostics[0].message).toMatch(/inside program\(\)/); }); }); @@ -193,14 +196,14 @@ describe("input: the plan and the build's plugins", () => { describe("input: the interpreter", () => { it("a key and a click are true on the one frame they arrive", () => { - const sim = simulate(loop(`if (keyPressed(CurrentPlayer, "F2")) n += 1; if (clicked(CurrentPlayer, "right")) v += 10;`)); + const sim = simulate(loop(`if (keyPressed(CurrentPlayer, "F7")) n += 1; if (clicked(CurrentPlayer, "right")) v += 10;`)); sim.step(); - sim.press("f2").step(); + sim.press("f7").step(); sim.step(); expect(sim.value("n")).toBe(1); - sim.press("F3").click("left").step(); + sim.press("Q").click("left").step(); expect([sim.value("n"), sim.value("v")]).toEqual([1, 0]); - sim.press("F2").click("right").step(); + sim.press("F7").click("right").step(); expect([sim.value("n"), sim.value("v")]).toEqual([2, 10]); }); diff --git a/tests/testing.test.ts b/tests/testing.test.ts index 1e24255..c060f62 100644 --- a/tests/testing.test.ts +++ b/tests/testing.test.ts @@ -178,7 +178,7 @@ describe("sim", () => { program(() => { let pos = { x: 1, y: 2 }; let seen = [0, 0, 0]; let label = "none"; let gold = 0; while (true) { - if (keyPressed(CurrentPlayer, "F2")) { pos.x += 10; seen[1] = 7; label = \`pressed \${pos.x}\`; } + if (keyPressed(CurrentPlayer, "F7")) { pos.x += 10; seen[1] = 7; label = \`pressed \${pos.x}\`; } const give = chatted(CurrentPlayer, "-give {n}"); if (give) { setResources(CurrentPlayer, "add", give.n, "ore"); } gold = minerals(CurrentPlayer); @@ -186,7 +186,7 @@ program(() => { } }, { owner: [P1, P2], name: "input" }); test("as the first player", (sim) => { - sim.press("F2").frames(2); + sim.press("F7").frames(2); expect(sim.program("input").pos).toEqual({ x: 11, y: 2 }); expect(sim.program("input").seen).toEqual([0, 7, 0]); expect(sim.program("input").label).toBe("pressed 11"); From 010e5bc0bdc1cc4b023c1023e6db1920c510a6ab Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 09:01:15 -0600 Subject: [PATCH 060/238] TrigScript 4, B2: on.killed, on.order, on.morphed, listeners at the top MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit on.killed hands a death in a fight and its killer: the dying record's hit points are 0 and +0x90 holds the last player whose attack hurt the unit (measured in the lifted game: Kill Unit, Remove Unit and a death inside a transport leave both as they were). A handler that takes the killer is copied for each of the eight, so an action can be for them. A removed flag is not built: a Remove Unit and a death in a transport leave the same record. on.order and on.morphed share one walk of the visible list a frame, each unit's order byte and type against a table of the pass's own; about 95 conditions and actions a unit for one, 140 for both, with a hint on the line. on.…(…) at the top of a file is a program of its own (__listen). An orders name table and the Order brand; handlers are declared => void, so an arrow that gives an action type-checks. sim.morph for the simulator; GameSim.kill with a killer leaves what a fight leaves. IR 19. Co-Authored-By: Claude Opus 5.5 --- compiler/compiler.ts | 13 ++- compiler/declarations.ts | 67 +++++++++++---- compiler/eud.ts | 4 +- compiler/gameSim.ts | 8 ++ compiler/generated/trigscriptPy.ts | 2 +- compiler/hoist.ts | 45 ++++++++++- compiler/ir.ts | 21 +++-- compiler/names.ts | 6 +- compiler/orders.ts | 31 +++++++ compiler/runtime.ts | 13 ++- compiler/simulateIr.ts | 49 ++++++++--- compiler/structured.ts | 126 ++++++++++++++++++++--------- compiler/testing.ts | 11 +++ compiler/views.ts | 3 +- compiler/world.ts | 3 + docs/ir.md | 25 +++++- docs/v4-plan.md | 65 +++++++++++++-- probes/events-2.ts | 73 +++++++++++++++++ probes/game-events-2.ts | 64 +++++++++++++++ python/trigscript.py | 76 ++++++++++++++--- tests/eud-build.test.ts | 15 +++- tests/events.test.ts | 64 ++++++++++++++- tests/functions.test.ts | 2 +- tests/script-structured.test.ts | 2 +- tests/service.test.ts | 4 +- 25 files changed, 682 insertions(+), 110 deletions(-) create mode 100644 compiler/orders.ts create mode 100644 probes/events-2.ts create mode 100644 probes/game-events-2.ts diff --git a/compiler/compiler.ts b/compiler/compiler.ts index e1bded7..016ab45 100644 --- a/compiler/compiler.ts +++ b/compiler/compiler.ts @@ -20,7 +20,7 @@ import type { TriggerRecord } from "../vendor/triggers"; import { MODULE_NAME } from "./api"; import { isTestFile, listTests, runTests, TestRegistry, TESTING_NAMES, type TestReport, type TestRunOptions } from "./testing"; import { DECLARATIONS_FILE, generateDeclarations } from "./declarations"; -import { isScriptClass, libraryCallName, libraryName, planClass, planProgram, transformer, type ProgramPlan } from "./hoist"; +import { isScriptClass, isTopListener, libraryCallName, libraryName, planClass, planListener, planProgram, transformer, type ProgramPlan } from "./hoist"; import { locate, resolveModule, runModules, type LinkedFile } from "./link"; import { checkProgram } from "./eud"; import { typeNumbers } from "./numbers"; @@ -289,6 +289,15 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa classPlans.set(node, plan); byPosition.set(`${fileIndex(sf)}:${node.getStart(sf)}`, { plan, sf, name: node.name!.text }); } + // `on.died(…)` at the top of a file: a program of its own, its body the statement. + if (isTopListener(ts, checker, node) && !isTestFile(name)) { + const plan = planListener(ts, checker, node); + plans.set(node, plan); + byPosition.set(`${fileIndex(sf)}:${node.getStart(sf)}`, { plan, sf }); + for (const e of plan.errors) nodeError(e.node, e.message); + for (const e of plan.hoisted) buildTime.push({ file: name, ...position(sf, e.getStart(sf), e.getEnd()) }); + return; + } if (ts.isCallExpression(node)) { const lib = libraryCallName(ts, checker, node); const arrow = node.arguments[0]; @@ -322,7 +331,7 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa for (const d of diagnostics) planned.add(`${d.file}:${d.line}`); /* ── Emit ── */ - const emitted = program.emit(undefined, undefined, undefined, false, { before: [transformer(ts, checker, { fileIndex, planFor: (arrow) => plans.get(arrow), planForClass: (cls) => classPlans.get(cls) })] }); + const emitted = program.emit(undefined, undefined, undefined, false, { before: [transformer(ts, checker, { fileIndex, planFor: (arrow) => plans.get(arrow), planForClass: (cls) => classPlans.get(cls), planForListener: (stmt) => plans.get(stmt) })] }); for (const d of emitted.diagnostics) { if (d.category === ts.DiagnosticCategory.Error) diagnostics.push({ file: ENTRY_FILE, line: 1, column: 1, endLine: 1, endColumn: 1, message: ts.flattenDiagnosticMessageText(d.messageText, "\n"), source: "typescript" }); } diff --git a/compiler/declarations.ts b/compiler/declarations.ts index 1ece333..0e35795 100644 --- a/compiler/declarations.ts +++ b/compiler/declarations.ts @@ -49,6 +49,8 @@ ${kw}type Weapon = N & Brand<"weapon">; ${kw}type Upgrade = N & Brand<"upgrade">; /** A technology (techs.*, or a raw techdata.dat id), for stats(). */ ${kw}type Tech = N & Brand<"tech">; +/** An order (orders.*, or a raw orders.dat id): what a unit is doing. */ +${kw}type Order = N & Brand<"order">; /** A player colour (colors.*), for stats(player).color. */ ${kw}type PlayerColor = N & Brand<"color">; ${kw}type ColorName = __COLOR_NAMES__; @@ -125,8 +127,8 @@ ${kw}interface Unit { readonly y: number; /** How many units it has killed, 0 … 255. */ kills: number; - /** The orders.dat id of what the unit is doing (3 is standing guard, 6 moving, 10 attacking). */ - readonly orderId: number; + /** What the unit is doing: \`if (u.orderId == orders.Move)\`. orders.PlayerGuard is standing guard, orders.AttackUnit attacking. */ + readonly orderId: Order; /** Frames until the unit can attack or cast again, 0 … 255; writing it holds the unit's fire that long. */ cooldown: number; /** What a mineral field or a geyser still holds. */ @@ -231,7 +233,7 @@ ${kw}function trigger(players: Player | readonly Player[], conditions: Condition * A map with a program in it needs StarCraft: Remastered: the programs are built into the * saved map by the eudplib plugin. trigger() makes ordinary triggers that play anywhere. */ -${kw}function program(body: () => void | Promise, options?: ProgramOptions): void; +${kw}function program(body: () => void, options?: ProgramOptions): void; /** * A function that runs in the game, for programs to call — from any file, imported like any * other: \`export const award = game((p: Player, n: number) => { setResources(p, "add", n, "ore"); })\`. @@ -439,11 +441,16 @@ ${kw}interface DeadUnit { } /** How many of an event's handlers can be waiting at once, when the handler is async (16 unless said, up to 255). */ ${kw}interface EventOptions { max?: number } +/** What on.order() looks for: a unit filter and, if you like, the order the unit takes up. */ +${kw}interface OrderFilter extends UnitFilter { order?: Order } +/** What on.killed() looks for: a unit filter and, if you like, the player who killed it. */ +${kw}interface KillFilter extends UnitFilter { by?: Player } /** - * What happens in the game, as it happens: on.created(…), on.died(…), on.chat(…), on.key(…) each runs its function for - * every time it happens from the next frame on, inside an async program only. Each is a task (it keeps listening after - * the program moves on, and the Task it gives back stops it). A function that is not async runs within the frame, once - * for each time; an async one is started as a task each time, so it can wait: \`max\` of them at once. + * What happens in the game, as it happens: on.created(…), on.died(…), on.chat(…) and the rest each runs its function for + * every time it happens from the next frame on. Each is a task (it keeps listening after the program moves on, and the + * Task it gives back stops it), inside an async program — or at the top of a script, where it listens from the start + * of the game as a program of its own. A function that is not async runs within the frame, once for each time; an + * async one is started as a task each time, so it can wait: \`max\` of them at once. */ ${kw}const on: { /** @@ -451,23 +458,48 @@ ${kw}const on: { * function is handed it in the next frame. A unit in a transport or a hangar comes on when it first comes out. * \`on.created(units.ZergZergling, (u) => { u.energy = 50; })\` */ - created(handler: (unit: Unit) => void | Promise, options?: EventOptions): Task; - created(filter: UnitType | UnitFilter, handler: (unit: Unit) => void | Promise, options?: EventOptions): Task; + created(handler: (unit: Unit) => void, options?: EventOptions): Task; + created(filter: UnitType | UnitFilter, handler: (unit: Unit) => void, options?: EventOptions): Task; /** * A unit dies or is removed, and the function is handed what it was and where, in the next frame — once for every * unit on.created() would have been handed. The filter is of the unit as it died. * \`on.died({ owner: P8 }, async (u) => { await sleep(seconds(2)); createUnit(P8, u.type, 1, locations.Spawn); })\` */ - died(handler: (unit: DeadUnit) => void | Promise, options?: EventOptions): Task; - died(filter: UnitType | UnitFilter, handler: (unit: DeadUnit) => void | Promise, options?: EventOptions): Task; + died(handler: (unit: DeadUnit) => void, options?: EventOptions): Task; + died(filter: UnitType | UnitFilter, handler: (unit: DeadUnit) => void, options?: EventOptions): Task; + /** + * A unit is killed in a fight — shot, burned, caught in a storm — and the function is handed what it was, where, and + * the player whose attack killed it. A unit a trigger or a program kills or removes, or one that goes down with its + * transport, has no killer and is not handed on (on.died() hears every one). + * \`on.killed({ owner: P8, by: P1 }, (u, by) => setResources(by, "add", 25, "ore"))\` + */ + killed(handler: (unit: DeadUnit, by: Player) => void, options?: EventOptions): Task; + killed(filter: UnitType | KillFilter, handler: (unit: DeadUnit, by: Player) => void, options?: EventOptions): Task; + /** + * A unit takes up a new order — told to move, starting to cast, sieging, going back to guard — and the function is + * handed it in the next frame, with the order it took up and the one it had. An order held for less than a frame is + * not seen. This looks at every unit on the map each frame: the hint says what it costs. + * \`on.order(units.ProtossHighTemplar, orders.CastPsionicStorm, (u) => print("Storm!"))\` + */ + order(handler: (unit: Unit, order: Order, was: Order) => void, options?: EventOptions): Task; + order(filter: UnitType | OrderFilter, handler: (unit: Unit, order: Order, was: Order) => void, options?: EventOptions): Task; + order(filter: UnitType | UnitFilter, order: Order, handler: (unit: Unit, order: Order, was: Order) => void, options?: EventOptions): Task; + /** + * A unit turns into another type and stays the same unit: a Siege Tank sieging, a Larva becoming an egg and the egg + * its unit, a Drone becoming a building, a High Templar becoming an Archon (its partner leaves, as on.died() hears). + * The function is handed the unit, now of its new type, and the type it was. The filter is of the new type. + * \`on.morphed(units.TerranSiegeTankSiegeMode, (u) => print("Sieged"))\` + */ + morphed(handler: (unit: Unit, was: UnitType) => void, options?: EventOptions): Task; + morphed(filter: UnitType | UnitFilter, handler: (unit: Unit, was: UnitType) => void, options?: EventOptions): Task; /** * A player types a line that fits the pattern, which is chatted()'s: \`on.chat("-give {n}", (p, { n }) => setResources(p, "add", n, "ore"))\`. * Every player P1 … P8 unless \`from\` names one (CurrentPlayer in a program for several players); \`p\` is known * when the script is built, so an action can be for them. */ - chat(pattern: P, handler: (player: Player, values: ChatValues

) => void | Promise, options?: EventOptions & { from?: Player }): Task; + chat(pattern: P, handler: (player: Player, values: ChatValues

) => void, options?: EventOptions & { from?: Player }): Task; /** A player presses a key, as keyPressed() sees it: \`on.key("F7", (p) => print("Help", { to: p }))\`. Every player P1 … P8 unless \`from\` names one. */ - key(key: Key, handler: (player: Player) => void | Promise, options?: EventOptions & { from?: Player }): Task; + key(key: Key, handler: (player: Player) => void, options?: EventOptions & { from?: Player }): Task; }; /** * What the players do, inside program() only. A key, a click and a typed line are true on the one @@ -589,7 +621,7 @@ function tables(kw: string, names: ScriptNames, compact: boolean): string { tableDecl(kw, names.locations), tableDecl(kw, names.switches, (k) => { const m = /^Switch(\d+)$/.exec(k); return m ? Number(m[1]) <= 16 : !isDefaultSwitch(k); }, "Switch1 … Switch256 exist; the first sixteen are listed. A switch given a name in the map is listed by that name."), `/** AI scripts, by StarEdit name ("Terran Custom Level") or four-character code. */\n${kw}const ${names.aiScripts.object}: { readonly [name: string]: AiScript };`, - ...[names.weapons, names.upgrades, names.techs].map((t) => tableDecl(kw, t, identifierKey)), + ...[names.weapons, names.upgrades, names.techs, names.orders].map((t) => tableDecl(kw, t, identifierKey)), ].join("\n"); } @@ -634,6 +666,7 @@ declare namespace JSX { declare function __jsx(tag: string, props: object | null, ...children: unknown[]): string; /** What the build adds after each class at the top of a file, for a program that makes an instance of it. Not for scripts. */ declare function __class(cls: unknown, descriptor: unknown): void; +declare function __listen(descriptor: unknown, at?: unknown): void; declare const __fragment: string; interface ArrayConstructor { new (arrayLength: number): T[]; @@ -652,7 +685,7 @@ const TESTING = ` export interface SimUnit { readonly type: UnitType; readonly owner: Player; readonly x: number; readonly y: number; readonly hp: number; readonly maxHp: number; readonly shields: number; readonly maxShields: number; readonly energy: number; - readonly kills: number; readonly resources: number; readonly invincible: boolean; readonly burrowed: boolean; readonly cloaked: boolean; readonly hallucinated: boolean; + readonly kills: number; readonly orderId: Order; readonly resources: number; readonly invincible: boolean; readonly burrowed: boolean; readonly cloaked: boolean; readonly hallucinated: boolean; /** Still on the map. */ readonly alive: boolean; } @@ -665,11 +698,13 @@ export interface SimUnit { export interface Sim { /** Units for a player at a location's centre, as createUnit() makes them. They are given back. */ place(player: Player, type: UnitType, at: Location, count?: number): SimUnit[]; - /** A unit dies: what a fight is in a test. \`by\`: the player whose kill it is. */ + /** A unit dies: what a fight is in a test. \`by\`: the player whose kill it is, and on.killed()'s killer. */ kill(unit: SimUnit, by?: Player): this; /** A unit goes without dying. */ remove(unit: SimUnit): this; give(unit: SimUnit, to: Player): this; + /** A unit turns into another type and stays the same unit, as a Siege Tank sieges: what on.morphed() hears. In the simulator only. */ + morph(unit: SimUnit, type: UnitType): this; /** A unit to a location's centre, or to a point in pixels. */ move(unit: SimUnit, to: Location | { x: number; y: number }): this; diff --git a/compiler/eud.ts b/compiler/eud.ts index f1bbaf2..b684300 100644 --- a/compiler/eud.ts +++ b/compiler/eud.ts @@ -157,6 +157,8 @@ function scans(program: Program, out: LineHint[]) { for (const body of bodiesOf(program)) statements(body, (s) => { if (s.kind === "for" && s.sorts) hint(s.at, "sorts in the frame", `${s.sorts} is sorted where it stands, within the frame: each item is taken in hand and the ones that go after it moved up. Nearly in order already, that is a pass over the list; in no order it is the length squared — a few dozen items are nothing, a few hundred every frame will be felt.`); }); // A call that stands in an expression (`unitsOf(P1).some(…)` in an if) has statements of its own, which the walk over statements does not reach. const everywhere = (body: Stmt[], visit: (s: Stmt) => void) => { statements(body, visit); expressions(body, (e) => { if (e.kind === "call") statements(e.call.body, visit); }); }; + // on.order() / on.morphed(): the frame's events pass looks at every unit on the map, for as long as the game runs. + for (const body of bodiesOf(program)) everywhere(body, (s) => { if (s.kind === "unitLoop" && (s.source === "ordered" || s.source === "morphed")) hint(s.at, "watches every unit", `Every frame, for the rest of the game, each unit on the map has its ${s.source === "ordered" ? "order" : "type"} looked at: about 100 conditions and actions a unit (on.order() and on.morphed() together, about 140), so 200 units cost about 20,000 a frame of Remastered's 500,000. Paid once however many listeners there are.`); }); for (const body of bodiesOf(program)) everywhere(body, (s) => { if (s.kind === "unitLoop" && !s.source) hint(s.at, "scans units", `Looks at ${SLOTS} every time the line runs${each}, and runs the body for the ones that match. Fine once or a few times a second; inside a loop that runs every frame, ask whether it needs to.`); }); for (const body of bodiesOf(program)) expressions(body, () => {}, (u) => hint(u.at, u.by === "random" ? "scans units ×2" : "scans units", u.by === "random" ? `Looks at ${SLOTS} twice every time the line runs${each}: once to count the units that match, once to take the one drawn.` @@ -228,7 +230,7 @@ function assigned(body: Stmt[], functions: Map, into = new Set case "store": case "declareArray": case "push": case "pop": case "setLength": into.add(whole(s.array)); break; case "storeText": into.add(whole(s.addr)); break; case "releaseText": into.add(whole(s.block)); break; - case "unitLoop": into.add(s.decl.id); s.body.forEach(stmt); break; + case "unitLoop": into.add(s.decl.id); if (s.was) into.add(s.was.id); s.body.forEach(stmt); break; case "if": s.then.forEach(stmt); s.else?.forEach(stmt); break; case "while": case "for": s.body.forEach(stmt); if (s.kind === "for") s.update.forEach(stmt); break; case "do": s.body.forEach(stmt); break; diff --git a/compiler/gameSim.ts b/compiler/gameSim.ts index 87939e3..e909576 100644 --- a/compiler/gameSim.ts +++ b/compiler/gameSim.ts @@ -334,11 +334,19 @@ export class GameSim implements TestWorldLike { kill(unit: SimUnit, by?: number): this { if (!unit.alive) return this; this.game.call(COUNT_DEATH, { edi: UNITS + UNIT_BYTES * unit.slot, edx: by ?? unit.owner, ecx: 0 }); + // A killer named is a death in a fight, as the game leaves one behind: no hit points, and the killer the last player + // whose attack hurt the unit (+0x90), which is what on.killed() reads. + if (by !== undefined) { + const at = UNITS + UNIT_BYTES * unit.slot; + this.game.m.setInt32(at + 0x08, 0, true); this.game.u8[at + 0x90] = by; + this.game.wrote?.(at + 0x08, 4); this.game.wrote?.(at + 0x90, 1); + } return this.onOne(unit, (loc) => this.act(this.record(ActionType.KillUnitAt, { location: loc, player: unit.owner, unit: unit.type, count: 1 }), unit.owner)); } remove(unit: SimUnit): this { return this.onOne(unit, (loc) => this.act(this.record(ActionType.RemoveUnitAt, { location: loc, player: unit.owner, unit: unit.type, count: 1 }), unit.owner)); } + morph(): this { throw new ExpectError("morph: the lifted game turns a unit into another type only through a player's command, which a test cannot give yet; test on.morphed() in the simulator."); } give(unit: SimUnit, to: number): this { return this.onOne(unit, (loc) => this.act(this.record(ActionType.GiveUnits, { location: loc, player: unit.owner, number: to, unit: unit.type, count: 1 }), unit.owner)); } diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index b1714fb..fcc47ff 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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 18\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# 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 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# 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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):\n self.created, self.died = created, died\n self.seen = SlotTable()\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):\n return epd + (EPD(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 run(self):\n self.made_n << 0\n self.gone_n << 0\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\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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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, count = (EVENTS.made, EVENTS.made_n) if source == \"created\" else (EVENTS.gone, EVENTS.gone_n)\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 == \"created\":\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 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 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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 (EVENTS.made_n if source == \"created\" else EVENTS.gone_n) >= 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 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 op == \">\":\n return av > b\n return av >= b\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 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 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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":

, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\nINPUT = Input(IR.get(\"input\"))\n_DIED = uses_source(IR.get(\"programs\", []), \"died\")\nEVENTS = Events(uses_source(IR.get(\"programs\", []), \"created\"), _DIED) if _DIED or uses_source(IR.get(\"programs\", []), \"created\") 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 if INPUT.held:\n INPUT.frame()\n if EVENTS is not None:\n EVENTS.run()\n for prog in PROGRAMS:\n prog.run()\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 json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 19\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# 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 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# 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\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\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 = EUDArray([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 self.store[self.rows()] = value\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 = EUDArray([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\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 self.store[self.player_of()] = value\n else:\n self.store << value\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 self.rowed = rows is not None and values 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 self.store = EUDArray([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 out << self.store[self.at(index)]\n EUDEndIf()\n return out\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 self.store[self.at(index)] = value\n return\n if EUDIf()(index <= self.length - 1):\n self.store[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 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\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 @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 # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + 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 cells[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 _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push)\n return _HEAP\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: EUDArray([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 = heap()\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 self.handle[name] << value\n\n def length(self):\n return self.field(\"len\")\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\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 ptr, length, room, k = self.heap[\"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.heap[\"cells\"][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 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 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 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.heap[\"cells\"][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 = heap()\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: EUDArray([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: EUDArray([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\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\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 # 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 # 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 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 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:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\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 return self.array(e[\"array\"], e).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 == \"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 t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\":\n a, b = [self.num(x) for x in 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 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 binary(self, e):\n a, b = self.num(e[\"left\"]), self.num(e[\"right\"])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b)\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 return (a * b) & U32 if both else 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 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 q, r = f_div(as_var(a), b) if unsigned else f_div_towards_zero(fresh(a), fresh(b))\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 if op in (\"<\", \"<=\", \"!=\"):\n return EUDOr(sv >= SIGN, relation(sv, op, u_))\n return EUDAnd(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 return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b):\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.\"\"\"\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 # 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.array(e[\"array\"], e).get(self.num(e[\"index\"]))) >= 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.num(e[\"left\"]), self.num(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 return (EUDAnd if k == \"and\" else 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 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, slot by slot: `body(ref, next_, exit_)` for every unit on the map the\n filter matches. Every test is one condition whose address moves on with the slot, so a slot\n that does not match costs a trigger or two.\"\"\"\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 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 head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(ptr >= UNIT_END, exit_)\n EUDJumpIf(empty, next_)\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 DoActions([SetMemory(c + 4, Add, UNIT_SIZE // 4) for c, _ in moving] + [ptr.AddNumber(UNIT_SIZE), epd.AddNumber(UNIT_SIZE // 4)])\n EUDJump(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 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\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 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 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\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 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 self.var(st[\"target\"], st).set(self.num(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 value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\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 == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"remark\":\n pass\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\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 for_(self, st, ctx):\n head, update, exit_ = Forward(), Forward(), Forward()\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 self.straight(st[\"update\"], ctx):\n EUDJump(head)\n exit_ << NextTrigger()\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 if count is None:\n DoActions(Action(**fields))\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\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\":\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\":\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 if self.straight(st[\"body\"], inner):\n EUDJump(head)\n done << NextTrigger()\n T[\"release\"](held)\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 try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\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 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 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 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 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 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 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 op == \">\":\n return av > b\n return av >= b\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 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 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 == \"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\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 rows.append([_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin])\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\":
, \"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 heap's 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 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 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_of(s).items()}})\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\"]))\n else:\n row.update(at=_place(a.store))\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 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\"]), \"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\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 if INPUT.held:\n INPUT.frame()\n if EVENTS is not None:\n EVENTS.run()\n for prog in PROGRAMS:\n prog.run()\n"; diff --git a/compiler/hoist.ts b/compiler/hoist.ts index 6517e1f..1ecab1c 100644 --- a/compiler/hoist.ts +++ b/compiler/hoist.ts @@ -115,7 +115,7 @@ function owningDeclaration(ts: typeof TS, decl: TS.Declaration): TS.Node { } /** What `on.…` listens for (TrigScript 4, B2): `on.created(filter, handler)` and the rest. */ -export const ON_EVENTS = ["created", "died", "chat", "key"] as const; +export const ON_EVENTS = ["created", "died", "killed", "order", "morphed", "chat", "key"] as const; /** The library calls that must not appear inside a program body: they would run at build time, silently. */ const FORBIDDEN_INSIDE = new Set(["trigger", "program", "hyperTriggers", "game"]); @@ -198,13 +198,38 @@ export function planClass(ts: typeof TS, checker: TS.TypeChecker, cls: TS.ClassD } } +/** + * `on.died(…)` written as a statement at the top of a script's file (TrigScript 4, B2): it listens from the start of the + * game, as a program of its own whose body is that one statement. + */ +export function isTopListener(ts: typeof TS, checker: TS.TypeChecker, node: TS.Node): node is TS.ExpressionStatement { + return ts.isExpressionStatement(node) && ts.isSourceFile(node.parent) && !node.parent.isDeclarationFile && ts.isCallExpression(node.expression) && isOnCall(ts, checker, node.expression); +} + +/** The plan for a listener at the top of a file (`isTopListener`), made as a program's is. */ +export function planListener(ts: typeof TS, checker: TS.TypeChecker, stmt: TS.ExpressionStatement): ProgramPlan { + const forced = new Set(); + for (;;) { + const before = forced.size; + const plan = planOnce(ts, checker, stmt, {}, forced); + if (forced.size === before) return plan; + } +} + /** Whether a class is one of the script's own, declared at the top of one of its files: a program may make instances of it. */ export function isScriptClass(ts: typeof TS, decl: TS.Node): decl is TS.ClassDeclaration { return ts.isClassDeclaration(decl) && !!decl.name && ts.isSourceFile(decl.parent) && !decl.parent.isDeclarationFile && !decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword); } -function planOnce(ts: typeof TS, checker: TS.TypeChecker, of: TS.ArrowFunction | TS.FunctionExpression | TS.ClassDeclaration, options: PlanOptions, forced: Set): ProgramPlan { +function planOnce(ts: typeof TS, checker: TS.TypeChecker, of: TS.ArrowFunction | TS.FunctionExpression | TS.ClassDeclaration | TS.ExpressionStatement, options: PlanOptions, forced: Set): ProgramPlan { if (ts.isClassDeclaration(of)) return planClassOnce(ts, checker, of, forced); + if (ts.isExpressionStatement(of)) { + // A block that holds the statement where it stands: its range the statement's, so that it has a place in the file. + const body = ts.setTextRange(ts.factory.createBlock([of]), of); + (body as { parent: TS.Node }).parent = of.parent; + const plan: ProgramPlan = { root: of, body, hoisted: [], index: new Map(), game: new Set(), consts: new Map(), constList: [], tree: [], errors: [], grows: new Set() }; + return planWithin(ts, checker, plan, undefined, options, forced); + } const arrow = of; const plan: ProgramPlan = { arrow, root: arrow.body, body: ts.isBlock(arrow.body) ? arrow.body : undefined as unknown as TS.Block, hoisted: [], index: new Map(), game: new Set(), consts: new Map(), constList: [], tree: [], errors: [], grows: new Set() }; return planWithin(ts, checker, plan, arrow, options, forced); @@ -651,6 +676,8 @@ export interface TransformContext { planFor: (arrow: TS.ArrowFunction | TS.FunctionExpression) => ProgramPlan | undefined; /** The plan for a class of the script's own (`planClass`), made before the transform. */ planForClass?: (cls: TS.ClassDeclaration) => ProgramPlan | undefined; + /** The plan for a listener at the top of a file (`planListener`), made before the transform. */ + planForListener?: (stmt: TS.ExpressionStatement) => ProgramPlan | undefined; } /** @@ -690,6 +717,20 @@ export function transformer(ts: typeof TS, checker: TS.TypeChecker, ctx: Transfo } } } + // `on.died(…)` at the top of a file: handed to the run as a program of its own (`__listen`), which listens from the + // start of the game. + if (isTopListener(ts, checker, node)) { + const plan = ctx.planForListener?.(node); + if (plan) { + const descriptor = f.createObjectLiteralExpression([ + f.createPropertyAssignment("__trigscript", f.createStringLiteral("program")), + f.createPropertyAssignment("at", at(node)), + f.createPropertyAssignment("pos", f.createNumericLiteral(node.getStart(sf))), + f.createPropertyAssignment("hoisted", hoistedFunction(ts, checker, plan, context)), + ], true); + return f.createExpressionStatement(f.createCallExpression(f.createIdentifier("__listen"), undefined, [descriptor, at(node)])); + } + } // `class Squad { … }` at the top of a file: the class as it is, and after it what its bodies compute when the script is // built, handed to the run (`__class`), for a program that makes an instance of it. if (ts.isClassDeclaration(node) && node.name && ts.isSourceFile(node.parent)) { diff --git a/compiler/ir.ts b/compiler/ir.ts index 9c495bf..52ef62f 100644 --- a/compiler/ir.ts +++ b/compiler/ir.ts @@ -10,6 +10,7 @@ import type { ActionRecord, ConditionRecord } from "../vendor/triggers"; import type { InputSource } from "./input"; /** + * 19: more events — `unitLoop.source` also `killed` (a death in a fight, the killer in `was`), `ordered` (a unit that took up a new order, the one it had in `was`) and `morphed` (a unit that turned into another type, the type it was in `was`); `unitLoop.event` filters them by the new order or the killer. * 18: events — `unitLoop.source`: the units that came onto the map (`created`) or died (`died`) since the frame before, as the frame's events pass found them, where a loop without one goes through the unit table. * 17: texts built a piece at a time — `buildText` writes parts into a buffer (`reset` starts it empty) and `built` is what the buffer holds, which is how a JSX text is put together without copying it at every piece; `char` among the parts, one byte from a number (a colour code). * 16: tasks — `start`: a task that runs beside what started it, a copy of an async function's body with the arguments it was started with; `cancel` and `running` on what `start()` gives back. A task's variables and arrays (`VarDecl.task`, `ArrayDecl.task`) are a row a task, as many rows as tasks of it can run at once. @@ -28,7 +29,7 @@ import type { InputSource } from "./input"; * 3: reads (`read`), `random(n)` as a number, the bitwise operators and `print` with its text in parts. * 2: a record's text and sound are written out in the JSON (1 had the map's string indices); `cyclesPerSecond` is gone. */ -export const IR_VERSION = 18; +export const IR_VERSION = 19; /** The most a buffer of `buildText` holds, in bytes; python/trigscript.py has the same number. Past it the text is cut. */ export const BUILD_BYTES = 2047; @@ -163,6 +164,12 @@ export type ReadSource = */ export interface UnitFilter { type?: number; owner?: number; at?: number } +/** + * Where the units of an events pass loop come from (`unitLoop.source`): the ones that came onto the map, died, died in a + * fight, took up a new order, or turned into another type since the pass of the frame before. + */ +export type EventSource = "created" | "died" | "killed" | "ordered" | "morphed"; + /** A number of a unit on the map. Hit points, shields and energy are whole points; the timers and the cooldown frames. */ export type UnitNumField = "hp" | "maxHp" | "shields" | "maxShields" | "energy" | "owner" | "type" | "x" | "y" | "kills" | "orderId" | "cooldown" | "resources" | "stim" | "ensnare" | "plague" | "lockdown" | "maelstrom" | "irradiate" | "stasis"; @@ -487,9 +494,13 @@ export type Stmt = * `source` (`on.created` / `on.died`): instead of the table, the units of this frame's events pass — the ones that * came onto the map since the pass of the frame before (`created`; one that has gone since is passed over), or that * died or were removed since then (`died`; the unit of the turn is the dead one, its fields read as it died, and - * nothing done to it does anything). + * nothing done to it does anything); `killed` (those of `died` that died in a fight: their hit points 0, a player's + * attack the last that hurt them); `ordered` (living units whose order is not the one the pass before found); `morphed` + * (living units whose type is not the one the pass before found). `was`, when there: set at each turn to the killer, + * the order the unit had, or the type it was. `event`: `ordered` only the units whose new order is `order`; `killed` + * only the units `by` killed. */ - | { kind: "unitLoop"; decl: VarDecl; filter: UnitFilter; source?: "created" | "died"; body: Stmt[]; at: At; label: string } + | { kind: "unitLoop"; decl: VarDecl; filter: UnitFilter; source?: EventSource; was?: VarDecl; event?: { order?: number; by?: number }; body: Stmt[]; at: At; label: string } /** `u.hp = 40`, `u.invincible = true`: nothing happens when the unit is none or gone. */ | { kind: "unitWrite"; unit: UnitExpr; field: UnitNumField | UnitFlag; value: NumExpr | BoolExpr; at: At; label: string } /** `u.kill()`, `u.order("move", there)`: nothing happens when the unit is none or gone. */ @@ -534,7 +545,7 @@ export type Stmt = * that hands on what happened, so a turn when nothing did does nothing. Once it has begun, it is given a turn only in * a frame the events pass found units of `source`, or one of `inputs` arrived. A lowering may also give it every turn. */ - | { kind: "start"; task: number; max: number; call: Call; handle?: string; listen?: { source?: "created" | "died"; inputs?: InputSource[] }; at: At; label: string } + | { kind: "start"; task: number; max: number; call: Call; handle?: string; listen?: { source?: EventSource; inputs?: InputSource[] }; at: At; label: string } /** `t.cancel()`: the task stops where it is. Nothing happens when it has ended, or the number is no task's. */ | { kind: "cancel"; task: NumExpr; at: At; label: string } /** A trigger action; each of `variables` names a field that takes an expression's value instead of the record's. */ @@ -645,7 +656,7 @@ export function declarations(body: Stmt[]): VarDecl[] { case "storeText": expr(s.index); text(s.value); break; case "releaseText": expr(s.index); break; case "textLoop": text(s.of); out.push(s.decl); s.body.forEach(stmt); break; - case "unitLoop": out.push(s.decl); s.body.forEach(stmt); break; + case "unitLoop": out.push(s.decl); if (s.was) out.push(s.was); s.body.forEach(stmt); break; case "unitWrite": unit(s.unit); init(s.value); break; case "unitDo": unit(s.unit); if (s.verb.do === "damage" || s.verb.do === "heal") expr(s.verb.amount); break; case "tableWrite": init(s.value); break; diff --git a/compiler/names.ts b/compiler/names.ts index 2927513..7e10545 100644 --- a/compiler/names.ts +++ b/compiler/names.ts @@ -16,6 +16,7 @@ import { PlayerGroup, SWITCH_COUNT } from "../vendor/triggers"; import { AI_SCRIPT_CHOICES, aiScriptCode, PLAYER_GROUP_CHOICES, UNIT_CLASS_CHOICES } from "../vendor/triggerDefs"; import { UNIT_NAMES } from "../vendor/units"; import { TECH_NAMES, UPGRADE_NAMES, WEAPON_NAMES } from "../vendor/gameNames"; +import { ORDER_NAMES } from "./orders"; /** The location slot the game calls Anywhere (0-based); a script names it `locations.Anywhere`, value 64. */ export const ANYWHERE_INDEX = 63; @@ -45,10 +46,12 @@ export interface ScriptNames { weapons: NameTable; upgrades: NameTable; techs: NameTable; + /** orders.dat ids: what a unit is doing (`u.orderId`) and what `on.order()` listens for. */ + orders: NameTable; } /** Every table, in the order the declarations list them. */ -export const allTables = (n: ScriptNames): NameTable[] => [n.players, n.units, n.locations, n.switches, n.aiScripts, n.weapons, n.upgrades, n.techs]; +export const allTables = (n: ScriptNames): NameTable[] => [n.players, n.units, n.locations, n.switches, n.aiScripts, n.weapons, n.upgrades, n.techs, n.orders]; /** What a map contributes to the tables; every part is optional (the fixed lists stand without a map). */ export interface NameSources { @@ -151,6 +154,7 @@ export function scriptNames(src: NameSources = {}): ScriptNames { weapons: table("weapons", "Weapon", "Weapons, for stats() and a unit type's groundWeapon / airWeapon.", [...WEAPON_NAMES.map((name, id) => ({ value: id, keys: keysFor(name) })), { value: WEAPON_NAMES.length, keys: ["None"] }]), upgrades: table("upgrades", "Upgrade", "Upgrades, for stats().", UPGRADE_NAMES.map((name, id) => ({ value: id, keys: keysFor(name) }))), techs: table("techs", "Tech", "Technologies, for stats().", TECH_NAMES.map((name, id) => ({ value: id, keys: keysFor(name) }))), + orders: table("orders", "Order", "Orders, what a unit is doing: u.orderId, and what on.order() listens for.", ORDER_NAMES.map((name, id) => ({ value: id, keys: [name] }))), }; } diff --git a/compiler/orders.ts b/compiler/orders.ts new file mode 100644 index 0000000..5ec3b36 --- /dev/null +++ b/compiler/orders.ts @@ -0,0 +1,31 @@ +/** + * The game's orders, one per orders.dat id: what a unit is doing (`u.orderId`) and what `on.order(…)` listens for. The + * names are the community's, as BWAPI and scm-sim give them; the name tables offer them as identifiers (`orders.Move`). + */ +export const ORDER_NAMES: readonly string[] = [ + "Die", "Stop", "Guard", "PlayerGuard", "TurretGuard", "BunkerGuard", "Move", "ReaverStop", "Attack1", "Attack2", "AttackUnit", + "AttackFixedRange", "AttackTile", "Hover", "AttackMove", "InfestedCommandCenter", "UnusedNothing", "UnusedPowerup", "TowerGuard", + "TowerAttack", "VultureMine", "StayInRange", "TurretAttack", "Nothing", "Unused24", "DroneStartBuild", "DroneBuild", "CastInfestation", + "MoveToInfest", "InfestingCommandCenter", "PlaceBuilding", "PlaceProtossBuilding", "CreateProtossBuilding", "ConstructingBuilding", "Repair", + "MoveToRepair", "PlaceAddon", "BuildAddon", "Train", "RallyPointUnit", "RallyPointTile", "ZergBirth", "ZergUnitMorph", "ZergBuildingMorph", + "IncompleteBuilding", "IncompleteMorphing", "BuildNydusExit", "EnterNydusCanal", "IncompleteWarping", "Follow", "Carrier", "ReaverCarrierMove", + "CarrierStop", "CarrierAttack", "CarrierMoveToAttack", "CarrierIgnore2", "CarrierFight", "CarrierHoldPosition", "Reaver", "ReaverAttack", + "ReaverMoveToAttack", "ReaverFight", "ReaverHoldPosition", "TrainFighter", "InterceptorAttack", "ScarabAttack", "RechargeShieldsUnit", + "RechargeShieldsBattery", "ShieldBattery", "InterceptorReturn", "DroneLand", "BuildingLand", "BuildingLiftOff", "DroneLiftOff", "LiftingOff", + "ResearchTech", "Upgrade", "Larva", "SpawningLarva", "Harvest1", "Harvest2", "MoveToGas", "WaitForGas", "HarvestGas", "ReturnGas", + "MoveToMinerals", "WaitForMinerals", "MiningMinerals", "Harvest3", "Harvest4", "ReturnMinerals", "Interrupted", "EnterTransport", "PickupIdle", + "PickupTransport", "PickupBunker", "Pickup4", "PowerupIdle", "Sieging", "Unsieging", "WatchTarget", "InitCreepGrowth", "SpreadCreep", + "StoppingCreepGrowth", "GuardianAspect", "ArchonWarp", "CompletingArchonSummon", "HoldPosition", "QueenHoldPosition", "Cloak", "Decloak", + "Unload", "MoveUnload", "FireYamatoGun", "MoveToFireYamatoGun", "CastLockdown", "Burrowing", "Burrowed", "Unburrowing", "CastDarkSwarm", + "CastParasite", "CastSpawnBroodlings", "CastEMPShockwave", "NukeWait", "NukeTrain", "NukeLaunch", "NukePaint", "NukeUnit", + "CastNuclearStrike", "NukeTrack", "InitializeArbiter", "CloakNearbyUnits", "PlaceMine", "RightClickAction", "SuicideUnit", "SuicideLocation", + "SuicideHoldPosition", "CastRecall", "Teleport", "CastScannerSweep", "Scanner", "CastDefensiveMatrix", "CastPsionicStorm", "CastIrradiate", + "CastPlague", "CastConsume", "CastEnsnare", "CastStasisField", "CastHallucination", "Hallucination2", "ResetCollision", "ResetHarvestCollision", + "Patrol", "CTFCOPInit", "CTFCOPStarted", "CTFCOP2", "ComputerAI", "AtkMoveEP", "HarassMove", "AIPatrol", "GuardPost", "RescuePassive", + "Neutral", "ComputerReturn", "InitializePsiProvider", "SelfDestructing", "Critter", "HiddenGun", "OpenDoor", "CloseDoor", "HideTrap", + "RevealTrap", "EnableDoodad", "DisableDoodad", "WarpIn", "Medic", "MedicHeal", "HealMove", "MedicHoldPosition", "MedicHealToIdle", + "CastRestoration", "CastDisruptionWeb", "CastMindControl", "DarkArchonMeld", "CastFeedback", "CastOpticalFlare", "CastMaelstrom", "JunkYardDog", "Fatal", +]; + +/** The orders a unit is given by `u.order(…)` and the Order action: move, patrol, attack (an attack-move). */ +export const ORDER_OF_VERB: Readonly> = { move: 6, patrol: 152, attack: 14 }; diff --git a/compiler/runtime.ts b/compiler/runtime.ts index c7855b7..62ba658 100644 --- a/compiler/runtime.ts +++ b/compiler/runtime.ts @@ -627,13 +627,22 @@ export function createRuntime(names: ScriptNames, collector: Collector, options: rt.ui = Object.freeze({ lines: () => { throw new ScriptError("ui.lines() shows a text in a program's lines, inside program(async () => { … }): ui.lines(P1, () => …)."); }, }); - const listens = (what: string, example: string) => () => { throw new ScriptError(`on.${what}() listens while the game runs, inside program(async () => { … }): ${example}.`); }; + const listens = (what: string, example: string) => () => { throw new ScriptError(`on.${what}() listens while the game runs: at the top of a script, or inside program(async () => { … }): ${example}.`); }; rt.on = Object.freeze({ created: listens("created", "on.created(units.ZergZergling, (u) => { … })"), died: listens("died", "on.died({ owner: P8 }, (u) => { … })"), + killed: listens("killed", "on.killed({ owner: P8 }, (u, by) => { … })"), + order: listens("order", "on.order(units.ProtossHighTemplar, orders.CastPsionicStorm, (u) => { … })"), + morphed: listens("morphed", "on.morphed(units.TerranSiegeTankSiegeMode, (u, was) => { … })"), chat: listens("chat", "on.chat(\"-give {n}\", (p, { n }) => { … })"), key: listens("key", "on.key(\"F7\", (p) => { … })"), }); + // `on.died(…)` at the top of a script: the transformer hands the call here as a program of its own, which listens from + // the start of the game (hoist.ts#transformer). + rt.__listen = (body: unknown, at?: unknown) => { + if (!isProgramDescriptor(body)) throw new ScriptError("on.…() at the top of a script is written directly, as a statement of its own."); + collector.entries.push({ kind: "program", descriptor: body, options: { owners: [0], perPlayer: false }, at: isAt(at) ? at : null }); + }; /* ── Text a program fills in ── */ rt.name = (player: unknown) => textMark("n", onePlayer(player, "name: player", 12)); @@ -722,6 +731,6 @@ export function runtimeNames(names: ScriptNames): string[] { out.push("seconds", "minutes", "frames", "cycles", "sleep", "until", "start", "rose", "once", "shared", "clamp", "u32", "i32"); out.push(...READER_NAMES, "races", "slots", "name", "color", "print"); out.push(...UNIT_CALL_NAMES, "colors", ...INPUT_CALL_NAMES); - out.push("textColors", "ui", "on", "__jsx", "__fragment", "__class"); + out.push("textColors", "ui", "on", "__jsx", "__fragment", "__class", "__listen"); return out; } diff --git a/compiler/simulateIr.ts b/compiler/simulateIr.ts index 45fe4e3..c7590ae 100644 --- a/compiler/simulateIr.ts +++ b/compiler/simulateIr.ts @@ -36,8 +36,9 @@ */ import { ActionType, Comparison, ConditionType, SetModifier, SWITCH_COUNT, SwitchAction, SwitchState, type ActionRecord, type ConditionRecord } from "../vendor/triggers"; import { emptyAction, ResourceType } from "../vendor/triggers"; +import { ORDER_OF_VERB } from "./orders"; import { BUILD_BYTES, HEAP_CELLS, HEAP_SMALLEST, STACK_DEPTH, TEXT_BYTES, TEXT_FIELD_BYTES, eachStart, heapCells, isTextExpr, programDeclarations, stackDepth } from "./ir"; -import type { TextExpr, ArrayDecl, At, BoolExpr, Call, NumExpr, Program, ReadSource, Stmt, TableCell, TextPart, UnitExpr, UnitFilter, UnitNumField, UnitVerb, VarDecl } from "./ir"; +import type { TextExpr, ArrayDecl, At, BoolExpr, Call, EventSource, NumExpr, Program, ReadSource, Stmt, TableCell, TextPart, UnitExpr, UnitFilter, UnitNumField, UnitVerb, VarDecl } from "./ir"; import { cellMax } from "./tables"; import { inputsOf, keyName, matchChat, parseChatPattern, type ChatPattern, type InputSource, type MouseButton } from "./input"; import { Simulation, type SimulationOptions } from "./simulate"; @@ -512,7 +513,8 @@ class ProgramRun { switch (verb.do) { case "kill": case "remove": this.sim.game.gone(u, verb.do === "kill", undefined, false); this.sim.unitEvent(this, verb.do === "kill" ? ActionType.KillUnit : ActionType.RemoveUnit, u, at); break; case "give": { const to = this.sim.slotOf(verb.to); if (to < 12) u.owner = to; break; } - case "order": this.sim.unitEvent(this, ActionType.Order, u, at, { location: verb.target, text: verb.order }); break; + // (The unit takes up the order and keeps it here: the simulator moves nothing, so it never arrives.) + case "order": u.orderId = ORDER_OF_VERB[verb.order]; this.sim.unitEvent(this, ActionType.Order, u, at, { location: verb.target, text: verb.order }); break; case "locate": this.sim.centre(verb.location, u.x, u.y); break; case "damage": case "heal": { // In the game's own units, 256 to a point, as the lowering computes it. @@ -951,11 +953,13 @@ class ProgramRun { this.declare(s.decl); // The units as they are when the loop starts, in table order; one that dies on the way is passed over. Those of // the frame's events pass (`on.created` / `on.died`): the ones that came on, and the ones that died, read as they died. - const dead = s.source === "died"; + const dead = s.source === "died" || s.source === "killed"; if (dead) this.asDied.add(s.decl.id); - for (const u of s.source ? this.sim.eventUnits(s.source, s.filter) : this.sim.matching(s.filter)) { + if (s.was) this.declare(s.was); + for (const { u, was } of s.source ? this.sim.eventUnits(s.source, s.filter, s.event) : this.sim.matching(s.filter).map((u) => ({ u, was: 0 }))) { if (!dead && !u.alive) continue; this.unitVars.set(s.decl.id, u); + if (s.was) this.store(s.was.id, was); const flow = yield* this.block(s.body, ctx); if (flow === "break") break; if (flow === "return") return flow; @@ -1566,20 +1570,39 @@ export class ProgramSimulation { * passes is neither. Each list is the latest first, as the game's lists keep them. */ private eventsFrom = 0; - private readonly seen = new Set(); - private events_: { created: SimUnit[]; died: SimUnit[] } = { created: [], died: [] }; + /** Each unit seen on the map, with its order and type as the pass before found them (on.order / on.morphed). */ + private readonly seen = new Map(); + private events_: Record = { created: [], died: [], killed: [], ordered: [], morphed: [] }; private eventsPass(): void { - const created: SimUnit[] = [], died: SimUnit[] = []; - for (const u of this.seen) if (!u.alive) { died.push(u); this.seen.delete(u); } + const out: Record = { created: [], died: [], killed: [], ordered: [], morphed: [] }; + // Latest first, as the game's lists keep them: what was seen before this pass, then what came on since. + for (const [u, last] of [...this.seen].reverse()) { + if (!u.alive) { + out.died.push({ u, was: 0 }); + if (u.killedBy !== undefined) out.killed.push({ u, was: u.killedBy }); + this.seen.delete(u); + continue; + } + if (u.orderId !== last.order) { out.ordered.push({ u, was: last.order }); last.order = u.orderId; } + if (u.type !== last.type) { out.morphed.push({ u, was: last.type }); last.type = u.type; } + } const all = this.game.units; - for (; this.eventsFrom < all.length; this.eventsFrom++) { const u = all[this.eventsFrom]; if (u.alive) { created.push(u); this.seen.add(u); } } - this.events_ = { created: created.reverse(), died: died.reverse() }; + for (; this.eventsFrom < all.length; this.eventsFrom++) { const u = all[this.eventsFrom]; if (u.alive) { out.created.push({ u, was: 0 }); this.seen.set(u, { order: u.orderId, type: u.type }); } } + out.created.reverse(); + this.events_ = out; } - /** What this frame's events pass found that a filter matches: the units that came on and are still here, or the ones that died, as they died. */ - eventUnits(source: "created" | "died", f: UnitFilter): SimUnit[] { + /** + * What this frame's events pass found that a filter matches, each with what the unit was (the killer, the order it had, + * the type it was): the units that came on, took up an order or turned into another type and are still here, or the + * ones that died, as they died. + */ + eventUnits(source: EventSource, f: UnitFilter, event: { order?: number; by?: number } = {}): { u: SimUnit; was: number }[] { const owners = f.owner === undefined ? undefined : this.slotsOf(f.owner); - return this.events_[source].filter((u) => (source === "died" || u.alive) && this.game.fits(u, owners, f.type, f.at)); + const by = event.by === undefined ? undefined : this.slotsOf(event.by); + const dead = source === "died" || source === "killed"; + return this.events_[source].filter(({ u, was }) => (dead || u.alive) && this.game.fits(u, owners, f.type, f.at) + && (event.order === undefined || u.orderId === event.order) && (!by || by.includes(was))); } /** One frame: every program in order, from where it left off. */ diff --git a/compiler/structured.ts b/compiler/structured.ts index 2dac1a0..85d9e29 100644 --- a/compiler/structured.ts +++ b/compiler/structured.ts @@ -60,7 +60,8 @@ import type * as TS from "typescript"; import { ActionType, PlayerGroup } from "../vendor/triggers"; import type { ActionRecord } from "../vendor/triggers"; import type { HoistedThunks, ProgramPlan } from "./hoist"; -import { declarationOf, isOnCall, isScriptClass, libraryCallName } from "./hoist"; +import { declarationOf, isOnCall, isScriptClass, libraryCallName, ON_EVENTS } 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 } from "./runtime"; @@ -69,10 +70,12 @@ import { markViewWrites, type View } from "./views"; import { DEFAULT_COLOR, EMPTY_CELL, EMPTY_COLOR, FILLED_CELL, JSX_ELEMENTS, TEXT_COLORS, colorCode } from "./jsx"; import { HANDLE_PARTS, Scope, TEXT_PARTS, THIS, UNIT_PARTS, type Binding, type Place, type RowField, type RowShape } from "./scope"; import { ACTIONS_WITH_MODIFIER, LowerError } from "./lower"; -import { I32_MAX, I32_MIN, IR_VERSION, TASK_MAX, TEXT_BYTES, U32_MAX, UNIT_FLAGS, UNIT_NUM_FIELDS, UNIT_WRITABLE, declarations, eachCall, eachStart, programDeclarations, textHasId, type ActionVariable, type ArithOp, type ArrayDecl, type At, type BoolExpr, type Call, type CompareOp, type FuncDecl, type NumExpr, type Program, type Stmt, type TextExpr, type TextPart, type UnitExpr, type UnitFlag, type UnitNumField, type UnitVerb, type VarDecl } from "./ir"; +import { I32_MAX, I32_MIN, IR_VERSION, TASK_MAX, TEXT_BYTES, U32_MAX, UNIT_FLAGS, UNIT_NUM_FIELDS, UNIT_WRITABLE, declarations, eachCall, eachStart, programDeclarations, textHasId, type ActionVariable, type ArithOp, type ArrayDecl, type At, type BoolExpr, type Call, type CompareOp, type EventSource, type FuncDecl, type NumExpr, type Program, type Stmt, type TextExpr, type TextPart, type UnitExpr, type UnitFlag, type UnitNumField, type UnitVerb, type VarDecl } from "./ir"; /** The outcome of a thunk, kept so it runs once whatever asks. */ type Outcome = { ok: true; value: unknown } | { ok: false; error: unknown }; +/** What an on.…() of units looks for, known when the script is built: the units, and on.order()'s order or on.killed()'s killer. */ +interface EventFilter { type?: number; owner?: number; at?: number; order?: number; by?: number } /** A body the walker can be in: a program's, or a `game()` function's from any file. */ export interface Body { @@ -4264,7 +4267,8 @@ export class Structured { private inAsync(node: TS.Node): boolean { const { ts } = this; for (let n = node.parent; n; n = n.parent) if (ts.isFunctionLike(n)) return !!ts.getModifiers(n as TS.FunctionLikeDeclaration)?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); - return false; + // A listener at the top of a file is a program of its own, and an async one. + return ts.isExpressionStatement(this.body.plan.root); } /** A promise as a statement on its own: in a program, what is not awaited never runs beside it. */ @@ -5685,23 +5689,32 @@ export class Structured { */ private onEvent(call: TS.CallExpression, handle?: VarDecl) { const { ts } = this; - const what = (call.expression as TS.PropertyAccessExpression).name.text as "created" | "died" | "chat" | "key"; + const what = (call.expression as TS.PropertyAccessExpression).name.text as (typeof ON_EVENTS)[number]; const at = this.at(call), label = this.label(call); const named = `on.${what}()`; - if (!this.inAsync(call)) { this.c.error(call, `${named} listens beside the program as a task: inside program(async () => { … }).`); return; } + if (!this.inAsync(call)) { this.c.error(call, `${named} listens beside the program as a task: at the top of a script, or inside program(async () => { … }).`); return; } const args = [...call.arguments]; const isFn = (a: TS.Expression) => { const u = this.unwrap(a); return ts.isArrowFunction(u) || ts.isFunctionExpression(u); }; - let filter: { type?: number; owner?: number; at?: number } = {}; + const ofUnits = what !== "chat" && what !== "key"; + let filter: EventFilter = {}; let fnArg: TS.Expression | undefined, optArg: TS.Expression | undefined, word = ""; - if (what === "created" || what === "died") { + if (ofUnits) { const f = args.findIndex(isFn); - if (f !== 0 && f !== 1) { this.c.error(call, `${named} takes what to look for, if you like, and a function written where it is used: on.${what}(units.ZergZergling, (u) => { … }).`); return; } - if (f === 1) { const got = this.eventFilter(args[0], named); if (!got) return; filter = got; } + const example = what === "order" ? "on.order(units.ProtossHighTemplar, orders.CastPsionicStorm, (u) => { … })" : `on.${what}(units.ZergZergling, (u) => { … })`; + if (f < 0 || f > (what === "order" ? 2 : 1)) { this.c.error(call, `${named} takes what to look for, if you like, and a function written where it is used: ${example}.`); return; } + if (f >= 1) { const got = this.eventFilter(args[0], named, what); if (!got) return; filter = got; } + // on.order(filter, order, handler): the order as the second argument. + if (f === 2) { + const o = this.evaluate(args[1]); + if (!o || typeof o.value !== "number" || !Number.isInteger(o.value) || o.value < 0 || o.value >= ORDER_NAMES.length) { this.c.error(args[1], `on.order() takes the order after the filter: orders.CastPsionicStorm.`); return; } + if (filter.order !== undefined) { this.c.error(args[1], "The filter already names the order."); return; } + filter.order = o.value; + } fnArg = args[f]; optArg = args[f + 1]; if (args.length > f + 2) { this.c.error(args[f + 2], `${named} takes the filter, the function and the options.`); return; } } else { const h = args[0] && this.evaluate(args[0]); - if (!h || typeof h.value !== "string") { this.c.error(args[0] ?? call, what === "chat" ? `on.chat() takes the pattern first, as chatted() does: on.chat("-give {n}", (p, { n }) => { … }).` : `on.key() takes the key first, as keyPressed() does: on.key("F2", (p) => { … }).`); return; } + if (!h || typeof h.value !== "string") { this.c.error(args[0] ?? call, what === "chat" ? `on.chat() takes the pattern first, as chatted() does: on.chat("-give {n}", (p, { n }) => { … }).` : `on.key() takes the key first, as keyPressed() does: on.key("F7", (p) => { … }).`); return; } word = h.value; fnArg = args[1]; optArg = args[2]; if (!fnArg || !isFn(fnArg)) { this.c.error(args[1] ?? call, `${named} takes a function written where it is used, after the ${what === "chat" ? "pattern" : "key"}.`); return; } @@ -5719,10 +5732,10 @@ export class Structured { if (k === "max") { if (typeof v !== "number" || !Number.isInteger(v) || v < 1 || v > 255) { this.c.error(optArg, `${named}'s max is how many of its handlers can be waiting at once: a whole number from 1 to 255.`); return; } max = v; - } else if (k === "from" && (what === "chat" || what === "key")) { + } else if (k === "from" && !ofUnits) { if (typeof v !== "number" || !Number.isInteger(v) || ((v < 0 || v > 7) && v !== CURRENT_PLAYER)) { this.c.error(optArg, `${named}'s from is one player who can type and press keys: P1 … P8, or CurrentPlayer.`); return; } from = v; - } else { this.c.error(optArg, `${named} has no option "${k}"; it has max${what === "chat" || what === "key" ? " and from" : ""}.`); return; } + } else { this.c.error(optArg, `${named} has no option "${k}"; it has max${ofUnits ? "" : " and from"}.`); return; } } } const fn = this.unwrap(fnArg) as TS.ArrowFunction | TS.FunctionExpression; @@ -5754,33 +5767,61 @@ export class Structured { this.claimTask(c, inner); this.emit({ kind: "start", task: inner, max, call: c, at, label }, call); }; + const source: EventSource | undefined = what === "order" ? "ordered" : ofUnits ? what as EventSource : undefined; const body = this.collect(() => { const loop = this.collect(() => { // What happened since the frame before: the first frame of the task, the frame it was started in, looks at nothing. this.emit({ kind: "sleep", cycles: 1, at, label }, call); - if (what === "created") { - const v = this.newVar(name(0, "(unit)"), "unit", at, { temp: true }); - const turn = this.collect(() => run([{ kind: "var", v }])); - this.emit({ kind: "unitLoop", decl: v, filter: { ...filter }, source: "created", body: turn, at, label }, call); - } else if (what === "died") { - const v = this.newVar("(dead unit)", "unit", at, { temp: true }); + if (source) { + const { order, by, ...units } = filter; + const event = { ...(order !== undefined ? { order } : {}), ...(by !== undefined ? { by } : {}) }; + const dead = source === "died" || source === "killed"; + const v = this.newVar(dead ? "(dead unit)" : name(0, "(unit)"), "unit", at, { temp: true }); + // The second value the handler is handed, when the filter does not say it for certain: the killer, or what the + // unit was doing or was. Only kept when the handler takes it. + const wasAt = source === "ordered" ? 2 : 1; + const known = source === "killed" ? by : undefined; + const was = source !== "created" && source !== "died" && fn.parameters.length > wasAt && known === undefined ? this.newVar(name(wasAt, "(was)"), "number", at, { temp: true }) : undefined; const turn = this.collect(() => { - // What it was and where, taken as it died; what the filter says for certain is known when the script is built. - const fields = new Map(); - const known: Record = { type: filter.type !== undefined && filter.type < 228 ? filter.type : undefined, owner: filter.owner !== undefined && filter.owner !== CURRENT_PLAYER ? filter.owner : undefined }; - // Only what the handler reads is read from the dead unit: each field is a read of the game. - const wanted = this.fieldsRead(fn, 0); - for (const f of ["type", "owner", "x", "y"] as const) { - const k = known[f]; - if (k !== undefined) { fields.set(f, { kind: "value", value: k }); continue; } - if (wanted && !wanted.has(f)) { fields.set(f, { kind: "value", value: 0 }); continue; } - const fv = this.newVar(`${name(0, "(dead unit)")}.${f}`, "number", at, { temp: true }); - this.emit({ kind: "declare", decl: fv, init: { kind: "unitField", unit: unitRef(v), field: f, at, label }, at, label }, call); - fields.set(f, { kind: "var", v: fv }); + const bound: Binding[] = []; + if (dead) { + // What it was and where, taken as it died; what the filter says for certain is known when the script is built. + const fields = new Map(); + const sure: Record = { type: units.type !== undefined && units.type < 228 ? units.type : undefined, owner: units.owner !== undefined && units.owner !== CURRENT_PLAYER ? units.owner : undefined }; + // Only what the handler reads is read from the dead unit: each field is a read of the game. + const wanted = this.fieldsRead(fn, 0); + for (const f of ["type", "owner", "x", "y"] as const) { + const k = sure[f]; + if (k !== undefined) { fields.set(f, { kind: "value", value: k }); continue; } + if (wanted && !wanted.has(f)) { fields.set(f, { kind: "value", value: 0 }); continue; } + const fv = this.newVar(`${name(0, "(dead unit)")}.${f}`, "number", at, { temp: true }); + this.emit({ kind: "declare", decl: fv, init: { kind: "unitField", unit: unitRef(v), field: f, at, label }, at, label }, call); + fields.set(f, { kind: "var", v: fv }); + } + bound.push({ kind: "record", fields }); + } else bound.push({ kind: "var", v }); + if (source === "ordered" && fn.parameters.length > 1) { + // The order it took up: known when the filter names it, else read from the unit as it is now. + if (order !== undefined) bound.push({ kind: "value", value: order }); + else { + const ov = this.newVar(name(1, "(order)"), "number", at, { temp: true }); + this.emit({ kind: "declare", decl: ov, init: { kind: "unitField", unit: unitRef(v), field: "orderId", at, label }, at, label }, call); + bound.push({ kind: "var", v: ov }); + } } - run([{ kind: "record", fields }]); + if (source === "killed" && known !== undefined) bound.push({ kind: "value", value: known }); + else if (source === "killed" && was) { + // The killer is one of eight players: a copy of the handler for each, so that it is known when the script + // is built there and an action can be for them (on.chat's player is the same). + for (let p = 0; p < 8; p++) { + const then = this.collect(() => run([...bound, { kind: "value", value: p }])); + this.emit({ kind: "if", cond: { kind: "compare", op: "==", left: varRef(was), right: num(p), at, label }, then, at, label }, call); + } + return; + } else if (was) bound.push({ kind: "var", v: was }); + run(bound); }); - this.emit({ kind: "unitLoop", decl: v, filter: { ...filter }, source: "died", body: turn, at, label }, call); + this.emit({ kind: "unitLoop", decl: v, filter: units, source, ...(was ? { was } : {}), ...(Object.keys(event).length ? { event } : {}), body: turn, at, label }, call); } else { const players = from !== undefined ? [from] : this.c.perPlayer ? [CURRENT_PLAYER] : [0, 1, 2, 3, 4, 5, 6, 7]; let parsed: ChatPattern | undefined; @@ -5819,7 +5860,7 @@ export class Structured { }); const listener: Call = this.mark({ name: named, at, label, params: [], body }, call); this.claimTask(listener, task); - this.emit({ kind: "start", task, max: 1, call: listener, ...(handle ? { handle: handle.id } : {}), listen: what === "created" || what === "died" ? { source: what } : { inputs: heard }, at, label }, call); + this.emit({ kind: "start", task, max: 1, call: listener, ...(handle ? { handle: handle.id } : {}), listen: source ? { source } : { inputs: heard }, at, label }, call); } /** @@ -5866,19 +5907,23 @@ export class Structured { return varRef(t); } - /** What on.created() / on.died() looks for: a unit type, or a filter as unitsOf() takes one; known when the script is built. */ - private eventFilter(arg: TS.Expression, named: string): { type?: number; owner?: number; at?: number } | null { + /** + * What an on.…() of units looks for: a unit type, or a filter as unitsOf() takes one — with the order for on.order() and + * the killer for on.killed(); known when the script is built. + */ + private eventFilter(arg: TS.Expression, named: string, what: string): EventFilter | null { const h = this.evaluate(arg); if (!h) { this.notConstant(arg, `What ${named} looks for`); return null; } const v = h.value; - const out: { type?: number; owner?: number; at?: number } = {}; - const fail = () => { this.c.error(arg, `${named} looks for a unit type — units.ZergZergling — or a filter such as { type: units.TerranMarine, owner: P1, at: locations.Base }.`); return null; }; + const out: EventFilter = {}; + const extra = what === "order" ? "order" : what === "killed" ? "by" : null; + const fail = () => { this.c.error(arg, `${named} looks for a unit type — units.ZergZergling — or a filter such as { type: units.TerranMarine, owner: P1, at: locations.Base${extra === "order" ? ", order: orders.Move" : extra === "by" ? ", by: P2" : ""} }.`); return null; }; if (typeof v === "number") out.type = v; else if (v && typeof v === "object" && !Array.isArray(v) && !isGameValue(v)) { for (const [k, x] of Object.entries(v as Record)) { if (x === undefined || x === null) continue; - if ((k !== "type" && k !== "owner" && k !== "at") || typeof x !== "number" || !Number.isInteger(x)) return fail(); - out[k] = x; + if ((k !== "type" && k !== "owner" && k !== "at" && k !== extra) || typeof x !== "number" || !Number.isInteger(x)) return fail(); + out[k as keyof EventFilter] = x; } } else return fail(); if (out.type === 229) delete out.type; @@ -5886,6 +5931,9 @@ export class Structured { if (out.owner !== undefined && (out.owner < 0 || (out.owner > 11 && out.owner !== CURRENT_PLAYER))) { this.c.error(arg, `${named}'s owner is one player: P1 … P12, or CurrentPlayer.`); return null; } if (out.at !== undefined && (out.at === 0 || out.at === 64)) delete out.at; if (out.at !== undefined && (out.at < 0 || out.at > 255)) return fail(); + if (out.order !== undefined && (out.order < 0 || out.order >= ORDER_NAMES.length)) { this.c.error(arg, `${named}'s order is one of orders.*: orders.Move, orders.CastPsionicStorm …`); return null; } + // A killer is one of the eight players who can fight (a neutral player's units do not attack), or CurrentPlayer. + if (out.by !== undefined && (out.by < 0 || (out.by > 7 && out.by !== CURRENT_PLAYER))) { this.c.error(arg, `${named}'s by is the player who killed the unit: P1 … P8, or CurrentPlayer.`); return null; } return out; } diff --git a/compiler/testing.ts b/compiler/testing.ts index 04d0162..51a2f79 100644 --- a/compiler/testing.ts +++ b/compiler/testing.ts @@ -249,6 +249,7 @@ export interface TestWorldLike { kill(unit: SimUnit, by?: number): this; remove(unit: SimUnit): this; give(unit: SimUnit, to: number): this; + morph(unit: SimUnit, type: number): this; move(unit: SimUnit, to: number | { x: number; y: number }): this; press(key: string, player?: number): this; click(button?: "left" | "right" | "middle", player?: number): this; @@ -409,6 +410,16 @@ export class TestSim implements TestWorldLike { kill(unit: SimUnit, by?: number): this { this.world.game.gone(unit, true, by); return this; } remove(unit: SimUnit): this { this.world.game.gone(unit, false); return this; } give(unit: SimUnit, to: number): this { if (unit.alive) unit.owner = to; return this; } + /** A unit turns into another type and stays the same unit: the new type's hit points and shields, in the same part of them. */ + morph(unit: SimUnit, type: number): this { + if (!unit.alive) return this; + const s = this.world.game.stats(type) ?? {}; + const maxHp = Math.max(1, s.hp ?? unit.maxHp), maxShields = Math.max(0, s.shields ?? 0); + unit.hp = Math.max(1, Math.round((unit.hp * maxHp) / Math.max(1, unit.maxHp))); + unit.shields = unit.maxShields ? Math.round((unit.shields * maxShields) / unit.maxShields) : maxShields; + Object.assign(unit, { type, maxHp, maxShields }); + return this; + } /** A unit to a location's centre, or to a point. */ move(unit: SimUnit, to: number | { x: number; y: number }): this { const p = typeof to === "number" ? this.world.game.centreOf(to) : to; diff --git a/compiler/views.ts b/compiler/views.ts index be31548..374ac5a 100644 --- a/compiler/views.ts +++ b/compiler/views.ts @@ -59,7 +59,8 @@ function writes(s: Stmt, arrays: Map): string[] { case "store": case "push": case "pop": case "setLength": case "declareArray": return [array(s.array)]; case "storeText": return [array(s.addr), array(s.block), array(s.chars)]; case "releaseText": return [array(s.block)]; - case "textLoop": case "unitLoop": return [s.decl.id]; + case "textLoop": return [s.decl.id]; + case "unitLoop": return s.was ? [s.decl.id, s.was.id] : [s.decl.id]; default: return []; } } diff --git a/compiler/world.ts b/compiler/world.ts index 4dc3626..65f0e31 100644 --- a/compiler/world.ts +++ b/compiler/world.ts @@ -37,6 +37,8 @@ export interface SimUnit { slot: number; /** The place's uniqueness byte while this unit is in it. */ uid: number; + /** Once it has died in a fight: the player whose attack killed it (on.killed()). */ + killedBy?: number; } export type SimUnitInit = Partial> & { type: number; owner: number }; export interface SimBounds { left: number; top: number; right: number; bottom: number } @@ -192,6 +194,7 @@ export class World { unit.alive = false; if (this.table[unit.slot] === unit) this.table[unit.slot] = null; if (!killed || !fought) return; + if (by !== undefined) unit.killedBy = by; this.onDeath(unit); if (by !== undefined) this.killed.set(unit.type * 4096 + by, (this.killed.get(unit.type * 4096 + by) ?? 0) + 1); } diff --git a/docs/ir.md b/docs/ir.md index a99faa9..e433707 100644 --- a/docs/ir.md +++ b/docs/ir.md @@ -3,7 +3,7 @@ What a `program(() => { … })` body means, written down as data. The compiler's front end (`compiler/structured.ts`) turns the TypeScript into this, `python/trigscript.py` lowers it to eudplib when the map is saved, and `compiler/simulateIr.ts` interprets it for -Simulate and the tests. **Version 18** (17 had no events: no `source` on a `unitLoop` and no `listen` on a `start`; 16 had no `buildText`, no `built` and no `char` part: a text was made whole or not at all; 15 had no tasks: no `start`, `cancel`, `running`, and no `task` on a declaration; 14 had no `parallel`: nothing ran beside a program's body; 13 had no text in the cells of an array: no `textAt`, `storeText`, `releaseText`; 12 had no texts: a text was the parts of a `print` and nothing else; 11 had no `slice` and no `through`: every array was cells of its own; 10 had no recursion: no `recursive`, no `saves`, and the heap's top set aside for a stack nothing used; 9 had no functions that are called: every `call` carried a body of its own; 8 had no `unitAt` / `unitPart`, so no array could hold a unit; 7 had no arrays that grow; 6 had no arrays; 5 had unsigned numbers only, the two-sided reading of `+` and `−`, no `>>>` and no `unsigned` anywhere; 4 had no input, no `centerLocation`, and one `variable` on an action where 5 has a list; 3 had no units and no tables; 2 had no reads, no `random(n)`, no bitwise operators +Simulate and the tests. **Version 19** (18 had only two kinds of event: a `unitLoop`'s `source` was `created` or `died`, with no `was` and no `event`; 17 had no events: no `source` on a `unitLoop` and no `listen` on a `start`; 16 had no `buildText`, no `built` and no `char` part: a text was made whole or not at all; 15 had no tasks: no `start`, `cancel`, `running`, and no `task` on a declaration; 14 had no `parallel`: nothing ran beside a program's body; 13 had no text in the cells of an array: no `textAt`, `storeText`, `releaseText`; 12 had no texts: a text was the parts of a `print` and nothing else; 11 had no `slice` and no `through`: every array was cells of its own; 10 had no recursion: no `recursive`, no `saves`, and the heap's top set aside for a stack nothing used; 9 had no functions that are called: every `call` carried a body of its own; 8 had no `unitAt` / `unitPart`, so no array could hold a unit; 7 had no arrays that grow; 6 had no arrays; 5 had unsigned numbers only, the two-sided reading of `+` and `−`, no `>>>` and no `unsigned` anywhere; 4 had no input, no `centerLocation`, and one `variable` on an action where 5 has a list; 3 had no units and no tables; 2 had no reads, no `random(n)`, no bitwise operators and no `print`; 1 had the map's string indices in the records and a `cyclesPerSecond` on the program, for the death-counter backend 3.0 removed). The types are in `compiler/ir.ts`; this is the reference for anyone reading the lowering or writing @@ -168,7 +168,7 @@ frame's `saves`, and a `slice` is never declared. | `store` | `array`, `index`, `value` | `hp[i] = value`; `hp[i] += v` arrives as a store of `hp[i] + v`. The value is evaluated, then the index; nothing is stored past either end. | | `assignBool` | `target`, `value` | `target = value` for a boolean. | | `assignUnit` | `target`, `value` | `target = value` for a unit variable. | -| `unitLoop` | `decl`, `filter`, `source?`, `body` | `for (const u of unitsAt(…))`: the body once for every unit the filter matches, in the order of the game's unit table, within the frame (the compiler refuses a `sleep` inside). `decl` is the unit of the turn; `break` and `continue` are the loop's. With `source` (`on.created` / `on.died`, since version 18) the units are this frame's events pass's instead (*Events*, below). | +| `unitLoop` | `decl`, `filter`, `source?`, `was?`, `event?`, `body` | `for (const u of unitsAt(…))`: the body once for every unit the filter matches, in the order of the game's unit table, within the frame (the compiler refuses a `sleep` inside). `decl` is the unit of the turn; `break` and `continue` are the loop's. With `source` (`on.created` / `on.died`, since version 18; `killed`, `ordered`, `morphed` since 19) the units are this frame's events pass's instead, `was` a number variable set at each turn and `event` a filter of the event's own (*Events*, below). | | `unitWrite` | `unit`, `field`, `value` | `u.hp = 40`, `u.invincible = true`. Nothing happens when the unit is none or gone. Hit points at 0 kill. | | `unitDo` | `unit`, `verb` | `{ do: "kill" }`, `{ do: "remove" }`, `{ do: "give", to }`, `{ do: "order", order, target }` (a location), `{ do: "damage" \| "heal", amount, percent }`, `{ do: "locate", location }`. Nothing happens when the unit is none or gone; an amount is evaluated either way. | | `tableWrite` | `cell`, `value`, `scaled?`, `boolean?` | A cell of the game's tables. `value` is a number expression, a boolean one (`boolean`), or `{ kind: "text", text }` for a name. `scaled`: the number is already what the cell stores. | @@ -194,14 +194,15 @@ frame's `saves`, and a `slice` is never declared. ### Events -`on.created`, `on.died`, `on.chat` and `on.key` are each a `start` with `max` 1 and `listen`, whose body is +`on.created`, `on.died`, `on.killed`, `on.order`, `on.morphed`, `on.chat` and `on.key` are each a `start` with `max` 1 and `listen`, whose body is `while (true) { sleep 1; … }`: the first turn, in the frame it is started in, only waits, so a listener hands on what happens from the next frame. What it hands on is a `unitLoop` with a `source`, or for a typed line or a key an `if` on the `input` for each player it listens to, unrolled (the player is a value of the script in each copy). A handler that is not async is inlined there; an async one is a `start` of its own each time, its `params` copies of what it is handed. Once a listener has begun, the lowering may skip its turn in a frame nothing it listens for happened (`listen.source`: the pass found none of those; `listen.inputs`: none -arrived), since such a turn does nothing; the simulator gives it every turn. +arrived), since such a turn does nothing; the simulator gives it every turn. A listener written at the top of +a file is a program of its own (owner Player 1, not per player) whose body is that one `start`. The **events pass** runs once a frame before the programs, when the IR has a `unitLoop` with a `source`. It walks two of the game's lists (1.16.1's addresses: the visible units at 0x628430 and the dying ones at @@ -221,6 +222,22 @@ dying record, whose fields read as the unit was when it died — its slot is not the next frame's dying pass — and the front end only reads its fields. The visible list is the latest first; the order of a frame's deaths is the dying list's, which is not the same in the two games. +The other three sources (version 19): + +- `killed`: the `died` turns whose record has no hit points (+0x08 is 0) and one of the eight players as the + last whose attack hurt the unit (+0x90, a byte; 8 when none has). That is what a death in a fight leaves; Kill + Unit, Remove Unit, a program's `kill()` and a unit that went down with its transport leave the hit points and, + unless the unit was hurt before, 8. `was` is that player. `event.by`: only the units that player killed (13, + the current player). The front end copies the handler for each of the eight killers when it takes the killer, + so that it is a value of the script in each copy. +- `ordered` and `morphed`: when the IR has either, the pass first walks the whole visible list and holds each + unit's order byte (+0x4D) and type (its low byte) against a table of its own, a cell a slot as the uniqueness + cells are. A unit whose cell differs and whose slot was seen before this pass is handed on, `was` the cell's old + value; a unit not seen before only has its cells set. So an order or a type held for less than a frame is not + seen, and a unit's first order is not a change. `event.order` (`ordered`): only the units whose order is now that + one. This walk costs about 100 conditions and actions a unit on the map every frame for one of the two, 140 for + both (lifted game, 2026-10-17). + ### Tasks Each `start` statement is a body of its own — a loop unrolled when the script is built has one diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 3547b2f..16290e4 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -957,10 +957,65 @@ on.key("F2", (p) => print("Help", { to: p }), { from: P1 }); F2 moved the camera (the game's own key) and said nothing; a second probe (Q, F2–F5, F7, F9, each through `on.key`, and F2 through `keyPressed`) heard only Q, F7 and F9. So F2–F5 join F6 as keys Remastered keeps: they are not a `Key`, and the examples use F7. F1, F10–F12 not played. -- **Not yet:** `on.order` (each unit's order against the last frame's: a scan, or a pass over the visible list), - `u.killedBy` (the killer's kill count rises a frame before; or the unit's +0x90, last attacking player, from the - dying record — both to check), a `removed` flag on a DeadUnit, a morph event, listeners at the top of a script - without a program around them. +**As built, second part (2026-10-17, branch `b2-events-2`).** `on.killed`, `on.order`, `on.morphed`, and a listener +at the top of a script; IR 19. + +```ts +on.killed({ owner: P8 }, (u, by) => setResources(by, "add", 25, "ore")); // the killer, a player the action can be for +on.order(units.ProtossHighTemplar, orders.CastPsionicStorm, (u) => print("Storm!")); +on.morphed(units.TerranSiegeTankSiegeMode, (u, was) => print(`sieged from ${was}`)); +on.died(units.TerranMarine, (u) => print("Marine down")); // at the top of the file: listens from the start +``` + +- **Who killed a unit: the dying record says.** Measured in the lifted game first (a fight, Kill Unit, Remove Unit, + marines inside a dropship killed by trigger, a marine hurt and later killed by trigger): a death in a fight leaves + hit points 0 and the killer's player at +0x90 (lastAttackingPlayer, as BWAPI and scm-sim name it); Kill Unit leaves + the hit points and +0x90 as they were (8 when nobody ever hurt the unit); Remove Unit the same with the sprite gone. + So `on.killed` is the `died` turns with hp 0 and +0x90 below 8, the killer read from +0x90. The plan's other way + (the killer's kill count rising a frame before) is not needed. A unit a program damages to death (`u.damage`) + with an earlier attacker would count for that attacker: not seen yet, said here. +- **`removed` is not built: the game does not tell.** A Remove Unit and a unit killed inside a transport leave the + same record — no sprite, hit points as they were, +0x90 untouched; 336 bytes compared, nothing else differs but + timers. A flag that is wrong for a hero dying in a dropship is worse than none, so `on.killed` takes its place: + "died in a fight" is what a map maker asking "was it removed?" usually wants. An Archon's partner leaves the same + way (one High Templar turns into the Archon in its own slot, the other goes without a sprite). +- **The killer as a value of the script.** `setResources(by, …)` needs its player when the map is built, so a handler + that takes `by` is copied for each of the eight killers (`if (was == p)`), as `on.chat` is for each typer; with + `{ by: P1 }` in the filter it is known and not copied. First found when the eudplib fixture refused `setResources(by, + …)`: the obvious way to reward a killer did not compile. +- **Orders and morphs: one walk of the visible list.** A unit's order byte and type are compared every frame with a + table of the events pass's own (a cell a slot, as the uniqueness cells). The type is the same unit changing in + place, measured in the lifted game: a Siege Tank goes 5 → 30 in its slot at the frame it finishes turning (order 98, + Sieging, the frame before); one High Templar of two becomes the Archon in its slot. `on.order` hands the unit, the + order it took up and the one it had (`{ order }` in the filter, or the plan's three-argument form); `on.morphed` the + unit and the type it was, its filter on the new type. Each verb's order, from the lifted game: `move` 6 (Move), + `patrol` 152, `attack` 14 (AttackMove); the simulator sets them. New units take up Guard (2) then PlayerGuard (3) + in the game, which `on.order` hears; their first order is not a change. +- **What the walk costs** (lifted game, about 115 units, idle): 178 conditions and actions a frame without it; one + of `on.order` / `on.morphed` about 11,000 (≈ 95 a unit), both about 16,000 (≈ 140). First built at 16,000 / 21,000: + reading the uniqueness byte only when something changed, and the type as its low byte, took a third off. The + pointer chase is about half of what is left. A hint on the line says it. +- **Listeners at the top of a script.** `on.…(…)` as a statement at the top of a file is planned as a program whose + body is that statement (a block made around it, `hoist.ts#planListener`), and the transformer replaces it with + `__listen(descriptor)`: a program of its own, owner Player 1, listening from the start. +- **Handlers return what they like.** The declarations had `=> void | Promise`, which refuses `(p) => + setResources(…)` (an expression body that gives an action) — the examples in the declarations were written that + way. Now `=> void`, which an async function also is. +- **`orders`**: a name table (`orders.Move`, `orders.CastPsionicStorm`; the community's names, as BWAPI and scm-sim + have them), the `Order` brand, and `u.orderId` an `Order`. Typed chat patterns (`{n}` a number, `{u:unit}` a unit + type, `{k:ore|gas}` the word's place) were already in part 1, from `chatted()`. +- **Tests:** `sim.morph(unit, type)` for the simulator (the lifted game turns a unit into another type only through + a player's command, which a test cannot give yet: `GameSim.morph` says so); `GameSim.kill(unit, by)` now leaves the + record a fight does (hp 0, +0x90 = by), so `on.killed` hears it in both. +- **Checked.** `tests/events.test.ts` (12), eudplib fixture `eventsMore` (three programs, two at the top of the + file), `probes/game-events-2.ts` (3) in the lifted game and the simulator at the same frames, every older game + probe; `game-check` of the Remastered probe: 0 accesses that end the game. **Probe for Remastered:** + `Maps/TrigScript/trigscript-b2-events-2.scx` with its answer sheet — a fight (killer both ways), a program's kill, + Psionic Storm (order and kills), an Archon, a Siege Tank both ways, a Drone into a Hatchery and a Larva into an + egg into a Drone, a move order, 300 Overlords with the walk on. Not played yet. +- **Not yet:** a morph in a test in the lifted game (scm-sim's test handle would take a player's command); `on.order` + only for units of the filter's types (skip the order read for the rest: cheaper, but a unit that morphs into a + watched type would bring a stale order once); whether a program's `u.damage()` to 0 credits an earlier attacker. ### B3. Function values (4.5.0) @@ -1284,7 +1339,7 @@ passes near them. | Idea | Built from | Where | | --- | --- | --- | | EUD error code to line: Remastered's "EUD ERROR: 0x…" is the refused address inverted (R0); the editor reads the code and points at the line that touches that address | the trigger map and trace (A1), R0 | now: a day | -| Typed chat patterns: `on.chat("-give {n}", (p, { n }) => …)` with `n: number` worked out by template literal types | B2 | B2 | +| Typed chat patterns: `on.chat("-give {n}", (p, { n }) => …)` with `n: number` worked out by template literal types | B2 | built: `chatted()`'s patterns, which `on.chat` took over in B2 | | A line's values over time beside it, as a small graph, from the debugger's recording | A5 | after A5's open items | | Changes kept while the game runs: edit the script, rebuild, carry the variables over by name into a snapshot and go on | A2's variable map, scm-sim snapshots | after B1 (a task's place has to carry over too) | | Fuzzing: random inputs on every seat at the lifted game's speed, looking for desyncs, games that end and failed expectations, then the smallest input that still fails | A2, A3 | after B5 | diff --git a/probes/events-2.ts b/probes/events-2.ts new file mode 100644 index 0000000..cfae8c6 --- /dev/null +++ b/probes/events-2.ts @@ -0,0 +1,73 @@ +// The second events probe (TrigScript 4, B2, second part): does Remastered hand on what on.killed, on.order and +// on.morphed listen for, and does a listener at the top of a script listen? +// on.killed reads two fields of the dying record: hit points (+0x08) and the last player whose attack hurt the unit +// (+0x90); the lifted game leaves 0 and the killer there after a fight, its hit points and 8 after Kill Unit. on.order and +// on.morphed walk the visible units every frame and compare each one's order byte (+0x4D) and type (+0x64) with a table of +// their own. Everything read is allowed by Remastered's emulator (the dump, R0); none of it has been played. +// +// What to look for, by the second (each step says so in the middle of the screen too): +// A 3 a fight beside your base: six Marines of yours, four Zerglings of the computer's. Each Zergling that falls says +// "killed 37 by P1" and gives you 25 minerals; a Marine that falls says "killed 0 by P2", and also +// "top: Marine down" (that one is a listener at the top of the script). +// B 12 two Firebats killed by the program: "died 32" twice, and no "killed". +// C 15 your turn, at your own pace: two High Templar with Psionic Storm and full energy, and a Siege Tank with Siege +// Mode, beside your base. Type -storm: eight Zerglings of the computer's to the right of your base. Storm them. +// "Storm!" when a Templar starts casting; "killed 37 by P1" for each Zergling the storm kills. +// Then merge the two Templar into an Archon: "morphed 67 -> 68", and "died 67" for the other one. +// D siege the tank: "morphed 5 -> 30"; unsiege it: "morphed 30 -> 5". +// E type -zerg: two Drones beside your base and 1000 minerals. Make one a Hatchery anywhere it fits: "morphed +// 41 -> 131" when it starts. When the Hatchery is done (about 80 seconds), make a Larva a Drone: "morphed +// 35 -> 36" (the egg) at once, then "morphed 36 -> 41" when it hatches. +// F right-click a Marine somewhere: "move from 3" (3 is standing guard). Hold position, stop, patrol: nothing. +// G type -many: 300 Overlords of yours in one frame, with every unit's order and type looked at each frame. +// Does the game slow? Type -clear to take them off again. +// Host it as a multiplayer game, alone: a typed line (-storm, -zerg, -many) reaches the map only there. +// npx tsx scripts/build-fixture.mts probes/events-2.ts fixtures/eud/events-2.scx +// npx tsx scripts/test-game.mts probes/events-2.ts (A and B in the lifted game) +import { test, expect, type Sim, program, print, createUnit, setResources, sleep, seconds, unitsOf, on, units, orders, techs, stats, P1, P2, type Location } from "trigscript"; + +const HOME = 1 as Location, AWAY = 2 as Location; + +// At the top of the script: a program of its own, listening from the start of the game. +on.died(units.TerranMarine, () => print("top: Marine down")); + +program(async () => { + stats(P1).researched[techs.PsionicStorm] = true; + stats(P1).researched[techs.TankSiegeMode] = true; + on.killed((u, by) => print(`killed ${u.type} by P${by + 1}`)); + on.killed({ by: P1 }, () => setResources(P1, "add", 25, "ore")); + on.died((u) => { if (u.type != units.TerranMarine && u.type != units.ZergZergling && u.type != units.ZergOverlord) print(`died ${u.type}`); }); + on.created(units.ProtossHighTemplar, (u) => { u.energy = 200; }); + on.order(units.ProtossHighTemplar, orders.CastPsionicStorm, () => print("Storm!")); + on.order({ owner: P1, type: units.TerranMarine, order: orders.Move }, (u, order, was) => print(`move from ${was}`)); + on.morphed({ owner: P1 }, (u, was) => print(`morphed ${was} -> ${u.type}`)); + on.chat("-storm", () => createUnit(P2, units.ZergZergling, 8, AWAY)); + on.chat("-zerg", () => { setResources(P1, "add", 1000, "ore"); createUnit(P1, units.ZergDrone, 2, HOME); }); + on.chat("-many", () => { for (let i = 0; i < 12; i++) createUnit(P1, units.ZergOverlord, 25, HOME); }); + on.chat("-clear", () => { for (const u of unitsOf(P1, { type: units.ZergOverlord })) u.remove(); }); + + await sleep(seconds(3)); + print("A: a fight. Expect \"killed 37 by P1\" for each Zergling (25 minerals each), \"killed 0 by P2\" and \"top: Marine down\" for each Marine.", { position: "center" }); + createUnit(P1, units.TerranMarine, 6, HOME); + createUnit(P2, units.ZergZergling, 4, HOME); + + await sleep(seconds(9)); + print("B: two Firebats killed by the program. Expect \"died 32\" twice and no \"killed\".", { position: "center" }); + createUnit(P1, units.TerranFirebat, 2, HOME); + await sleep(seconds(1)); + for (const u of unitsOf(P1, { type: units.TerranFirebat })) u.kill(); + + await sleep(seconds(2)); + createUnit(P1, units.ProtossHighTemplar, 2, HOME); + createUnit(P1, units.TerranSiegeTankTankMode, 1, HOME); + print("C to F: your turn. -storm for Zerglings to storm, then an Archon, siege the tank, -zerg for a Hatchery and a Drone, move a Marine. See the answer sheet.", { position: "center" }); +}, { owner: P1, name: "events2" }); + +/** How many times a text was shown. (The game's own lines fold the same text shown twice in a frame, so the actions are counted.) */ +const said = (sim: Sim, text: string | RegExp) => sim.events.filter((e) => { const t = e.text.replace(/^Display Text Message — /, ""); return e.text !== t && (typeof text === "string" ? t === text : text.test(t)); }).length; + +test("B: a program's kill is a death, not a kill", (sim) => { + sim.seconds(14); + expect(said(sim, "died 32")).toBe(2); + expect(said(sim, /^killed 32/)).toBe(0); +}); diff --git a/probes/game-events-2.ts b/probes/game-events-2.ts new file mode 100644 index 0000000..0ff2df5 --- /dev/null +++ b/probes/game-events-2.ts @@ -0,0 +1,64 @@ +// TrigScript 4, B2 second part: on.killed, on.order and a listener at the top of a script — run in the lifted game as +// well as in the simulator; the two have to agree frame for frame. (on.morphed needs a player's command in the game, +// which a test cannot give: the simulator's tests and the Remastered probe cover it.) +// npx tsx scripts/test-game.mts probes/game-events-2.ts +// A program first runs on frame 2. Built like a probe (Player 1 Terran, Player 2 a Zerg computer, location 1 "Home"). +import { test, expect, program, print, createUnit, sleep, frames, unitsOf, on, units, orders, setResources, P1, P2, type Location } from "trigscript"; + +const HOME = 1 as Location; + +// At the top of the script: a program of its own, listening from the start. +on.created(units.TerranGhost, (u) => print(`ghost ${u.owner}`)); + +program(async () => { + let killed = 0, byP2 = 0, killer = -1, deaths = 0, moved = 0, patrols = 0, attacks = 0, from = -1; + on.killed((u, by) => { killed++; killer = by; setResources(by, "add", 10, "ore"); }); + on.killed({ by: P2 }, () => { byP2++; }); + on.died(() => { deaths++; }); + on.order({ owner: P1, order: orders.Move }, (u, order, was) => { moved++; from = was; }); + on.order(units.TerranFirebat, orders.Patrol, () => { patrols++; }); + on.order(units.TerranFirebat, orders.AttackMove, () => { attacks++; }); + // Frame 4: two Marines and two Firebats. + await sleep(frames(2)); + createUnit(P1, units.TerranMarine, 2, HOME); + createUnit(P1, units.TerranFirebat, 2, HOME); + // Frame 8: the Marines told to move, the Firebats to patrol; frame 10: to attack. + await sleep(frames(4)); + for (const u of unitsOf(P1, { type: units.TerranMarine })) u.order("move", HOME); + for (const u of unitsOf(P1, { type: units.TerranFirebat })) u.order("patrol", HOME); + await sleep(frames(2)); + for (const u of unitsOf(P1, { type: units.TerranFirebat })) u.order("attack", HOME); +}, { owner: P1, name: "events2" }); + +test("on.killed: a death in a fight with its killer; a death by a program is not one", (sim) => { + sim.frames(5); + const [a, b] = sim.units({ owner: P1, type: units.TerranMarine }); + sim.kill(a, P2); + sim.frames(1); + expect(sim.program("events2").killed).toBe(1); + expect(sim.program("events2").killer).toBe(P2); + expect(sim.program("events2").byP2).toBe(1); + expect(sim.resources(P2).ore).toBe(10); + // Killed without a killer: a death, not a kill. + sim.kill(b); + sim.frames(1); + expect(sim.program("events2").killed).toBe(1); + expect(sim.program("events2").deaths).toBe(2); +}); + +test("on.order: the order a unit took up, and the one it had", (sim) => { + sim.frames(9); + // Every Marine of Player 1's, the two made and the base's own. + expect(sim.program("events2").moved).toBe(sim.count(P1, units.TerranMarine)); + expect(sim.program("events2").from).toBe(orders.PlayerGuard); + expect(sim.program("events2").patrols).toBe(2); + sim.frames(2); + expect(sim.program("events2").attacks).toBe(2); +}); + +test("a listener at the top of a script", (sim) => { + sim.frames(3); + sim.place(P1, units.TerranGhost, HOME, 3); + sim.frames(1); + expect(sim.printed().filter((t) => t === "ghost 0").length).toBeGreaterThan(0); +}); diff --git a/python/trigscript.py b/python/trigscript.py index 7b97b3e..ee36a9d 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -68,7 +68,7 @@ from eudplib.core.mapdata.stringmap import ForceAddString from eudplib.memio.rwcommon import br1, br2, bw1 -IR_VERSION = 18 +IR_VERSION = 19 FRAMES_PER_SECOND = 24 # Where the game keeps what a read reads (1.16.1 addresses, which Remastered emulates). The player # tables are the ones Magenta's probes 5 and 8 read in the game. @@ -100,6 +100,9 @@ VISIBLE_HEAD, DYING_HEAD = 0x628430, 0x62842C # How many units one frame's events pass hands on of each kind; past that the rest are handed on in the next frame. EVENT_CAP = 512 +# 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 +# with no hit points and a player there died in a fight, and that player killed it (on.killed). +OFF_HP, OFF_LAST_ATTACKER = 0x08 // 4, 0x90 // 4 # The location table, and the one an order borrows for the length of one action (put back after). MRGN = 0x58DC60 SCRATCH_LOCATION = 255 @@ -1203,9 +1206,17 @@ class Events: links a dying unit in behind the other end, which no map can read), so only a walk both ways sees every record in both. A record stays on it only until its sprite is gone. Nothing of the game is written.""" - def __init__(self, created, died): - self.created, self.died = created, died + def __init__(self, created, died, ordered=False, morphed=False): + self.created, self.died, self.ordered, self.morphed = created, died, ordered, morphed self.seen = SlotTable() + # on.order / on.morphed: each slot's order (masked 0xFF00, as it sits in its dword) and type as the pass before found + # them, and this frame's units that changed, each with what it was. + self.orders = SlotTable() if ordered else None + self.types = SlotTable() if morphed else None + self.turned, self.turned_was = (EUDArray(EVENT_CAP), EUDArray(EVENT_CAP)) if ordered else (None, None) + self.turned_n = EUDVariable(0) # initial: the pass's own state, set at its start + self.changed, self.changed_was = (EUDArray(EVENT_CAP), EUDArray(EVENT_CAP)) if morphed else (None, None) + self.changed_n = EUDVariable(0) # initial: the pass's own state, set at its start # Each a unit's pointer, which f_cunitepdread_epd reads back cheaply with its EPD: within the frame a slot is not # given to another unit (that takes a dying pass), so its uniqueness byte need not be kept. self.made = EUDArray(EVENT_CAP) if created else None @@ -1216,8 +1227,8 @@ def __init__(self, created, died): # head (pointer, EPD). Nothing is linked in but behind the head, so with both the same nothing came in since. self.bounds = {VISIBLE_HEAD: [EUDVariable(0) for _ in range(5)]} # initial: no pass before - def cell(self, epd): - return epd + (EPD(self.seen) - EPD(UNIT_TABLE)) + def cell(self, epd, table=None): + return epd + (EPD(table or self.seen) - EPD(UNIT_TABLE)) def walk(self, head, step): """`step(ptr, epd, uid, cell, stop)` for each record from the head to the one behind the head at the pass before.""" @@ -1282,9 +1293,40 @@ def walk_all(self, head, step): EUDJump(top) stop << NextTrigger() + def watch(self): + """on.order / on.morphed: every unit on the map, its order and its type against what the pass before found. A unit + not seen before (it comes on in this pass) only has them kept; the units that came on are taken after this. The + type is read as its low byte (there are 228). Whether the unit was seen is asked only when something changed.""" + top, done = Forward(), Forward() + ptr, epd = f_cunitepdread_epd(EPD(VISIBLE_HEAD)) + top << NextTrigger() + EUDJumpIf(ptr == 0, done) + for table, buf, was, n, off, mask in ((self.orders, self.turned, self.turned_was, self.turned_n, OFF_OWNER_ORDER, 0xFF00), + (self.types, self.changed, self.changed_was, self.changed_n, OFF_TYPE, 0xFF)): + if table is None: + continue + now = f_maskread_epd(epd + off, mask) + cell = self.cell(epd, table) + if EUDIfNot()(MemoryEPD(cell, Exactly, now)): + if EUDIf()([MemoryXEPD(epd + OFF_UID, Exactly, f_dwread_epd(self.cell(epd)), 0xFF00), n < EVENT_CAP]): + buf[n] = ptr + old = f_dwread_epd(cell) + was[n] = f_bitrshift(old, 8) if mask == 0xFF00 else old + n += 1 + EUDEndIf() + f_dwwrite_epd(cell, now) + EUDEndIf() + f_cunitepdread_epd(epd + 1, ret=[ptr, epd]) + EUDJump(top) + done << NextTrigger() + def run(self): self.made_n << 0 self.gone_n << 0 + self.turned_n << 0 + self.changed_n << 0 + if self.ordered or self.morphed: + self.watch() def came(ptr, epd, uid, cell, stop): if EUDIfNot()(MemoryEPD(cell, Exactly, uid)): @@ -2025,16 +2067,27 @@ def event_loop(self, source, flt, node, body): it died (its slot is not given to another unit before the next frame's dying pass).""" if EVENTS is None: raise Fail("trigscript: a loop over the events pass's units, and no pass%s" % where(node)) - cells, count = (EVENTS.made, EVENTS.made_n) if source == "created" else (EVENTS.gone, EVENTS.gone_n) + cells, was, count = {"created": (EVENTS.made, None, EVENTS.made_n), "died": (EVENTS.gone, None, EVENTS.gone_n), + "killed": (EVENTS.gone, None, EVENTS.gone_n), "ordered": (EVENTS.turned, EVENTS.turned_was, EVENTS.turned_n), + "morphed": (EVENTS.changed, EVENTS.changed_was, EVENTS.changed_n)}[source] + event = node.get("event") or {} i = fresh(0) head, next_, exit_ = Forward(), Forward(), Forward() head << NextTrigger() EUDJumpIf(i >= count, exit_) ptr, epd = f_cunitepdread_epd(EPD(cells) + i) - if source == "created": + if source in ("created", "ordered", "morphed"): # Gone since the pass: killed or removed by a program before this turn (its slot is still its own). EUDJumpIfNot([MemoryEPD(epd + OFF_SPRITE, AtLeast, 1), MemoryXEPD(epd + OFF_OWNER_ORDER, AtLeast, 0x100, 0xFF00)], next_) match = [] + if source == "killed": + # Died in a fight: no hit points, and one of the eight players the last whose attack hurt it. + match += [MemoryEPD(epd + OFF_HP, Exactly, 0), MemoryXEPD(epd + OFF_LAST_ATTACKER, AtMost, 7, 0xFF)] + by = event.get("by") + if by is not None: + match.append(MemoryXEPD(epd + OFF_LAST_ATTACKER, Exactly, self.current() if by == CURRENT_PLAYER else int(by), 0xFF)) + if event.get("order") is not None: + match.append(MemoryXEPD(epd + OFF_OWNER_ORDER, Exactly, int(event["order"]) << 8, 0xFF00)) owner = flt.get("owner") if owner is not None: who = self.current() if owner == CURRENT_PLAYER else int(owner) @@ -2057,6 +2110,9 @@ def event_loop(self, source, flt, node, body): if kind in GROUP_BITS: group = fresh(f_bread(UNITS_GROUP + f_maskread_epd(epd + OFF_TYPE, 0xFFFF))) EUDJumpIf((group & GROUP_BITS[kind]) == 0, next_) + if node.get("was"): + # What the unit was: the killer, or the order or the type the pass before found. + self.declare(node["was"]).set(f_maskread_epd(epd + OFF_LAST_ATTACKER, 0xFF) if source == "killed" else was[i]) body(UnitRef(ptr, epd, None), next_, exit_) next_ << NextTrigger() i += 1 @@ -3281,7 +3337,7 @@ def heard(self, listen): """Whether a listener has anything to hand on this frame: units the events pass found, or one of its inputs.""" source = listen.get("source") if source is not None: - return (EVENTS.made_n if source == "created" else EVENTS.gone_n) >= 1 + return {"created": EVENTS.made_n, "died": EVENTS.gone_n, "killed": EVENTS.gone_n, "ordered": EVENTS.turned_n, "morphed": EVENTS.changed_n}[source] >= 1 found = fresh(0) for inp in listen.get("inputs", []): if EUDIf()(as_var(INPUT.read(inp, self, listen)) >= 1): @@ -4023,8 +4079,8 @@ def at(o): INPUT = Input(IR.get("input")) -_DIED = uses_source(IR.get("programs", []), "died") -EVENTS = Events(uses_source(IR.get("programs", []), "created"), _DIED) if _DIED or uses_source(IR.get("programs", []), "created") else None +_SOURCES = {s: uses_source(IR.get("programs", []), s) for s in ("created", "died", "killed", "ordered", "morphed")} +EVENTS = Events(_SOURCES["created"], _SOURCES["died"] or _SOURCES["killed"], _SOURCES["ordered"], _SOURCES["morphed"]) if any(_SOURCES.values()) else None PROGRAMS = [Lowering(p) for p in IR.get("programs", [])] diff --git a/tests/eud-build.test.ts b/tests/eud-build.test.ts index e682b58..8a4e53d 100644 --- a/tests/eud-build.test.ts +++ b/tests/eud-build.test.ts @@ -336,6 +336,17 @@ const FIXTURES: Record = { await until(() => made >= 10); deaths.cancel(); });`, + eventsMore: `program(async () => { + let storms = 0, sieged = 0; + on.killed((u, by) => { setResources(by, "add", 25, "ore"); print(\`\${u.type} at \${u.x}\`); }); + on.killed({ owner: P8, by: P1 }, async (u) => { await sleep(frames(4)); createUnit(P8, u.type, 1, locations.Anywhere); }); + on.order(units.ProtossHighTemplar, orders.CastPsionicStorm, (u) => { storms++; }); + on.order({ owner: P1 }, (u, order, was) => { if (was == orders.PlayerGuard && order == orders.Move) u.heal(1); }); + on.morphed(units.TerranSiegeTankSiegeMode, (u, was) => { sieged++; print(\`from \${was}\`); }); + await until(() => storms + sieged >= 10); + }); + on.died(units.TerranMarine, (u) => print(\`marine down at \${u.x}\`)); + on.chat("-hi", (p) => print("hello", { to: p }));`, eventsPerPlayer: `program(async () => { let mine = 0; on.created({ owner: CurrentPlayer }, () => { mine++; }); @@ -367,7 +378,7 @@ function build(name: string, src: string): { out: number; triggers: number } { const r = compileScript(ts, { [name.endsWith("Tsx") ? "main.tsx" : "main.ts"]: src, ...(FIXTURE_FILES[name] ?? {}) }, NAMES, { lib: LIB }); expect(r.diagnostics).toEqual([]); // One program a fixture, but for the numbers probe, which has a second one for every player. - expect(r.ir.length).toBe(name === "numbers" || name === "arraysProbe" || name === "functions" || name === "functionsProbe" || name === "callbacksProbe" || name === "insideProbe" || name === "stringsProbe" || name === "classesProbe" || name === "mapProbe" ? 2 : name === "leftoversProbe" ? 1 : name === "recursionProbe" ? 3 : 1); + expect(r.ir.length).toBe(name === "numbers" || name === "arraysProbe" || name === "functions" || name === "functionsProbe" || name === "callbacksProbe" || name === "insideProbe" || name === "stringsProbe" || name === "classesProbe" || name === "mapProbe" ? 2 : name === "leftoversProbe" ? 1 : name === "recursionProbe" || name === "eventsMore" ? 3 : 1); const ir = serializeIr(r.ir, r.strings, r.input); const dir = mkdtempSync(join(tmpdir(), "trigscript-eud-")); const irPath = join(dir, "trigscript.json"); @@ -599,6 +610,6 @@ describe("the IR as the lowering reads it", () => { const ir = JSON.parse(serializeIr(r.ir, r.strings)); const actions = ir.programs[0].body.filter((s: { kind: string }) => s.kind === "action").map((s: { record: { text: unknown; wav: unknown } }) => [s.record.text, s.record.wav]); expect(actions).toEqual([["hello", 0], [0, "sound\\x.wav"]]); - expect(ir.version).toBe(18); + expect(ir.version).toBe(19); }); }); diff --git a/tests/events.test.ts b/tests/events.test.ts index 4a45a41..254425d 100644 --- a/tests/events.test.ts +++ b/tests/events.test.ts @@ -1,5 +1,5 @@ /** - * TrigScript 4, slice B2: events. `on.created`, `on.died`, `on.chat` and `on.key` are each a task that, from the next + * TrigScript 4, slice B2: events. `on.created`, `on.died`, `on.killed`, `on.order`, `on.morphed`, `on.chat` and `on.key` are each a task that, from the next * frame on, runs its handler for every time the thing happens: the units come from the frame's events pass (a loop with * a `source` in the IR), the lines and keys from what chatted() and keyPressed() read. The interpreter here; * `eud-build.test.ts` builds the same through eudplib, and `probes/game-events.ts` runs it in the game. @@ -38,7 +38,7 @@ describe("events: the front end", () => { it("what is wrong is said", () => { expect(messages(`on.created((u) => {});`)).toEqual([]); - expect(raw(`});\nprogram(() => { on.died((u) => {}); `).diagnostics.map((d) => d.message)).toContain("on.died() listens beside the program as a task: inside program(async () => { … })."); + expect(raw(`});\nprogram(() => { on.died((u) => {}); `).diagnostics.map((d) => d.message)).toContain("on.died() listens beside the program as a task: at the top of a script, or inside program(async () => { … })."); expect(messages(`on.created(units.ZergZergling);`)[0]).toContain("is not assignable"); expect(messages(`on.key("F6", (p) => {});`)[0]).toContain("F6"); expect(messages(`on.created((u) => {}, { max: 4 });`)[0]).toContain("max is for an async handler"); @@ -103,3 +103,63 @@ describe("events: in the simulator", () => { expect(s.events.filter((e) => e.action.type === ActionType.SetResources).map((e) => e.action.player)).toEqual([1]); }); }); + +describe("events, second part: killed, order, morphed, at the top of a script", () => { + it("on.killed hands a death in a fight, with its killer; a trigger's kill and a removal are not handed on", () => { + const r = compile(` + on.killed((u, by) => { print(\`killed \${u.type} by \${by}\`); }); + on.killed({ by: P2 }, (u) => { print(\`P2 got \${u.owner}\`); }); + on.died((u) => { print(\`died \${u.type}\`); }); + await sleep(frames(1)); + for (const u of unitsOf(P1)) u.kill();`); + const s = sim(r).run(1); + // Between frames 0 and 1 the zergling dies in a fight, P2's kill (the test's kill(unit, by)); in frame 1 the program + // kills the marine, which the pass of frame 2 finds. + s.game.gone(s.game.living().find((u) => u.type === ZERGLING)!, true, 1); + s.run(3); + expect(printed(s)).toEqual([`1:killed ${ZERGLING} by 1`, "1:P2 got 7", `1:died ${ZERGLING}`, `2:died ${MARINE}`]); + }); + + it("on.order hands the unit, the order it took up and the one it had; a filter on the order", () => { + const r = compile(` + on.order((u, order, was) => { print(\`\${u.type} \${was} -> \${order}\`); }); + on.order(units.TerranMarine, orders.Patrol, (u) => { print("patrol"); }); + on.order({ order: orders.Move }, (u, order) => { print(\`moving \${order}\`); }); + await sleep(frames(1)); + for (const u of unitsOf(P1)) u.order("move", HOME); + await sleep(frames(2)); + for (const u of unitsOf(P1)) u.order("patrol", HOME);`); + expect(printed(sim(r).run(6))).toEqual([`2:${MARINE} 3 -> 6`, "2:moving 6", `4:${MARINE} 6 -> 152`, "4:patrol"]); + }); + + it("on.morphed hands the unit as it is now and the type it was; the filter is of the new type", () => { + const r = compile(` + on.morphed((u, was) => { print(\`\${was} -> \${u.type}\`); }); + on.morphed(units.TerranSiegeTankSiegeMode, (u) => { print("sieged"); });`); + const s = sim(r).run(2); + s.game.living().find((u) => u.type === MARINE)!.type = 30; + s.run(1); + expect(printed(s)).toEqual([`2:${MARINE} -> 30`, "2:sieged"]); + }); + + it("a listener at the top of a script listens from the start, as a program of its own", () => { + const r = compileScript(ts, { "main.ts": `const HOME = 1 as Location; +on.created(units.ZergZergling, (u) => { print(\`made \${u.owner}\`); }); +on.chat("-hi", (p) => print("hello", { to: p })); +program(async () => { await sleep(frames(2)); createUnit(P8, units.ZergZergling, 1, HOME); });` }, NAMES, { lib: LIB }); + expect(r.diagnostics.map((d) => d.message)).toEqual([]); + expect(r.ir).toHaveLength(3); + const s = sim(r).run(2); + s.type("-hi", 0).run(2); + expect(printed(s)).toEqual(["2:hello", "3:made 7"]); + }); + + it("what is wrong is said", () => { + expect(messages(`on.order(units.TerranMarine, 999, (u) => {});`)[0]).toContain("on.order() takes the order after the filter"); + expect(messages(`on.killed({ by: P9 }, (u) => {});`)[0]).toContain("by is the player who killed the unit"); + // (TypeScript says it first: 'by' does not exist in type 'UnitFilter'.) + expect(messages(`on.died({ by: P2 }, (u) => {});`).join("\n")).toContain("'by' does not exist"); + expect(messages(`on.morphed({ order: orders.Move }, (u) => {});`).join("\n")).toContain("'order' does not exist"); + expect(messages(`on.order({ order: orders.Move }, orders.Patrol, (u) => {});`).join("\n")).toContain("'order' does not exist"); + }); +}); diff --git a/tests/functions.test.ts b/tests/functions.test.ts index 0e2231a..0914d48 100644 --- a/tests/functions.test.ts +++ b/tests/functions.test.ts @@ -175,7 +175,7 @@ describe("functions: arrays, and functions inside functions", () => { it("the functions go to the lowering with the program, their text written out", () => { const r = compile("function say(n: number) { displayText(`n`); setDeaths(P1, units.TerranMarine, 'set', n); } let k = 1; say(k); say(k + 1);"); const ir = JSON.parse(serializeIr(r.ir, r.strings)); - expect(ir.version).toBe(18); + expect(ir.version).toBe(19); expect(ir.programs[0].functions).toHaveLength(1); expect(JSON.stringify(ir.programs[0].functions)).toContain('"text":"n"'); }); diff --git a/tests/script-structured.test.ts b/tests/script-structured.test.ts index 5ed6e72..1ccd3d9 100644 --- a/tests/script-structured.test.ts +++ b/tests/script-structured.test.ts @@ -82,7 +82,7 @@ describe("structured: loops and branches", () => { expect(r.triggers).toEqual([]); expect(r.strings).toEqual([]); expect(r.ir).toHaveLength(1); - expect(r.ir[0]).toMatchObject({ version: 18, owner: 0, owners: [0], perPlayer: false }); + expect(r.ir[0]).toMatchObject({ version: 19, owner: 0, owners: [0], perPlayer: false }); const sim = run(r, 6); expect(sim.events.map((e) => `${e.cycle}:${ActionType.Victory === e.action.type ? "Victory" : e.action.type}`)).toEqual(["2:Victory"]); expect(value(sim, "n")).toBe(6); diff --git a/tests/service.test.ts b/tests/service.test.ts index a202618..867d0f5 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -264,9 +264,9 @@ describe("service: saving applies the script and builds its programs", () => { expect(compiles).toBe(before); expect(Object.keys(input.plugins)).toEqual(["trigscript", "eudTurbo"]); expect(input.plugins.trigscript).toEqual({ ir: "/work/files/trigscript.json" }); - expect(input.sources!.trigscript).toContain("IR_VERSION = 18"); + expect(input.sources!.trigscript).toContain("IR_VERSION = 19"); const ir = JSON.parse(input.files!["trigscript.json"]); - expect(ir.version).toBe(18); + expect(ir.version).toBe(19); expect(ir.programs).toHaveLength(1); expect(JSON.stringify(ir)).toContain('"text":"tick"'); // The library going away takes the contribution with it; coming back, it is made again. From e96c5d2a87509b53eb0457b5edc5860303ad41c3 Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 09:11:00 -0600 Subject: [PATCH 061/238] TrigScript 4, B2: the second events probe's A and B, played in Remastered Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 16290e4..f892ecf 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1012,7 +1012,7 @@ on.died(units.TerranMarine, (u) => print("Marine down")); // at probe; `game-check` of the Remastered probe: 0 accesses that end the game. **Probe for Remastered:** `Maps/TrigScript/trigscript-b2-events-2.scx` with its answer sheet — a fight (killer both ways), a program's kill, Psionic Storm (order and kills), an Archon, a Siege Tank both ways, a Drone into a Hatchery and a Larva into an - egg into a Drone, a move order, 300 Overlords with the walk on. Not played yet. + egg into a Drone, a move order, 300 Overlords with the walk on. **Played in Remastered, 2026-10-17, A and B:** the fight said "killed 37 by P1" for each of the four Zerglings and gave 25 minerals each (75 after three, 100 after four), and for the one Marine that fell "killed 0 by P2" and the top-of-file listener's "top: Marine down"; the program's two Firebat kills said "died 32" twice and no "killed". So Remastered's dying record has hit points 0 and the killer at +0x90 after a fight, as 1.16.1's does. C to G not reported yet. - **Not yet:** a morph in a test in the lifted game (scm-sim's test handle would take a player's command); `on.order` only for units of the filter's types (skip the order read for the rest: cheaper, but a unit that morphs into a watched type would bring a stale order once); whether a program's `u.damage()` to 0 credits an earlier attacker. From 04c1e37e6828e6b52eb922742ac591079460b92a Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 09:13:28 -0600 Subject: [PATCH 062/238] TrigScript 4, B2: the second events probe's C and D, played in Remastered Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index f892ecf..4fbe8c9 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1012,7 +1012,7 @@ on.died(units.TerranMarine, (u) => print("Marine down")); // at probe; `game-check` of the Remastered probe: 0 accesses that end the game. **Probe for Remastered:** `Maps/TrigScript/trigscript-b2-events-2.scx` with its answer sheet — a fight (killer both ways), a program's kill, Psionic Storm (order and kills), an Archon, a Siege Tank both ways, a Drone into a Hatchery and a Larva into an - egg into a Drone, a move order, 300 Overlords with the walk on. **Played in Remastered, 2026-10-17, A and B:** the fight said "killed 37 by P1" for each of the four Zerglings and gave 25 minerals each (75 after three, 100 after four), and for the one Marine that fell "killed 0 by P2" and the top-of-file listener's "top: Marine down"; the program's two Firebat kills said "died 32" twice and no "killed". So Remastered's dying record has hit points 0 and the killer at +0x90 after a fight, as 1.16.1's does. C to G not reported yet. + egg into a Drone, a move order, 300 Overlords with the walk on. **Played in Remastered, 2026-10-17, A and B:** the fight said "killed 37 by P1" for each of the four Zerglings and gave 25 minerals each (75 after three, 100 after four), and for the one Marine that fell "killed 0 by P2" and the top-of-file listener's "top: Marine down"; the program's two Firebat kills said "died 32" twice and no "killed". So Remastered's dying record has hit points 0 and the killer at +0x90 after a fight, as 1.16.1's does. C and D (same day): "Storm!" when a Templar began casting (on.order, CastPsionicStorm); merging the Archon said "died 67" and "morphed 67 -> 68" while it was still summoning — the type changes when the merge starts; the tank said "morphed 5 -> 30" sieging and "morphed 30 -> 5" unsieging. Kills by the storm not seen yet (no Zerglings were under it). E to G not reported yet. - **Not yet:** a morph in a test in the lifted game (scm-sim's test handle would take a player's command); `on.order` only for units of the filter's types (skip the order read for the rest: cheaper, but a unit that morphs into a watched type would bring a stale order once); whether a program's `u.damage()` to 0 credits an earlier attacker. From 0c04c196b072b03382799a4091a761c3501bbb57 Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 09:16:43 -0600 Subject: [PATCH 063/238] TrigScript 4, B2: the events probe's Zerglings to storm stand where the player sees them -storm made the computer's Zerglings at Away, which is in the dark beside the base: the player never saw them. Now the neutral player's, at Home, and every typed command says it was heard. Co-Authored-By: Claude Opus 5.5 --- probes/events-2.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/probes/events-2.ts b/probes/events-2.ts index cfae8c6..c3dd597 100644 --- a/probes/events-2.ts +++ b/probes/events-2.ts @@ -11,7 +11,7 @@ // "top: Marine down" (that one is a listener at the top of the script). // B 12 two Firebats killed by the program: "died 32" twice, and no "killed". // C 15 your turn, at your own pace: two High Templar with Psionic Storm and full energy, and a Siege Tank with Siege -// Mode, beside your base. Type -storm: eight Zerglings of the computer's to the right of your base. Storm them. +// Mode, beside your base. Type -storm: eight Zerglings of the neutral player's beside your base, standing still. Storm them. // "Storm!" when a Templar starts casting; "killed 37 by P1" for each Zergling the storm kills. // Then merge the two Templar into an Archon: "morphed 67 -> 68", and "died 67" for the other one. // D siege the tank: "morphed 5 -> 30"; unsiege it: "morphed 30 -> 5". @@ -24,9 +24,9 @@ // Host it as a multiplayer game, alone: a typed line (-storm, -zerg, -many) reaches the map only there. // npx tsx scripts/build-fixture.mts probes/events-2.ts fixtures/eud/events-2.scx // npx tsx scripts/test-game.mts probes/events-2.ts (A and B in the lifted game) -import { test, expect, type Sim, program, print, createUnit, setResources, sleep, seconds, unitsOf, on, units, orders, techs, stats, P1, P2, type Location } from "trigscript"; +import { test, expect, type Sim, program, print, createUnit, setResources, sleep, seconds, unitsOf, on, units, orders, techs, stats, P1, P2, P12, type Location } from "trigscript"; -const HOME = 1 as Location, AWAY = 2 as Location; +const HOME = 1 as Location; // At the top of the script: a program of its own, listening from the start of the game. on.died(units.TerranMarine, () => print("top: Marine down")); @@ -41,10 +41,11 @@ program(async () => { on.order(units.ProtossHighTemplar, orders.CastPsionicStorm, () => print("Storm!")); on.order({ owner: P1, type: units.TerranMarine, order: orders.Move }, (u, order, was) => print(`move from ${was}`)); on.morphed({ owner: P1 }, (u, was) => print(`morphed ${was} -> ${u.type}`)); - on.chat("-storm", () => createUnit(P2, units.ZergZergling, 8, AWAY)); - on.chat("-zerg", () => { setResources(P1, "add", 1000, "ore"); createUnit(P1, units.ZergDrone, 2, HOME); }); - on.chat("-many", () => { for (let i = 0; i < 12; i++) createUnit(P1, units.ZergOverlord, 25, HOME); }); - on.chat("-clear", () => { for (const u of unitsOf(P1, { type: units.ZergOverlord })) u.remove(); }); + // (The neutral player's Zerglings stand still where you can see them; the computer's at Away were made in the dark.) + on.chat("-storm", () => { print("-storm: eight Zerglings"); createUnit(P12, units.ZergZergling, 8, HOME); }); + on.chat("-zerg", () => { print("-zerg: two Drones"); setResources(P1, "add", 1000, "ore"); createUnit(P1, units.ZergDrone, 2, HOME); }); + on.chat("-many", () => { print("-many: 300 Overlords"); for (let i = 0; i < 12; i++) createUnit(P1, units.ZergOverlord, 25, HOME); }); + on.chat("-clear", () => { print("-clear"); for (const u of unitsOf(P1, { type: units.ZergOverlord })) u.remove(); }); await sleep(seconds(3)); print("A: a fight. Expect \"killed 37 by P1\" for each Zergling (25 minerals each), \"killed 0 by P2\" and \"top: Marine down\" for each Marine.", { position: "center" }); From 68f642a7cfbc6d41065627a541c1d4515588dc78 Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 09:19:09 -0600 Subject: [PATCH 064/238] TrigScript 4, B2: the Zerglings to storm are the player's own Co-Authored-By: Claude Opus 5.5 --- probes/events-2.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/probes/events-2.ts b/probes/events-2.ts index c3dd597..1109b0e 100644 --- a/probes/events-2.ts +++ b/probes/events-2.ts @@ -11,7 +11,7 @@ // "top: Marine down" (that one is a listener at the top of the script). // B 12 two Firebats killed by the program: "died 32" twice, and no "killed". // C 15 your turn, at your own pace: two High Templar with Psionic Storm and full energy, and a Siege Tank with Siege -// Mode, beside your base. Type -storm: eight Zerglings of the neutral player's beside your base, standing still. Storm them. +// Mode, beside your base. Type -storm: eight Zerglings of yours beside your base, standing still. Storm them. // "Storm!" when a Templar starts casting; "killed 37 by P1" for each Zergling the storm kills. // Then merge the two Templar into an Archon: "morphed 67 -> 68", and "died 67" for the other one. // D siege the tank: "morphed 5 -> 30"; unsiege it: "morphed 30 -> 5". @@ -24,7 +24,7 @@ // Host it as a multiplayer game, alone: a typed line (-storm, -zerg, -many) reaches the map only there. // npx tsx scripts/build-fixture.mts probes/events-2.ts fixtures/eud/events-2.scx // npx tsx scripts/test-game.mts probes/events-2.ts (A and B in the lifted game) -import { test, expect, type Sim, program, print, createUnit, setResources, sleep, seconds, unitsOf, on, units, orders, techs, stats, P1, P2, P12, type Location } from "trigscript"; +import { test, expect, type Sim, program, print, createUnit, setResources, sleep, seconds, unitsOf, on, units, orders, techs, stats, P1, P2, type Location } from "trigscript"; const HOME = 1 as Location; @@ -41,8 +41,9 @@ program(async () => { on.order(units.ProtossHighTemplar, orders.CastPsionicStorm, () => print("Storm!")); on.order({ owner: P1, type: units.TerranMarine, order: orders.Move }, (u, order, was) => print(`move from ${was}`)); on.morphed({ owner: P1 }, (u, was) => print(`morphed ${was} -> ${u.type}`)); - // (The neutral player's Zerglings stand still where you can see them; the computer's at Away were made in the dark.) - on.chat("-storm", () => { print("-storm: eight Zerglings"); createUnit(P12, units.ZergZergling, 8, HOME); }); + // (Your own Zerglings stand still where you can see them: the computer's at Away were made in the dark, and the + // neutral player's were not made in Remastered.) + on.chat("-storm", () => { print("-storm: eight Zerglings"); createUnit(P1, units.ZergZergling, 8, HOME); }); on.chat("-zerg", () => { print("-zerg: two Drones"); setResources(P1, "add", 1000, "ore"); createUnit(P1, units.ZergDrone, 2, HOME); }); on.chat("-many", () => { print("-many: 300 Overlords"); for (let i = 0; i < 12; i++) createUnit(P1, units.ZergOverlord, 25, HOME); }); on.chat("-clear", () => { print("-clear"); for (const u of unitsOf(P1, { type: units.ZergOverlord })) u.remove(); }); From 6c4066c9ee8e166db4d65d56fa279c99ae02a322 Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 09:22:22 -0600 Subject: [PATCH 065/238] TrigScript 4, B2: a unit killed by its own player has no killer Played in Remastered: a storm on the player's own units was heard by on.died and not on.killed. The lifted game shows why: the game keeps no attacker for friendly fire (+0x90 stays 8), as it counts no kill. The simulator and GameSim.kill now leave no killer when the killer is the owner. Co-Authored-By: Claude Opus 5.5 --- compiler/declarations.ts | 3 ++- compiler/gameSim.ts | 3 ++- compiler/world.ts | 3 ++- docs/v4-plan.md | 2 +- tests/events.test.ts | 8 ++++++++ 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/compiler/declarations.ts b/compiler/declarations.ts index 0e35795..152e72d 100644 --- a/compiler/declarations.ts +++ b/compiler/declarations.ts @@ -470,7 +470,8 @@ ${kw}const on: { /** * A unit is killed in a fight — shot, burned, caught in a storm — and the function is handed what it was, where, and * the player whose attack killed it. A unit a trigger or a program kills or removes, or one that goes down with its - * transport, has no killer and is not handed on (on.died() hears every one). + * transport, has no killer and is not handed on (on.died() hears every one); nor does one its own player killed (a + * storm on your own units), for which the game keeps no attacker, as it counts no kill. * \`on.killed({ owner: P8, by: P1 }, (u, by) => setResources(by, "add", 25, "ore"))\` */ killed(handler: (unit: DeadUnit, by: Player) => void, options?: EventOptions): Task; diff --git a/compiler/gameSim.ts b/compiler/gameSim.ts index e909576..23ad51d 100644 --- a/compiler/gameSim.ts +++ b/compiler/gameSim.ts @@ -338,7 +338,8 @@ export class GameSim implements TestWorldLike { // whose attack hurt the unit (+0x90), which is what on.killed() reads. if (by !== undefined) { const at = UNITS + UNIT_BYTES * unit.slot; - this.game.m.setInt32(at + 0x08, 0, true); this.game.u8[at + 0x90] = by; + // (A unit killed by its own player keeps no attacker: the game records none for friendly fire.) + this.game.m.setInt32(at + 0x08, 0, true); if (by !== unit.owner) this.game.u8[at + 0x90] = by; this.game.wrote?.(at + 0x08, 4); this.game.wrote?.(at + 0x90, 1); } return this.onOne(unit, (loc) => this.act(this.record(ActionType.KillUnitAt, { location: loc, player: unit.owner, unit: unit.type, count: 1 }), unit.owner)); diff --git a/compiler/world.ts b/compiler/world.ts index 65f0e31..70057bd 100644 --- a/compiler/world.ts +++ b/compiler/world.ts @@ -194,7 +194,8 @@ export class World { unit.alive = false; if (this.table[unit.slot] === unit) this.table[unit.slot] = null; if (!killed || !fought) return; - if (by !== undefined) unit.killedBy = by; + // (The game keeps no attacker for a unit its own player killed — a storm on your own units — so there is no killer then.) + if (by !== undefined && by !== unit.owner) unit.killedBy = by; this.onDeath(unit); if (by !== undefined) this.killed.set(unit.type * 4096 + by, (this.killed.get(unit.type * 4096 + by) ?? 0) + 1); } diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 4fbe8c9..44eb33c 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1012,7 +1012,7 @@ on.died(units.TerranMarine, (u) => print("Marine down")); // at probe; `game-check` of the Remastered probe: 0 accesses that end the game. **Probe for Remastered:** `Maps/TrigScript/trigscript-b2-events-2.scx` with its answer sheet — a fight (killer both ways), a program's kill, Psionic Storm (order and kills), an Archon, a Siege Tank both ways, a Drone into a Hatchery and a Larva into an - egg into a Drone, a move order, 300 Overlords with the walk on. **Played in Remastered, 2026-10-17, A and B:** the fight said "killed 37 by P1" for each of the four Zerglings and gave 25 minerals each (75 after three, 100 after four), and for the one Marine that fell "killed 0 by P2" and the top-of-file listener's "top: Marine down"; the program's two Firebat kills said "died 32" twice and no "killed". So Remastered's dying record has hit points 0 and the killer at +0x90 after a fight, as 1.16.1's does. C and D (same day): "Storm!" when a Templar began casting (on.order, CastPsionicStorm); merging the Archon said "died 67" and "morphed 67 -> 68" while it was still summoning — the type changes when the merge starts; the tank said "morphed 5 -> 30" sieging and "morphed 30 -> 5" unsieging. Kills by the storm not seen yet (no Zerglings were under it). E to G not reported yet. + egg into a Drone, a move order, 300 Overlords with the walk on. **Played in Remastered, 2026-10-17, A and B:** the fight said "killed 37 by P1" for each of the four Zerglings and gave 25 minerals each (75 after three, 100 after four), and for the one Marine that fell "killed 0 by P2" and the top-of-file listener's "top: Marine down"; the program's two Firebat kills said "died 32" twice and no "killed". So Remastered's dying record has hit points 0 and the killer at +0x90 after a fight, as 1.16.1's does. C and D (same day): "Storm!" when a Templar began casting (on.order, CastPsionicStorm); merging the Archon said "died 67" and "morphed 67 -> 68" while it was still summoning — the type changes when the merge starts; the tank said "morphed 5 -> 30" sieging and "morphed 30 -> 5" unsieging. The storm, played again on Zerglings of the player's own (the computer's had been made out of sight): every unit it killed — eight Zerglings, two Marines, both Templar — was handed to `on.died` and none to `on.killed`, minerals unchanged. **Friendly fire leaves no killer:** the lifted game with a storm cast through player commands gives the computer's Zerglings hp 0 and +0x90 = P1, the player's own hp 0 and +0x90 = 8 (one bitten first by a computer Zergling: 1). The game keeps no attacker for a unit its own player hurts, as it counts no kill for it (scm-sim's T-30). So `on.killed` hears a unit killed by another player; the simulator's `kill(u, owner)` and `GameSim.kill(u, owner)` now leave no killer either, and the declarations say so. E to G not reported yet. - **Not yet:** a morph in a test in the lifted game (scm-sim's test handle would take a player's command); `on.order` only for units of the filter's types (skip the order read for the rest: cheaper, but a unit that morphs into a watched type would bring a stale order once); whether a program's `u.damage()` to 0 credits an earlier attacker. diff --git a/tests/events.test.ts b/tests/events.test.ts index 254425d..9165757 100644 --- a/tests/events.test.ts +++ b/tests/events.test.ts @@ -120,6 +120,14 @@ describe("events, second part: killed, order, morphed, at the top of a script", expect(printed(s)).toEqual([`1:killed ${ZERGLING} by 1`, "1:P2 got 7", `1:died ${ZERGLING}`, `2:died ${MARINE}`]); }); + it("a unit killed by its own player has no killer, as the game keeps none", () => { + const r = compile(`on.killed((u, by) => print(\`killed by \${by}\`)); on.died(() => print("died"));`); + const s = sim(r).run(1); + s.game.gone(s.game.living().find((u) => u.type === ZERGLING)!, true, 7); + s.run(2); + expect(printed(s)).toEqual(["1:died"]); + }); + it("on.order hands the unit, the order it took up and the one it had; a filter on the order", () => { const r = compile(` on.order((u, order, was) => { print(\`\${u.type} \${was} -> \${order}\`); }); From 11284cfe9fcc64093988512c847b88383125be5e Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 09:23:29 -0600 Subject: [PATCH 066/238] TrigScript 4, B2: a Drone into a Hatchery, played in Remastered Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 44eb33c..fe8a923 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1012,7 +1012,7 @@ on.died(units.TerranMarine, (u) => print("Marine down")); // at probe; `game-check` of the Remastered probe: 0 accesses that end the game. **Probe for Remastered:** `Maps/TrigScript/trigscript-b2-events-2.scx` with its answer sheet — a fight (killer both ways), a program's kill, Psionic Storm (order and kills), an Archon, a Siege Tank both ways, a Drone into a Hatchery and a Larva into an - egg into a Drone, a move order, 300 Overlords with the walk on. **Played in Remastered, 2026-10-17, A and B:** the fight said "killed 37 by P1" for each of the four Zerglings and gave 25 minerals each (75 after three, 100 after four), and for the one Marine that fell "killed 0 by P2" and the top-of-file listener's "top: Marine down"; the program's two Firebat kills said "died 32" twice and no "killed". So Remastered's dying record has hit points 0 and the killer at +0x90 after a fight, as 1.16.1's does. C and D (same day): "Storm!" when a Templar began casting (on.order, CastPsionicStorm); merging the Archon said "died 67" and "morphed 67 -> 68" while it was still summoning — the type changes when the merge starts; the tank said "morphed 5 -> 30" sieging and "morphed 30 -> 5" unsieging. The storm, played again on Zerglings of the player's own (the computer's had been made out of sight): every unit it killed — eight Zerglings, two Marines, both Templar — was handed to `on.died` and none to `on.killed`, minerals unchanged. **Friendly fire leaves no killer:** the lifted game with a storm cast through player commands gives the computer's Zerglings hp 0 and +0x90 = P1, the player's own hp 0 and +0x90 = 8 (one bitten first by a computer Zergling: 1). The game keeps no attacker for a unit its own player hurts, as it counts no kill for it (scm-sim's T-30). So `on.killed` hears a unit killed by another player; the simulator's `kill(u, owner)` and `GameSim.kill(u, owner)` now leave no killer either, and the declarations say so. E to G not reported yet. + egg into a Drone, a move order, 300 Overlords with the walk on. **Played in Remastered, 2026-10-17, A and B:** the fight said "killed 37 by P1" for each of the four Zerglings and gave 25 minerals each (75 after three, 100 after four), and for the one Marine that fell "killed 0 by P2" and the top-of-file listener's "top: Marine down"; the program's two Firebat kills said "died 32" twice and no "killed". So Remastered's dying record has hit points 0 and the killer at +0x90 after a fight, as 1.16.1's does. C and D (same day): "Storm!" when a Templar began casting (on.order, CastPsionicStorm); merging the Archon said "died 67" and "morphed 67 -> 68" while it was still summoning — the type changes when the merge starts; the tank said "morphed 5 -> 30" sieging and "morphed 30 -> 5" unsieging. The storm, played again on Zerglings of the player's own (the computer's had been made out of sight): every unit it killed — eight Zerglings, two Marines, both Templar — was handed to `on.died` and none to `on.killed`, minerals unchanged. **Friendly fire leaves no killer:** the lifted game with a storm cast through player commands gives the computer's Zerglings hp 0 and +0x90 = P1, the player's own hp 0 and +0x90 = 8 (one bitten first by a computer Zergling: 1). The game keeps no attacker for a unit its own player hurts, as it counts no kill for it (scm-sim's T-30). So `on.killed` hears a unit killed by another player; the simulator's `kill(u, owner)` and `GameSim.kill(u, owner)` now leave no killer either, and the declarations say so. E, first half (first build): `-zerg` made the two Drones, and a Drone told to mutate into a Hatchery said "morphed 41 -> 131" as it began. On the first build `-storm`'s computer Zerglings went to Away, past the edge of what the player could see (they are made there in the lifted game; not looked for in Remastered); the second build's Player 12 Zerglings at Home were not seen, whether made or not unknown. Rest of E, F, G not reported yet. - **Not yet:** a morph in a test in the lifted game (scm-sim's test handle would take a player's command); `on.order` only for units of the filter's types (skip the order read for the rest: cheaper, but a unit that morphs into a watched type would bring a stale order once); whether a program's `u.damage()` to 0 credits an earlier attacker. From fd33cddace1a8069000fe540cdd88745b032ddf0 Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 09:30:35 -0600 Subject: [PATCH 067/238] TrigScript 4, B2: the second events probe played through in Remastered Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/v4-plan.md b/docs/v4-plan.md index fe8a923..ad15e37 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -1012,7 +1012,7 @@ on.died(units.TerranMarine, (u) => print("Marine down")); // at probe; `game-check` of the Remastered probe: 0 accesses that end the game. **Probe for Remastered:** `Maps/TrigScript/trigscript-b2-events-2.scx` with its answer sheet — a fight (killer both ways), a program's kill, Psionic Storm (order and kills), an Archon, a Siege Tank both ways, a Drone into a Hatchery and a Larva into an - egg into a Drone, a move order, 300 Overlords with the walk on. **Played in Remastered, 2026-10-17, A and B:** the fight said "killed 37 by P1" for each of the four Zerglings and gave 25 minerals each (75 after three, 100 after four), and for the one Marine that fell "killed 0 by P2" and the top-of-file listener's "top: Marine down"; the program's two Firebat kills said "died 32" twice and no "killed". So Remastered's dying record has hit points 0 and the killer at +0x90 after a fight, as 1.16.1's does. C and D (same day): "Storm!" when a Templar began casting (on.order, CastPsionicStorm); merging the Archon said "died 67" and "morphed 67 -> 68" while it was still summoning — the type changes when the merge starts; the tank said "morphed 5 -> 30" sieging and "morphed 30 -> 5" unsieging. The storm, played again on Zerglings of the player's own (the computer's had been made out of sight): every unit it killed — eight Zerglings, two Marines, both Templar — was handed to `on.died` and none to `on.killed`, minerals unchanged. **Friendly fire leaves no killer:** the lifted game with a storm cast through player commands gives the computer's Zerglings hp 0 and +0x90 = P1, the player's own hp 0 and +0x90 = 8 (one bitten first by a computer Zergling: 1). The game keeps no attacker for a unit its own player hurts, as it counts no kill for it (scm-sim's T-30). So `on.killed` hears a unit killed by another player; the simulator's `kill(u, owner)` and `GameSim.kill(u, owner)` now leave no killer either, and the declarations say so. E, first half (first build): `-zerg` made the two Drones, and a Drone told to mutate into a Hatchery said "morphed 41 -> 131" as it began. On the first build `-storm`'s computer Zerglings went to Away, past the edge of what the player could see (they are made there in the lifted game; not looked for in Remastered); the second build's Player 12 Zerglings at Home were not seen, whether made or not unknown. Rest of E, F, G not reported yet. + egg into a Drone, a move order, 300 Overlords with the walk on. **Played in Remastered, 2026-10-17, A and B:** the fight said "killed 37 by P1" for each of the four Zerglings and gave 25 minerals each (75 after three, 100 after four), and for the one Marine that fell "killed 0 by P2" and the top-of-file listener's "top: Marine down"; the program's two Firebat kills said "died 32" twice and no "killed". So Remastered's dying record has hit points 0 and the killer at +0x90 after a fight, as 1.16.1's does. C and D (same day): "Storm!" when a Templar began casting (on.order, CastPsionicStorm); merging the Archon said "died 67" and "morphed 67 -> 68" while it was still summoning — the type changes when the merge starts; the tank said "morphed 5 -> 30" sieging and "morphed 30 -> 5" unsieging. The storm, played again on Zerglings of the player's own (the computer's had been made out of sight): every unit it killed — eight Zerglings, two Marines, both Templar — was handed to `on.died` and none to `on.killed`, minerals unchanged. **Friendly fire leaves no killer:** the lifted game with a storm cast through player commands gives the computer's Zerglings hp 0 and +0x90 = P1, the player's own hp 0 and +0x90 = 8 (one bitten first by a computer Zergling: 1). The game keeps no attacker for a unit its own player hurts, as it counts no kill for it (scm-sim's T-30). So `on.killed` hears a unit killed by another player; the simulator's `kill(u, owner)` and `GameSim.kill(u, owner)` now leave no killer either, and the declarations say so. E, first half (first build): `-zerg` made the two Drones, and a Drone told to mutate into a Hatchery said "morphed 41 -> 131" as it began. On the first build `-storm`'s computer Zerglings went to Away, past the edge of what the player could see (they are made there in the lifted game; not looked for in Remastered); the second build's Player 12 Zerglings at Home were not seen, whether made or not unknown. Rest of E, F, G (same day): a Larva into a Drone said "morphed 35 -> 36" when it became an egg and "morphed 36 -> 41" when it hatched (an Overlord egg "36 -> 42"); a Marine told to move said "move from 3". G: no lag, but `-many` made only about 23 of its 300 Overlords — Remastered said "WARNING: Unit unplaceable! (Zerg Overlord)" for the rest, as the lifted game does not (it put 168 in a 64-pixel box): Remastered keeps a created air unit inside the location, and Home was full. So the walk with ~60 units on the map; 300 not played. **Every event of the second part works in Remastered** but the storm on an enemy, which the lifted game covers. - **Not yet:** a morph in a test in the lifted game (scm-sim's test handle would take a player's command); `on.order` only for units of the filter's types (skip the order read for the rest: cheaper, but a unit that morphs into a watched type would bring a stale order once); whether a program's `u.damage()` to 0 credits an earlier attacker. From 22b54305697f14b4eef4978dd379370ee863368c Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 09:49:32 -0600 Subject: [PATCH 068/238] TrigScript 4: Find an EUD Error, from Remastered's code back to the line The code is the address the trigger named, bits inverted (R0). The map is played in the lifted game with nobody at it, then the tests, every EUD access written down under the line that made it (EudTracer over the page's EUD and cost taps, no scm-sim change); the lines that touched the address, or the same field of another unit, are marked, and what lies there is said with the allowlist's verdict. test-game.mts --error=CODE runs the same path. Co-Authored-By: Claude Opus 5.5 --- compile.ts | 4 + compiler/eudError.ts | 233 ++++++++++++++++++++++++++++++++++++++++ compiler/gameTests.ts | 43 ++++++-- docs/v4-plan.md | 48 ++++++++- editor.ts | 124 +++++++++++++++++++-- ko.ts | 26 +++++ package.json | 2 +- scripts/test-game.mts | 38 +++++-- tests/eud-error.test.ts | 177 ++++++++++++++++++++++++++++++ 9 files changed, 670 insertions(+), 25 deletions(-) create mode 100644 compiler/eudError.ts create mode 100644 tests/eud-error.test.ts diff --git a/compile.ts b/compile.ts index 7aff1c5..e9f650f 100644 --- a/compile.ts +++ b/compile.ts @@ -48,6 +48,10 @@ export const compilerUrl = (dist: string = DEFAULT_DIST) => export const gametestUrl = (dist: string = DEFAULT_DIST) => dist === DEFAULT_DIST ? `https://cdn.jsdelivr.net/gh/scm-js/plugin-trigscript@v${VERSION}/dist/gametest.js` : `${dist.replace(/\/+$/, "")}/gametest.js`; +/** What Remastered is known to allow (`data/allowlist.json`, copied into `dist/` by the build), for Find EUD Error: fetched only then. */ +export const allowlistUrl = (dist: string = DEFAULT_DIST) => + dist === DEFAULT_DIST ? `https://cdn.jsdelivr.net/gh/scm-js/plugin-trigscript@v${VERSION}/dist/allowlist.json` : `${dist.replace(/\/+$/, "")}/allowlist.json`; + /** * What the worker is told to import. The editor's loader turns a fetched plugin into * `blob:` modules, and the compiler's own blob URL is the compiler the main thread would diff --git a/compiler/eudError.ts b/compiler/eudError.ts new file mode 100644 index 0000000..d9404ac --- /dev/null +++ b/compiler/eudError.ts @@ -0,0 +1,233 @@ +/** + * Remastered's "EUD ERROR: 0x…", back to the line of the script (TrigScript 4, from R0 and A1). + * + * When Remastered's emulation of 1.16.1's memory refuses a read or a write, it ends the game and shows a code. R0 + * found what the code is: the address the trigger named, its bits inverted (probe D of the dump's probe ended on + * "EUD ERROR: 0xFFA632E3", a read of 0x59CD1C, the first unit's +0x74). So the code says exactly which address, and + * not which trigger. Which line it was comes from a run in the lifted game with every EUD access written down under + * the line that made it (`EudTracer`): the lines that touched that address are the answer. + * + * The run is not the person's game. A unit's place in the unit table differs from game to game, so a unit field is + * matched by the field in any unit as well as by the exact address; a line the run never reached is not found. + */ +import { verdictOf, type Allowlist, type Verdict } from "./allowlist"; +import type { CostLines } from "./gameCost"; +import { TABLE_FIELDS, type TableKind } from "./tables"; + +/** The address an error code names, or null when the text holds no code. Takes "EUD ERROR: 0xFFA632E3", "0xFFA632E3" or "FFA632E3". */ +export function addressOfEudError(text: string): number | null { + const m = /EUD\s*ERROR\s*:?\s*(?:0x)?([0-9a-f]{1,8})\b/i.exec(text) ?? /^\s*(?:0x)?([0-9a-f]{1,8})\s*$/i.exec(text); + return m ? ~parseInt(m[1], 16) >>> 0 : null; +} + +/** The code Remastered shows for an address (the other way round, for a probe's answer sheet and the tests). */ +export function eudErrorOf(address: number): string { + return `0x${(~address >>> 0).toString(16).toUpperCase().padStart(8, "0")}`; +} + +const hex = (n: number) => `0x${n.toString(16)}`; + +/** 1.16.1's unit table: 1,700 units of 336 bytes. */ +export const UNIT_TABLE = { base: 0x59cca8, stride: 336, count: 1700 }; + +/** The fields of a unit (CUnit), by offset and width, as 1.16.1 lays them out. */ +const UNIT_FIELDS: [number, number, string][] = [ + [0x00, 4, "previous unit"], [0x04, 4, "next unit"], [0x08, 4, "hit points"], [0x0c, 4, "sprite"], + [0x10, 4, "move target position"], [0x14, 4, "move target unit"], [0x18, 4, "next waypoint"], [0x1c, 4, "next target waypoint"], + [0x20, 1, "movement flags"], [0x21, 1, "facing direction"], [0x22, 1, "turn radius"], [0x23, 1, "velocity direction"], + [0x24, 2, "flingy"], [0x26, 1, "unknown 0x26"], [0x27, 1, "movement type"], [0x28, 2, "x"], [0x2a, 2, "y"], + [0x2c, 4, "halt x"], [0x30, 4, "halt y"], [0x34, 4, "top speed"], [0x38, 4, "speed 1"], [0x3c, 4, "speed 2"], + [0x40, 4, "current speed"], [0x44, 2, "acceleration"], [0x46, 1, "direction 2"], [0x47, 1, "velocity direction 2"], + [0x48, 4, "unknown 0x48"], [0x4c, 1, "owner"], [0x4d, 1, "order"], [0x4e, 1, "order state"], [0x4f, 1, "order signal"], + [0x50, 2, "order unit type"], [0x52, 2, "unknown 0x52"], [0x54, 1, "order timer"], [0x55, 1, "ground weapon cooldown"], + [0x56, 1, "air weapon cooldown"], [0x57, 1, "spell cooldown"], [0x58, 4, "order target position"], [0x5c, 4, "order target unit"], + [0x60, 4, "shields"], [0x64, 2, "unit type"], [0x66, 2, "unknown 0x66"], [0x68, 4, "previous unit of its player"], + [0x6c, 4, "next unit of its player"], [0x70, 4, "subunit"], [0x74, 4, "order queue head"], [0x78, 4, "order queue tail"], + [0x7c, 4, "auto target unit"], [0x80, 4, "connected unit"], [0x84, 1, "order queue count"], [0x85, 1, "order queue timer"], + [0x86, 1, "unknown 0x86"], [0x87, 1, "attack notify timer"], [0x88, 2, "previous unit type"], [0x8a, 1, "last event timer"], + [0x8b, 1, "last event colour"], [0x8c, 2, "unused 0x8c"], [0x8e, 1, "rank increase"], [0x8f, 1, "kills"], + [0x90, 1, "last attacking player"], [0x91, 1, "secondary order timer"], [0x92, 1, "AI action flag"], [0x93, 1, "user action flags"], + [0x94, 2, "button set"], [0x96, 1, "cloaked"], [0x97, 1, "movement state"], [0x98, 10, "build queue"], [0xa2, 2, "energy"], + [0xa4, 1, "build queue slot"], [0xa5, 1, "unique id"], [0xa6, 1, "secondary order"], [0xa7, 1, "building overlay state"], + [0xa8, 2, "hit point gain"], [0xaa, 2, "shield gain"], [0xac, 2, "remaining build time"], [0xae, 2, "previous hit points"], + [0xb0, 16, "loaded units"], [0xc0, 16, "what its type keeps (mines, interceptors, nuke…)"], [0xd0, 12, "what a building keeps (resources, …)"], + [0xdc, 4, "status flags"], [0xe0, 1, "resource type"], [0xe1, 1, "wireframe randomiser"], [0xe2, 1, "secondary order state"], + [0xe3, 1, "recent order timer"], [0xe4, 4, "visibility"], [0xe8, 4, "secondary order position"], [0xec, 4, "unit being built"], + [0xf0, 4, "previous burrowed unit"], [0xf4, 4, "next burrowed unit"], [0xf8, 8, "rally point"], [0x100, 4, "path"], + [0x104, 1, "pathing collision interval"], [0x105, 1, "pathing flags"], [0x106, 1, "unused 0x106"], [0x107, 1, "being healed"], + [0x108, 8, "contour bounds"], [0x110, 2, "remove timer"], [0x112, 2, "defensive matrix hit points"], [0x114, 1, "defensive matrix timer"], + [0x115, 1, "stim timer"], [0x116, 1, "ensnare timer"], [0x117, 1, "lockdown timer"], [0x118, 1, "irradiate timer"], + [0x119, 1, "stasis timer"], [0x11a, 1, "plague timer"], [0x11b, 1, "storm timer"], [0x11c, 4, "irradiated by"], + [0x120, 1, "irradiating player"], [0x121, 1, "parasites"], [0x122, 1, "cycle counter"], [0x123, 1, "blind"], + [0x124, 1, "maelstrom timer"], [0x125, 1, "unused 0x125"], [0x126, 1, "acid spore count"], [0x127, 9, "acid spore timers"], + [0x130, 2, "3×3 attack sequence"], [0x132, 2, "unused 0x132"], [0x134, 4, "computer player's data"], [0x138, 2, "air strength"], + [0x13a, 2, "ground strength"], [0x13c, 16, "unit finder bounds"], [0x14c, 4, "repulse"], +]; + +/** A unit field's name, by the byte. */ +export function unitFieldAt(offset: number): string { + const f = UNIT_FIELDS.find(([at, n]) => offset >= at && offset < at + n); + return f ? (offset === f[0] ? f[2] : `${f[2]} (+${offset - f[0]})`) : `+${hex(offset)}`; +} + +/** Other tables of 1.16.1's a script reaches, by name; a row of `stride` bytes, `count` of them. */ +const TABLES: { name: string; base: number; stride: number; count: number; row?: (i: number) => string }[] = [ + { name: "deaths", base: 0x58a364, stride: 4, count: 12 * 228, row: (i) => `of unit type ${Math.floor(i / 12)} for P${(i % 12) + 1}` }, + { name: "kills", base: 0x5878a4, stride: 4, count: 12 * 228, row: (i) => `of unit type ${Math.floor(i / 12)} for P${(i % 12) + 1}` }, + { name: "minerals", base: 0x57f0f0, stride: 4, count: 12, row: (i) => `of P${i + 1}` }, + { name: "gas", base: 0x57f120, stride: 4, count: 12, row: (i) => `of P${i + 1}` }, + { name: "switches", base: 0x58dc40, stride: 4, count: 8, row: (i) => `${32 * i + 1} to ${32 * i + 32}` }, + { name: "locations", base: 0x58dc60, stride: 20, count: 255, row: (i) => `location ${i + 1}` }, + { name: "text lines", base: 0x640b60, stride: 218, count: 13, row: (i) => `line ${i + 1}` }, + { name: "keys of the local machine", base: 0x596a18, stride: 1, count: 256 }, + { name: "mouse buttons of the local machine", base: 0x6cddc0, stride: 4, count: 1 }, + { name: "mouse position of the local machine", base: 0x6cddc4, stride: 8, count: 1 }, + { name: "current player", base: 0x6509b0, stride: 4, count: 1 }, +]; + +/** Where an address lies, in words: "unit slot 0 of 1,700, order queue head (+0x74)". With `list`, an entry's name where nothing else knows it. */ +export function placeOfAddress(address: number, list?: Allowlist): string { + const u = address - UNIT_TABLE.base; + if (u >= 0 && u < UNIT_TABLE.stride * UNIT_TABLE.count) { + const at = u % UNIT_TABLE.stride; + return `the unit table: slot ${Math.floor(u / UNIT_TABLE.stride)} of ${UNIT_TABLE.count.toLocaleString("en")}, ${unitFieldAt(at)} (+${hex(at)})`; + } + for (const t of TABLES) { + const off = address - t.base; + if (off < 0 || off >= t.stride * t.count) continue; + const i = Math.floor(off / t.stride), within = off % t.stride; + return `${t.name}${t.row ? `, ${t.row(i)}` : ""}${t.stride > 4 && within ? ` (+${within})` : ""}`; + } + for (const [kind, fields] of Object.entries(TABLE_FIELDS) as [TableKind, (typeof TABLE_FIELDS)[TableKind]][]) { + for (const f of fields) { + const width = f.width === "bit" ? 4 : f.width, off = address - f.base; + if (off < 0 || off % f.stride >= width) continue; + const i = Math.floor(off / f.stride); + if (i < (f.keyed ? 12 : TABLE_SIZE_OF[kind])) return `stats(): the ${kind} table's ${f.name}, entry ${i}`; + } + } + const named = list?.entries.find((e) => !/^0x[0-9a-f]+$/i.test(e.name) && covers(e, address)); + return named ? named.name : "no table TrigScript knows"; +} +const TABLE_SIZE_OF: Record = { unit: 228, weapon: 130, upgrade: 61, tech: 44, player: 12 }; +function covers(e: Allowlist["entries"][number], byte: number): boolean { + const off = byte - e.base; + if (off < 0) return false; + if (!e.stride) return off < e.width; + return off % e.stride < e.width && Math.floor(off / e.stride) < (e.count ?? 1); +} + +/** One dword a run read or wrote, under the line that did it (null: eudplib's own work, or the map's other triggers). */ +export interface EudTraceRow { file: string | null; line: number | null; address: number; write: boolean; mask: number; times: number } + +/** + * Every EUD read and write of a run in the lifted game, by line: what the host's EUD tap says (scm-sim's + * `installEud`), each put under the line its record ran for. The cost tap says the line, as it does for the debugger + * (A5): it is told of each record before the record runs, with eudplib's variables and functions put under the line + * that ran just before them, so a read inside eudplib's `f_dwread_epd` is the line that called it. + */ +export class EudTracer { + private group = -1; + private readonly seen = new Map(); + private readonly places: readonly { file: string; line: number }[]; + private readonly host: EudTraceHost; + + constructor(host: EudTraceHost, lines: CostLines | null) { + this.host = host; + this.places = lines?.places ?? []; + } + + /** Whether the host can trace at all: an EUD tap, and a cost tap to say the line. */ + static can(host: EudTraceHost): boolean { return !!host.eud && !!host.cost && "trace" in host.cost; } + + start(): void { + const { cost, eud } = this.host; + if (cost) cost.trace = (_record, group) => { this.group = group; }; + if (eud) eud.listen = (_record, address, write, mask) => { + const key = `${this.group}|${address}|${write ? 1 : 0}`, was = this.seen.get(key); + if (was) { was[0] = (was[0] | mask) >>> 0; was[1]++; } else this.seen.set(key, [mask >>> 0, 1]); + }; + } + + stop(): void { + const { cost, eud } = this.host; + if (cost) cost.trace = null; + if (eud) eud.listen = null; + this.group = -1; + } + + /** What was traced; plain data for the way back to the editor. */ + rows(): EudTraceRow[] { + return [...this.seen].map(([key, [mask, times]]) => { + const [g, address, write] = key.split("|").map(Number); + const at = g >= 0 ? this.places[g] : undefined; + return { file: at?.file ?? null, line: at?.line ?? null, address, write: write === 1, mask, times }; + }); + } +} + +/** The two taps of a host's game, by shape (scm-sim's `EudTap` and `CostTap`: TrigScript never imports the engine). */ +export interface EudTraceHost { + readonly eud?: { listen: ((record: number, address: number, write: boolean, mask: number, was: number) => void) | null }; + readonly cost?: { trace?: ((record: number, group: number, action: boolean) => void) | null }; +} + +/** A line that touched the address, or the same field of another unit. */ +export interface EudErrorLine { + file: string; + line: number; + /** "exact": this very address; "field": the same field of another unit (units sit elsewhere in the person's game). */ + match: "exact" | "field"; + read: boolean; + write: boolean; + times: number; +} + +export interface EudErrorAnswer { + /** As Remastered shows it, "0xFFA632E3". */ + code: string; + address: number; + place: string; + /** What the allowlist says of a read and a write there, and what that rests on. */ + read: Verdict; + write: Verdict; + evidence: string[]; + /** The lines, those that touched the very address first, then by how often. */ + lines: EudErrorLine[]; + /** Accesses to it under no line: eudplib's start and its own work at the top of a frame, or the map's other triggers. */ + unlined: { exact: number; field: number }; +} + +/** Which lines of a traced run touched `address` (or, in the unit table, the same field of any unit), and what it is. */ +export function findEudError(address: number, rows: readonly EudTraceRow[], list?: Allowlist): EudErrorAnswer { + const dword = address & ~3; + const u = address - UNIT_TABLE.base; + const field = u >= 0 && u < UNIT_TABLE.stride * UNIT_TABLE.count ? u % UNIT_TABLE.stride & ~3 : null; + const byLine = new Map(); + const unlined = { exact: 0, field: 0 }; + for (const r of rows) { + let match: EudErrorLine["match"] | null = null; + if (r.address === dword) match = "exact"; + else if (field !== null) { + const o = r.address - UNIT_TABLE.base; + if (o >= 0 && o < UNIT_TABLE.stride * UNIT_TABLE.count && o % UNIT_TABLE.stride === field) match = "field"; + } + if (!match) continue; + if (r.file === null || r.line === null) { unlined[match] += r.times; continue; } + const key = `${r.file}:${r.line}`, had = byLine.get(key); + if (!had) byLine.set(key, { file: r.file, line: r.line, match, read: !r.write, write: r.write, times: r.times }); + else { + had.times += r.times; + if (match === "exact") had.match = "exact"; + if (r.write) had.write = true; else had.read = true; + } + } + const lines = [...byLine.values()].sort((a, b) => (a.match === b.match ? 0 : a.match === "exact" ? -1 : 1) || b.times - a.times || a.file.localeCompare(b.file) || a.line - b.line); + const read = list ? verdictOf(list, address, false) : null, write = list ? verdictOf(list, address, true) : null; + const evidence = [...new Set([...(read?.entry?.evidence ?? []), ...(write?.entry?.evidence ?? [])])]; + if (list && !read?.entry && !write?.entry && list.outside) evidence.push(...list.outside.evidence); + return { + code: eudErrorOf(address), address, place: placeOfAddress(address, list), + read: read?.verdict ?? "unknown", write: write?.verdict ?? "unknown", evidence, lines, unlined, + }; +} diff --git a/compiler/gameTests.ts b/compiler/gameTests.ts index fc30af3..c7243f3 100644 --- a/compiler/gameTests.ts +++ b/compiler/gameTests.ts @@ -12,6 +12,7 @@ import { MODULE_NAME } from "./api"; import { GameSim, readLocations, type GameHandle } from "./gameSim"; import type { TriggerMap, VariableMap } from "./gameVariables"; import { CostLines } from "./gameCost"; +import { EudTracer, type EudTraceHost, type EudTraceRow } from "./eudError"; import { RecordingHandle, type DebugHost, type DebugLimits, type DebugRecording } from "./gameDebug"; import { here, locate, runModules, type LinkedFile } from "./link"; import type { ScriptNames } from "./names"; @@ -32,8 +33,18 @@ export interface GameTestPayload { variables?: VariableMap | null; /** The build's map of triggers (`readTriggerMap` of its log): with it, what each test's frames cost is read by line. */ triggers?: TriggerMap | null; + /** + * Every EUD read and write written down by line (`EudTracer`), and returned as the report's `trace`: what an "EUD + * ERROR" code is looked up in. Needs `triggers` and a host with both taps; said in `traceError` when it has not. + */ + trace?: boolean; + /** Before the tests, the map played this many frames with nobody at it (a script with no tests is traced so). */ + play?: number; } +/** What `runGameTests` returns: the report, and with `trace` asked, the run's reads and writes by line. */ +export type GameTestReport = TestReport & { error?: string; trace?: { rows: EudTraceRow[]; frames: number }; traceError?: string }; + /** The modules run again to register the tests: the registry, or what stopped them. */ function register(payload: GameTestPayload) { const linked = new Map(payload.modules); @@ -48,17 +59,37 @@ function register(payload: GameTestPayload) { return { linked, registry, where, error: failure ? `${failure.file ?? ENTRY_FILE}:${failure.line ?? 1}: ${failure.message}` : null }; } -export function runGameTests(game: GameHandle, payload: GameTestPayload): TestReport & { error?: string } { +export function runGameTests(game: GameHandle, payload: GameTestPayload): GameTestReport { + // (What a frame costs, where the host counts it and knows where the payload is.) + const costLines = payload.triggers && game.cost && game.payloadEnd ? new CostLines(payload.triggers, game.payloadEnd) : null; + if (costLines && game.cost) game.cost.group = costLines.group; + const host = game as GameHandle & EudTraceHost; + const traceError = !payload.trace ? undefined : !costLines ? "the build said nothing of where its triggers are" : !EudTracer.can(host) ? "the engine's page is from before EUD traces" : undefined; + const tracer = payload.trace && !traceError ? new EudTracer(host, costLines) : null; + let played = 0; + tracer?.start(); + try { + if (payload.play) { + game.reset(); + while (played < payload.play && !game.over) { game.step(); played++; } + } + const report: GameTestReport = payload.modules.length ? runRegistered(game, payload, costLines) : { list: [], results: [], only: [], ms: 0 }; + if (tracer) report.trace = { rows: tracer.rows(), frames: played + report.results.reduce((n, r) => n + r.frames, 0) }; + if (traceError) report.traceError = traceError; + return report; + } finally { + tracer?.stop(); + } +} + +function runRegistered(game: GameHandle, payload: GameTestPayload, costLines: CostLines | null): GameTestReport { const { registry, where, error } = register(payload); if (error) return { list: [], results: [], only: [], ms: 0, error }; const locations = payload.locations ?? readLocations(game); // A test of several players runs only where the game is one of several copies, one a player (A3); one game says so. const several = listTests(registry).list.filter((t) => t.kind === "test" && t.players); const ids = game.copy ? payload.ids : payload.ids ?? listTests(registry).list.filter((t) => t.kind === "test" && !t.players).map((t) => t.id); - // (What a frame costs, where the host counts it and knows where the payload is.) - const costLines = payload.triggers && game.cost && game.payloadEnd ? new CostLines(payload.triggers, game.payloadEnd) : null; - if (costLines && game.cost) game.cost.group = costLines.group; - const report = runTests(registry, { triggers: [], sources: [], ir: [], strings: [], locate: where }, { files: payload.files, ids, makeWorld: (_as, test) => new GameSim(game, { locations, variables: payload.variables, test, costLines }) }); + const report: GameTestReport = runTests(registry, { triggers: [], sources: [], ir: [], strings: [], locate: where }, { files: payload.files, ids, makeWorld: (_as, test) => new GameSim(game, { locations, variables: payload.variables, test, costLines }) }); if (!game.copy && !payload.ids) { const held: TestResult[] = several.filter((t) => !payload.files || payload.files.includes(t.file)).map((t) => ({ id: t.id, status: "skipped", message: `a test of ${t.players!.length} players runs as a copy of the game for each`, printed: [], events: [], frames: 0, ms: 0 })); report.results.push(...held); @@ -67,7 +98,7 @@ export function runGameTests(game: GameHandle, payload: GameTestPayload): TestRe } /** What a copy of the game runs (scm-sim's `runCopies` driver): the same tests, at this copy's computer. */ -export function drive(game: GameHandle, payload: GameTestPayload): TestReport & { error?: string } { +export function drive(game: GameHandle, payload: GameTestPayload): GameTestReport { return runGameTests(game, payload); } diff --git a/docs/v4-plan.md b/docs/v4-plan.md index ad15e37..6504415 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -2,7 +2,8 @@ Written 2026-10-15. Status: Part A built (A0–A5, each with its *As built*), and Remastered's emulator read from a dump (*R0*, 2026-10-16); Part B steered by three showpiece maps -(*Showpieces*, 2026-10-16), B1 built (both parts, 2026-10-16). The question it answers: now that there is a +(*Showpieces*, 2026-10-16), B1 and B2 built (2026-10-16/17), and an EUD error code taken back to its line +(*EUD error code to line*, 2026-10-17). The question it answers: now that there is a lifted copy of the game's own code (scm-sim's exact engine) beside eudplib, what does TrigScript become if it uses the game itself as its checker, and how far can the language go towards the TypeScript people actually write. @@ -1338,13 +1339,56 @@ passes near them. | Idea | Built from | Where | | --- | --- | --- | -| EUD error code to line: Remastered's "EUD ERROR: 0x…" is the refused address inverted (R0); the editor reads the code and points at the line that touches that address | the trigger map and trace (A1), R0 | now: a day | +| EUD error code to line: Remastered's "EUD ERROR: 0x…" is the refused address inverted (R0); the editor reads the code and points at the line that touches that address | the trigger map and trace (A1), R0 | built 2026-10-17, below | | Typed chat patterns: `on.chat("-give {n}", (p, { n }) => …)` with `n: number` worked out by template literal types | B2 | built: `chatted()`'s patterns, which `on.chat` took over in B2 | | A line's values over time beside it, as a small graph, from the debugger's recording | A5 | after A5's open items | | Changes kept while the game runs: edit the script, rebuild, carry the variables over by name into a snapshot and go on | A2's variable map, scm-sim snapshots | after B1 (a task's place has to carry over too) | | Fuzzing: random inputs on every seat at the lifted game's speed, looking for desyncs, games that end and failed expectations, then the smallest input that still fails | A2, A3 | after B5 | | A memory field's hover says what Remastered does with it ("read: fine, from the dump; write: ends the game, probe 6") | R0, Ghidra structs (the analysis has to be run again) | B7 | +#### EUD error code to line (built 2026-10-17) + +**Find an EUD Error…** (the palette and the More menu) asks for the code Remastered showed and answers with the +lines. In the editor it goes: + +1. **The code is read.** "EUD ERROR: 0xFFA632E3", "0xFFA632E3" or "FFA632E3"; the address is the code's bits inverted + (R0, the dump probe's step D: 0xFFA632E3 is 0x59CD1C). +2. **The address is named.** `compiler/eudError.ts` knows 1.16.1's unit table field by field (CUnit, slot and offset: + "the unit table: slot 0 of 1,700, order queue head (+0x74)"), the deaths and kills tables by player and unit type, + resources, switches, locations, the text lines, the local machine's keys and mouse, the current player, every + `stats()` field, and else an allowlist entry's name. The allowlist's read and write verdicts and their evidence come + with it (`dist/allowlist.json`, a copy the build makes, fetched the first time a code is looked up). +3. **The map is traced in the game.** Built as Save builds it, then `runGameTests` with two new payload fields: + `play` (3,000 frames with nobody at it, before the tests) and `trace`. `EudTracer` listens on the host's EUD tap + (scm-sim's `installEud`, which the page's test worker already gave the handle) and takes each access's line from + the cost tap's `trace`, which is told of each record before it runs, with eudplib's variables and functions under + the line that ran before them. So a unit field read inside `f_dwread_epd` is the line that called it. The rows come + back in the report (`trace`, or `traceError` saying why not). **No scm-sim change was needed.** +4. **The lines are found.** Those that touched the very dword first. In the unit table, a unit sits in a different + slot from game to game, so lines that touched the same field of another unit are found too, marked so. Accesses + under no line (eudplib's start, its work at the top of a frame, the map's other triggers) are counted apart. +5. **They are shown** as an inlay at the end of each line ("EUD ERROR 0xFFA632E3", with what the line did and what + Remastered does with it), in Problems (source "game"), in Output, and the cursor goes to the first. Like a line's + cost, they stay only while the file is the text that was traced. + +Under Node: `npx tsx scripts/test-game.mts --error=0x… [--frames=N]` runs the same path (the handle +given the EUD tap, `runGameTests` with `trace` and `play`). + +**Checked:** +- `tests/eud-error.test.ts` (11): the code both ways; the names; matching exact, by field, outside the unit table, + and a byte inside a dword; the allowlist's verdicts and `outside`; `EudTracer` over taps shaped like scm-sim's, with + CARRY; `runGameTests` playing frames for a script with no tests; `traceError` when the host or build cannot. +- In the lifted game: `probes/units.ts --error=0xFFA62F5F` (slot 3's hit points) names lines 38 (`sum += u.hp`), + 42 (`u.hp = …`), 105 (`u.hp < least`) and the other hit point lines, all as "the same field of another unit", + since no unit stood in slot 3. `probes/remastered-dump.ts --error=0xFFA632E3` names the place and the played + evidence and finds no line: step D's read is the probe's hand-written eudplib, not a line of the script. + +**Not checked:** the command in the editor itself, end to end (it needs the person's copy of the game on the +engine's page). **Found on the way:** A1's Node trace (`exact-eud.ts --trace`) keys rows by the raw origin, so +eudplib's functions are "fn:…", never a line; a unit field read through `f_dwread_epd` there looks like eudplib's +own. The tap's CARRY is what puts it on its line, which is why `--error` lives in `test-game.mts` and not in +`game-check.mts`. The same would sharpen A1's check (it currently reads those as eudplib's). + ## Slices | # | Slice | What it proves | Size | diff --git a/editor.ts b/editor.ts index 47efb29..be1228e 100644 --- a/editor.ts +++ b/editor.ts @@ -27,7 +27,10 @@ * Monaco and TypeScript come from the CDN on first open. */ import type { PluginApi } from "@scm-js/plugin-api"; -import { CompileSuperseded, gametestUrl, retainCompileWorker } from "./compile"; +import { CompileSuperseded, allowlistUrl, gametestUrl, retainCompileWorker } from "./compile"; +import type { Allowlist } from "./compiler/allowlist"; +import { addressOfEudError, findEudError, type EudErrorAnswer } from "./compiler/eudError"; +import type { GameTestReport } from "./compiler/gameTests"; import { ENTRY_FILE, normalizePath, type CompileResult, type LineHint, type ScriptDiagnostic, type ScriptFiles, type TriggerSource } from "./compiler/compiler"; import { HEAP_CELLS, HEAP_CELLS_MAX, HEAP_CELLS_MIN, STACK_CELLS_MAX, STACK_DEPTH, STACK_DEPTH_MAX, STACK_DEPTH_MIN, heapCells, stackDepth } from "./compiler/ir"; import { largestFrame } from "./compiler/recursion"; @@ -135,6 +138,8 @@ const TESTS_SLOW_MS = 2_000; const COST_SEEN = 500_000; /** A line that ran this many conditions and actions in one frame of a test in the game says so beside it: 2% of COST_SEEN. */ const COST_HINT = 10_000; +/** Frames the map is played with nobody at it when an EUD error is looked up, before the tests: two minutes of game time at Fastest. */ +const EUD_TRACE_FRAMES = 3_000; /** The costliest lines a test's results name. */ const COST_LINES = 3; /** The key the shortcuts are written with. */ @@ -349,6 +354,8 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa null, ...["runTests", "runFailedTests", "runTestsInGame"].map(menuItem), null, + menuItem("findEudError"), + null, ...["problems", "output", "panel", "explorer", "testing"].map(menuItem), null, menuItem("settings"), @@ -473,6 +480,13 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa let costFiles: ScriptFiles | null = null; /** The text of `dist/gametest.js`, fetched once. */ let gametest: Promise | null = null; + /** What Remastered is known to allow, fetched the first time an EUD error is looked up. */ + let allowlist: Promise | null = null; + /** + * The last EUD error looked up (Find EUD Error), and the files as they were when the game was traced: its lines are + * marked only while their file is still that text, as a line's cost is. + */ + let eudFound: { files: ScriptFiles; answer: EudErrorAnswer } | null = null; /** The files with a tab, in the order they were opened. */ let openTabs: string[] = [normalizePath(options.file ?? ENTRY_FILE)]; /** What Apply, Play and the builds of the programs reported, oldest first. */ @@ -679,8 +693,9 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa const renderProblems = () => { // A failing test and an only left in are warnings: said here, and no obstacle to a build unless the map's settings make one of them. const warnings = warningsOf(tests, t); - problemsView.badge(diagnostics.length + warnings.length); - if (diagnostics.length + warnings.length === 0) { problemsView.body.replaceChildren(el("div", { className: "tsd-empty" }, !ready ? "" : result ? t("No problems have been detected in the script.") : t("Checking…"))); return; } + const eud = eudLines(), found = eudFound?.answer; + problemsView.badge(diagnostics.length + warnings.length + eud.length); + if (diagnostics.length + warnings.length + eud.length === 0) { problemsView.body.replaceChildren(el("div", { className: "tsd-empty" }, !ready ? "" : result ? t("No problems have been detected in the script.") : t("Checking…"))); return; } problemsView.body.replaceChildren(el("ul", { className: "tsd-list" }, ...diagnostics.map((d) => el("li", { title: d.message, onClick: () => goTo(d.file, d.line, d.column) }, @@ -689,6 +704,13 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa el("span", { className: "src" }, d.source === "typescript" ? t("types") : d.source === "script" ? t("script") : t("compiler")), el("span", { className: "where" }, t("{file} [Ln {line}, Col {column}]", { file: d.file, line: d.line, column: d.column })), )), + ...(found ? eud.map((l) => + el("li", { title: eudLineNote(found, l), onClick: () => goTo(l.file, l.line) }, + shell.icon("error"), + el("span", { className: "msg" }, `EUD ERROR ${found.code}: ${found.place}`), + el("span", { className: "src" }, t("game")), + el("span", { className: "where" }, t("{file} [Ln {line}]", { file: l.file, line: l.line })), + )) : []), ...warnings.map((w) => el("li", { className: "tsd-warning", title: w.message, onClick: () => goTo(w.file, w.line) }, shell.icon("warning"), @@ -773,7 +795,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa * page reads the scenario out of it), with the build's maps of variables and triggers from its log, and the text of * `dist/gametest.js`. Null, the reason said in the status bar, when something is missing. */ - const prepareInGame = async (filter: Pick, ask: "tests" | "debug") => { + const prepareInGame = async (filter: Pick, ask: "tests" | "debug" | "trace") => { const engine = svc.engine(); if (!engine?.present || !engine.test || (ask === "debug" && !engine.debug)) { setStatus("error", ask === "debug" @@ -781,7 +803,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa : t("Running tests in the game needs the scm-sim plugin (0.11 or newer) on scmjs.dev: install or turn it on under Plugins ▸ Manage Plugins….")); return null; } - const notRun = (message: string) => (ask === "debug" ? t("Not debugged in the game: {message}", { message }) : t("Not run in the game: {message}", { message })); + const notRun = (message: string) => (ask === "debug" ? t("Not debugged in the game: {message}", { message }) : ask === "trace" ? t("Not looked up in the game: {message}", { message }) : t("Not run in the game: {message}", { message })); const status = await engine.status(); if (!status.available) { setStatus("error", notRun(status.reason ?? t("the engine cannot run a game now"))); return null; } if (!status.tests) { setStatus("error", t("Not run in the game: the engine's page is from before tests in the game. Reload the editor and try again.")); return null; } @@ -789,9 +811,15 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa if (!(await build()) || cancelled) return null; const a = await compileNow({ world: svc.world(), ...filter }); if (!a || cancelled) return null; - if (!a.compiled.ok) { setStatus("error", t("Tests not run: {n, plural, one {# problem} other {# problems}} in the script.", { n: a.compiled.diagnostics.length }), NOTICE_MS); shell.showPanel("problems"); return null; } - if (!a.compiled.modules) { setStatus("info", t("The script has no tests.")); return null; } - if (!a.compiled.ir.length) { setStatus("info", t("Tests run in the game only for a script with a program: without one, nothing of the script is built into the map.")); return null; } + if (!a.compiled.ok) { setStatus("error", ask === "trace" ? t("Not looked up: {n, plural, one {# problem} other {# problems}} in the script.", { n: a.compiled.diagnostics.length }) : t("Tests not run: {n, plural, one {# problem} other {# problems}} in the script.", { n: a.compiled.diagnostics.length }), NOTICE_MS); shell.showPanel("problems"); return null; } + // (An EUD error is looked up in the map played with nobody at it, then the tests when there are any.) + if (!a.compiled.modules && ask !== "trace") { setStatus("info", t("The script has no tests.")); return null; } + if (!a.compiled.ir.length) { + setStatus("info", ask === "trace" + ? t("The script has no program, so nothing of it is built into the map and no line of it can touch the game's memory: an EUD error came from the map's other triggers.") + : t("Tests run in the game only for a script with a program: without one, nothing of the script is built into the map.")); + return null; + } const names = svc.names()?.names; if (!names) { setStatus("error", t("No map is open.")); return null; } setStatus("busy", t("Building the map…")); @@ -935,6 +963,83 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa })); }; + /** The lines of the last EUD error looked up, while their file is the text that was traced. */ + const eudLines = () => { + if (!eudFound) return []; + const seen = eudFound.files; + return eudFound.answer.lines.filter((l) => seen[normalizePath(l.file)] === files[normalizePath(l.file)]); + }; + /** What a line did to the address, and what Remastered does with that, in a sentence. */ + const eudLineNote = (a: EudErrorAnswer, l: EudErrorAnswer["lines"][number]) => { + const did = l.read && l.write ? t("reads and writes") : l.write ? t("writes") : t("reads"); + const verdict = l.write ? a.write : a.read; + const said = verdict === "ends" ? t("Remastered ends the game on this.") : verdict === "ignored" ? t("Remastered does nothing with this.") : verdict === "allowed" ? t("Remastered was seen to allow this, so the game that ended did something else to it.") : t("This has not been seen in Remastered."); + return l.match === "exact" + ? t("In the game, this line {did} {place} ({times, plural, one {# time} other {# times}}). {said}", { did, place: a.place, times: l.times, said }) + : t("In the game, this line {did} the same field of another unit ({times, plural, one {# time} other {# times}}): a unit sits elsewhere in the table from one game to the next. {said}", { did, times: l.times, said }); + }; + const eudHints = (): LineHint[] => { + const a = eudFound?.answer; + return a ? eudLines().map((l) => ({ file: l.file, line: l.line, label: `EUD ERROR ${a.code}`, note: eudLineNote(a, l) })) : []; + }; + + /** + * An "EUD ERROR: 0x…" Remastered showed, back to the lines of the script (TrigScript 4): the code is the address + * the trigger named, bits inverted (R0). The map is built as Save builds it and played in the lifted game with + * nobody at it, then the tests run, every EUD access written down under its line; the lines that touched the + * address are marked, and what lies there is said. + */ + const lookUpEudError = async () => { + if (!editor || testsRunning || building || testing || !ready) return; + const answer = await api.ui.prompt(t("The code Remastered showed when the game ended (EUD ERROR: 0x…):"), { title: "TrigScript", value: "", placeholder: "EUD ERROR: 0xFFA632E3" }); + if (answer === null || cancelled) return; + const address = addressOfEudError(answer); + if (address === null) { setStatus("error", t("“{text}” is not an EUD error code: Remastered shows one as EUD ERROR: 0x and eight digits.", { text: answer.trim() })); return; } + testsRunning = true; + renderTesting(); + try { + const got = await prepareInGame({}, "trace"); + if (!got || cancelled) return; + const { engine, a, names, variables, triggers, module, bytes, built } = got; + if (!triggers) { setStatus("error", t("Not looked up: the build did not say where its triggers are. Update the eudplib plugin.")); return; } + setStatus("busy", t("Playing the map in the game, writing down what each line reads and writes…")); + allowlist ??= fetch(allowlistUrl(svc.dist())).then((r) => { if (!r.ok) throw new Error(`allowlist.json: ${r.status}`); return r.json() as Promise; }); + const [report, list] = await Promise.all([ + engine.test!(bytes, module, { modules: a.compiled.modules ?? [], names, variables, triggers, trace: true, play: EUD_TRACE_FRAMES }) as Promise, + allowlist.catch(() => { allowlist = null; return undefined; }), + ]); + if (cancelled) return; + if (report.error) { setStatus("error", t("Not looked up in the game: {message}", { message: report.error })); return; } + if (!report.trace) { setStatus("error", t("Not looked up in the game: {message}", { message: report.traceError ?? t("the engine's page sent back no trace") })); return; } + const found = findEudError(address, report.trace.rows, list); + eudFound = { files: built, answer: found }; + log(t("EUD ERROR {code}: {place}, at 0x{address}.", { code: found.code, place: found.place, address: found.address.toString(16) })); + for (const l of found.lines) log(` ${l.file}:${l.line} — ${eudLineNote(found, l)}`, false); + const ran = report.results.filter((r) => r.status !== "skipped").length; + const where = found.lines.slice(0, 3).map((l) => `${l.file}:${l.line}`).join(", ") + (found.lines.length > 3 ? "…" : ""); + const exact = found.lines.some((l) => l.match === "exact"); + const head = t("EUD ERROR {code} is {place}.", { code: found.code, place: found.place }); + const said = found.lines.length + ? exact + ? t("{n, plural, one {# line touched it} other {# lines touched it}} in the game: {where}.", { n: found.lines.length, where }) + : t("No line touched that very unit, but {n, plural, one {# line touches} other {# lines touch}} the same field of other units, and a unit sits elsewhere in the table from one game to the next: {where}.", { n: found.lines.length, where }) + : found.unlined.exact + found.unlined.field + ? t("No line of the script touched it: eudplib's own work did ({n, plural, one {# time} other {# times}}).", { n: found.unlined.exact + found.unlined.field }) + : ran + ? t("Nothing touched it in {frames} frames of the map played with nobody at it, nor in {tests, plural, one {# test} other {# tests}}: the line that did is one this run did not reach. A test that gets there will find it.", { frames: EUD_TRACE_FRAMES.toLocaleString(), tests: ran }) + : t("Nothing touched it in {frames} frames of the map played with nobody at it: the line that did is one this run did not reach. A test that gets there will find it.", { frames: EUD_TRACE_FRAMES.toLocaleString() }); + setStatus(found.lines.length ? "error" : "info", `${head} ${said}`); + const first = found.lines[0]; + if (first) goTo(first.file, first.line); + refreshLineHints(); + } catch (err) { + setStatus("error", t("Not looked up in the game: {message}", { message: (err as Error).message })); + } finally { + testsRunning = false; + render(); + } + }; + /** "0.2%", "31%": a count as a share of what Remastered was seen to run in a frame. */ const shareOf = (n: number) => `${(n / COST_SEEN * 100).toFixed(n < COST_SEEN / 10 ? 1 : 0)}%`; @@ -1725,6 +1830,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa { id: "test", label: t("Play the Map"), key: { code: "F5" }, run: () => { void test(); } }, { id: "runTests", label: t("Run All Tests"), run: () => { void runTests(); } }, { id: "runTestsInGame", label: t("Run All Tests in the Game"), run: () => { void runTestsInGame(); } }, + { id: "findEudError", label: t("Find an EUD Error…"), run: () => { void lookUpEudError(); } }, { id: "runTestAtCursor", label: t("Run Test at Cursor"), context: true, run: () => { void runTestAtCursor(); } }, { id: "runFailedTests", label: t("Run Failed Tests"), run: () => { void runFailedTests(); } }, { id: "testing", label: t("Show Testing"), run: () => testingView.show() }, @@ -1788,7 +1894,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa monaco = m; if (generated) setDeclarations(m, generated.decls); setHoverVariables(m, () => result?.variables ?? []); - setLineHints(m, () => [...(result?.hints ?? []), ...costHints(), ...debug.hints()]); + setLineHints(m, () => [...(result?.hints ?? []), ...costHints(), ...eudHints(), ...debug.hints()]); setMapRefs(m, mapRefs); // Uncover first: `done` puts the host back in its own place, and Monaco measures it where it lands. loadingCover.done(); diff --git a/ko.ts b/ko.ts index f4838fc..8743b5a 100644 --- a/ko.ts +++ b/ko.ts @@ -365,4 +365,30 @@ export const KO: Record = { "Debug Test at Cursor in the Game": "커서 위치의 테스트를 게임에서 디버그", "Toggle Breakpoint": "중단점 설정/해제", "Breakpoint: the debugger's Continue stops before this line runs (a test's line: at the end of the frame it asks for). Click to take it away.": "중단점: 디버거에서 계속하면 이 줄이 실행되기 전에 멈춥니다(테스트의 줄이면 그 줄이 요청한 프레임의 끝에서). 클릭하면 없어집니다.", + "game": "게임", + "Not looked up in the game: {message}": "게임에서 찾지 못함: {message}", + "Not looked up: {n, plural, one {# problem} other {# problems}} in the script.": "찾지 않음: 스크립트에 문제가 {n}개 있습니다.", + "The script has no program, so nothing of it is built into the map and no line of it can touch the game's memory: an EUD error came from the map's other triggers.": "스크립트에 프로그램이 없어 맵에 빌드되는 것이 없고, 어떤 줄도 게임의 메모리에 닿을 수 없습니다. EUD 오류는 맵의 다른 트리거에서 났습니다.", + "reads and writes": "읽고 씁니다", + "writes": "씁니다", + "reads": "읽습니다", + "Remastered ends the game on this.": "리마스터는 이때 게임을 끝냅니다.", + "Remastered does nothing with this.": "리마스터는 이것으로 아무것도 하지 않습니다.", + "Remastered was seen to allow this, so the game that ended did something else to it.": "리마스터가 이를 허용하는 것이 확인되었으므로, 끝난 게임은 여기에 다른 일을 했습니다.", + "This has not been seen in Remastered.": "리마스터에서 확인된 적이 없습니다.", + "In the game, this line {did} {place} ({times, plural, one {# time} other {# times}}). {said}": "게임에서 이 줄은 {place}을(를) {did} ({times}번). {said}", + "In the game, this line {did} the same field of another unit ({times, plural, one {# time} other {# times}}): a unit sits elsewhere in the table from one game to the next. {said}": "게임에서 이 줄은 다른 유닛의 같은 필드를 {did} ({times}번). 유닛이 테이블에서 놓이는 자리는 게임마다 다릅니다. {said}", + "The code Remastered showed when the game ended (EUD ERROR: 0x…):": "게임이 끝날 때 리마스터가 보여 준 코드 (EUD ERROR: 0x…):", + "“{text}” is not an EUD error code: Remastered shows one as EUD ERROR: 0x and eight digits.": "“{text}”은(는) EUD 오류 코드가 아닙니다. 리마스터는 EUD ERROR: 0x 뒤에 여덟 자리로 보여 줍니다.", + "Not looked up: the build did not say where its triggers are. Update the eudplib plugin.": "찾지 않음: 빌드가 트리거의 위치를 알려 주지 않았습니다. eudplib 플러그인을 업데이트하세요.", + "Playing the map in the game, writing down what each line reads and writes…": "게임에서 맵을 플레이하며 각 줄이 읽고 쓰는 것을 기록하는 중…", + "the engine's page sent back no trace": "엔진 페이지가 기록을 돌려주지 않았습니다", + "EUD ERROR {code}: {place}, at 0x{address}.": "EUD ERROR {code}: {place}, 주소 0x{address}.", + "EUD ERROR {code} is {place}.": "EUD ERROR {code}은(는) {place}입니다.", + "{n, plural, one {# line touched it} other {# lines touched it}} in the game: {where}.": "게임에서 {n}개 줄이 여기에 닿았습니다: {where}.", + "No line touched that very unit, but {n, plural, one {# line touches} other {# lines touch}} the same field of other units, and a unit sits elsewhere in the table from one game to the next: {where}.": "그 유닛에 닿은 줄은 없지만, {n}개 줄이 다른 유닛의 같은 필드에 닿습니다. 유닛이 테이블에서 놓이는 자리는 게임마다 다릅니다: {where}.", + "No line of the script touched it: eudplib's own work did ({n, plural, one {# time} other {# times}}).": "스크립트의 어떤 줄도 닿지 않았습니다. eudplib 자체의 작업이 닿았습니다 ({n}번).", + "Nothing touched it in {frames} frames of the map played with nobody at it, nor in {tests, plural, one {# test} other {# tests}}: the line that did is one this run did not reach. A test that gets there will find it.": "아무도 없이 맵을 {frames} 프레임 플레이하고 테스트 {tests}개를 돌리는 동안 아무것도 닿지 않았습니다. 닿은 줄은 이번 실행이 이르지 못한 줄입니다. 거기까지 가는 테스트가 있으면 찾을 수 있습니다.", + "Nothing touched it in {frames} frames of the map played with nobody at it: the line that did is one this run did not reach. A test that gets there will find it.": "아무도 없이 맵을 {frames} 프레임 플레이하는 동안 아무것도 닿지 않았습니다. 닿은 줄은 이번 실행이 이르지 못한 줄입니다. 거기까지 가는 테스트가 있으면 찾을 수 있습니다.", + "Find an EUD Error…": "EUD 오류 찾기…", }; diff --git a/package.json b/package.json index 02e6c3e..64c66b1 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "./package.json": "./package.json" }, "scripts": { - "build": "esbuild plugin.ts --bundle --format=esm --target=es2022 --platform=browser --outfile=dist/plugin.js --banner:js=\"// Generated by npm run build - do not edit; the source is plugin.ts.\" && esbuild compiler/entry.ts --bundle --format=esm --target=es2022 --platform=browser --outfile=dist/compiler.js --banner:js=\"// Generated by npm run build - do not edit; the source is compiler/entry.ts. The compile worker imports this when the plugin is compiled into the editor.\" && esbuild compiler/testingEntry.ts --bundle --format=esm --target=es2022 --platform=neutral --external:node:* --outfile=dist/testing.js --banner:js=\"// Generated by npm run build - do not edit; the source is compiler/testingEntry.ts. The plugin never loads this: it is what another repository's tests import to run triggers.\" && dts-bundle-generator --silent --no-banner --project tsconfig.json -o dist/testing.d.ts compiler/testingEntry.ts && esbuild compiler/gameTestEntry.ts --bundle --format=esm --target=es2022 --platform=neutral --outfile=dist/gametest.js --banner:js=\"// Generated by npm run build - do not edit; the source is compiler/gameTestEntry.ts. The plugin hands its text to a host of the lifted game, which runs a script's tests beside the game.\"", + "build": "esbuild plugin.ts --bundle --format=esm --target=es2022 --platform=browser --outfile=dist/plugin.js --banner:js=\"// Generated by npm run build - do not edit; the source is plugin.ts.\" && esbuild compiler/entry.ts --bundle --format=esm --target=es2022 --platform=browser --outfile=dist/compiler.js --banner:js=\"// Generated by npm run build - do not edit; the source is compiler/entry.ts. The compile worker imports this when the plugin is compiled into the editor.\" && esbuild compiler/testingEntry.ts --bundle --format=esm --target=es2022 --platform=neutral --external:node:* --outfile=dist/testing.js --banner:js=\"// Generated by npm run build - do not edit; the source is compiler/testingEntry.ts. The plugin never loads this: it is what another repository's tests import to run triggers.\" && dts-bundle-generator --silent --no-banner --project tsconfig.json -o dist/testing.d.ts compiler/testingEntry.ts && esbuild compiler/gameTestEntry.ts --bundle --format=esm --target=es2022 --platform=neutral --outfile=dist/gametest.js --banner:js=\"// Generated by npm run build - do not edit; the source is compiler/gameTestEntry.ts. The plugin hands its text to a host of the lifted game, which runs a script's tests beside the game.\" && node -e \"const fs=require('fs');fs.writeFileSync('dist/allowlist.json',JSON.stringify(JSON.parse(fs.readFileSync('data/allowlist.json','utf8'))))\"", "dev": "npm run build -- --watch", "typecheck": "tsc -p tsconfig.json", "test": "vitest run", diff --git a/scripts/test-game.mts b/scripts/test-game.mts index 3f4a1af..b0fff7a 100644 --- a/scripts/test-game.mts +++ b/scripts/test-game.mts @@ -4,7 +4,11 @@ * and nothing drawn, and every `test()` handed a `GameSim` over it (compiler/gameSim.ts) — the game put back to * frame 0 before each. The same tests then run in the simulator over the map's own world, and each is said twice. * - * npx tsx scripts/test-game.mts [map.scx] + * npx tsx scripts/test-game.mts [map.scx] [--error=CODE [--frames=N]] + * + * `--error=0xFFA632E3` (what Remastered said when it ended a game: "EUD ERROR: 0x…") runs the map as the editor's + * Find EUD Error does — N frames with nobody at it (3000), then the tests, every EUD access traced by line — and says + * which lines touched the address the code names, and what lies there. * * Needs a scm-sim checkout beside this repository (or SCM_SIM_DIR) with its lifted game generated. */ @@ -18,13 +22,20 @@ import { compileScript } from "../compiler/compiler"; import { readLocations, readUnitStats, type GameHandle } from "../compiler/gameSim"; import { mergeCopies } from "../compiler/gameCopies"; import { defaultScriptNames } from "../compiler/names"; +import { addressOfEudError, findEudError } from "../compiler/eudError"; +import type { Allowlist } from "../compiler/allowlist"; +import type { GameTestReport } from "../compiler/gameTests"; import type { TestReport, TestWorld } from "../compiler/testing"; import { defaultLib } from "../bundle/lib.mjs"; import { scriptFiles } from "./script-files.mts"; const root = resolve(import.meta.dirname, ".."); -const [script, baseMap] = process.argv.slice(2); -if (!script || !existsSync(script)) { console.error("usage: test-game.mts [map.scx]"); process.exit(2); } +const args = process.argv.slice(2); +const flag = (n: string) => args.find((a) => a.startsWith(`--${n}=`))?.slice(n.length + 3); +const [script, baseMap] = args.filter((a) => !a.startsWith("--")); +if (!script || !existsSync(script)) { console.error("usage: test-game.mts [map.scx] [--error=CODE [--frames=N]]"); process.exit(2); } +const errorAddress = flag("error") === undefined ? null : addressOfEudError(flag("error")!); +if (flag("error") !== undefined && errorAddress === null) { console.error(`--error=${flag("error")}: not a code (EUD ERROR: 0x… as Remastered shows it)`); process.exit(2); } const sim = resolve(process.env.SCM_SIM_DIR ?? join(root, "..", "scm-sim")); if (!existsSync(join(sim, "src", "exact", "testGame.ts"))) { console.error(`no scm-sim checkout with src/exact/testGame.ts at ${sim} (set SCM_SIM_DIR)`); process.exit(2); } const from = (p: string) => import(pathToFileURL(join(sim, p)).href); @@ -32,7 +43,7 @@ const from = (p: string) => import(pathToFileURL(join(sim, p)).href); const dir = mkdtempSync(join(tmpdir(), "trigscript-tests-")); try { // The map, built as Save builds it. - const built = join(dir, basename(script, ".ts") + ".scx"); + const built = join(dir, basename(script).replace(/\.tsx?$/, "") + ".scx"); const b = spawnSync("npx", ["tsx", join(root, "scripts", "build-fixture.mts"), resolve(script), built, ...(baseMap ? [resolve(baseMap)] : [])], { cwd: root, encoding: "utf8", maxBuffer: 256 << 20, env: { ...process.env, VARIABLE_MAP: "1", TRIGGER_MAP: "1" } }); if (b.status !== 0) { console.error(b.stdout.split("\n").slice(-20).join("\n"), b.stderr); process.exit(1); } @@ -50,11 +61,11 @@ try { writeFileSync(chkPath, classic); const run = await startExactGame(chkPath, { game: "ums" }); const strings = placeStrings(run.lifted, chk); - installEud(run.lifted, strings); + const eud = installEud(run.lifted, strings); // (eudplib's payload lies outside what the game's snapshot covers: the handle keeps it itself. What each frame of // the triggers ran is counted, A4.) const payloadEnd = 0x191943c8 + strings.bytes; - const handle = testGame(run.engine, run.lifted, [{ from: strings.gameTable, to: payloadEnd }], { cost: installCost(run.lifted), payloadEnd }) as GameHandle; + const handle = testGame(run.engine, run.lifted, [{ from: strings.gameTable, to: payloadEnd }], { cost: installCost(run.lifted), payloadEnd, ...(errorAddress !== null ? { eud } : {}) }) as GameHandle; // The tests in the simulator over the map's own world, as the editor runs them; then in the game, the way a host // of the game runs them: the compile's JavaScript handed to dist/gametest.js, loaded from its text. @@ -65,13 +76,26 @@ try { const world = await mapWorld(classic, locations, readUnitStats(handle)); const compiled = compileScript(ts, source, names, { lib: defaultLib(), tests: { world } }); const simulated = compiled.tests; - if (!compiled.modules) { console.error(compiled.ok ? "the script has no tests" : compiled.diagnostics.map((d) => `${d.file}:${d.line}: ${d.message}`).join("\n")); process.exit(1); } + if (!compiled.modules && !(errorAddress !== null && compiled.ok)) { console.error(compiled.ok ? "the script has no tests" : compiled.diagnostics.map((d) => `${d.file}:${d.line}: ${d.message}`).join("\n")); process.exit(1); } const bundle = join(dir, "gametest.js"); spawnSync("npx", ["esbuild", join(root, "compiler", "gameTestEntry.ts"), "--bundle", "--format=esm", "--target=es2022", "--platform=neutral", `--outfile=${bundle}`], { cwd: root }); const host = await import(`data:text/javascript;base64,${Buffer.from(readFileSync(bundle)).toString("base64")}`) as typeof import("../compiler/gameTestEntry"); // (Where the build put the programs' variables, for sim.program().) const variables = JSON.parse(readFileSync(built.replace(/\.scx$/, ".variables.json"), "utf8")); const triggerMap = JSON.parse(readFileSync(built.replace(/\.scx$/, ".triggers.json"), "utf8")); + if (errorAddress !== null) { + const frames = Number(flag("frames") ?? 3000); + const traced = host.runGameTests(handle, { modules: compiled.modules ?? [], names, locations, variables, triggers: triggerMap, trace: true, play: frames }) as GameTestReport; + if (traced.error || traced.traceError || !traced.trace) { console.error(`not traced: ${traced.error ?? traced.traceError ?? "no trace came back"}`); process.exit(1); } + const list = JSON.parse(readFileSync(join(root, "data", "allowlist.json"), "utf8")) as Allowlist; + const a = findEudError(errorAddress, traced.trace.rows, list); + console.log(`EUD ERROR ${a.code}: ${a.place}, at 0x${a.address.toString(16)}. Allowlist: read ${a.read}, write ${a.write}${a.evidence.length ? ` (${a.evidence.join("; ")})` : ""}`); + console.log(`traced: ${traced.trace.rows.length} distinct accesses over ${traced.trace.frames} frames (${frames} played, then ${traced.results.length} tests)`); + for (const l of a.lines) console.log(` ${l.file}:${l.line} ${l.read && l.write ? "reads and writes" : l.write ? "writes" : "reads"} ${l.match === "exact" ? "this address" : "the same field of another unit"}, ${l.times} times`); + if (a.unlined.exact + a.unlined.field) console.log(` eudplib's own work, under no line: ${a.unlined.exact} times this address, ${a.unlined.field} the same field of another unit`); + if (!a.lines.length && !(a.unlined.exact + a.unlined.field)) console.log(" nothing in the run touched it"); + process.exit(0); + } const inGame = host.runGameTests(handle, { modules: compiled.modules, names, locations, variables, triggers: triggerMap }) as TestReport & { error?: string }; if (inGame.error) { console.error(`the script did not run beside the game: ${inGame.error}`); process.exit(1); } const bySim = new Map((simulated?.results ?? []).map((r) => [r.id, r])); diff --git a/tests/eud-error.test.ts b/tests/eud-error.test.ts new file mode 100644 index 0000000..1b45ca7 --- /dev/null +++ b/tests/eud-error.test.ts @@ -0,0 +1,177 @@ +// Remastered's "EUD ERROR: 0x…" back to the line (TrigScript 4): the code read, the address named, and a traced run's +// lines found, without a game; a host shaped like scm-sim's taps stands in for one. +import { describe, expect, it } from "vitest"; +import { addressOfEudError, eudErrorOf, EudTracer, findEudError, placeOfAddress, UNIT_TABLE, type EudTraceRow } from "../compiler/eudError"; +import { CostLines } from "../compiler/gameCost"; +import { runGameTests } from "../compiler/gameTests"; +import type { Allowlist } from "../compiler/allowlist"; +import type { GameHandle } from "../compiler/gameSim"; +import type { TriggerMap } from "../compiler/gameVariables"; +import { defaultScriptNames } from "../compiler/names"; + +const END = 0x19200000; +const MAP: TriggerMap = { payload: 4000, triggers: [[0, 1, 2, "main.ts:12:5"], [2400, 0, 1, "eudplib"], [1200, 0, 1, "lib.ts:30:3"]] }; +const START = END - 4000; +const cond = (trigger: number, i: number) => START + trigger + 8 + 20 * i; +const act = (trigger: number, i: number) => START + trigger + 8 + 320 + 32 * i; +const unit = (slot: number, offset: number) => UNIT_TABLE.base + slot * UNIT_TABLE.stride + offset; + +describe("the code", () => { + it("is the address with its bits inverted, as the dump probe's step D showed (0x59CD1C, the first unit's +0x74)", () => { + expect(addressOfEudError("EUD ERROR: 0xFFA632E3")).toBe(0x59cd1c); + expect(addressOfEudError("eud error 0xffa632e3")).toBe(0x59cd1c); + expect(addressOfEudError(" 0xFFA632E3 ")).toBe(0x59cd1c); + expect(addressOfEudError("FFA632E3")).toBe(0x59cd1c); + expect(eudErrorOf(0x59cd1c)).toBe("0xFFA632E3"); + expect(addressOfEudError(eudErrorOf(0x6509b0))).toBe(0x6509b0); + }); + + it("is not found in text that holds none", () => { + expect(addressOfEudError("the game ended")).toBeNull(); + expect(addressOfEudError("")).toBeNull(); + expect(addressOfEudError("0x123456789")).toBeNull(); + }); +}); + +describe("where an address lies", () => { + it("names a unit's slot and field", () => { + expect(placeOfAddress(0x59cd1c)).toBe("the unit table: slot 0 of 1,700, order queue head (+0x74)"); + expect(placeOfAddress(unit(3, 0x08))).toBe("the unit table: slot 3 of 1,700, hit points (+0x8)"); + expect(placeOfAddress(unit(1699, 0x2a))).toBe("the unit table: slot 1699 of 1,700, y (+0x2a)"); + expect(placeOfAddress(unit(2, 0x9a))).toBe("the unit table: slot 2 of 1,700, build queue (+2) (+0x9a)"); + }); + + it("names the game's other tables, stats() fields, and an allowlist entry where nothing else knows", () => { + expect(placeOfAddress(0x58a364 + 4 * (2 + 12 * 7))).toBe("deaths, of unit type 7 for P3"); + expect(placeOfAddress(0x57f0f0 + 4)).toBe("minerals, of P2"); + expect(placeOfAddress(0x640b60 + 218 * 2 + 5)).toBe("text lines, line 3 (+5)"); + expect(placeOfAddress(0x6509b0)).toBe("current player"); + expect(placeOfAddress(0x663888 + 2 * 5)).toBe("stats(): the unit table's minerals, entry 5"); + const list: Allowlist = { version: 1, entries: [ + { name: "0x700000", base: 0x700000, width: 16, read: "allowed", write: "allowed", evidence: [], from: "dump" }, + { name: "the game's clock", base: 0x700000, width: 4, read: "allowed", write: "ends", evidence: ["probe 9"] }, + ] }; + expect(placeOfAddress(0x700000, list)).toBe("the game's clock"); + expect(placeOfAddress(0x700008, list)).toBe("no table TrigScript knows"); + }); +}); + +describe("finding the lines", () => { + const rows: EudTraceRow[] = [ + { file: "main.ts", line: 20, address: unit(5, 0x74), write: false, mask: 0xffffffff, times: 3 }, + { file: "main.ts", line: 12, address: unit(0, 0x74), write: false, mask: 0xffffffff, times: 1 }, + { file: "main.ts", line: 12, address: unit(0, 0x74), write: true, mask: 0xffffffff, times: 1 }, + { file: "lib.ts", line: 4, address: unit(0, 0x78), write: false, mask: 0xffffffff, times: 9 }, + { file: null, line: null, address: unit(0, 0x74), write: false, mask: 0xffffffff, times: 7 }, + { file: null, line: null, address: unit(9, 0x74), write: false, mask: 0xffffffff, times: 2 }, + ]; + + it("puts the lines that touched the very address first, then those that touched the field in another unit", () => { + const a = findEudError(0x59cd1c, rows); + expect(a.code).toBe("0xFFA632E3"); + expect(a.lines).toEqual([ + { file: "main.ts", line: 12, match: "exact", read: true, write: true, times: 2 }, + { file: "main.ts", line: 20, match: "field", read: true, write: false, times: 3 }, + ]); + expect(a.unlined).toEqual({ exact: 7, field: 2 }); + }); + + it("matches a byte inside the dword, and only the exact address outside the unit table", () => { + expect(findEudError(0x59cd1e, rows).lines[0]).toMatchObject({ line: 12, match: "exact" }); + const deaths = 0x58a364 + 4 * 30; + const a = findEudError(deaths, [ + { file: "main.ts", line: 3, address: deaths, write: true, mask: 0xff, times: 1 }, + { file: "main.ts", line: 4, address: deaths + 4 * 12, write: true, mask: 0xff, times: 1 }, + ]); + expect(a.lines.map((l) => l.line)).toEqual([3]); + }); + + it("says what the allowlist knows of the address", () => { + const list: Allowlist = { version: 1, entries: [ + { name: "unit table: order queue head", base: UNIT_TABLE.base + 0x74, stride: 336, count: 1700, width: 4, read: "ends", write: "ends", evidence: ["the dump probe, step D"] }, + ], outside: { verdict: "ends", evidence: ["no region"] } }; + const a = findEudError(0x59cd1c, rows, list); + expect([a.read, a.write]).toEqual(["ends", "ends"]); + expect(a.evidence).toEqual(["the dump probe, step D"]); + const outside = findEudError(0x100, [], list); + expect([outside.read, outside.evidence]).toEqual(["ends", ["no region"]]); + }); +}); + +/** The two taps, in small: the cost tap tells of each record (CARRY resolved) before the EUD tap tells of its access. */ +function taps() { + const lines = new CostLines(MAP, END); + let last = -1; + const cost = { group: lines.group as ((r: number) => number) | null, trace: null as ((record: number, group: number, action: boolean) => void) | null, report: () => { throw new Error("not read here"); } }; + const eud = { listen: null as ((record: number, address: number, write: boolean, mask: number, was: number) => void) | null }; + /** A record runs and touches `address`. */ + const run = (record: number, address: number, write = false, mask = 0xffffffff) => { + let g = cost.group!(record); + if (g === -2) g = last; else last = g; + cost.trace?.(record, g < 0 ? -1 : g, write); + eud.listen?.(record, address, write, mask, 0); + }; + return { lines, cost, eud, run, frame: () => { last = -1; } }; +} + +describe("EudTracer", () => { + it("writes each access down under the line its record ran for, eudplib's functions under the line that called them", () => { + const { lines, cost, eud, run, frame } = taps(); + const tracer = new EudTracer({ cost, eud }, lines); + tracer.start(); + run(act(2400, 0), unit(0, 0x0c)); // eudplib's work at the top of the frame: no line yet + run(cond(0, 0), unit(0, 0x08)); // line 12 + run(act(2400, 0), unit(0, 0x74)); // eudplib's f_dwread_epd, called by line 12 + run(act(1200, 0), unit(1, 0x08), true, 0xff00); + run(act(1200, 0), unit(1, 0x08), true, 0x00ff); + frame(); + run(0x51a284, 0x58a364); // the map's own triggers, outside the payload + tracer.stop(); + expect(cost.trace).toBeNull(); + expect(eud.listen).toBeNull(); + expect(tracer.rows()).toEqual([ + { file: null, line: null, address: unit(0, 0x0c), write: false, mask: 0xffffffff, times: 1 }, + { file: "main.ts", line: 12, address: unit(0, 0x08), write: false, mask: 0xffffffff, times: 1 }, + { file: "main.ts", line: 12, address: unit(0, 0x74), write: false, mask: 0xffffffff, times: 1 }, + { file: "lib.ts", line: 30, address: unit(1, 0x08), write: true, mask: 0xffff, times: 2 }, + { file: null, line: null, address: 0x58a364, write: false, mask: 0xffffffff, times: 1 }, + ]); + }); + + it("needs a host with both taps", () => { + const { cost, eud } = taps(); + expect(EudTracer.can({ cost, eud })).toBe(true); + expect(EudTracer.can({ cost })).toBe(false); + expect(EudTracer.can({ eud, cost: { } })).toBe(false); + }); +}); + +describe("runGameTests with a trace", () => { + /** A game that runs line 12's read of unit 0's +0x74 every frame; tests are not asked for. */ + function game(withEud = true) { + const t = taps(); + let frame = 0; + const handle = { + m: new DataView(new ArrayBuffer(8)), u8: new Uint8Array(8), localPlayer: 0, over: false, + get frame() { return frame; }, + step() { frame++; t.frame(); t.run(cond(0, 0), 0x59cd1c); }, + reset() { frame = 0; }, + lines: () => [], action: () => 0, chat() {}, call: () => 0, + cost: t.cost, payloadEnd: END, ...(withEud ? { eud: t.eud } : {}), + } as unknown as GameHandle; + return handle; + } + + it("plays the map with nobody at it and hands the trace back, for a script with no tests", () => { + const report = runGameTests(game(), { modules: [], names: defaultScriptNames(), triggers: MAP, trace: true, play: 5 }); + expect(report.error).toBeUndefined(); + expect(report.trace?.frames).toBe(5); + expect(report.trace?.rows).toEqual([{ file: "main.ts", line: 12, address: 0x59cd1c, write: false, mask: 0xffffffff, times: 5 }]); + expect(findEudError(addressOfEudError("EUD ERROR: 0xFFA632E3")!, report.trace!.rows).lines).toMatchObject([{ file: "main.ts", line: 12, match: "exact" }]); + }); + + it("says why there is no trace when the host or the build cannot give one", () => { + expect(runGameTests(game(false), { modules: [], names: defaultScriptNames(), triggers: MAP, trace: true, play: 1 }).traceError).toMatch(/before EUD traces/); + expect(runGameTests(game(), { modules: [], names: defaultScriptNames(), triggers: null, trace: true, play: 1 }).traceError).toMatch(/where its triggers are/); + }); +}); From 7696363f6375acca846c100a47e98aeff7426863 Mon Sep 17 00:00:00 2001 From: Jeany Date: Sat, 17 Oct 2026 12:07:04 -0600 Subject: [PATCH 069/238] TrigScript 4, S4: Bad Apple drawn with fog of war, played in Remastered The vision countdown (0x51CE98) written to 1 after each trigger pass makes the next frame wipe the visible bits; a brush is a unit made on a tile and removed in the same pass, its sight shape lighting the tiles through the draw. Remastered's own frame step is 1.16.1's line for line. Each brush size is a unit type of its own (a sight write reaches Remastered a frame late), and the Classic minimap draws a tile a pixel. Two fog probes played, then the whole video: 5,216 frames exact in the lifted game, the song in the map, 23 ms between picture and sound with no drift. The video, the song and the encoded table stay out of the repository; build-fixture takes the table as a data file (EXTRA_FILE). Co-Authored-By: Claude Opus 5.5 --- docs/v4-plan.md | 281 ++++++++++++++++++++++++++++++- probes/bad-apple.ts | 13 ++ probes/fog-probe-2.py | 288 ++++++++++++++++++++++++++++++++ probes/fog-probe-2.ts | 14 ++ probes/fog-probe.py | 236 ++++++++++++++++++++++++++ probes/fog-probe.ts | 16 ++ probes/lib/bad-apple-encode.mts | 129 ++++++++++++++ probes/lib/bad-apple-gen.mts | 134 +++++++++++++++ probes/lib/bad-apple-map.mts | 40 +++++ probes/lib/fog-base-map.mts | 26 +++ probes/lib/fog-encode.mts | 42 +++++ probes/lib/fog-gen-2.mts | 214 ++++++++++++++++++++++++ probes/lib/fog-gen.mts | 236 ++++++++++++++++++++++++++ probes/lib/fog-masks.json | 1 + scripts/build-fixture.mts | 4 + 15 files changed, 1673 insertions(+), 1 deletion(-) create mode 100644 probes/bad-apple.ts create mode 100644 probes/fog-probe-2.py create mode 100644 probes/fog-probe-2.ts create mode 100644 probes/fog-probe.py create mode 100644 probes/fog-probe.ts create mode 100644 probes/lib/bad-apple-encode.mts create mode 100644 probes/lib/bad-apple-gen.mts create mode 100644 probes/lib/bad-apple-map.mts create mode 100644 probes/lib/fog-base-map.mts create mode 100644 probes/lib/fog-encode.mts create mode 100644 probes/lib/fog-gen-2.mts create mode 100644 probes/lib/fog-gen.mts create mode 100644 probes/lib/fog-masks.json diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 6504415..7372fb7 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -3,7 +3,8 @@ Written 2026-10-15. Status: Part A built (A0–A5, each with its *As built*), and Remastered's emulator read from a dump (*R0*, 2026-10-16); Part B steered by three showpiece maps (*Showpieces*, 2026-10-16), B1 and B2 built (2026-10-16/17), and an EUD error code taken back to its line -(*EUD error code to line*, 2026-10-17). The question it answers: now that there is a +(*EUD error code to line*, 2026-10-17); a fourth showpiece, Bad Apple, with fog of war found to work as a +framebuffer in the lifted game (*S4*, 2026-10-17). The question it answers: now that there is a lifted copy of the game's own code (scm-sim's exact engine) beside eudplib, what does TrigScript become if it uses the game itself as its checker, and how far can the language go towards the TypeScript people actually write. @@ -1115,6 +1116,7 @@ already says so. | Idea | What it needs | Known | | --- | --- | --- | | Terrain as a framebuffer | tile buffer writes | **No.** Magenta 10: Remastered does not draw them. Dropped. (My first answer on 2026-10-15 proposed it; that was wrong.) | +| Fog of war as a framebuffer | the vision countdown `0x51CE98`, sight writes, Map Revealers made and removed in one pass | **Yes in the lifted game** (S4, 2026-10-17); Remastered's drawing of it is the probe | | Units moved freely every frame (bullet hell, orbits) | location + Move Unit, B6 for the paths | position writes end the game (Magenta 6); moving by action is ordinary, so the probe is how many a frame before it lags (A4's dial) | | Panels of text: objectives, leaderboard label, unit names as live UI | made texts in those fields | **Yes**, the strings spike (2026-09-19): re-read every draw, written only on the target's machine | | Custom command buttons | button set data in memory | unknown: probe | @@ -1332,6 +1334,283 @@ const aim = await sync(localMouse()); // Local → Point, the sa frame in Remastered; then the same in a two-person game, to see each machine reads only its own. +### S4. Bad Apple + +Added 2026-10-17. The shadow-play music video drawn by the game while the song plays, as the +renderer showpiece before a raycaster. It asks the questions a renderer has to answer — +where the game can draw, how fine, how much a frame costs — on a video that is mostly the +same from one frame to the next. A raycaster is the opposite (every column changes when the +player turns), so whatever S4 builds has to be cheap for a whole new frame too, and S4's +scene cuts are where that is measured. + +**Where it draws.** Three surfaces, best first: + +| Surface | Resolution | Known | +| --- | --- | --- | +| Fog of war, on the minimap | 128 × 96 on a 128 × 96 map, one tile a minimap pixel | works in the lifted game (below); Remastered's drawing of it is the probe | +| Units as pixels, on the minimap | ~40 × 30 filled (the 1,700-unit limit) | S2's unit-pixel probe; grey levels if Remastered's minimap takes custom colours (unknown) | +| Glyphs in the text lines | 11 rows × ~60 cells | S2, played; which glyphs exist is read from Remastered's font (below) | + +#### Fog of war as a framebuffer + +Found 2026-10-17 in the 1.16.1 decompile, OpenBW and the lifted game: + +- **How the game keeps fog.** Each tile has a byte of "visible" bits and a byte of + "explored" bits, a bit a player. A unit reveals at once when it is made or moves + (`refresh_unit_vision`), and revealing only ever adds: nothing turns a tile dark again + except a wipe. The wipe is in the frame step (`0x4D94B0`): a countdown at **`0x51CE98`** + goes 100 → 0, and at 0 every tile's visible bits are cleared (`0x480880`) and every unit + reveals again. Optical Flare sets the countdown to 1, so the game itself already + asks for a wipe on the next frame. +- **Remastered's emulator allows `0x51CE98` both ways** (`data/remastered.json`, from R0), + and units.dat's sight range (`0x663238`) both ways too; `stats(t).sight` is already in + the language, played by Magenta. +- **The order of a frame** (`0x4D9670`): the frame step (countdown, wipe, units reveal) + runs before the triggers, and the screen is drawn after both. + +So a frame of video is painted like this, in one trigger pass: + +1. for each brush: set the revealer's sight, centre a location on the brush, Create Unit a + Map Revealer there (it reveals at once), Remove Unit it; +2. write 1 to the countdown. + +The revealed tiles stay visible through the draw with no unit left on the map. The next +frame's step wipes them before the triggers paint the next picture, so nothing ghosts. +Tiles revealed once stay *explored*, so the picture is three-toned: never seen (black), +fog (dark) and visible (bright). The video opens on black, which is the never-seen board. + +**Run in the lifted game** (`fog-spike`, scratch harness, 2026-10-17): the countdown is +seen at 99 … 0 every 100 frames untouched; written to 1, the next frame wipes. With two +brushes a frame (sight 2 and 4, the sight byte changed between the two Create Units) and +the positions alternating, every frame shows exactly that frame's two brushes visible and +the last frame's as fog. No ghost, no unit left alive. + +**The brushes are the game's sight masks.** A Map Revealer is a flyer (units.dat flag +`0x4`), and air sight takes a path of its own (`reveal_sight_at`, in the air branch): it +leaves out the 3 × 3 core, so **sight 0 reveals nothing**, and every mask is a little +lopsided (one row more above the centre than below). Measured, in tiles: sight 1 = 3 × 4 +(12), 2 = 28, 3 = 52, … 11 = 480. A ground unit's sight 0 is a plain 3 × 3, but ground +sight is cut by higher ground, and a revealer made ground (its flyer flag cleared before it +is made; `0x664080` is allowed) is untried. + +**What a frame costs, worked out on test pictures** (128 × 96, greedy cover of the visible +pixels with masks that never touch a dark one): + +| Picture | Brushes | Missed | +| --- | --- | --- | +| A dancer, black on white | 140 | 0.3% | +| An apple, black on white | 83 | 0.5% | +| "Bad Apple" in type | 143 | 0.5% | +| The dancer, white on black | 88 | 10% | + +With five sight values (1, 2, 4, 8, 11) instead of all eleven it takes about 5% more +brushes. Thin *dark* detail is kept (fog fills the gaps between brushes: fingers 3 pixels +wide came out), thin *bright* detail under three tiles is lost. Bad Apple is mostly dark +figures on white, the case this surface draws best. For the white-on-dark scenes the +encoder may cover a few dark pixels next to a thin bright line, so it is drawn a pixel +too thick rather than not at all. + +Every frame is drawn whole, so there is no diff to keep and a scene cut costs what any +frame costs. At ~150 brushes and some ten actions each (location, create, remove, a sight +write when the size changes), a frame is a few thousand actions against A4's budget of +500k. The data is ~3 bytes a brush (x, y, size): at most ~2.4 MB for 3:39 at 23.8 frames +a second before using what frames share. How big a map Remastered loads is not known. + +**Read in Remastered's own code** (the Ghidra project `scr12310-full` and the R0 emulator, +2026-10-17; offsets from the image base): +- **The countdown write reaches the game.** The emulator's region for `0x51CE98` is backed + by a real variable at `+0x1067C0C` (it held 20 in the dump), and Remastered's frame step + (`+0x31C610`) is 1.16.1's line for line: 100 → 0, the flag, the wipe (`+0x3606C0`, the + same OR of 0xFF into every tile). Optical Flare's routine still sets it to 1. The sight + table is backed at `+0xE5A860` (Marine 7, Ghost 9, Vulture 8, Goliath 8). +- **The minimap draws fog a tile at a time, at once** (`+0x361980`): each tile's visible + and explored bits against the local player's masks pick one of the remap tables at + `+0xAD0200` (never seen, fog, visible), and the terrain's minimap colours go through it + into the picture. No blur, no fading: the minimap shows the bits as they are when it is + drawn. +- **Fog is in the multiplayer checksum** (`+0x3A0FF0` hashes one row of the tile array a + frame). Painting is the same on every machine, so this is safe; a fog write made on one + machine only would desync. +- Not found: the code that builds the main view's fog. The shaders have no fog shader; + `mask.glsl` (black, alpha from a texture's red) and `blur.glsl` (a five-tap Gaussian) are + the likely pair, which would give the soft edges, but whether it fades over time is the + probe's to see. + +**Open, for a probe in Remastered:** +- how the main view draws fog that changes every frame: at once or faded (fading would blur + motion), and how soft the edges are; +- how often the minimap is redrawn, and how the three tones look on snow (Ice) against dark + ground; +- whether a created Map Revealer lands on the location's centre (Remastered makes them; + 1.16.1 refuses: `lifted-create-unit-findings`), and whether a sight write between two + Create Units in one pass holds as in the lifted game; +- how many brushes a frame before the game stutters (the unit-pixel probe made 80 a frame + with no slowdown); +- a revealer made as a ground unit, for the 3 × 3 brush. + +**The probe, built 2026-10-17** (`probes/fog-probe.ts` + `probes/fog-probe.py`, made by +`probes/lib/fog-gen.mts` on a 128 × 96 snow map from `probes/lib/fog-base-map.mts`; copied +to `Maps/TrigScript/trigscript-s4-fog-probe.scx` with an answers sheet). Steps A–H: a brush +jumping with the countdown written (A) and not (B), the eleven sizes (C), a brush sliding a +tile a frame (D), four pictures (E), a 72-frame animation (F), the dancer painted 1, 2, 4 +and 8 times a frame (G), ground revealers beside air ones (H). + +Run first in the lifted game with Overlords for the revealers (1.16.1 keeps no Map Revealer +a trigger makes), the tile array read every frame against the encoder's own picture: +**0 tiles differ** in every picture, every animation frame and at 8 paints a frame. A brush +centred on the last two rows or columns is moved inward to fit the map, so the encoder keeps +centres two tiles from the edge (the dancer then misses 58 tiles instead of 27). Ground at +sight 0 is a 3 × 3 (9 tiles), at sight 1 21 tiles. + +Cost (A4's tap, conditions + actions a frame): a picture of 84–143 brushes 25k–42k; the +animation 41k on average, 48k at most; 8 paints a frame (~1,100 brushes) 322k, still under +the 500k budget. About 300 a brush read from the table, most of it `f_dwbreak` unpacking +x, y and size from one dword: a table of pixel positions laid out ready to copy is the first +thing to make cheaper. `game-check`: 0 accesses Remastered ends the game on, 0 ignored, 0 +never seen. + +**Played 2026-10-17** (Remastered, hosted as multiplayer alone, recorded at 2560 × 1440; +frames taken out of the recording and set beside the encoder's pictures): +- **The countdown write works.** A: the brush's old place drops to fog the frame it jumps; + B, with the write left out, both places stay lit until the game's own wipe. +- **A sight write reaches the game a frame late.** Every brush of a pass came out at the + size written last in the pass before: the sizes row (C) was eleven blobs of sight 11, and + the dancer (E) lost her arms and legs to brushes of the largest size. A and D, one size a + frame, were right. So the size can't change within a pass: round 2 gives each size a unit + type of its own (eleven flyers, none with a sight upgrade), each type's sight written to + the same value every frame. +- **The minimap is blurred** in Remastered graphics: soft round blobs, not tiles. The + routine read in the code (`+0x361980`, 2 × 2 pixels a tile) is likely the Classic path; + round 2 is played in both. +- **On the screen** fog over snow is a soft, only slightly darker grey; the strong contrast + is never-seen black against lit snow, which can be had only once per tile. +- **"Cannot create more units."** at 8 paints a frame (~1,100 made and removed): a removed + unit keeps its slot until the next frame. The lifted game agrees (round 2: from 959 a + frame, not at 822). A picture needs 84–160. +- **No slowdown** at any step: the step texts stay on the same offset from game time. + +**Round 2** (`probes/fog-probe-2.*`, `probes/lib/fog-gen-2.mts`, copied to +`Maps/TrigScript/trigscript-s4-fog-probe-2.scx` with an answers sheet): A again, the sizes +as eleven types (C), the pictures (E), the animation (F), ground Map Revealers at sight 0 +in steps of their own (H), and 3 to 10 paints a frame for the unit cap (I). In the lifted +game: 0 tiles differ in C, E and F; the dancer costs 32k a frame (41k with the sight byte +written per brush); `game-check` clean. + +**Round 2 played 2026-10-17**, once in each graphics mode (both recorded, minimaps set side +by side frame for frame): +- **One unit type per size works.** The sizes row grows left to right in both modes, and the + pictures keep their thin parts: the dancer has her hair, both arms, the fingers and both + legs; "Bad Apple" reads as type. +- **Classic graphics draws the minimap a tile a pixel, sharp** (the `+0x361980` routine); + Remastered graphics blurs the same picture by a few tiles. Classic is the one that makes + the video. A map cannot choose the mode, so the map says so when it starts. +- **Ground revealers work in Remastered:** the Map Revealer with its flyer flag cleared, at + sight 0, drew 3 × 3 squares beside the air brushes' 3 × 4. (The flag write took effect + within the pass, unlike the sight write.) A twelfth, finer brush. +- **"Cannot create more units."** from 7 paints a frame (959 made and removed), not at 6 + (822) — Remastered and the lifted game agree to the step. A picture needs 84–160. +- **Never-seen black is a third tone that runs out.** The first picture over a board that + earlier steps had partly lit came out a patchwork: black where nothing had been lit yet, + fog grey where something had. A video has to light the whole board once first (or use + the black on purpose, once), so every figure is the same fog grey. +- **The camera's box is drawn on the minimap**, over the picture. The video's map needs room + off the board to park the camera: a 128 × 128 map with the board in the top 96 rows keeps + the minimap at a tile a pixel and puts the box below the picture. +- On the main view, fog over snow is a slightly darker grey in both modes, soft-edged in + Remastered. The main view is not where the picture is. + +So the renderer is settled: twelve brushes (a ground 3 × 3 and eleven flyer sizes, each its +own unit type, sights set every frame), made and removed in one pass, the countdown written +after it, the board lit once before the first picture, watched in Classic graphics. + +**The video, built 2026-10-17** (the user: fetch one, put the song in the map). The PV +(YouTube FtutLA63Cp8, 480 × 360, 219.1 s) taken to 5,216 frames at the game's own rate +(`fps=1000/42`, area-averaged to 128 × 96, threshold at half); `probes/lib/bad-apple-encode.mts` +(a binary heap over every placement of twelve brushes — the ground 3 × 3 and the eleven air +shapes — then a 3 × 3 on every light tile still dark, so a thin light line is drawn a tile +thick rather than lost): 166 s on 20 processes, **117 brushes a frame on average, 386 at +most**, 0.74% of tiles off from the threshold picture, a table of 2.45 MB (1.34 MB gzipped; +only 19% of brushes carry over from the frame before as they are). `bad-apple-map.mts` +(128 × 128 snow, the song in the archive), `bad-apple-gen.mts` (the eudplib: the board lit once +at frame 2, ten seconds to switch to Classic, the song and the video from frame 240). The +table goes in as a data file (`EXTRA_FILE` in `build-fixture.mts`): a plugin's source may hold +512 KB, data files 4 MB in all — a limit a built-in video import has to plan around. + +In the lifted game: **all 5,216 frames on the board exactly as encoded**, nothing lit below +it; 27.7k actions a frame on average, 90k at most; `game-check` clean (618,861 accesses, none +Remastered refuses). Maps: OGG song 4.4 MB, an 8-bit WAV fallback 6.3 MB. Not known until +played: whether Remastered loads a map this size, plays OGG from a map, and keeps the song in +time with the frames. + +**Played 2026-10-17: it works.** The OGG map loads, the song plays, and the video runs on +the Classic minimap from start to end (the user's recording, 2560 × 1440). Measured by +correlating the board's brightness in the recording with the source frames, and the +recording's sound with the source song, over the whole run and in 25-second windows: +the picture follows the source with a correlation of 0.998; **the picture leads the sound +by 23 ms**, the same in every window to within one recorded frame — no drift over 3½ +minutes, no stutter. Left as it is (half a game frame, below notice). + +The weak point is contrast: fog over snow against lit snow is about 100 against 140 on the +minimap. The minimap's fog colours come from a remap table (1.16.1 `0x59C2C0`, four 256-byte +tables by visible / explored state, read by `0x4A3A40`; Remastered `+0xAD0200`), but the table +lies outside every region Remastered's emulator maps (a read ends the game). So contrast comes +from the terrain: the tile whose lit and fogged minimap colours lie furthest apart. + +#### Glyphs in the text lines + +Read from the install through kascade (2026-10-17). The text lines' shapes come from +`font/bl.ttf` ("BlizzardGlobal", TrueType, 512 units an em), the only font in the install +that fits every played fact (■ □ ● one width, ㅁ U+3000 O ▒ another, █ ▀ ▄ ░ ▓ ▌ ▐ Ⅰ +missing). The game's font sets (`+0x3484A0`, `+0x3466E0`) chain a Latin font first, then +bl.ttf; a Korean, Japanese or Chinese client puts Kostar, UDTypos or arud first instead. + +| Advance | Glyphs | +| --- | --- | +| 427 | ■ □ ▣ ▤ ▥ ▦ ▧ ▨ ▩ ▼ ○ ● ◐ ◑ | +| 358 | ▲ △ ◆ ◈ | +| 393 | ▶ ▷ ◀ ◁ | +| 512 | ▽ ◇ ◎ ▒, box drawing U+2500–254B, arrows, U+3000, full-width forms, kana, CJK, Hangul jamo, 〓 ☆ | +| 461 | Hangul syllables | + +- **No Braille** and no half or quarter blocks: ▒ is the only one of U+2580–259F. +- **A grey ramp at one width** (427, ink measured in the outlines): ○ 11%, □ 13%, ◐/◑ 18%, + ▤ 19.5%, ▣ 20.5%, ▧ 22%, ▦ 24%, ● 25%, ▩ 27%, ■ 33%. Four levels: □ ▤ ▩ ■. ◐ ◑ split a cell + left and right; nothing splits it top and bottom. +- The 427 shapes are small squares with a gap between them; ▒ and the box-drawing glyphs + fill their whole 512 cell and join edge to edge. +- Six 427 cells (2,562) line up with five 512 cells (2,560). +- **A Korean client breaks the board:** Kostar draws the shapes and Hangul all at one width + (900 of 1,000), so ■ and ㅁ come out the same width there. +- Not checked: which size the text lines use (font10 is the likely one), and how the + widths round to pixels in the game. A probe line of ■▣■▣ over □□□□ answers both. + +So the text lines can carry a four-level picture, but only 11 rows of it: the fog is the +picture, and the text lines are for lyrics and a small second copy. + + +#### Everything else that makes it look good + +- **The sound.** 23.8 frames a second on Fastest against the video's 30: frames are chosen + by time. Open: whether Remastered plays a 3:39 sound from a map (OGG or WAV) and how + large; whether the frames keep time against it (under 500k actions they should); chunks + restarted every few seconds as the fallback. +- **Every surface at once:** the video in fog on the minimap; the main view centred on the + board, where the same fog is drawn with Remastered's soft edges; the lyrics in the + transmission line, coloured word by word with S2's JSX; the time in the leaderboard + label. +- **The encoding is most of the look:** frames blended before thresholding at 23.8 fps, + the error spent on outlines (the silhouette's edge is what people recognise), brushes + kept from one frame to the next where they still fit. +- **The music and the video are not ours.** The song is Alstroemeria Records', the video + is Anira's. Fan remakes are a long tradition, but whether a map with the song in it goes + on scmjs.dev or the registry is the user's decision; the video alone with the song + played beside it is the safe form. + +**Needs from the language:** a video imported at build time (`import video from +"./bad-apple.webm"` → a table of brushes, B8's machinery), typed memory for the countdown +(B7: `memory.vision.countdown`, allowed both ways by R0), and the lifted game's test of the +whole video read back from the tile array and held against the source frames (A2). + ### Smaller things that show well Each is small and makes the editor feel like it knows the game. They go in where a slice diff --git a/probes/bad-apple.ts b/probes/bad-apple.ts new file mode 100644 index 0000000..1b77828 --- /dev/null +++ b/probes/bad-apple.ts @@ -0,0 +1,13 @@ +// Bad Apple!! in the fog of war (TrigScript 4, S4). The script below only keeps the map's triggers running every +// frame; the video is badApple, hand-written eudplib made by probes/lib/bad-apple-gen.mts from the encoder's brushes. +// Host it as a multiplayer game, alone, with Classic graphics. Built with (the video and the song are not ours and +// stay out of the repository): +// ffmpeg -i