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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 192 additions & 6 deletions compiler/backend/emit.ts

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions compiler/backend/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export type Dword =
*/
| { getter: "dest" | "next" }
/** A scratch cell of the graph's (a variable of the Python side's it makes): where a cell's value comes out. */
| { temp: string };
| { temp: string }
/** The `word`-th dword of a printed text's template (`Graph.templates`), which the layout puts on a dword's start. */
| { template: string; word: number };

/**
* One of the Python side's fixed-point functions (part 3): a product or a quotient of two variables, or of one and a
Expand Down Expand Up @@ -82,7 +84,8 @@ export interface Cond { dword: Dword; compare: Compare; value: number; mask?: nu
* trigger of theirs shows it); only a graph made for a map of our own has one (`emitBody`): its text is put in the
* map's strings when the map is made.
*/
export interface Act { dword: Dword; modifier: Modifier; value: Value; mask?: number; at?: At; display?: string }
/** `show`: instead of a write, Display Text of a printed text's template (`Graph.templates`), as it is when shown. */
export interface Act { dword: Dword; modifier: Modifier; value: Value; mask?: number; at?: At; display?: string; show?: string }

/**
* A trigger: its conditions all hold, its actions run, in order. Then the walk goes to `next` — a label of the graph's,
Expand Down Expand Up @@ -135,7 +138,12 @@ export interface Entry { from: string; into: Dword; add: Num; scale?: number }
* `STACK`), this many cells: cell triggers as a routed variable's (`layout.ts`), each holding its value in the number
* of its one action, which adds it where its destination says and goes where its next pointer says.
*/
export interface Graph { entries: Entry[]; triggers: Trigger[]; arrays?: { id: string; stride: number }[]; stacks?: { id: string; cells: number }[] }
/**
* `templates`: the texts a map of our own prints with values in them, each its bytes as built — the text, and a field
* of 12 bytes on a dword's start for each number, which the program fills before it shows the text (0x0D, which draws
* nothing, wherever nothing goes) — ending in a 0.
*/
export interface Graph { entries: Entry[]; triggers: Trigger[]; arrays?: { id: string; stride: number }[]; stacks?: { id: string; cells: number }[]; templates?: { id: string; bytes: number[] }[] }

/**
* A stack, as an array's id (`STACK:` and the function's): `{ element }` is its first cell's number, `{ epdOf }` that
Expand Down
35 changes: 31 additions & 4 deletions compiler/backend/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export interface OwnMap {
address: number;
/** Where each variable's cell is, by id. */
vars: Map<string, number>;
/** Where each printed text's template is: the string the map gives it is to start there. */
templates: Map<string, number>;
}

