scmJS docs

api.document

DocumentApi

The open map: what it says, and the three ways of writing to it — edit (terrain and objects, one undo entry), update (the tables every dialog's OK writes) and sections (raw bytes). Opening, saving, exporting and closing are here too.

Members

isOpen

isOpen(): boolean;

info

info(): DocumentInfo | null;

scenario

scenario(): ReadonlyScenario | null;

The live scenario, for reading — see ReadonlyScenario. To change the map, use edit, update or sections.

const scn = api.document.scenario();
if (scn) {
  const marines = scn.units.filter((u) => u.unitId === 0);
  api.ui.status(`${marines.length} marines on a ${scn.width} × ${scn.height} map`);
}
Try it

edit

edit<R>(label: string, build: (tx: EditTransaction) => Sync<R>): EditResult;

Run build against a transaction and record what it did as one undo entry. Operations apply as they are called, so later ones see earlier ones' results. Returns an all-zero result with changed: false when no map is open.

build is synchronous — see Sync. Await what you need (graphics, a pick, a fetch) before the call, then write in one go. The transaction is closed when build returns; a call on it after that throws.

A builder that throws is rolled back: what it had applied is taken off the map again, nothing reaches the history or the modified flag, and the error reaches you.

// One undo entry called "Fill", however many operations it takes.
await api.tileset.load();                 // the async part first
const [, ground] = api.terrain.types();   // the tileset's second flat terrain; none without the graphics
const result = api.document.edit("Fill", (tx) => {
  if (ground) tx.stampTerrain({ x0: 0, y0: 0, x1: 8, y1: 8 }, ground.id);
  tx.placeUnit(api.consts.unit.startLocation, 0, 4 * api.consts.tile, 4 * api.consts.tile);
});
api.ui.status(`${result.tiles} tiles, ${result.units} units`);
Try it

update

update<R>(label: string, build: (tx: UpdateTransaction) => Sync<R>): UpdateResult;

The second kind of write: the tables and settings the dialogs edit — triggers, the string table, switch names, the scenario's own properties — as one transaction, the way a settings dialog's OK applies its whole form at once. Operations apply as they are called; the commit marks the map modified, bumps what the chrome reads and records what changed as one undo entry under label. build is synchronous, as edit's is, and closed when it returns.

An update is not rolled back, so a builder that throws leaves what it wrote: the sections it touched are committed — the map is modified, the chrome re-reads, the undo entry holds what was written — and the error reaches you.

api.document.update("Rename", (tx) => {
  tx.properties({ name: "Lost Temple", description: "Four players." });
});
Try it

undo

undo(): string | null;

redo

redo(): string | null;

history

history(): DocumentHistory;

The undo and redo stacks' tops — the labels the Edit menu shows — and their depths, without moving anything.

const { undo } = api.document.history();
if (undo && (await api.ui.confirm(`Undo "${undo}"?`))) api.document.undo();
Try it

id

id(): number | null;

The id of the map in front — the one info(), scenario() and every write are about — or null with no map. Stable for the map's life, so a plugin can key what it keeps per map on it. (Null too for a scenario a test put in place without the editor.)

list

list(): OpenDocumentInfo[];

Every open map, in the order the editor lists them, the one in front marked active. Empty with no map. The list changes only through open, create, activate and close (by any plugin or the user), each of which fires the "document" event.

for (const map of api.document.list()) api.log(map.id, map.name, map.active ? "(in front)" : "");
Try it

activate

activate(id: number): boolean;

Bring an open map to the front, as clicking its tab does: its history, selections and view come back as they were left, and the "document" event fires with reason "switch". Nothing is re-read. True once it is in front (at once, when it already was); false for an id that is not open. Call it for something the user asked for — the map changing under someone mid-stroke is not a thing a plugin should do on its own.

open

open(file: File | Blob | Uint8Array, fileName?: string, options?: OpenDocumentOptions): Promise<boolean>;

Open a map file (.scx / .scm / .chk), the way File ▸ Open does: beside the open map or in place of it as options.into (or, omitted, the editor's preference) says. In place of a map with unsaved changes, when Preferences say to ask, the Close Scenario dialog comes first and the user may cancel. Resolves true once the file is the map in front, false when the user kept the current map or the file could not be read (the status bar says why). options.viewOnly opens it to look at only.

const [file] = await api.ui.pickFiles({ accept: ".scx,.scm,.chk" });
if (file) await api.document.open(file, file.name, { into: "new" });
Try it
// A replay's map, to draw over and not to change.
await api.document.open(chk, "Lost Temple.scx", { viewOnly: { label: "Replay" } });

create

create(options: NewDocumentOptions): Promise<boolean>;

A blank map the way File ▸ New makes one: flat ground of the tileset's default terrain (or terrainId), an ISOM lattice to match, every section a fresh map needs. Beside the open map or in place of it as open decides, through the same unsaved-changes gate. Resolves true once the new map is in front, false when the user kept the current one.

await api.document.create({ width: 128, height: 128, tileset: "jungle", name: "Sketch", startLocations: 4 });
Try it

export

export(options?: ExportOptions): Promise<File | null>;

The open map as a file, as Save would write it — the remembered save options, the archive extras included — unless saveOptions says otherwise. Null when no map is open.

// The bytes Save would write, without saving: ready to upload or hand to another tool.
const file = await api.document.export({ format: "scx" });
if (file) api.ui.status(`${file.name}: ${file.size} bytes`);
Try it

save

save(options?: SaveDocumentOptions): Promise<boolean>;

File ▸ Save: write the map back where it came from with the options last confirmed for it — into the file when the browser gave a handle, else through the browser's save dialog or as a download; a map with no file yet goes through the Save dialog. Resolves true once written, false when the user dismissed a dialog or the write failed (the status bar and a toast say so).

saveAs

saveAs(options?: SaveDocumentOptions): Promise<boolean>;

File ▸ Save As (or Save Copy As with copy): the Save dialog, resolving as save does.

close

close(id?: number): Promise<boolean>;

File ▸ Close: the map in front, or the open map id names, through the same unsaved-changes gate as open (a map behind is brought to the front first, so the question is about what the user sees). Resolves true once the map is closed — another open map is then in front, or isOpen() is false — false when the user kept it or no map has that id.

renderImage

renderImage(options?: Partial<MapImageOptions>): Promise<Blob | null>;

A picture of the map as File ▸ Export ▸ Image draws it, as a PNG. pixelsPerTile is the one dial (32 is the game's art 1:1, 1 is a minimap); the other options default as the dialog's do. Needs the tileset graphics — without them, or without a map, null.

// A minimap-sized picture of the whole map, four pixels to a tile.
const png = await api.document.renderImage({ pixelsPerTile: 4 });
if (png) await api.ui.saveFile(png, "minimap.png");
Try it

test

test(bytes: Uint8Array, fileName: string, options?: { launch?: boolean; }): Promise<TestMapOutcome | null>;

Tools ▸ Test Map for bytes of the plugin's own — a built map that is not the open one. On the desktop the file is written into the game's Maps\scmJS folder and, with launch (the preference when omitted), the game is started; in a browser it is written into the test folder the user picked once. It never downloads: a browser with no folder answers null, so a plugin that has already saved the file says "open it with Test Map" instead of handing out a second copy. fileName gets an archive's extension if it lacks one.

resize

resize(options: ResizeDocumentOptions): ResizeResult | null;

Scenario ▸ Resize / Crop Map: a transaction outside the undo model that drops both history stacks (as the dialog does). Content keeps its place relative to anchor (a 3 × 3 grid, row-major, 4 = centre, the default); the new ground is terrainId (the tileset's default when omitted); objects outside the new bounds are dropped, locations clamped unless clampLocations is false. Null with no map.

changeTileset

changeTileset(options: ChangeTilesetOptions): Promise<ChangeTilesetResult | null>;

Scenario ▸ Map Properties ▸ Tileset: change ERA and lay the terrain again with the new tileset's terrain (tile numbers do not carry across tilesets; the doodads go with them, everything else stays). Waits for the new graphics so the fill uses real tiles. Like resize, a transaction outside the undo model. Null with no map.

extras

readonly extras: ExtrasApi;

sections

readonly sections: SectionsApi;

buildSteps

readonly buildSteps: BuildStepsApi;

Types

Declarations only this group names.

CombineModetype

type CombineMode = 
/** Zeroed fixed buffer, each occurrence copied over the front in file order. */
"overlay"
/** Every occurrence's records are kept, in file order. */
 | "append"
/** Only the last occurrence is used — of those at least the section's minimum length. */
 | "last"
/** Only the first occurrence is used — of those at least the section's minimum length. */
 | "first"
/**
 * A settings table: each occurrence changes only the entries it marks as not using the
 * game's own values (`EntryLayout`), the rest keep what an earlier one gave them.
 */
 | "entries";

How the game combines repeated occurrences of a section. Which mode applies is a per-section fact (see sections/registry.ts), not something the container can infer.

TerrainPicktype

type TerrainPick = {
	kind: "terrain";
	id: number;
	variation?: number;
} | {
	kind: "tile";
	id: number;
};

What Replace Terrain matches or writes: a flat terrain type (by ISOM id) or one exact tile.

Boundsinterface

interface Bounds
left: number
top: number
right: number
bottom: number

LocationPatchinterface

interface LocationPatch
name?: string
left?: number
top?: number
right?: number
bottom?: number
elevationFlags?: number

TestRoutetype

type TestRoute = "desktop" | "folder" | "download";

TestMapOutcomeinterface

interface TestMapOutcome
route: TestRoute
path: string

The file's name, or its full path when the desktop wrote it.

launched: boolean
message?: string

Why the game did not start, when it was asked to.

SectionKnowledgeinterface

interface SectionKnowledge

What the registry knows about a section name, sized for one map.

name: string
what: string

"Placed units", "String table", …

mode: CombineMode

How the game combines repeated occurrences.

size: number | null

The fixed buffer the game reads the section into, for this map's size; null when the length varies.

stride: number | null

The record length of a list section, or null.

minimum: number | null

The shortest copy the game takes, for a "last" or "first" section; shorter ones are passed over. Null when any length is taken.

modelled: boolean

Whether the editor decodes the section into its model and re-encodes it on save.

SectionInfointerface

interface SectionInfo

One occurrence of a section in the file, as Save would write it.

index: number

Position in the file's section list — what the byte-level calls take.

name: string

The four characters as stored ("VER " keeps its space); junk in a protected map.

offset: number

Byte offset of the eight-byte chunk header within the CHK.

size: number

Payload length.

declaredSize: number

The length field as written; differs from size only when the file ended early.

truncated: boolean
occurrence: number

Which occurrence of this name it is, 0-based, and how many the file has.

occurrences: number
dirty: boolean

Whether the editor has unsaved changes it will encode into this section on save.

spec: SectionKnowledge | null

The registry entry, or null for a section the editor has never heard of.

RebuildResultinterface

interface RebuildResult

What rebuildSections did: the parser's remarks and the sections actually re-encoded.

warnings: string[]
rebuilt: string[]

PlayerPatchinterface

interface PlayerPatch
type?: number
race?: number
color?: number

COLR index 0–15 (playable slots).

rgb?: [ number, number, number ] | null

A CRGB custom colour, or null to go back to the palette colour (playable slots).

force?: number

0-based force (playable slots).

ForcePatchinterface

interface ForcePatch
name?: string
allied?: boolean
alliedVictory?: boolean
sharedVision?: boolean
randomStart?: boolean
flags?: number

The whole flag word, applied before the booleans.

players?: number[]

0-based playable slots the force should contain (others keep theirs); moves them from their forces.

UnitTypePatchinterface

interface UnitTypePatch
useDefault?: boolean

Explicitly back to (or off) the dat defaults; setting any number below turns it off.

name?: string

Custom name; "" restores the default.

hitPoints?: number
shields?: number
armor?: number
buildTime?: number
mineralCost?: number
gasCost?: number
weapons?: { id: number; damage?: number; bonus?: number; }[]
available?: { player: number | "default"; value: boolean | "default"; }[]

PUNI: player 0-based or "default" (the default column); value true / false, or "default" to follow the default column again.

UpgradePatchinterface

interface UpgradePatch
useDefault?: boolean
mineralCost?: number
mineralFactor?: number
gasCost?: number
gasFactor?: number
timeCost?: number
timeFactor?: number
levels?: { player: number | "default"; start?: number; max?: number; useDefault?: boolean; }[]

player 0-based or "default"; useDefault: true puts a player back on the default column.

TechPatchinterface

interface TechPatch
useDefault?: boolean
mineralCost?: number
gasCost?: number
researchTime?: number
energyCost?: number
state?: { player: number | "default"; available?: boolean; researched?: boolean; useDefault?: boolean; }[]

player 0-based or "default"; useDefault: true puts a player back on the default column.

ResizeResultinterface

interface ResizeResult
dx: number
dy: number
unitsDropped: number
spritesDropped: number
doodadsDropped: number
locationsClamped: number
isomRebuilt: boolean

True when ISOM was reconstructed from the tiles; false when it is the flat fill's lattice.

CuwpSlotPatchinterface

interface CuwpSlotPatch

A slot patch the way the dialog and the plugin API both apply one: only the named fields move.

hitPointsPercent?: number | null

null leaves the field at the unit's default (clears its valid bit); a number sets it and the bit.

shieldsPercent?: number | null
energyPercent?: number | null
resources?: number | null
hangar?: number | null
cloaked?: boolean | null

null leaves the state alone; a boolean forces it.

burrowed?: boolean | null
inTransit?: boolean | null
hallucinated?: boolean | null
invincible?: boolean | null

MapFormattype

type MapFormat = "scx" | "scm" | "chk";

SaveOptionsinterface

interface SaveOptions
format: MapFormat
compression: ArchiveCompression

How every archive member is compressed; ignored for a bare .chk.

encrypt: boolean

Encrypt the members as StarEdit does. Every StarCraft build reads it.

omitExtras: string[]

Archive members left out of the file, by name.

stripTerrainEditing: boolean

Leave out ISOM, TILE and DD2: the terrain-editing data the game never reads.

stripBookkeeping: boolean

Leave out IVER, IVE2, IOWN, UPUS, SWNM and WAV: editor bookkeeping the game never reads.

stripUnknown: boolean

Leave out sections whose names the format reference does not know.

mergeRepeats: boolean

Collapse a section that occurs more than once into the bytes the game would act on.

dropTrailing: boolean

Drop bytes after the last section header the file could parse.

BuiltByinterface

interface BuiltBy

A step as the manifest remembers it.

id: string

plugin id/step id.

label: string

The step's label when it ran, for a notice about a step that is no longer there.

ChangeTilesetResultinterface

interface ChangeTilesetResult
from: number
to: number
doodadsDropped: number
spritesDropped: number

Overlay sprites that belonged to the dropped doodads.

refilled: boolean

StartLayouttype

type StartLayout = "ring" | "corners";

StartPlacementResultinterface

interface StartPlacementResult
changes: UnitChange[]
placed: ({ x: number; y: number; } | null)[]

Per player (0-based), where the start location landed, or null when nothing within reach fit.

removed: number

Primitivetype

type Primitive = string | number | boolean | bigint | symbol | null | undefined;

ReadonlyTypedtype

type ReadonlyTyped<T> = {
	readonly [K in keyof T as K extends "set" | "fill" | "copyWithin" | "reverse" | "sort" ? never : K]: T[K];
};

A typed array without the calls that write to it, and with a read-only index.

DeepReadonlytype

type DeepReadonly<T> = T extends Primitive | ((...args: never[]) => unknown) ? T : T extends Uint8Array | Uint16Array | Uint32Array ? ReadonlyTyped<T> : T extends Set<infer V> ? ReadonlySet<DeepReadonly<V>> : T extends Map<infer K, infer V> ? ReadonlyMap<K, DeepReadonly<V>> : T extends readonly (infer V)[] ? readonly DeepReadonly<V>[] : {
	readonly [K in keyof T]: DeepReadonly<T[K]>;
};

T with every property, element and entry read-only, all the way down: arrays become readonly arrays, a Set or Map its read-only form, a typed array ReadonlyTyped.

ReadonlyScenariotype

type ReadonlyScenario = DeepReadonly<Scenario>;

The open map as a plugin sees it: the editor's own live object, typed so that writing to it does not compile. A direct write would skip undo, the file's dirty tracking, the repaint and a shared map's other editors — every change goes through document.edit, document.update or document.sections.

The object is live, so a read after an edit sees the edit; copy what you need to keep ([...scn.units], scn.tiles.slice()). A copy of a record is an ordinary one: { ...scn.units[0], x: 64 } is a UnitRecord to hand to tx.addUnits.

Synctype

type Sync<T> = T extends PromiseLike<unknown> ? "a transaction builder must be synchronous: await before document.edit() / update(), not inside it" : T;

A transaction builder's return type: whatever it likes, so long as it is not a promise.

Everything asynchronous in this API is a promise — await it. The two transaction builders (document.edit and document.update) are the exception, and deliberately so: their operations apply as they are called and one commit closes the transaction when the builder returns, so an async builder would commit the part of its work that ran before the first await and leave the rest with nowhere to land: the transaction is closed once the builder returns, and every call on it throws from then on. Do the awaiting before the call:

await api.tileset.load();                       // the async part, first
api.document.edit("Fill", tx => tx.stampTerrain(rect, id));  // then the write

TypeScript refuses an async builder because of this type; runTransaction and runUpdate also catch one at runtime, for a plugin written in plain JavaScript.

DocumentInfointerface

interface DocumentInfo
name: string
description: string
width: number
height: number
tileset: TilesetId
era: number

ERA as stored (the tileset is era & 7).

version: number

CHK VER: 59 original, 63 hybrid, 205 Brood War, 206 Remastered.

fileName: string | null
modified: boolean
viewOnly: boolean

Open to look at only (OpenDocumentOptions.viewOnly): every write throws and it is never saved.

EditResultinterface

interface EditResult

What one document.edit changed, per list.

changed: boolean
tiles: number
isom: number
units: number
sprites: number
doodads: number
locations: number
fog: number
notes: string[]

MapFileFormattype

type MapFileFormat = "scx" | "scm" | "chk";

ExportOptionsinterface

interface ExportOptions
format?: MapFileFormat

The container: the open file's (else scx) — scx / scm archives, chk the bare scenario.

fileName?: string

The file's name; defaults to the open file's, or the scenario name plus the format.

saveOptions?: Partial<Omit<SaveOptions, "format">>

Compression, encryption and what is left out: over the options Save last used for this map (or its defaults — PKWARE and encryption for a new map, the way it was opened for an opened one), so the bytes are what Save would write unless you say otherwise.

built?: boolean

False gives the map without its build steps (document.buildSteps): the scenario as the editor holds it. By default the file is the one Save would write, steps run — except from inside a step, where it is always the map without them.

SaveDocumentOptionsinterface

interface SaveDocumentOptions

document.save / saveAs.

copy?: boolean

Write a copy: the document keeps its own name, handle and clean state.

ChangeTilesetOptionsinterface

interface ChangeTilesetOptions
tileset: TilesetId
terrainId?: number

ISOM id of the terrain the map is refilled with; the tileset's default when omitted.

keepTiles?: boolean

Keep the tile numbers and change only ERA (what SCMDraft's switch does).

BuildPurposetype

type BuildPurpose = "save" | "test" | "export";

Why the map is leaving the editor: File ▸ Save (and its copies), Tools ▸ Test Map, or document.export.

BuildStepInputinterface

interface BuildStepInput
map: Uint8Array

The map as Save would write it without the steps, or the step before this one's output: an .scx / .scm archive, the archive extras inside.

fileName: string
purpose: BuildPurpose
signal: AbortSignal

Aborted when the user chooses to save without waiting.

BuildStepSpecinterface

interface BuildStepSpec

A compiler that stands between the map and the disk. The editor keeps showing the map the user edits; whenever the map leaves — Save, Test Map, export — every step that applies is run over the bytes, in the order the plugins were activated, and what the last one returns is what is written. The editor stores the map as it was before the steps inside the same file and gives that back on open, so a step's output (generated triggers, a payload in the string table) never shows up in the editor.

A step that throws, or that the user stops waiting for, does not stop a save: the map is written without the steps and a notice says why. Test Map stops instead, since a map without its built part is not the one to test. A bare .chk is never built.

id: string

Unique within the plugin; the file remembers plugin id/id.

label: string

What the notice says is running: "eudplib".

applies(): boolean

Whether the open map has anything for this step. Cheap and synchronous; false leaves Save exactly as it was.

run(input: BuildStepInput): Promise<Uint8Array>

Build. Return an archive (or a bare scenario) — the editor takes the scenario and any new members from it and lays the final file out with the user's save options; the map's own members are written as they were. Throw an Error whose message says what is wrong in the user's terms.

api.document.buildSteps.add({
  id: "stamp",
  label: "Build stamp",
  // Only maps that carry this file are built; every other Save is untouched.
  applies: () => api.document.extras.get("my-plugin\\stamp.txt") !== null,
  async run({ map, purpose }) {
    api.log(`building ${map.length} bytes for ${purpose}`);
    return map; // an archive in, an archive out
  },
});
Try it

BeforeBuildSpecinterface

interface BeforeBuildSpec

Work a plugin does on the document before the map's bytes are produced for Save, Test Map or export — the place to bring generated content up to date, such as writing a script's triggers into the trigger list with document.update. It runs before any build step is asked whether it applies, so a step can rely on what it did.

id: string

Unique within the plugin.

label: string

What a failure notice names: "TrigScript".

applies?(): boolean

Whether there is anything to do for the open map. Cheap and synchronous; every save asks. Always, when omitted.

run(ctx: { purpose: BuildPurpose; signal: AbortSignal; }): void | Promise<void>

May be asynchronous and may write to the document. A throw does not stop the save: the map is written as it stands and a notice shows the error's message, so word it for the user. Inside it document.export() answers the map as it stands, without running anything.

BuildStepsApiinterface

interface BuildStepsApi
add(spec: BuildStepSpec): Disposable
before(spec: BeforeBuildSpec): Disposable

Run spec.run before the map's bytes are produced, every time the map leaves the editor.

builtBy(): BuiltBy[] | null

The steps behind the file the open map came from or was last saved to; null for a plain map.

ExtrasApiinterface

interface ExtrasApi

The files stored in the map archive next to staredit\scenario.chk: custom sounds, graphics, and anything a plugin wants to keep with the map. Names are archive paths with backslashes (staredit\wav\hello.wav). They are written on Save.

list(): string[]
get(name: string): Uint8Array | null
set(name: string, bytes: Uint8Array): void

Add or replace a member; marks the map modified.

remove(name: string): boolean

Remove a member; true when there was one.

// Keep a small JSON file inside the map, in a folder of the plugin's own.
const name = "my-plugin\\notes.json";
const bytes = api.document.extras.get(name);
const notes = bytes ? JSON.parse(new TextDecoder().decode(bytes)) : { opened: 0 };
notes.opened++;
api.document.extras.set(name, new TextEncoder().encode(JSON.stringify(notes)));
Try it

DocumentHistoryinterface

interface DocumentHistory

What document.history() answers.

undo: string | null

Label of the entry Undo would take back; null with nothing to undo.

redo: string | null
undoDepth: number
redoDepth: number

ResizeDocumentOptionsinterface

interface ResizeDocumentOptions

Scenario ▸ Resize / Crop Map's form. Width and height are clamped to 1…256, the anchor to 0…8.

width: number
height: number
anchor?: number

3 × 3 grid, row-major: 0 top-left … 4 centre … 8 bottom-right. Default 4.

terrainId?: number

ISOM id of the terrain for the new area (a TerrainType.id); the tileset's default when omitted.

clampLocations?: boolean

Pull locations that hang past the new edge back inside; default true.

OpenIntotype

type OpenInto = "new" | "current";

Where a map opened or created goes. "new" opens it beside the open one, which stays open behind it; "current" opens it in place of the open one, through the unsaved-changes gate. Omitted, the editor follows its preference (several maps at once, on by default). Either way the untouched blank map the editor started on is replaced, not kept.

OpenDocumentOptionsinterface

interface OpenDocumentOptions
into?: OpenInto
viewOnly?: boolean | { label?: string; }

Open the map to look at, not to change — a replay's map, a preview. It always opens beside the open map (into is ignored), so nothing is replaced and nothing is asked. Panning, zooming, the layers and selecting work; every tool, menu and dialog that would change it is off, and every write throws — edit, update, sections (except replaceFile(…, { asOpened: true }), a reading of the file rather than an edit), resize, changeTileset, extras, the clipboard's cut and paste, save and saveAs unless as a copy, sync.start. It is never kept in Open Recent, and closing it asks nothing. File ▸ Edit a Copy opens an ordinary copy. label is what the tab and the status bar call it ("Replay"); true is the plain "View only".

OpenDocumentInfointerface

interface OpenDocumentInfo

One open map, as document.list() reports it. active marks the one in front — the one every other call reads and writes.

id: number

Stable for the map's life in this session; never reused.

name: string

The scenario's name.

fileName: string | null
tileset: TilesetId
width: number
height: number
modified: boolean
active: boolean
viewOnly: boolean

Open to look at only; see OpenDocumentOptions.viewOnly.

NewDocumentOptionsinterface

interface NewDocumentOptions

File ▸ New's form: size, tileset, and the two strings the dialog asks for.

width: number
height: number
tileset: TilesetId
name?: string

Untitled Scenario when omitted.

into?: OpenInto

Beside the open map or in place of it; see OpenInto.

description?: string
terrainId?: number

ISOM id of the terrain to fill with; the tileset's default ground when omitted.

startLocations?: number

Start locations to lay down for players 1..N, as tx.placeStartLocations would. Part of making the map, so a fresh scenario has no history to undo them from.

startLayout?: StartLayout

RawEditResultinterface

interface RawEditResult

What a raw section edit reported: the parser's remarks about the file it produced.

warnings: string[]

Warnings from parsing the edited file (a truncated section, no usable DIM, …); empty when it read cleanly.

ReplaceFileOptionsinterface

interface ReplaceFileOptions

How document.sections.replaceFile installs the bytes.

asOpened?: boolean

Take the bytes as the file that was opened rather than as an edit: the map stays unmodified, so closing it does not ask to save, while Save writes the bytes as given. For a plugin that repairs a map as it opens — nothing the user did changed it. Use it only right after an "open", or to put back what was opened; anything else would hide a change from the user. The history is dropped either way.

SectionsApiinterface

interface SectionsApi

The scenario file at the byte level: every section occurrence in the order Save would write them, with the bytes it would write — unsaved edits already encoded — and raw edits to any of them.

A raw edit is a different kind of transaction from document.edit: the edited file is parsed again from scratch and installed as the open document, so the change reaches every part of the editor whether or not it models the section — and, as with Resize, the undo history is dropped and every selection cleared. The map is marked modified and the "document" event fires. Keep your own undo if you need one.

Indices are positions in list() and shift when a section is inserted or removed before them; take a fresh list() after every edit.

list(): SectionInfo[]

Every occurrence in file order; empty without a map.

bytes(index: number): Uint8Array

A copy of one occurrence's payload.

combined(name: string): Uint8Array | null

The bytes the game acts on for a name — repeated occurrences combined the way the game combines them (SectionKnowledge.mode) — or null when the file has none.

file(): Uint8Array

The whole CHK as Save would write it (the archive extras are document.extras).

spec(name: string): SectionKnowledge | null

What the editor knows about a section name, sized for the open map; null for an unknown name.

known(): SectionKnowledge[]

Every section the editor knows, sized for the open map.

write(index: number, bytes: Uint8Array): RawEditResult

Replace one occurrence's payload.

rename(index: number, name: string): RawEditResult

Rename one occurrence (four characters; shorter names are padded with spaces).

insert(index: number, name: string, bytes: Uint8Array): RawEditResult

Insert a section before index (list().length appends).

remove(index: number): RawEditResult
move(from: number, to: number): RawEditResult

Move the occurrence at from so that it sits at to.

replaceFile(bytes: Uint8Array, options?: ReplaceFileOptions): RawEditResult

Replace the whole CHK, the way File ▸ Open reads one. The map is marked modified, unless asOpened says the bytes are a reading of the file as it was opened.

chkOf(file: Uint8Array): Promise<Uint8Array>

The CHK inside a map file — an .scx / .scm archive's staredit\scenario.chk, or the bytes themselves for a bare CHK — the way File ▸ Open reads it, for replaceFile. Reads only; rejects with the reason for a file it cannot read.

trailing(): Uint8Array | null

Bytes after the last chunk the reader could parse — what follows a chunk header with a negative length, say. Save writes them back as they are; a replaceFile without them drops them. Null when the file ends cleanly.

required(): string[]

The sections a file of the open map's revision must carry to load, as Check Map tests them (STRx in place of STR on a Remastered file).

defaults(name: string): Uint8Array | null

The bytes File ▸ New would write for a section on a map of this size, tileset and revision — StarEdit's defaults for a settings table, the fixed VCOD, an empty list, null terrain. Null for a section the editor cannot produce: one it does not model, or an optional one a new map has no value for (CRGB, SWNM).

rebuild(names?: readonly string[]): RebuildResult

Re-encode sections from the editor's model, the way Save writes a dirty one, and install the result like any other raw edit. This is what turns a protected file back into a plain one: repeated occurrences collapse into one, a truncated or oversized section comes back at the size the model encodes to, a string table whose offsets point nowhere is rewritten with every string the editor could read. Names the editor does not model, and modelled ones whose model is absent (no ISOM, no settings table), are left as they are and missing from rebuilt; omit names for every modelled section the map has a model for.

// The file's sections, largest first.
const sections = api.document.sections.list().sort((a, b) => b.size - a.size);
for (const s of sections) api.log(s.name, `${s.size} bytes`, s.spec ? "" : "(unknown to the editor)");
Try it

IsomRebuildResultinterface

interface IsomRebuildResult

What tx.rebuildIsom did.

created: boolean

The map had no usable ISOM, so one was created.

changed: number

Lattice values that changed (every one of a created section).

diamonds: number

Diamonds the rebuild resolved from the tiles.

unresolved: number

Diamonds it had to guess — under doodads or off the edge.

Cellstype

type Cells = Rect | Iterable<number>;

Cells for the bulk terrain operations: a tile rect, or cell indices (y * width + x).

EditTransactioninterface

interface EditTransaction
readonly scenario: ReadonlyScenario
readonly width: number
readonly height: number
tileAt(x: number, y: number): number

MTXM (what the game draws) at a cell.

groundAt(x: number, y: number): number

TILE (the ground without doodads) at a cell.

setTile(x: number, y: number, id: number): void

One tile into both sections.

setTiles(cells: Cells, id: number): number

Many tiles into both sections; returns how many changed.

stampTerrain(cells: Cells, terrainId: number, variation?: number): number

The Rect brush: flat left/right pairs by column parity, one random variation per pair (variation pins it). Needs the tileset graphics; returns tiles changed.

fillFlat(rect: Rect, terrainId: number): number

Lay terrain the way a new map is laid over rect, ISOM lattice included when the map has one.

paintIsom(d: Diamond, terrainId: number, extent?: number): boolean

The isometric brush on one diamond: sets its ISOM value and regenerates the tiles around it, cliffs and shores included. Needs ISOM and the tileset; returns whether the terrain could be painted there.

rebuildIsom(): IsomRebuildResult | null

Reconstruct the ISOM section from the tiles — for a map that arrived without one, or whose lattice no longer matches after Rect / Tile edits: exact for terrain that was laid down isometrically, a best guess under doodads and for hand-placed tiles. A missing or wrongly sized ISOM is created (undo removes it again); an existing one gets only the diamonds that differ. Needs the tileset graphics; null without them.

makeUnit(unitId: number, owner: number, x: number, y: number): UnitRecord

A StarEdit-style unit record (fresh serial, valid/used masks) centred on map pixels.

addUnits(records: readonly UnitRecord[]): number[]
removeUnits(indices: readonly number[]): number
updateUnits(indices: readonly number[], patch: (u: UnitRecord) => Partial<UnitRecord>): number
placeUnit(unitId: number, owner: number, x: number, y: number): number

A unit the way the Units palette places one: a building snaps its placement box to the tile grid (when the palette's Snap to grid is on), anything else lands where you say, and nothing leaves the map. No placement checks — ask canPlaceUnit first if you want them. Returns the record's index.

canPlaceUnit(unitId: number, x: number, y: number): boolean

Whether the Units palette's placement checks, with its current options, allow a unit of this type centred there.

makeSprite(kind: SpriteKind, id: number, owner: number, x: number, y: number, opts?: { flipped?: boolean; disabled?: boolean; }): SpriteRecord
addSprites(records: readonly SpriteRecord[]): number[]
removeSprites(indices: readonly number[]): number
placeSprite(kind: SpriteKind, id: number, owner: number, x: number, y: number, opts?: { flipped?: boolean; disabled?: boolean; }): number

makeSprite + addSprites in one, kept on the map; returns the record's index.

placeDoodad(doodadId: number, tx: number, ty: number, owner?: number): number

Stamp a doodad (a dddata.bin id) at a tile; returns its record index, or -1 when unknown or off the map.

removeDoodads(indices: readonly number[]): number
convertDoodads(indices: readonly number[]): number

Make doodads plain terrain: their records go, their tiles stay (in TILE as well as MTXM, so the cells are ground to every tool from here on) and an overlay stays as an ordinary sprite. The way to touch up a ramp or a cliff piece tile by tile. Returns records converted.

paste(clip: Clip, tx: number, ty: number, options?: PasteOptionsSpec): PasteResult

Stamp a Clip with its top-left tile at (tx, ty) inside this transaction — the clipboard's paste without the clipboard: the clip can come from anywhere (clipboard.copy, a library, a file), the user's own clip is left alone, and several stamps, or a stamp and the strokes around it, make one undo step. Every part the clip carries is laid down unless parts says otherwise; mode is "merge" unless given. Terrain and doodads from another tileset are refused, as a paste refuses them, and anything off the map is skipped, both said in the result's notes.

addLocation(bounds: Bounds, name?: string, elevationFlags?: number): number

A location in the lowest free slot (pixel bounds); returns the slot, or -1 when the table is full.

editLocation(index: number, patch: LocationPatch): boolean
removeLocations(indices: readonly number[]): number
setFog(cells: Cells, players: number, mode: FogMode): number

Set ("fog") or clear the players bits (bit n = player n + 1) over cells; creates MASK on first use.

replaceTerrain(from: TerrainPick, to: TerrainPick, rect?: Rect): number

Tools ▸ Replace Terrain: every tile matching from (a flat terrain by ISOM id, or one exact tile) becomes to, over rect or the whole map; pairs are laid as the Rect brush lays them. Returns tiles changed. Terrain picks need the graphics.

fillArea(x: number, y: number, fill: { terrainId: number; } | { tileId: number; }, match?: "terrain" | "tile"): number

The bucket fill: the 4-connected area around (x, y) of the same terrain type (match: "terrain", the Rect fill) or the same exact tile ("tile"), laid with terrainId as the Rect brush would, or set to tileId. Returns tiles changed.

placeBlend(x: number, y: number, side: Side, id: number): boolean

The Blend brush: id goes on the cell beside the anchor tile on side (terrain.blendCandidates ranks what fits). Returns whether the cell was on the map.

tilesFromIsom(): number | null

What StarEdit does after an isometric edit: regenerate every tile from the ISOM lattice (the reverse of rebuildIsom). Needs ISOM and the graphics; returns tiles changed, or null without them.

mirror(cells: Cells): number[]

The cells and their images under the symmetry mode (Tools ▸ Symmetry…), each once — what the built-in brushes paint over. With the mode off, the cells as given.

mirrorPoint(px: number, py: number): { x: number; y: number; }[]

A map pixel and its images under the symmetry mode, the original first.

moveUnits(indices: readonly number[], dx: number, dy: number, snap?: boolean): number

Shift units by a pixel delta; buildings re-snap to the grid when snap (the palette's option when omitted). Returns records changed.

placeStartLocations(options: { players: number; layout?: StartLayout; margin?: number; replace?: boolean; }): StartPlacementResult

Tools ▸ Auto-place Start Locations: one per player on a ring or in the corners, each moved to the nearest spot the placement checks accept; replace removes the existing ones first. Players count from 1.

updateSprites(indices: readonly number[], patch: (r: SpriteRecord) => Partial<SpriteRecord>): number
moveSprites(indices: readonly number[], dx: number, dy: number): number

Shift sprites by a pixel delta, clamped to the map. Returns records changed.

updateDoodads(indices: readonly number[], patch: { owner?: number; disabled?: number; }): number

Change a doodad's owner or disabled flag (its tiles stay). Returns records changed.

restoreAnywhere(): boolean

Put Anywhere (slot 63) back to the whole map; returns whether it had to move.

invertFog(players: number): number

Flip every tile's fog bit for players; returns tiles changed.

copyFog(from: number, to: number): number

Copy one player's fog (0-based) onto the players in the to bit mask (bit n = player n + 1); returns tiles changed.

floodFog(x: number, y: number, player: number, players: number, mode: FogMode): number

Fog or clear the connected area around (x, y) that shares player's fog state, for players; returns tiles changed.

note(text: string): void

A line for the status bar, appended to the label.

UpdateResultinterface

interface UpdateResult

What one document.update changed: every section it marked dirty, in the order the operations touched them, and changed false when they were all no-ops.

changed: boolean
sections: string[]
notes: string[]

TriggerListUpdateinterface

interface TriggerListUpdate

TRIG or MBRF as a list. Operations apply as they are called, like document.edit's.

list(): TriggerRecord[]

The list as it stands, cloned.

count(): number
set(list: TriggerRecord[]): void

Replace the whole list.

add(trigger: TriggerRecord, at?: number): number

Insert one (at the end without at); returns its index.

replace(index: number, trigger: TriggerRecord): boolean

Replace one record; false when there is none at index.

remove(indices: readonly number[]): number
move(from: number, to: number): boolean
fromText(source: string, options?: { replace?: boolean; }): number

Parse the text format (triggers.text.print's inverse) and append the result, or replace the list with replace: true. Strings the text names are interned as it parses. Throws with the line number when the text does not parse.

StringsUpdateinterface

interface StringsUpdate

The string table. Nothing is ever renumbered: set overwrites a slot (every record pointing at it sees the new text) and intern appends rather than reusing a slot something else may share.

list(): (string | null)[]
intern(text: string): number

The index of text: an identical entry when there is one, else a new one. 0 for "".

set(index: number, text: string): void

Overwrite one slot.

apply(list: (string | null)[]): void

Install a whole table; unreferenced trailing blanks are dropped, other indices keep their place.

import(text: string): { replaced: number; added: number; }

File ▸ Import ▸ Strings: index<TAB>text lines (exchange.formatStrings writes them) set in place, indices past the end appended. Returns how many were replaced and added.

SwitchesUpdateinterface

interface SwitchesUpdate
names(): string[]
setName(index: number, name: string): void

Name a switch (0-based); "" clears the name. Creates SWNM on the first one.

UpdateTransactioninterface

interface UpdateTransaction

The second kind of write (see document.update): the tables and settings the editor's own dialogs edit. Operations apply to the scenario as they are called — a string interned on one line is there for the trigger added on the next — and the commit at the end marks the map modified and tells the chrome to re-read.

readonly scenario: ReadonlyScenario
readonly triggers: TriggerListUpdate

TRIG.

readonly briefing: TriggerListUpdate

MBRF, the mission briefing's own list of the same records.

readonly strings: StringsUpdate
readonly switches: SwitchesUpdate

SWNM.

properties(patch: { name?: string; description?: string; }): void

SPRP: the scenario's name and description ("" restores the file-name default).

readonly players: PlayersUpdate

OWNR / SIDE / COLR / CRGB / FORC: the Player Settings and Player Colors dialogs.

readonly forces: ForcesUpdate

FORC: the Force Settings dialog.

readonly unitTypes: UnitTypesUpdate

UNIS / UNIx and PUNI: the Unit Settings dialog.

readonly upgrades: UpgradesUpdate

UPGS / UPGx and UPGR / PUPx: the Upgrade Settings dialog.

readonly techs: TechsUpdate

TECS / TECx and PTEC / PTEx: the Technology Settings dialog.

readonly sounds: SoundsUpdate

WAV and the archive's sound files: the Sound Editor.

readonly cuwp: CuwpUpdate

UPRP / UPUS: the Create Unit with Properties slots (Triggers ▸ Unit Properties Slots…).

setVersion(version: MapVersion, extendedStrings?: boolean): void

Scenario ▸ Map Revision: VER / TYPE and, moving to or from Remastered, the string table's width.

setTextEncoding(encoding: TextEncoding): void

Scenario ▸ Map Revision: how the string table's text is written to bytes. A map's file carries no note of it; the editor guesses on open (document.version().textEncoding) and this is the correction. The strings are unchanged; the table is re-encoded on save.

note(text: string): void

A line for the status bar, appended to the label.

PlayersUpdateinterface

interface PlayersUpdate
list(): PlayerSlotView[]

All 12 slots, 0-based, with the effective colour and force.

set(slot: number, patch: PlayerPatch): boolean

Patch one slot; colours and forces apply to the eight playable slots only.

// Make Player 2 a Zerg computer.
const computer = api.names.playerTypes().find((t) => t.label === "Computer");
const zerg = api.names.races().find((r) => r.label === "Zerg");
if (computer && zerg) {
  api.document.update("Player 2: Zerg computer", (tx) => { tx.players.set(1, { type: computer.value, race: zerg.value }); });
}
Try it

ForcesUpdateinterface

interface ForcesUpdate
list(): ForceView[]
set(force: number, patch: ForcePatch): boolean

UnitTypesUpdateinterface

interface UnitTypesUpdate
get(unitId: number): UnitTypeView

The effective row for a units.dat id — the dat's numbers where the type is on "use default".

set(unitId: number, patch: UnitTypePatch): boolean

Patch one type. Setting any number turns "use default" off for it (seeding the untouched columns from the dat, as the dialog does); useDefault: true puts it back. Hit points are whole points. name is the custom name ("" restores the default); available edits PUNI.

UpgradesUpdateinterface

interface UpgradesUpdate
get(upgradeId: number): UpgradeView
set(upgradeId: number, patch: UpgradePatch): boolean

TechsUpdateinterface

interface TechsUpdate
get(techId: number): TechView
set(techId: number, patch: TechPatch): boolean

SoundsUpdateinterface

interface SoundsUpdate
list(): SoundRow[]

The 512 WAV slots in use, joined with the archive (present says whether the file is there).

add(path: string, bytes?: Uint8Array): number

Put path (staredit\wav\name.wav, or just name.wav) in the first free slot — the existing slot when it is already listed — and, with bytes, store the file in the archive. Returns the slot, or -1 when all 512 are taken.

remove(slot: number, deleteFile?: boolean): boolean

Clear a slot; with deleteFile, remove the archive member too.

CuwpUpdateinterface

interface CuwpUpdate

The 64 Create Unit with Properties slots. A slot is addressed 0-based here; the action that uses one stores the number 1-based (target = slot + 1), as CuwpSlotView.index

    1. A view's field is null where the created units keep the type's default.
list(): CuwpSlotView[]
get(index: number): CuwpSlotView | null
set(index: number, patch: CuwpSlotPatch, used?: boolean): boolean

Patch one slot: a number sets the field and its "applied" bit, null clears the bit; a boolean forces a special state, null leaves it to the unit. used is StarEdit's "in use" tick, on by itself once the slot sets anything.

clear(index: number): boolean

Back to an empty, unticked slot.

Seen in

scmJS 0.7.3 + main@e2d8ba5 · Generated from the repository. The source is under the MIT license, which does not cover what ATTRIBUTION.md lists. StarCraft and Brood War are trademarks of Blizzard Entertainment; this project ships none of their data.