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.
- isOpen
- info
- scenario
- edit
- update
- undo
- redo
- history
- id
- list
- activate
- open
- create
- export
- save
- saveAs
- close
- renderImage
- test
- resize
- changeTileset
- extras
- sections
- buildSteps
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 itedit
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 itupdate
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 itundo
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 itid
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 itactivate
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 itexport
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 itsave
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 ittest
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.
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.
LocationPatchinterface
interface LocationPatch
name?: string | |
|---|---|
left?: number | |
top?: number | |
right?: number | |
bottom?: number | |
elevationFlags?: number |
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 ( |
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 |
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; |
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: |
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; }[] |
|
TechPatchinterface
interface TechPatch
useDefault?: boolean | |
|---|---|
mineralCost?: number | |
gasCost?: number | |
researchTime?: number | |
energyCost?: number | |
state?: { player: number | "default"; available?: boolean; researched?: boolean; useDefault?: boolean; }[] |
|
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 |
|
|---|---|
shieldsPercent?: number | null | |
energyPercent?: number | null | |
resources?: number | null | |
hangar?: number | null | |
cloaked?: boolean | null |
|
burrowed?: boolean | null | |
inTransit?: boolean | null | |
hallucinated?: boolean | null | |
invincible?: boolean | null |
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 |
|
|---|---|
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 |
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 |
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 ( |
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 |
|---|---|
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 ( |
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 |
|---|---|
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 |
|---|---|
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 |
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 itBeforeBuildSpecinterface
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 |
BuildStepsApiinterface
interface BuildStepsApi
add(spec: BuildStepSpec): Disposable | |
|---|---|
before(spec: BeforeBuildSpec): Disposable | Run |
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 itDocumentHistoryinterface
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 |
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 ( |
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 |
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 |
|
into?: OpenInto | Beside the open map or in place of it; see |
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 |
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 |
|---|
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 ( |
file(): Uint8Array | The whole CHK as Save would write it (the archive extras are |
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 |
remove(index: number): RawEditResult | |
move(from: number, to: number): RawEditResult | Move the occurrence at |
replaceFile(bytes: Uint8Array, options?: ReplaceFileOptions): RawEditResult | Replace the whole CHK, the way File ▸ Open reads one. The map is marked modified,
unless |
chkOf(file: Uint8Array): Promise<Uint8Array> | The CHK inside a map file — an |
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 |
required(): string[] | The sections a file of the open map's revision must carry to load, as Check Map tests them ( |
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 |
// 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 itIsomRebuildResultinterface
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 ( |
fillFlat(rect: Rect, terrainId: number): number | Lay terrain the way a new map is laid over |
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(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 |
|
placeDoodad(doodadId: number, tx: number, ty: number, owner?: number): number | Stamp a doodad (a |
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 |
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 ( |
replaceTerrain(from: TerrainPick, to: TerrainPick, rect?: Rect): number | Tools ▸ Replace Terrain: every tile matching |
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 ( |
placeBlend(x: number, y: number, side: Side, id: number): boolean | The Blend brush: |
tilesFromIsom(): number | null | What StarEdit does after an isometric edit: regenerate every tile from the ISOM
lattice (the reverse of |
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 |
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; |
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 |
copyFog(from: number, to: number): number | Copy one player's fog (0-based) onto the players in the |
floodFog(x: number, y: number, player: number, players: number, mode: FogMode): number | Fog or clear the connected area around (x, y) that shares |
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 |
replace(index: number, trigger: TriggerRecord): boolean | Replace one record; false when there is none at |
remove(indices: readonly number[]): number | |
move(from: number, to: number): boolean | |
fromText(source: string, options?: { replace?: boolean; }): number | Parse the text format ( |
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 |
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: |
SwitchesUpdateinterface
interface SwitchesUpdate
names(): string[] | |
|---|---|
setName(index: number, name: string): void | Name a switch (0-based); |
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 ( |
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 ( |
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 itForcesUpdateinterface
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); |
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 ( |
|---|---|
add(path: string, bytes?: Uint8Array): number | Put |
remove(slot: number, deleteFile?: boolean): boolean | Clear a slot; with |
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
- A view's field is
nullwhere the created units keep the type's default.
- A view's field is
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, |
clear(index: number): boolean | Back to an empty, unticked slot. |