export interface LayoutInput {
Expand All @@ -53,6 +55,17 @@ export interface LayoutInput {
text: (t: string) => number;
/** The cells of the lists the graph reads where they are, by array id. */
arrays: Map<string, number[]>;
/**
* A printed text's template's index in the map's string table (1-based), for Display Text: the map maker points
* that string at the template, which the layout puts after the triggers (`OwnMap.templates`).
*/
template?: (id: string) => number;
/**
* Where each template already is, when the map maker has put it among the map's strings (the game's own table of
* strings names only its first 64 KB in 1.16.1, which the lifted game is): the layout then leaves them out of the
* payload. Unset, the payload holds them.
*/
templateAt?: Map<string, number>;
}

/**
Expand Down Expand Up @@ -163,6 +176,13 @@ export function layout(input: LayoutInput): OwnMap {
listData.push({ at: next, values, stride: a.stride });
next += 4 * a.stride * Math.max(1, values.length) + 4;
}
// The templates of texts printed with values, each on a dword's start (where the map maker put them, or here).
const templates = new Map<string, number>(input.templateAt ?? []);
for (const t of graph.templates ?? []) {
if (templates.has(t.id)) continue;
templates.set(t.id, next);
next += 4 * Math.ceil(t.bytes.length / 4);
}
const field = (d: Extract<Dword, { at: string }>): number => {
const t = at.get(d.at);
if (t === undefined) throw new Error(`no trigger ${d.at}`);
Expand All @@ -184,6 +204,11 @@ export function layout(input: LayoutInput): OwnMap {
// (A scratch cell of the graph's is the cell of a variable named after it, as the copy from it says.)
if ("temp" in d) return address({ var: `@${d.temp}` });
if ("element" in d && isStack(d.element)) return stackOf(d.element) + 348;
if ("template" in d) {
const t = templates.get(d.template);
if (t === undefined) throw new Error(`no template ${d.template}`);
return t + 4 * d.word;
}
if ("element" in d) {
const l = lists.get(d.element);
if (l === undefined) throw new Error(`the graph reads ${d.element} without saying so`);
Expand Down Expand Up @@ -211,7 +236,7 @@ export function layout(input: LayoutInput): OwnMap {
const encoded = triggers.map((t) => ({
t,
conds: t.conds.map((c) => ({ c, player: epd(address(c.dword)) })),
acts: t.acts.map((a) => ({ a, player: a.display !== undefined ? 0 : epd(address(a.dword)), number: a.display !== undefined ? 0 : value(a.value) })),
acts: t.acts.map((a) => ({ a, player: a.display !== undefined || a.show !== undefined ? 0 : epd(address(a.dword)), number: a.display !== undefined || a.show !== undefined ? 0 : value(a.value) })),
}));
const size = next - base;
const out = new Uint8Array(pad + size);
Expand All @@ -233,8 +258,9 @@ export function layout(input: LayoutInput): OwnMap {
});
acts.forEach(({ a, player, number }, k) => {
const o = n + 328 + 32 * k;
if (a.display !== undefined) {
w32(o + 4, input.text(a.display));
if (a.display !== undefined || a.show !== undefined) {
if (a.show !== undefined && !input.template) throw new Error(`a printed text (${a.show}) with no string to show it by`);
w32(o + 4, a.show !== undefined ? input.template!(a.show) : input.text(a.display!));
dv.setUint8(pad + o + 26 - base, 9);
dv.setUint8(pad + o + 28 - base, 4);
return;
Expand All @@ -250,6 +276,7 @@ export function layout(input: LayoutInput): OwnMap {
w32(n + 2376, 4); // preserved
});
for (const l of listData) l.values.forEach((v, i) => w32(l.at + 4 * l.stride * i, v));
for (const t of graph.templates ?? []) if (!input.templateAt?.has(t.id)) t.bytes.forEach((b, i) => dv.setUint8(pad + templates.get(t.id)! + i - base, b));
// Each cell trigger: Set Deaths, add (its destination and next pointer written by every routing), preserved.
for (let k = 0; k < cellTriggers; k++) {
const c = cellBase + 72 * k;
Expand All @@ -260,7 +287,7 @@ export function layout(input: LayoutInput): OwnMap {
}
const vars = new Map<string, number>([...cells].filter(([k]) => k.startsWith("var:")).map(([k, a]) => [k.slice(4), a]));
routes.forEach((id, k) => vars.set(id, cellBase + 72 * k + 348));
return { payload: out, trig: startUp(owner, base), address: base, vars };
return { payload: out, trig: startUp(owner, base), address: base, vars, templates };
}

/**
Expand Down
40 changes: 40 additions & 0 deletions docs/v4-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -3648,6 +3648,46 @@ same at the same frames built both ways, every one ok, the overflow line in red

Not yet: an array declared in a function that calls itself (its handle on the stack), units and texts kept on it.

### Texts with numbers in them, on our own backend (2026-10-19)

Step 2 of the way off eudplib. eudplib's build makes a number into text by dividing it by ten for each digit, a bit
at a time, and writes the text a byte at a time through a cursor (a read of the dword, then a write). The proposal
was a table of 1,000 three-digit cells; working it out, the table only saves the last step — a number still has to be
cut into groups of three, a division by 1,000 — and the harder part is where the digits go: a number's width changes
from print to print, so everything after it moves a byte, and the game writes whole dwords. What was built instead:

- **A template for each print**, laid out when the map is built: its text, and for each number a field of 12 bytes
on a dword's start, the room before the number filled with 0x0D — a carriage return, which StarCraft draws as
nothing; eudplib pads its strings with it, and its own prints carry it around every number (the lifted game's text
lines through eudplib: `A: \r0\r\r\r, \r\r7…`). Nothing in a text ever moves. A byte from a value (a colour from JSX)
is one byte of the template, 0x0D for a 0.
- **One routine writes a number into a field**, shared by every print: the field set to 0x0D; the number's size (a
signed one's sign apart); how many digits, by comparisons with powers of ten (four at most); then for each digit
from the first a trigger each for 8, 4, 2 and 1 times its power of ten, which takes that much from the size and
adds it to the digit's byte when the size holds it. The '0's and the minus sign go in with the trigger the
comparisons land on. About four triggers a digit.
- **A print** hands each number to the routine, copies its three dwords into the field (routed, as any copy), sets
its bytes, and shows the template with Display Text. The template is a string of the map's: a placeholder three
bytes longer, the template written in it from its first dword's start and the string's offset moved there
(`scripts/build-own.mts`) — among the map's own strings, since 1.16.1's table names only the first 64 KB of them.
- **The values in JavaScript's order**, worked out before a later value's call (the rewriter).

| `probes/own-print.ts` (lifted game, Remastered's weights) | Own | eudplib |
| --- | ---: | ---: |
| One text, one number (frame 6) | 0.001 ms | 0.013 ms |
| A hundred texts of two numbers (frame 13) | **0.133 ms** | 2.411 ms |

Checked: the random programs print now (words, one of them Korean, numbers signed and `u32`, bytes from values): about
9,600 texts in the suite compared byte for byte with what JavaScript makes of them (less the 0x0D), as graphs and from
the laid-out bytes, and in the programs with functions that call themselves. `probes/own-print.ts`: its twenty lines
the same at the same frames built both ways, once the 0x0D are taken out of both; game-check: 178 accesses, none ending
the game.

- **Played in Remastered (2026-10-19):** `trigscript-own-print.scx` printed every line word for word as the lifted
game — the Korean too — with nothing drawn where the 0x0D are, in Remastered's graphics and, after F5, in Classic's.

Not yet: a fixed number printed (`3.25`), a player's name or colour, a text of the program's; `position: "center"`.

## Part C — the game, pushed

Each is a library over Part B, offered only after its probe. "Known" means a played probe
Expand Down
30 changes: 30 additions & 0 deletions probes/own-print.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// TrigScript 4, the backend: texts printed with numbers in them, in a map of our own (scripts/build-own.mts). Each text
// is laid out when the map is built, a field of 12 bytes for each number padded with 0x0D — a carriage return, which
// StarCraft draws as nothing — and a routine shared by every print writes the number's digits into it. Built both ways (through eudplib,
// and as a map of our own), the lifted game should print the same lines at the same frames once the 0x0D are left out;
// in the game, both should look the same: no gaps, boxes or marks where the padding is. The values are variables, so
// nothing here is worked out when the map is built. (A byte from a value — a colour from JSX — is checked by the
// emitter's random programs, not here.)
// npx tsx scripts/build-own.mts probes/own-print.ts /tmp/own-print.scx
// npx tsx scripts/build-fixture.mts probes/own-print.ts /tmp/own-print-eud.scx
import { program, print, sleep, frames } from "trigscript";

program(() => {
let zero = 0, seven = 7, n = 42, k = 1000, big = 65535, minus = -1, low = -2147483648, high = 2147483647;
let apples = 12, score = 1234, full: u32 = 4294967295;
print(`A: ${zero}, ${seven}, ${n}, ${k}, ${big}`);
print(`B: ${minus}, ${low}, ${high}`);
print(`C: ${full} as a u32, ${minus} as a number`);
print(`${apples} apples come first in E`);
print(`F: 점수 ${score}점`);
sleep(frames(4));
// G: a countdown, a line a frame.
let left = 3;
while (left > 0) { print(`G: ${left}...`); left--; sleep(frames(1)); }
print("G: go");
sleep(frames(4));
// H: a hundred texts in one frame (the last ones stay on the screen).
let i = 0;
while (i < 100) { print(`H: line ${i} of ${100 - i} left`); i++; }
print("Own print: done.");
});
30 changes: 25 additions & 5 deletions scripts/build-own.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { join, resolve } from "node:path";
import ts from "typescript";
import { compileScript } from "../compiler/compiler";
import { emitBody } from "../compiler/backend/emit";
import { layout } from "../compiler/backend/layout";
import { STRINGS, layout } from "../compiler/backend/layout";
import type { ArrayDecl, FuncDecl, VarDecl } from "../compiler/ir";
import type { Graph } from "../compiler/backend/graph";
import { defaultScriptNames } from "../compiler/names";
Expand Down Expand Up @@ -76,6 +76,15 @@ applyUnitChanges(scn, addUnits(scn, placed));
// The texts the graph shows, in the strings first: the payload goes after them.
const texts = new Map<string, number>();
for (const t of (graph as Graph).triggers) for (const a of t.acts) if (a.display !== undefined && !texts.has(a.display)) texts.set(a.display, internString(scn, a.display));
// A string for each text printed with values in it, as long as its template and three bytes more: once the strings are
// laid out, the template goes in it from its first dword's start on, and the string's offset is moved there (the
// game's own table of strings names only its first 64 KB in 1.16.1, so the template stays among them, as eudplib's
// buffer does).
const templates = new Map<string, number>();
for (const t of (graph as Graph).templates ?? []) {
const name = `(TrigScript text ${t.id})`;
templates.set(t.id, internString(scn, name + "\r".repeat(Math.max(0, t.bytes.length + 3 - name.length))));
}
markDirty(scn, "TRIG", "UNIT", "STRx");
const chk = serializeScenario(scn);

Expand All @@ -93,10 +102,21 @@ const strx = sections.find((s) => s.name === "STRx");
if (!strx) { console.error("the map came out with no STRx"); process.exit(1); }
const values = new Map<string, number[]>();
for (const a of program.arrays ?? []) if (a.values) values.set(a.id, a.values);
const own = layout({ graph, strings: strx.body.length, owner: program.owner, text: (t) => texts.get(t)!, arrays: values });
const withPayload = new Uint8Array(strx.body.length + own.payload.length);
withPayload.set(strx.body);
withPayload.set(own.payload, strx.body.length);
// (STRx: the count, then a four-byte offset a string from the section's start; the section is at STRINGS in the game.)
const strings = new Uint8Array(strx.body);
const offsets = new DataView(strings.buffer);
const templateAt = new Map<string, number>();
for (const t of (graph as Graph).templates ?? []) {
const index = templates.get(t.id)!;
const at = (STRINGS + offsets.getUint32(4 * index, true) + 3) & ~3;
strings.set(t.bytes, at - STRINGS);
offsets.setUint32(4 * index, at - STRINGS, true);
templateAt.set(t.id, at);
}
const own = layout({ graph, strings: strings.length, owner: program.owner, text: (t) => texts.get(t)!, arrays: values, template: (id) => templates.get(id)!, templateAt });
const withPayload = new Uint8Array(strings.length + own.payload.length);
withPayload.set(strings);
withPayload.set(own.payload, strings.length);
strx.body = withPayload;
const trig = sections.find((s) => s.name === "TRIG");
if (trig) trig.body = own.trig;
Expand Down
Loading
Loading