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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1456,11 +1456,14 @@ if (target) target.order("move", locations.Exit);

A unit has `hp`, `shields` and `energy` in whole points, `maxHp` / `maxShields` of its
type, `owner`, `type`, `x`, `y`, `kills`, `orderId`, `cooldown`, `resources` (a mineral
field's), `buildTime` (frames left of its making), `rank`, `matrix` (frames left of a
field's), `buildTime` (frames left of its making; a building finished early keeps the hit
points it had reached, and a Terran one under a third burns, so heal it after), `rank`, `matrix` (frames left of a
defensive matrix), `speed` (how fast it is moving now, in 256ths of a pixel a frame), the
timers `stim` `ensnare` `plague` `lockdown` `maelstrom` `irradiate` `stasis` in the game's
own ticks, and the booleans `invincible`, `hallucinated`, `speedUpgrade`, `cloaked`,
`burrowed`, `underAttack`, `hasTarget` (its order is aimed at a unit) and `inTransport`.
`burrowed`, `underAttack` (the game raised its under-attack alert for it within the last
second or so, which it does for a hit out of the player's view, not one on screen),
`hasTarget` (its order is aimed at a unit) and `inTransport`.
Hit points, shields, energy, kills, the cooldown (the ground weapon's, the air weapon's and
the spells' at once), the resources, the build time, the rank, the matrix (a write gives
250 points of matrix for that many frames; 0 takes it away), the timers, `invincible`,
Expand Down
10 changes: 9 additions & 1 deletion compiler/backend/units.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,15 @@ const FIELDS: Partial<Record<UnitNumField, Bits>> = {
stim: word(0x115, 1), ensnare: word(0x116, 1), lockdown: word(0x117, 1), irradiate: word(0x118, 1), stasis: word(0x119, 1), plague: word(0x11a, 1), maelstrom: word(0x124, 1),
buildTime: word(0xac, 2), rank: word(0x8e, 1), matrix: word(0x114, 1), speed: word(0x38, 4),
};
const HP = 0x08 >> 2, SHIELDS = 0x60 >> 2, ENERGY = word(0xa2, 2), STATUS = 0xdc >> 2, ATTACKED = word(0x87, 1), USER_ACTION = word(0x93, 1);
const HP = 0x08 >> 2, SHIELDS = 0x60 >> 2, ENERGY = word(0xa2, 2), STATUS = 0xdc >> 2, USER_ACTION = word(0x93, 1);
/**
* "Under attack": the attack-notify timer (+0x87), which the game starts when it raises its "under attack" alert for the
* unit — a hit out of the player's view — and runs for about a second. A unit hit on screen in a game played alone does
* not show it (Remastered, Magenta probe 6, 2026-10-25). The last-event bytes BWAPI reads instead (+0x8A timer, +0x8B
* 174 = was hit) are in a dword Remastered's EUD emulator ends the game on (data/allowlist.json, +0x88), so this is the
* one "recently hit" signal a map may read.
*/
const ATTACKED = word(0x87, 1);
/** The status bits a flag is (cloaked: both of two). */
const FLAG_BITS: Partial<Record<UnitFlag, number>> = { invincible: 0x04000000, hallucinated: 0x40000000, burrowed: 0x10, cloaked: 0x300, speedUpgrade: 0x10000000, inTransport: 0x40 };
/** The unit an order is aimed at (a pointer, 0 for none), and the defensive matrix's hit points (a word, 256 a point) beside its timer. */
Expand Down
11 changes: 9 additions & 2 deletions compiler/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,11 @@ ${kw}interface Unit {
readonly orderId: Order;
/** Frames until the unit can attack or cast again, 0 … 255; writing it holds the unit's fire that long (the ground weapon's, the air weapon's and the spells' alike). */
cooldown: number;
/** Frames left of a unit's or building's making, 0 … 65535; 0 once complete. Writing it lengthens or shortens the making. */
/**
* Frames left of a unit's or building's making, 0 … 65535; 0 once complete. Writing it lengthens or shortens the making.
* A building finished early keeps the hit points it had reached, and a Terran one under a third of its full hit points
* burns down (Remastered, played): heal it after.
*/
buildTime: number;
/** The unit's rank, added to the type's (the one shown on its panel: Private, Corporal …), 0 … 255. */
rank: number;
Expand All @@ -173,7 +177,10 @@ ${kw}interface Unit {
speedUpgrade: boolean;
readonly cloaked: boolean;
readonly burrowed: boolean;
/** True for about a second after something hit the unit. */
/**
* True for about a second after the game raised its "under attack" alert for the unit: a hit out of the player's view.
* A unit hit on the player's screen does not count — the game keeps no other "recently hit" mark a map may read.
*/
readonly underAttack: boolean;
/** True while the unit's order is aimed at another unit (attacking, following, repairing …). */
readonly hasTarget: boolean;
Expand Down
4 changes: 3 additions & 1 deletion compiler/gameSim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ class GameUnit implements SimUnit {
get hasTarget() { return this.read("hasTarget", () => this.m.getUint32(this.at + 0x5c, true) !== 0); }
get inTransport() { return this.read("inTransport", () => this.flag(0x00000040)); }
// 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;
stim = 0; ensnare = 0; plague = 0; lockdown = 0; maelstrom = 0; irradiate = 0; stasis = 0;
/** The attack-notify timer (+0x87) running: the game raised its under-attack alert for the unit within the last second or so. */
get underAttack() { return this.read("underAttack", () => this.game.u8[this.at + 0x87] !== 0); }
}

export class GameSim implements TestWorldLike {
Expand Down
23 changes: 21 additions & 2 deletions tests/emitter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1481,6 +1481,7 @@ describe("a map of our own, units", () => {
put(at + 0x38, u.speed ?? 0);
put(at + 0x5c, u.hasTarget ? 0x700000 + 4 : 0);
put(at + 0x8e, u.rank ?? 0, 1);
if (u.underAttack) put(at + 0x87, 12, 1);
put(at + 0xac, u.buildTime ?? 0, 2);
put(at + 0x114, u.matrix ?? 0, 1);
put(at + 0xdc, (u.invincible ? 0x04000000 : 0) | (u.hallucinated ? 0x40000000 : 0) | (u.speedUpgrade ? 0x10000000 : 0) | (u.inTransport ? 0x40 : 0) | (u.burrowed ? 0x10 : 0));
Expand Down Expand Up @@ -1586,7 +1587,7 @@ describe("a map of our own, units", () => {
const units: SimUnitInit[] = [
{ type: 106, owner: 0, x: 400, y: 300, buildTime: 900, rank: 2, speed: 0 },
{ type: 0, owner: 0, x: 420, y: 340, speed: 1024, hasTarget: true, speedUpgrade: true, matrix: 100 },
{ type: 0, owner: 0, x: 430, y: 330, inTransport: true, hallucinated: true },
{ type: 0, owner: 0, x: 430, y: 330, inTransport: true, hallucinated: true, underAttack: true },
];
same([
" const cc = first({ type: units.TerranCommandCenter })!;",
Expand All @@ -1595,7 +1596,7 @@ describe("a map of our own, units", () => {
" let second: Unit | null = null;",
" for (const u of unitsOf(P1, { type: units.TerranMarine })) second = u;",
" print(`${cc.buildTime} ${cc.rank} ${cc.speed} ${m.speed} ${m.hasTarget} ${m.speedUpgrade} ${m.matrix} ${m.inTransport} ${m.hallucinated}`);",
" if (second) print(`${second.inTransport} ${second.hallucinated} ${second.hasTarget} ${second.speedUpgrade}`);",
" if (second) print(`${second.inTransport} ${second.hallucinated} ${second.hasTarget} ${second.speedUpgrade} ${second.underAttack} ${m.underAttack}`);",
" cc.buildTime = 60; cc.rank = 7; m.matrix = 40; m.speedUpgrade = false; m.hallucinated = true; cc.rank += 1;",
" print(`${cc.buildTime} ${cc.rank} ${m.matrix} ${m.speedUpgrade} ${m.hallucinated}`);",
" m.matrix = 0; cc.buildTime = 70000; m.rank = 300;",
Expand All @@ -1617,6 +1618,24 @@ describe("a map of our own, units", () => {
].join("\n"), UNITS, HOME);
});

it("reads a location's size into consts and writes its edges from them, as Magenta's put-back row does", () => {
same([
" const b = bounds(1 as Location);",
" const w = b.right - b.left;",
" const h = b.bottom - b.top;",
" b.left = 576; b.top = 256;",
" b.right = 576 + w; b.bottom = 256 + h;",
" print(`${b.left} ${b.top} ${b.right} ${b.bottom}`);",
" const c = bounds(1 as Location);",
" c.left += 64; c.right += 64;",
" print(`${c.left} ${c.right}`);",
" const d = bounds(1 as Location);",
" const w2 = d.right - d.left;",
" d.left = 100; d.right = 100 + w2;",
" print(`${d.left} ${d.right}`);",
].join("\n"), UNITS, HOME);
});

it("walks a table every place of which has been handed out once, place 0 among them, one place a turn and two", () => {
const full: SimUnitInit[] = Array.from({ length: 1700 }, (_, i) => ({ type: i % 3 ? 0 : 37, owner: i % 3 ? 0 : 1, x: 100 + (i % 50), y: 100 + Math.floor(i / 50) }));
same([
Expand Down
Loading