A plugin for scmJS, the browser-based StarCraft 1 / Brood War map editor. Magenta is a trigger editor for map makers: every trigger reads as a sentence with its parameters as chips, one search box adds any condition or action, and the map is the picker. It carries a catalogue of StarCraft: Remastered EUD conditions and actions that read and edit like the game's own, and a few things the game has no record for — copying one death counter into another, comparing two — done as runs of ordinary triggers it generates and keeps up to date.
Magenta writes only the map's triggers (TRIG) and one member of its own in the archive, so the Classic trigger editor, TrigEdit, TrigScript, the AI and Import / Export Triggers all keep working on the same records.
In scmJS: Plugins ▸ Manage Plugins…, paste
https://github.com/scm-js/plugin-magenta
and press Add. To pin a version, add a ref: github:scm-js/plugin-magenta@v0.1.0.
Triggers ▸ Magenta…, or Ctrl+Shift+M, with a map open. The panel floats beside the map
and can be resized from its corner; the map stays in reach while it is open.
The left pane is the map's triggers in the order the game runs them. A trigger's title is its first Comment action, the convention SCMDraft uses, so a title given here shows in every editor. Any further Comment is a note and shows among the actions. A trigger without one shows its sentences. Badges say what is special about a row: EUD for a trigger that reads or writes memory, +34 for one that Magenta generated triggers for, a lock for a run another plugin owns, ! for a problem.
- Search filters by anything in the sentences; the ⋯ menu narrows the list to triggers with a problem, or to EUD ones.
- New adds a trigger after the selected one, in its folder. Type its name and press Enter.
- Folders are for you; the game does not see them. ⋯ ▸ New folder… puts the selected trigger in a new one; drag triggers between folders and to reorder; double-click a folder to rename or remove it. The order in the game is the list from top to bottom, folders included.
- Drag a trigger to move it.
Alt+↑/Alt+↓do the same from the keyboard,Ctrl+Dduplicates — the copy gets build rows and counter steps of its own, so editing one never changes the other —Deletedeletes,Ctrl+/disables or enables every row of it. Ctrl+Ccopies the selected trigger as text in TrigEdit's syntax;Ctrl+Vpastes text from SCMDraft or TrigEdit after the selection.- The panel floats over the map by default; ⋯ ▸ Dock on the right (or Magenta's page in Edit ▸ Preferences, under Plugins) puts it in the right dock instead, where the list stacks over the trigger. The divider between the list and the trigger drags; double-click it, or the ☰ button, to hide the list and give the trigger the whole width. Size, dock and list width are remembered.
Ctrl+Z/Ctrl+Yundo and redo inside the panel. Triggers sit outside the editor's own undo, so this history is Magenta's; it is dropped when the panel closes, and when another editor (Classic, TrigEdit, a plugin) changes the triggers — an undo step is the whole list, and undoing over someone else's change would take that change back too.
The right pane is the open trigger: its name, who it runs for, its conditions and its actions, each a sentence. Click a chip to change it; every chip opens a small picker with a filter field, and arrow keys and Enter work in it.
- A location chip flashes the location on the map as you hover its rows, and has Pick on map: click a location on the map to choose it. Show glides the view there.
- A unit chip has Pick on map too: click any unit to take its type.
- A player chip shows each slot's colour and whether it is human, computer or inactive. For Deaths and Set Deaths it also takes a memory address, for EUD by hand.
- A text chip is a small text box with the colour and effect codes as buttons.
- A switch chip renames the switch from its picker.
- A death counter is "deaths of unit for player" until you name it: press name… on the row, and the cell reads by that name in every trigger, as a chip that picks among the named counters or makes a new one on a cell nothing else uses.
Add a condition… and Add an action… are search boxes: type a few letters of what you want ("give", "hp", "wait", "key") and press Enter; the list shows the native conditions and actions, the EUD catalogue and the counter steps together. Click the empty box to browse all of them by category.
The box also takes the arguments. "give 3 zealots to player 2 at beacon" inserts a Give Units row with the count, the unit, the player and the location already set; "marine max hp 80" the max-HP action for the Marine at 80; "player 1 brings at least 2 marines to beacon" the Bring condition filled in. Unit names, the map's location and switch names, weapons, "player 2" or "p2", the player groups, "at least / at most / exactly", "up by / down by" and numbers are all recognised, and what the box recognised shows next to each hit before you press Enter.
Recipes… next to New inserts a whole trigger, or a few, to start from: give units at a beacon, a countdown that ends the game, respawn a unit, minerals per kill, reinforcements every minute, win by holding a location, defeat when nothing is left, a message at the start, a key that gives minerals, a unit type's stats. Each comes titled, with its chips on sensible defaults (the map's first location, Current Player, a Marine) and a note saying what to change.
Start from the map. Right-click a placed unit on the map and take New trigger about this Marine…, or right-click inside a location for New trigger at Beacon…. The panel opens with a short list to start from: when this unit dies, when it is at the location it stands in, give it to the player who comes, set its hit points (EUD, by its placed-unit slot); when a unit comes to the location, create units there, kill everything there. The one you pick is inserted after the selection with the unit, its owner and the location already in the chips.
Rows have tools on hover: disable (⊘), move up and down, remove. With a row focused, Delete
removes it, Alt+↑ / Alt+↓ move it, Ctrl+/ disables it, Enter opens its first chip.
Problems appear under the row they belong to: a trigger that never fires, two conditions that contradict each other, a Wait inside a preserved trigger, a location or string the map no longer has, a sound that is not in the archive, a memory write Remastered does not allow. On a map whose revision is below Remastered (Scenario ▸ Map Revision), every row only Remastered runs — an EUD row, a counter step, a build row — gets a dim line saying so, and a Set revision to Remastered button above the rows marks the map so older clients refuse it instead of playing it with those rows doing nothing.
Under the rows, In plain words folds out to a short paragraph on the open trigger: who runs it, when it fires, what it does, whether it fires once or keeps firing, on which clock, and how long its Waits hold the owner's other triggers. Below that is what the trigger shares with the rest of the list: each switch, counter, location and the countdown timer it touches, with the triggers that set or read the same thing. Click a name to go to that trigger. A run Magenta generated counts as the trigger that asked for it.
⋯ ▸ Dry run… opens a second panel that runs the triggers here, without the game. It starts from the map as it is — the placed units, the locations, the player slots and forces — and runs the list the way the game does: every player in order, each player's triggers in list order, once and then done unless preserved. Step runs one cycle, Run 10 ten, Run on until someone wins or loses or nothing has happened for a while.
The panel shows:
- The selected trigger, with a verdict on each condition and the value the run saw ("has 0"), for the owner picked at the top. This is the answer to "why did it not fire".
- Cannot tell: the conditions the run could not decide — a key press, a chat command, a build row's answer, a memory read of the game's own state. Tick one to pretend it holds.
- State: the switches that are set, the counters and memory cells with their values, the countdown timer, each player's minerals and gas and how many units they have. Click a value to change it; Put units adds units of a type for a player at a location, which is how to bring a unit to a beacon without the game.
- Log: what fired, the text that was displayed, who won or lost, and where the run had to skip something (an AI script, an order, a build row).
The run knows switches, death counters and the EUD cells that share their table, the countdown timer and elapsed time, resources and scores, units at locations and the actions that create, kill, remove, give and move them, Move Location, alliances, victory and defeat, and a Wait, which holds the owner's other triggers until it ends. It does not fight, move units on its own, or count kills; Modify Unit, Order, sounds, the leader board and the view are skipped. A cycle is two seconds, or a frame when the map runs every frame. Resources start at 0, as in a Use Map Settings game. Editing a trigger while the panel is open stops the run until Reset, which starts over from the map as it is now.
The catalogue (src/catalogue/eud.json) offers conditions and actions the game has no record
for, each a fixed address in the game's memory that a Deaths condition or Set Deaths action
reaches through the deaths table. In the panel they look like any other row, with an EUD
tag; hover the tag for the address, the field's width, whether Remastered lets a trigger read
and write it, and the entry's notes. What is there today:
| Group | Entries |
|---|---|
| Units | max hit points, max shields, armor, build time, mineral and gas cost, supply used and provided, sight range, target acquisition range, ground and air weapon — per unit type. Since 0.3: the speed of a unit type, its name from one of the map's strings, its size (small, medium, large), what it looks like (another type's graphics), and its flags: detector, permanently cloaked, able to cloak or burrow, regenerating, invincible, hero, organic, mechanical, robotic |
| Weapons | damage, damage bonus per upgrade, cooldown, damage factor, range, minimum range |
| Upgrades | mineral and gas cost, research time, maximum level — per upgrade |
| Technologies | mineral and gas cost, research time, energy cost — per technology |
| Players | minerals, gas, an upgrade's level, whether a technology is researched, the stance toward another player, shared vision. Since 0.3: the player's colour, the supply provided, used and capped (by race), and as conditions what the player's slot holds, their race, and whether they have left |
| Game | game speed (a read: writing it does nothing), the trigger timer, the local player, the mouse's position on screen, the screen's position on the map, a keyboard key's state, the frames the game has run, the seconds on the game clock |
| Placed units | hit points, shields, energy, owner, type, position, invincibility, the hallucination flag, cloak — for the unit in a given slot of the game's unit table. The chip lists the map's units with their slots: the first placed unit takes slot 0 and every later one counts down from 1699, as seen in the game. Start locations take no slot, and a unit of a human player who is not in the game takes none either, so later slots shift when a player is missing |
A few things to know:
- Any of these makes the map an EUD map: only StarCraft: Remastered plays it, and the lobby marks it. The narrow fields (a byte, a word, one bit) use Remastered's masked records, which 1.16.1 ignores. Magenta does not change the map's revision for you: a Brood War 1.04 map with EUD rows still opens in 1.16.1, where the rows do nothing. The line under each such row and its Set revision to Remastered button are the reminder.
- Remastered allows a trigger to write only some of the game's memory. An entry it will not let you write is offered as a condition only, and a hand-made write to such an address shows as a problem.
- The mouse, the keyboard, the screen and the local player are local to each computer: a trigger that reads them runs differently on each player's machine, so keep what follows to text, sounds and the view, or it desyncs the game.
- Run triggers every frame (⋯ menu) adds a trigger at the end of the list that sets the trigger timer to 0 each cycle, so the whole list runs every frame instead of every two seconds — what a key or mouse read needs to catch anything. Every Wait and every preserved trigger in the map then runs on that clock.
- A few entries are several records in one row: a unit type's speed is four fields of the game's movement table (the row takes the top speed in pixels a frame and derives the rest), and a player's colour is the unit byte and the minimap byte. The row reads as one and edits as one; in another editor the records show one by one, and the row comes back when they stand together in order. A unit type's speed and looks go through the game data (which movement table a unit type uses), so they are offered once the editor has it.
- What a unit type is named comes from the map's strings: the chip opens the text box, as a Display Text row's does.
- Each entry records where its address came from, and
verifiedturns on once a map has shown it working in Remastered; an entry no game has shown yet wears a dashed EUD tag, and its hover says so. The first runs (2026-09-12) verified the units.dat writes, a weapon's damage, a player's minerals and the placed-unit fields; the probe maps (2026-09-14) the speed, name, looks, flags, colour, supply, upgrade and technology rows. Each address is from Armoha's eud-book (seeATTRIBUTION.md).
A record whose address the catalogue does not know still reads: "memory at 0x…", with the numbers as chips. Maps made with other tools open with their EUD triggers translated where the catalogue can, and untouched where it cannot.
Death counters are the map maker's variables, and the game can only set them, add to them and compare them with numbers. Magenta adds three steps and one comparison that work between counters, as runs of ordinary triggers it generates:
- Copy a counter into another, Add a counter to another, Subtract a counter from another — actions. The trigger that carries one gets a private flag action, and a run of generated triggers follows it in the list, one per bit of the counter (a 16-bit run is half the size, for a counter that stays under 65,536). The run fires in the same cycle, right after the trigger, whatever else the trigger does: the trigger's own rows run first, and the triggers after the run see the result.
- Compare two counters — a condition: "A is greater than / at least / equal to / less than / at most B". A run before the trigger works out the two differences every cycle, and the trigger's conditions read the answer. One comparison per trigger.
- Run this trigger for each player… (⋯ menu) makes one copy of the trigger per player you tick, each owned by that player, with a group of your choice (Current Player by default) replaced by the player in every condition and action. The trigger itself becomes the template and stops running.
Played in Remastered on 2026-09-12: the copy and both comparisons landed in the cycle they were asked for.
The generated runs are hidden in Magenta's list — the trigger they belong to shows +N — and in the Classic editor and TrigEdit they are locked and fenced as Magenta's, with an Open Magenta button that comes back here. Edit the step's chips and the run is rebuilt; remove the row and the run goes with it. The runs use Remastered's masked reads, so they are Remastered-only like the catalogue.
Some rows have no record in the game's own trigger set and only work in a map built by euddraft, which the eudplib plugin runs inside the editor for you (Magenta needs that plugin; the editor installs it with Magenta). They read like any other row, with a BUILD tag, and the trigger that carries one shows a BUILD badge in the list.
Conditions:
- The chat said
-healexactly: fires in the cycle a player sends that message, for every player at once. The chat said-setfollowed by a number matches-set 250and puts the 250 in a counter named Chat number, for the trigger's other rows to use.^…$writes a pattern of your own, as in^-give .*$; the chat plugin wants two.*in one, and Magenta adds the second. - Player 1 pressed A, Player 1 clicked the left button (or the right, or since 0.4 the middle), Player 1's mouse is over Beacon: input that works in multiplayer. Each player's keys, clicks and mouse go out as game commands (the MSQC plugin), so every client agrees on them in the same cycle; the plain EUD reads in the catalogue are local to one computer and cannot do that. With Current Player as the player, one trigger serves everyone. Typing in chat does not count as key presses.
- Any Marine owned by Player 1 at Beacon has hit points below 20: a check over every unit of a kind, every cycle; also shields, energy, kills, x or y, the order id, remaining build time, resources and weapon cooldown, below, above or exactly; since 0.4 hit points %, shields % and energy % against the type's maximum ("below 30%"), and the unit type id and owner; and Any Marine owned by Player 1 is under attack, is targeting something, is burrowed, is in a transport, is moving — or is not.
Actions:
- Show
Score: {Score} pointsto everyone: text with counters' values in it. Write{Counter name}where a value goes. - Set A to A times B / divided by / modulo / a random number below N: the counter maths the game cannot do, in one row.
- For each Marine owned by Player 1 at Beacon: set hit points to 100: a pass over every unit of a kind; also set shields, energy or kills, kill, remove, make invincible or vulnerable, hallucinate, give or take the speed upgrade, give to a player with the colour, selection and control groups following (which the owner byte in the catalogue does not), and center a location on it, so the trigger's other actions can act on that unit through the location. Any unit, anyone and anywhere are the wide settings. Since 0.2: order to move / patrol / attack-move to a location (the game's own order, one unit at a time through a location named Magenta scratch that Magenta makes and moves), apply a spell effect — stim, ensnare, plague, lockdown, stasis, maelstrom, irradiate or a defensive matrix — for so many seconds (the effect without the spell's overlay graphic), hold fire (its cooldowns rewritten each cycle, so put it in a trigger that runs every frame), set the resources of mineral fields and geysers, the remaining build time, the rank, and walk through anything or collide again. Since 0.4: take 20 hit points or give 20 hit points (also shields and energy) — damage and healing that stop at 0 and at the type's maximum, and a unit whose hit points reach 0 dies; a damage zone or a healing aura is one such row in a trigger that fires every cycle.
- Take the Marine owned by Player 1 with the least hit points: kill it, center Pick on it, value into Weakest: the one unit with the least or greatest of a stat, the nearest to a location, or since 0.4 at random (a different one each time the trigger fires) or the nearest to a player's mouse within so many pixels — the unit under the cursor, which with a synced click makes click-to-act gameplay. It does the pass verb you choose to that unit, centres a location on it for the trigger's other actions, and puts the value (the distance, for the nearest) in a counter — each of those optional.
- Set A to the number of Marines owned by Player 1 at Beacon: a count into a counter.
- Set A to the hit points of the first Marine owned by Player 1 at Beacon: a read into a counter; also shields, energy, kills, x, y, the order id, remaining build time, resources, weapon cooldown, and the yes-or-no fields as 1 or 0.
- Move Beacon to 640, 320 keeping its size: a location placed by numbers, in map pixels (32 per tile), with a new width and height if you give one; or since 0.4 Move Beacon by 64, 0 — shifted from where it is, for a moving platform or a scrolling lane.
Text with counters in it also takes {Player 1} for a player's name and {Player 1's colour} to switch to that player's colour, and the colour and effect buttons in the text
box insert the game's own codes.
The Build dialog has a few map-wide things too, chosen once per map: the camera follows
a location for everyone, gliding with an inertia and a top speed (the cammove plugin).
It follows while a switch named cammove is set, so triggers turn it on and off with Set
Switch; Magenta names a free switch, makes the helper location cammoveLoc the plugin needs,
and adds a trigger that sets the switch at the start unless you untick that;
a sound looped as background music (the bgmplayer plugin; a plain WAV's length is read
from the file, otherwise type the seconds); air units pass through one another
(noAirCollision); and lifting the sprite and image limits (unlimiter).
In the map these are one private counter cell each: the trigger sets it (an action) or reads
it (a condition), so it stays an ordinary trigger everywhere. ⋯ ▸ Build EUD map… takes
the map as it stands, adds the code behind the rows, and saves the built map beside the
source, name-eud.scx. The build runs in the editor, through the eudplib plugin: the first
one downloads that plugin's runtime (about 15 MB, from jsDelivr) after asking you, and later
builds are a few seconds each. Nothing about the map leaves your machine. A Build button in the panel's head
says where the map stands — how many triggers need a build, and whether the last built map
is built from the map as it is, stale because the map changed since, or not built;
the record of the last build (its time, file and the map's revision) is kept with the map.
The dialog checks the map before sending it and lists what would go wrong, each with a
Show link to the trigger: the player slot, unit type and location slots synced input
keeps for itself must still be free, a row's locations must still exist, a chat message
must be one the chat plugin takes, the camera's location must have a name, the music must
be in the archive, and the eudplib plugin must be installed and on. Only StarCraft:
Remastered plays a built map. Keep the source map: the built one is what players get, the
way a compiled program is, and while the editor opens it, the rows behind its triggers are
gone into eudplib's code.
An action row does its work once every trigger has had its turn, in the cycle its trigger
fired — so a row below it in the same trigger still sees the map as it was, and a trigger in
the next cycle sees the result. A row that moves a location (Move a location, a pick's box,
a pass's center a location on it) followed by a native row on that location gets a warning
saying so. Two such rows in one trigger, or in two triggers, run in the order the list shows
them. A chat command or an input fires once per message, press or click, before the triggers
of that cycle, and a check is fresh every cycle. A key press or a click read the local way — the
catalogue's Keyboard key state, the mouse, the local player — is each computer's own; a
trigger that reads one and changes the game for everyone gets a warning, since the computers
can disagree and the game drops out of sync. The synced rows are the ones to use. Synced input needs a few things of its own in the map, which Magenta takes:
one location slot for MSQC, eight in a row for the players' mice, a player slot nobody
uses (Player 11) and a unit type that must not appear in the map (the Valkyrie by default).
A key press arrives once per press, the way Remastered reports it; there is no "while the
key is held", so a map that moves a unit while a key is down works from repeated presses. The code behind them is Magenta's own
euddraft plugin, python/magenta.py in this repository, which the build hands to eudplib
along with the map; it turns the rows into eudplib code, and nothing you write in a row is code.
Folders, counter names, the record of each generated run and of each build row live in one
member of the map archive, magenta\magenta.json. Every trigger is in TRIG as the game
reads it, so the map plays without the member — but a build row's sentence is only in the
member: without it the trigger keeps a bare counter action, and a Build has nothing to add
for it. Names and folders and the ability to rebuild the runs go with it too. Save leaves the
member in unless you tick the plugin members out in the Save dialog. A member Magenta cannot
read — written by a newer Magenta, or damaged — is not written over: the panel says so at
the top, keeps the triggers read-only, and offers to drop the data and start over.
maps/ holds four small maps that check the EUD and counter work in StarCraft: Remastered;
npm run maps (which needs a sibling scm-js checkout, and its extracted jungle tileset for
proper terrain) writes them again. Play each as Use Map Settings; the lobby marks them as
EUD maps. Each says on screen what to look for. All four were played on 2026-09-12; the catalogue's verified flags and notes carry what they showed.
| Map | What it checks |
|---|---|
magenta-eud-1-units-dat.scx |
Whole-dword writes (Marine max HP, Player 1's minerals), word writes (Zealot shields), a masked byte at an odd address (Ghost armor), and on the beacon a weapon's damage and cooldown. |
magenta-eud-2-bits-placed-units.scx |
The unit table by slot (the first placed marine gets 5 HP and invincibility, then Player 2 on the beacon), one bit of a dword (vision), a tech byte (Stim Packs), and a read of the slot's unit type. |
magenta-eud-3-reads.scx |
Triggers every frame; reads of the local player, the game speed, the mouse crossing the middle of the screen, and the A key's states. |
magenta-aplus-counters.scx |
A comparison (A > B) at start, a copy of A into B on the beacon, then B = 1234 and A = B: the generated runs, 153 triggers in all. |
Five more, the probe maps, test the candidates in docs/candidates.md — one key per
candidate, and each map says on screen what to press and what to look for. npx tsx scripts/make-probe-maps.mts --build ../plugin-eudplib writes them; 6, 7, 10 and 11 need a build
(the eudplib plugin's own command-line builder, run from a checkout of that repository), and
their -eud.scx copies are the ones to play.
| Map | What it probes |
|---|---|
magenta-probe-5-tables.scx |
Fixed-address writes and reads, no build: flingy speed, upgrade and tech costs, units.dat flags, the unit's name from the map's strings, player colour (two routes), supply, game speed, the frame counter, slot types. |
magenta-probe-6-units-eud.scx |
The per-unit verbs and reads of Magenta's euddraft plugin: orders (two routes), spell timers, cooldown lock, resource amounts, cloak, no-clip, position, the weakest and nearest unit, and scans for attacking, under attack, target, burrowed, moving. |
magenta-probe-7-input-eud.scx |
Held keys and the mouse through MSQC, chat commands with a number in them. |
magenta-probe-8-catalogue.scx |
The 0.3 entries as the catalogue itself writes them (speed as four records, colour as two, the name from a string, the looks through units.dat, the flags, supply by race, upgrade and technology costs), the reads probe 5 left unreported (the frame counter, the clock, slot type and race), and the entries no map had touched: sight range, weapon range and cooldown, build time, gas, an upgrade's level, alliance, a placed unit's energy, shields, position and cloak, the screen. |
magenta-probe-9-rest.scx |
What 8 left unverified: size class by a vulture's shots, the flags with an effect and the rest read back, unit costs and supply, sight, weapon swaps, a weapon's cooldown, factor, bonus and minimum range, vision and alliance both ways, the hallucination flag, and the slot, race, supply-used, cloak, position and screen reads. |
What a run of these settles, in the catalogue: the verified flag on each entry that worked,
which way round the vision bit goes (map 2 with two players), and what the key states 1 and 2
mean (map 3; settled: 1 on the press, 0 otherwise). The slot rule is settled: the first created unit is slot 0, later ones count down from 1699,
and start locations and removed units take no slot (map 2 is the probe that showed it).
magenta.open({ index?: number }) opens the panel on a trigger; the Classic editor's Open Magenta button on a generated row calls it.magenta.describe(aTriggerRecord) answers its conditions and actions as sentences, the catalogue's included.
npm install
npm test # vitest: the sentences, the catalogue, lowering and recognition, the counters, the checks, the expansions in a simulator
npm run typecheck
npm run build # dist/plugin.js, the bundle the editor loadssrc/model/ is the pure half and has the tests: sentences and templates, the EUD arithmetic
and the catalogue, counters, checks, the sidecar, the expansions and a small simulator that
proves them. src/ui/ is the panel, plain DOM in the editor's own widget classes. vendor/
is copied from the editor (the record layout, the definitions table, the unit names) so the
tests run without it; the editor is the source of truth.
To develop against a local editor, serve this folder over http with CORS and add
http://localhost:3131/ as the plugin's address in Plugins ▸ Manage Plugins…; npm run dev
rebuilds the bundle as you edit.
MIT. The catalogue's addresses are from eud-book, MIT; see ATTRIBUTION.md.