@scm-js/plugin-api 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/index.d.ts +3411 -0
  4. package/package.json +18 -0
package/index.d.ts ADDED
@@ -0,0 +1,3411 @@
1
+ /**
2
+ * Type declarations for the scmJS plugin API — version 1.
3
+ *
4
+ * Generated by `npm run build:plugin-types` in github.com/scm-js/scm-js and published
5
+ * to github.com/scm-js/plugin-api. Do not edit: the contract is `src/plugins/api.ts`
6
+ * there, and `docs/plugins.md` is the guide.
7
+ */
8
+ /**
9
+ * CHK is a flat stream of chunks: 4-byte name, int32 length, then that many bytes.
10
+ *
11
+ * StarCraft applies chunks *sequentially into fixed-size buffers*, so a section that
12
+ * appears twice is not "last one wins" — the later copy overwrites only as many bytes
13
+ * as it carries. Protected maps lean on this constantly, so the reader keeps every
14
+ * occurrence in file order and `layer()` reproduces the overwrite semantics.
15
+ */
16
+ export interface ChkSection {
17
+ /** 4-character chunk name, latin1. May contain junk in protected maps. */
18
+ name: string;
19
+ /** Byte offset of the chunk's *name* within the CHK. */
20
+ offset: number;
21
+ /** Length field as written, which may disagree with `data.length` when truncated. */
22
+ declaredSize: number;
23
+ data: Uint8Array;
24
+ /** Set when the declared size ran past the end of the file. */
25
+ truncated?: boolean;
26
+ }
27
+ export interface ChkFile {
28
+ sections: ChkSection[];
29
+ /** Bytes after the last parseable chunk header, if any. */
30
+ trailing?: Uint8Array;
31
+ }
32
+ /**
33
+ * How the game combines repeated occurrences of a section. Which mode applies is a
34
+ * per-section fact (see sections/registry.ts), not something the container can infer.
35
+ */
36
+ export type CombineMode =
37
+ /** Zeroed fixed buffer, each occurrence copied over the front in file order. */
38
+ "overlay"
39
+ /** Every occurrence's records are kept, in file order. */
40
+ | "append"
41
+ /** Only the last occurrence is used. */
42
+ | "last"
43
+ /** Only the first occurrence is used. */
44
+ | "first";
45
+ /**
46
+ * STR/STRx string table.
47
+ *
48
+ * Index 0 means "no string" and is never stored. Indices are referenced from TRIG,
49
+ * MRGN, SPRP and friends — several of which we round-trip as raw bytes — so the table
50
+ * must keep its index space stable across a save. Entries are therefore addressed by
51
+ * position, never renumbered.
52
+ */
53
+ export interface StringTable {
54
+ /** `strings[i]` is string index `i`; slot 0 is always null. */
55
+ strings: (string | null)[];
56
+ /** True when the source section was STRx (Remastered, 32-bit count and offsets). */
57
+ extended: boolean;
58
+ }
59
+ export interface UnitRecord {
60
+ serial: number;
61
+ x: number;
62
+ y: number;
63
+ unitId: number;
64
+ relationType: number;
65
+ /** Bit set of which "special properties" fields below are meaningful. */
66
+ validProperties: number;
67
+ validStates: number;
68
+ owner: number;
69
+ hitPointsPercent: number;
70
+ shieldPercent: number;
71
+ energyPercent: number;
72
+ resourceAmount: number;
73
+ hangarUnits: number;
74
+ stateFlags: number;
75
+ unused: number;
76
+ relatedSerial: number;
77
+ }
78
+ export interface SpriteRecord {
79
+ /** sprites.dat id for a pure sprite, units.dat id for a unit sprite (see `SpriteFlag.PureSprite`). */
80
+ spriteId: number;
81
+ x: number;
82
+ y: number;
83
+ owner: number;
84
+ unused: number;
85
+ /**
86
+ * `SpriteFlag` bits. StarEdit writes a doodad's overlay sprite with the doodad's whole
87
+ * CV5 flag word here, so real maps carry the terrain bits (0x80, 0x100, …) too.
88
+ */
89
+ flags: number;
90
+ }
91
+ /**
92
+ * One placed doodad, as StarEdit records it (the game never reads this section: it sees
93
+ * only the doodad's tiles in MTXM and its overlay in THG2). `doodadId` is the index into
94
+ * the tileset's `dddata.bin`, not a CV5 group; `x`/`y` are the pixel centre of the
95
+ * footprint, so the top-left tile is `x / 32 - width / 2`.
96
+ */
97
+ export interface DoodadRecord {
98
+ doodadId: number;
99
+ x: number;
100
+ y: number;
101
+ owner: number;
102
+ /** 0 = enabled (every doodad in Blizzard's maps), 1 = disabled. */
103
+ disabled: number;
104
+ }
105
+ export interface LocationRecord {
106
+ left: number;
107
+ top: number;
108
+ right: number;
109
+ bottom: number;
110
+ nameIndex: number;
111
+ /** Bit 0 low ground, 1 medium, 2 high, 3 low air, 4 medium air, 5 high air. */
112
+ elevationFlags: number;
113
+ }
114
+ export interface Forces {
115
+ /** Force index (0-3) each of the 8 playable slots belongs to. */
116
+ playerForce: number[];
117
+ /** String index of each force's name. */
118
+ nameIndex: number[];
119
+ /** Bit 0 random start, 1 allied, 2 allied victory, 3 shared vision. */
120
+ flags: number[];
121
+ }
122
+ export interface PlayerRgb {
123
+ /** `[r, g, b]` for each of the 8 playable slots; only read when `mode` is `Custom`. */
124
+ rgb: [
125
+ number,
126
+ number,
127
+ number
128
+ ][];
129
+ /** A `ColorMode` per slot. */
130
+ mode: number[];
131
+ }
132
+ export interface UnitSettings {
133
+ /** 1 = the game uses units.dat / weapons.dat for this type and ignores the columns below. */
134
+ useDefault: Uint8Array;
135
+ /** Fixed point: hit points × 256, like units.dat. */
136
+ hitPoints: Uint32Array;
137
+ shields: Uint16Array;
138
+ armor: Uint8Array;
139
+ /** Game frames (15 per second at Fastest). */
140
+ buildTime: Uint16Array;
141
+ mineralCost: Uint16Array;
142
+ gasCost: Uint16Array;
143
+ /** String index of the custom name, 0 for the default one. */
144
+ nameIndex: Uint16Array;
145
+ /** Per weapons.dat id, always `WEAPONS_BW` long; UNIS only stores the first 100. */
146
+ weaponDamage: Uint16Array;
147
+ weaponBonus: Uint16Array;
148
+ }
149
+ /**
150
+ * Which players may build which unit types. Three tables, player-major (`puniIndex`):
151
+ * a per-player answer, the global default, and whether each player defers to it.
152
+ */
153
+ export interface UnitAvailability {
154
+ /** 1 = the player can build the type; read only where `playerUsesDefault` is 0. */
155
+ playerAvailable: Uint8Array;
156
+ /** 1 = the type is available to every player that uses the default. */
157
+ defaultAvailable: Uint8Array;
158
+ /** 1 = the player takes `defaultAvailable` for the type. */
159
+ playerUsesDefault: Uint8Array;
160
+ }
161
+ /**
162
+ * Research cost per upgrade: a base and a per-level factor for minerals, gas and time.
163
+ * Struct of arrays over 61 upgrades; the original sections only store the first 46.
164
+ */
165
+ export interface UpgradeSettings {
166
+ /** 1 = the game uses upgrades.dat for this upgrade and ignores the columns below. */
167
+ useDefault: Uint8Array;
168
+ mineralCost: Uint16Array;
169
+ mineralFactor: Uint16Array;
170
+ gasCost: Uint16Array;
171
+ gasFactor: Uint16Array;
172
+ /** Game frames. */
173
+ timeCost: Uint16Array;
174
+ timeFactor: Uint16Array;
175
+ }
176
+ /**
177
+ * How far each player may research each upgrade and where they start. Player-major
178
+ * tables (`upgradeIndex`), a global default pair, and a per-player "use the default" flag.
179
+ */
180
+ export interface UpgradeRestrictions {
181
+ playerMax: Uint8Array;
182
+ playerStart: Uint8Array;
183
+ defaultMax: Uint8Array;
184
+ defaultStart: Uint8Array;
185
+ playerUsesDefault: Uint8Array;
186
+ }
187
+ export interface TechSettings {
188
+ /** 1 = the game uses techdata.dat for this ability. */
189
+ useDefault: Uint8Array;
190
+ mineralCost: Uint16Array;
191
+ gasCost: Uint16Array;
192
+ /** Game frames. */
193
+ researchTime: Uint16Array;
194
+ energyCost: Uint16Array;
195
+ }
196
+ /** Whether each player may research each ability and whether they start with it. Same shape as `UpgradeRestrictions`. */
197
+ export interface TechRestrictions {
198
+ playerAvailable: Uint8Array;
199
+ playerResearched: Uint8Array;
200
+ defaultAvailable: Uint8Array;
201
+ defaultResearched: Uint8Array;
202
+ playerUsesDefault: Uint8Array;
203
+ }
204
+ export interface CuwpSlot {
205
+ /** `CuwpValid` bits. */
206
+ validProperties: number;
207
+ /** `CuwpField` bits. */
208
+ validFields: number;
209
+ /** Unused by the game; StarEdit writes 0. */
210
+ owner: number;
211
+ hitPointsPercent: number;
212
+ shieldsPercent: number;
213
+ energyPercent: number;
214
+ resources: number;
215
+ hangar: number;
216
+ /** `CuwpState` bits. */
217
+ stateFlags: number;
218
+ /** The record's last four bytes, kept for fidelity. */
219
+ unused: number;
220
+ }
221
+ export interface ConditionRecord {
222
+ /** 1-based location number, 0 = none. */
223
+ location: number;
224
+ /** `PlayerGroup`. */
225
+ player: number;
226
+ amount: number;
227
+ unitId: number;
228
+ /** `Comparison` for numeric conditions; `SwitchState` for Switch. */
229
+ comparison: number;
230
+ /** `ConditionType`. */
231
+ type: number;
232
+ /** `ResourceType` / `ScoreType` / switch number, per type. */
233
+ resource: number;
234
+ /** `ConditionFlag` bits. */
235
+ flags: number;
236
+ /** EUD mask word; 0 in ordinary maps. */
237
+ mask: number;
238
+ }
239
+ export interface ActionRecord {
240
+ /** 1-based source location, 0 = none. */
241
+ location: number;
242
+ /** String index for text / comment / leaderboard label, 0 = none. */
243
+ text: number;
244
+ /** String index of the WAV file name, 0 = none. */
245
+ wav: number;
246
+ /** Milliseconds for Wait / Transmission / Talking Portrait; WAV duration for Play WAV. */
247
+ time: number;
248
+ /** `PlayerGroup` (first). */
249
+ player: number;
250
+ /** Second player / destination location / amount / AI script code, per type. */
251
+ target: number;
252
+ /** Unit id / `ScoreType` / `ResourceType` / `AllianceStatus`, per type. */
253
+ unitId: number;
254
+ /** `ActionType` (or `BriefingActionType` in MBRF). */
255
+ type: number;
256
+ /** Unit count (0 = all) / `SetModifier` / `SwitchAction` / `Order` / `UnitState`, per type. */
257
+ modifier: number;
258
+ /** `ActionFlag` bits. */
259
+ flags: number;
260
+ padding: number;
261
+ /** EUD mask word; 0 in ordinary maps. */
262
+ mask: number;
263
+ }
264
+ export interface TriggerRecord {
265
+ conditions: ConditionRecord[];
266
+ actions: ActionRecord[];
267
+ /** `TriggerFlag` bits. */
268
+ flags: number;
269
+ /** 27 bytes, one per `PlayerGroup`; non-zero = the trigger runs for that group. */
270
+ players: number[];
271
+ /** The game's bookkeeping byte (offset 2399); StarEdit writes 0. */
272
+ currentAction: number;
273
+ }
274
+ /**
275
+ * A parsed scenario.
276
+ *
277
+ * `chk` holds every original section in file order and is the fidelity anchor: sections
278
+ * we do not model are re-emitted byte for byte, so opening and saving a map we only
279
+ * partly understand does not destroy the parts we don't. Only names listed in `dirty`
280
+ * are re-encoded on save.
281
+ */
282
+ export interface Scenario {
283
+ chk: ChkFile;
284
+ dirty: Set<string>;
285
+ warnings: string[];
286
+ /** TYPE, e.g. "RAWB" for Brood War. */
287
+ type: string;
288
+ /** VER: 59 original, 63 hybrid, 205 Brood War, 206 Remastered. */
289
+ fileVersion: number;
290
+ width: number;
291
+ height: number;
292
+ /** ERA value as stored; the meaningful tileset is `tilesetId`. */
293
+ era: number;
294
+ strings: StringTable;
295
+ nameIndex: number;
296
+ descriptionIndex: number;
297
+ playerTypes: number[];
298
+ /**
299
+ * IOWN: StarEdit's own copy of the player types — what its Player Settings dialog
300
+ * shows, where the game reads OWNR. The editor keeps the two in step; a file where they
301
+ * differ (another tool wrote one and not the other) is flagged by Check Map. Null when
302
+ * the file has no section.
303
+ */
304
+ editorPlayerTypes: number[] | null;
305
+ playerRaces: number[];
306
+ playerColors: number[];
307
+ /** CRGB, Remastered's per-slot colour choice; null when the file has none (every client then reads COLR). */
308
+ playerRgb: PlayerRgb | null;
309
+ forces: Forces;
310
+ /** UNIx if the file has one, else UNIS; null when it has neither (every type on its dat defaults). */
311
+ unitSettings: UnitSettings | null;
312
+ /** PUNI; null when the file has none (everything buildable by everyone). */
313
+ unitAvailability: UnitAvailability | null;
314
+ /** UPGx if the file has one, else UPGS; null when it has neither (every upgrade on its dat costs). */
315
+ upgradeSettings: UpgradeSettings | null;
316
+ /** PUPx else UPGR; null when absent (every player on the dat level caps, starting at 0). */
317
+ upgradeRestrictions: UpgradeRestrictions | null;
318
+ /** TECx else TECS; null when absent. */
319
+ techSettings: TechSettings | null;
320
+ /** PTEx else PTEC; null when absent (everything researchable, nothing researched). */
321
+ techRestrictions: TechRestrictions | null;
322
+ /** WAV: 512 string indices of the map's sound paths; null when the file has no section. */
323
+ wavs: number[] | null;
324
+ /** UPRP: the 64 Create Unit with Properties slots (the action names one 1-based); null when the file has no section. */
325
+ cuwp: CuwpSlot[] | null;
326
+ /** UPUS: StarEdit's "slot in use" byte per CUWP slot; null when the file has no section. */
327
+ cuwpUsed: boolean[] | null;
328
+ /** MTXM: what the game draws — terrain with the doodads stamped over it. */
329
+ tiles: Uint16Array;
330
+ /**
331
+ * TILE: StarEdit's copy of the terrain *without* doodads (a doodad's cells hold the
332
+ * ground it was placed on). Terrain brushes write both arrays; placing a doodad writes
333
+ * only `tiles`, and removing one restores its cells from here. A file without TILE
334
+ * starts with a copy of MTXM.
335
+ */
336
+ editorTiles: Uint16Array;
337
+ isom: Uint16Array | null;
338
+ mask: Uint8Array | null;
339
+ units: UnitRecord[];
340
+ sprites: SpriteRecord[];
341
+ doodads: DoodadRecord[];
342
+ locations: LocationRecord[];
343
+ /** TRIG, in execution order. */
344
+ triggers: TriggerRecord[];
345
+ /** MBRF: mission briefings, same record layout with briefing action types. */
346
+ briefing: TriggerRecord[];
347
+ /** SWNM: string index per switch (0 = unnamed); null when the file has no section. */
348
+ switchNames: number[] | null;
349
+ }
350
+ export type MapVersion = "original" | "hybrid" | "broodwar" | "remastered";
351
+ export interface Cv5Group {
352
+ /** Doodad groups store 1 here; terrain groups store a group type. */
353
+ index: number;
354
+ /** The full u16 flag word; see GroupFlag. */
355
+ flags: number;
356
+ /** Low byte of `flags`: walkability, creep and the unbuildable bit. */
357
+ buildability: number;
358
+ /** High byte of `flags`: view-blocking and the ground-height bits. */
359
+ groundHeight: number;
360
+ /**
361
+ * ISOM link on each side: values up to 48 are "soft" links shared with whatever
362
+ * terrain borders this piece, higher ones are "hard" links that pair pieces of the
363
+ * same edge set. Flat ground has the same soft link on all four sides.
364
+ */
365
+ edges: {
366
+ left: number;
367
+ top: number;
368
+ right: number;
369
+ bottom: number;
370
+ };
371
+ /**
372
+ * How cliff pieces stack vertically: a group whose `top` equals the `bottom` of the
373
+ * group above it continues that cliff face. Zero means nothing stacks that way.
374
+ */
375
+ stack: {
376
+ left: number;
377
+ top: number;
378
+ right: number;
379
+ bottom: number;
380
+ };
381
+ /** VX4 megatile index for each of the 16 slots in the group. */
382
+ megatiles: Uint16Array;
383
+ /** Set on doodad groups (`index` 1), which store a different record in the edge/stack bytes. */
384
+ doodad?: Cv5Doodad;
385
+ }
386
+ /**
387
+ * What a doodad group stores where terrain groups keep their ISOM links. A doodad spans
388
+ * one group per row: row `r` of the doodad whose first group is `g` is group `g + r`,
389
+ * with column `c` in megatile slot `c` (so its MTXM id is `((g + r) << 4) | c`). Every
390
+ * group of the doodad repeats the same `ddData`, `width` and `height`.
391
+ */
392
+ export interface Cv5Doodad {
393
+ /** sprites.dat id (DoodadFlag.SpriteOverlay) or units.dat id (DoodadFlag.UnitOverlay) drawn over the tiles. */
394
+ overlay: number;
395
+ /** 1-based `rez\stat_txt.tbl` index of the palette category ("Trees", "Cliff", …); 0 for the unlisted ones. */
396
+ nameIndex: number;
397
+ /** Index into `tileset\<name>\dddata.bin`, and what DD2 records store. */
398
+ ddData: number;
399
+ width: number;
400
+ height: number;
401
+ }
402
+ export interface Tileset {
403
+ /** 256 RGBA entries, 4 bytes each. */
404
+ palette: Uint8Array;
405
+ /** VR4: `minitiles[i * 64 + n]` is a palette index. */
406
+ minitiles: Uint8Array;
407
+ /** VX4 refs, 16 per megatile: bit 0 is horizontal flip, the rest is the minitile index. */
408
+ megatileRefs: Uint32Array;
409
+ megatileCount: number;
410
+ /** True when the source was .vx4ex (Remastered, 32-bit refs). */
411
+ extended: boolean;
412
+ /** VF4 flags, 16 per megatile. */
413
+ megatileFlags: Uint16Array;
414
+ groups: Cv5Group[];
415
+ }
416
+ /** One rotating run of palette entries, `min..max` inclusive. */
417
+ export interface PaletteBand {
418
+ min: number;
419
+ max: number;
420
+ }
421
+ /**
422
+ * All of a tileset's megatiles rendered once into a single image, so the viewport can
423
+ * draw terrain with one `drawImage` per visible tile instead of decoding minitiles on
424
+ * every frame.
425
+ */
426
+ export interface TilesetAtlas {
427
+ image: CanvasImageSource;
428
+ /** Megatiles per atlas row. */
429
+ columns: number;
430
+ tileSize: number;
431
+ count: number;
432
+ /** Packed 0xRRGGBB average of each megatile, for minimap and far-zoom drawing. */
433
+ averages: Uint32Array;
434
+ /** The cycling (water/lava) megatiles, or null when the tileset has none. */
435
+ animation: AtlasAnimation | null;
436
+ }
437
+ /**
438
+ * A second, small atlas holding only the megatiles that reference cycling palette
439
+ * entries. It is re-rasterised with the rotated palette on every step, while the main
440
+ * atlas stays at step 0; `atlasSource` picks the right one per megatile.
441
+ */
442
+ export interface AtlasAnimation {
443
+ image: HTMLCanvasElement;
444
+ columns: number;
445
+ /** Megatile → slot in this atlas, or -1 for megatiles that do not cycle. */
446
+ slot: Int32Array;
447
+ megatiles: Uint32Array;
448
+ bands: readonly PaletteBand[];
449
+ /** Steps until the cycle repeats. */
450
+ length: number;
451
+ /** The step the image currently shows. */
452
+ step: number;
453
+ pixels: ImageData;
454
+ /** Scratch palette, rotated in place each step. */
455
+ palette: Uint8Array;
456
+ }
457
+ export interface DoodadDef {
458
+ /** The `dddata.bin` index, which DD2 records store. */
459
+ id: number;
460
+ /** First CV5 group; row `r` is group `group + r`, column `c` is slot `c`. */
461
+ group: number;
462
+ width: number;
463
+ height: number;
464
+ /** Palette category, from stat_txt.tbl; "Unlisted" for groups without a name. */
465
+ category: string;
466
+ /** The first group's CV5 flag word — copied onto the overlay's THG2 record, as StarEdit does. */
467
+ flags: number;
468
+ /** The sprite (or unit) drawn over the tiles, or null. */
469
+ overlay: DoodadOverlay | null;
470
+ /** MTXM id per cell, row-major; 0 where the doodad leaves the ground alone. */
471
+ tiles: Uint16Array;
472
+ /** CV5 group required under each cell, row-major; 0 = no requirement. */
473
+ required: Uint16Array;
474
+ /**
475
+ * Whether any of its minitiles carries the VF4 ramp bit. StarEdit files ramps under the
476
+ * cliff categories with no name of their own; this is the only way to tell them apart.
477
+ */
478
+ ramp: boolean;
479
+ }
480
+ export interface DoodadOverlay {
481
+ kind: "sprite" | "unit";
482
+ /** sprites.dat or units.dat id. */
483
+ id: number;
484
+ flipped: boolean;
485
+ }
486
+ export interface DoodadCategory {
487
+ name: string;
488
+ doodads: DoodadDef[];
489
+ }
490
+ export interface DoodadCatalogue {
491
+ doodads: DoodadDef[];
492
+ byId: Map<number, DoodadDef>;
493
+ /** In first-appearance order, the way StarEdit's category drop-down lists them; "Unlisted" last. */
494
+ categories: DoodadCategory[];
495
+ /** Whether dddata.bin was available; without it nothing is ever refused for its ground. */
496
+ hasPlacementData: boolean;
497
+ }
498
+ declare const TILESET_FILENAMES: readonly [
499
+ "badlands",
500
+ "platform",
501
+ "install",
502
+ "ashworld",
503
+ "jungle",
504
+ "desert",
505
+ "ice",
506
+ "twilight"
507
+ ];
508
+ export type TilesetFileName = (typeof TILESET_FILENAMES)[number];
509
+ export interface LoadedTileset {
510
+ name: TilesetFileName;
511
+ tileset: Tileset;
512
+ atlas: TilesetAtlas;
513
+ /** The doodads the CV5 holds, with their placement rules when `<name>.dddata.bin` was extracted. */
514
+ doodads: DoodadCatalogue;
515
+ }
516
+ /** StarCraft tileset reference data. */
517
+ export type TilesetId = "badlands" | "platform" | "install" | "ashworld" | "jungle" | "desert" | "ice" | "twilight";
518
+ /**
519
+ * One ISOM terrain type: its id is the CV5 `index` of the flat left/right group pair
520
+ * that draws it, and the value the ISOM section stores for it.
521
+ */
522
+ export interface TerrainName {
523
+ id: number;
524
+ name: string;
525
+ }
526
+ export interface TerrainType extends TerrainName {
527
+ /** Even CV5 group of the flat pair; odd columns use `group + 1`. */
528
+ group: number;
529
+ height: 0 | 1 | 2;
530
+ buildable: boolean;
531
+ }
532
+ export type GroupKind = "terrain" | "edge" | "doodad" | "other";
533
+ export interface TileInfo {
534
+ id: number;
535
+ group: number;
536
+ slot: number;
537
+ /** VX4 megatile, or -1 when the id points at nothing drawable. */
538
+ megatile: number;
539
+ kind: GroupKind;
540
+ label: string;
541
+ height: 0 | 1 | 2;
542
+ buildable: boolean;
543
+ /** Walkable minitiles out of 16, from VF4. */
544
+ walkable: number;
545
+ }
546
+ export interface TileChange {
547
+ /** Flat index into `scenario.tiles`. */
548
+ at: number;
549
+ before: number;
550
+ after: number;
551
+ /**
552
+ * What `scenario.editorTiles` (TILE) held before a terrain change, when that differs
553
+ * from `before` (the cell was under a doodad). Filled in by `applyChanges` the first
554
+ * time a change is applied, so undo can put the ground back rather than the doodad tile.
555
+ */
556
+ under?: number;
557
+ }
558
+ export interface Rect {
559
+ x0: number;
560
+ y0: number;
561
+ /** Exclusive. */
562
+ x1: number;
563
+ y1: number;
564
+ }
565
+ /** What Replace Terrain matches or writes: a flat terrain type (by ISOM id) or one exact tile. */
566
+ export type TerrainPick = {
567
+ kind: "terrain";
568
+ id: number;
569
+ variation?: number;
570
+ } | {
571
+ kind: "tile";
572
+ id: number;
573
+ };
574
+ export interface Diamond {
575
+ x: number;
576
+ y: number;
577
+ }
578
+ export interface IsomCheck {
579
+ /** Rects that have tiles under them. */
580
+ rects: number;
581
+ /** Rects whose tiles are not what their ISOM resolves to (doodad tiles are excused). */
582
+ mismatched: number;
583
+ }
584
+ /** `checkIsom` with the verdict the palette and Check Map draw from it. */
585
+ export interface IsomReport extends IsomCheck {
586
+ /** More than `STALE_ISOM_SHARE` of the rects disagree with their tiles. */
587
+ stale: boolean;
588
+ }
589
+ export interface LocationStringChange {
590
+ index: number;
591
+ before: string | null;
592
+ after: string | null;
593
+ }
594
+ export interface LocationChange {
595
+ index: number;
596
+ before: LocationRecord;
597
+ after: LocationRecord;
598
+ /** A string slot the edit adds (a name no existing string matched); removed again on undo. */
599
+ string?: LocationStringChange;
600
+ }
601
+ export interface Bounds {
602
+ left: number;
603
+ top: number;
604
+ right: number;
605
+ bottom: number;
606
+ }
607
+ export interface LocationPatch {
608
+ name?: string;
609
+ left?: number;
610
+ top?: number;
611
+ right?: number;
612
+ bottom?: number;
613
+ elevationFlags?: number;
614
+ }
615
+ export type FogMode = "fog" | "clear";
616
+ export interface SpriteChange {
617
+ index: number;
618
+ before: SpriteRecord | null;
619
+ after: SpriteRecord | null;
620
+ }
621
+ export type SpriteKind = "pure" | "unit";
622
+ /** Unit type catalogue: StarEdit names by units.dat id, and the palette's grouping of them. */
623
+ export type RaceKey = "terran" | "zerg" | "protoss" | "neutral";
624
+ export interface UnitGroup {
625
+ race: RaceKey;
626
+ label: string;
627
+ /** units.dat ids, in palette order. */
628
+ units: number[];
629
+ }
630
+ export interface UnitsDat {
631
+ /** flingy.dat index. */
632
+ flingy: Uint8Array;
633
+ /** First subunit (turret), or NO_UNIT. */
634
+ subunit: Uint16Array;
635
+ /** Starting facing 0–31 (0 = up, 8 = right, 16 = down), or RANDOM_DIRECTION. */
636
+ direction: Uint8Array;
637
+ shieldEnable: Uint8Array;
638
+ shieldAmount: Uint16Array;
639
+ /** Fixed point: hit points × 256. */
640
+ hitPoints: Uint32Array;
641
+ elevation: Uint8Array;
642
+ /** UnitFlag bits. */
643
+ flags: Uint32Array;
644
+ /** StarEdit placement box, pixels. Buildings are placed on the tile grid by this box. */
645
+ placementWidth: Uint16Array;
646
+ placementHeight: Uint16Array;
647
+ /** Collision extents from the unit's centre, pixels. */
648
+ extentLeft: Uint16Array;
649
+ extentUp: Uint16Array;
650
+ extentRight: Uint16Array;
651
+ extentDown: Uint16Array;
652
+ mineralCost: Uint16Array;
653
+ vespeneCost: Uint16Array;
654
+ /** Game frames. */
655
+ buildTime: Uint16Array;
656
+ armor: Uint8Array;
657
+ /** weapons.dat ids, or NO_WEAPON. */
658
+ groundWeapon: Uint8Array;
659
+ airWeapon: Uint8Array;
660
+ /** Bit 0 Zerg, 1 Terran, 2 Protoss, 3 men, 4 building, 5 factory, 6 independent, 7 neutral. */
661
+ groupFlags: Uint8Array;
662
+ /** StarEdit availability flags; all zero for the legacy layout. */
663
+ availability: Uint16Array;
664
+ }
665
+ export interface FlingyDat {
666
+ /** sprites.dat index. */
667
+ sprite: Uint16Array;
668
+ }
669
+ export interface SpritesDat {
670
+ /** images.dat index. */
671
+ image: Uint16Array;
672
+ }
673
+ export interface ImagesDat {
674
+ /** 1-based images.tbl index of the GRP path, relative to `unit\`. */
675
+ grp: Uint32Array;
676
+ /** 1 when the GRP holds 17 frames per facing set (directions 0–16, the rest mirrored). */
677
+ graphicTurns: Uint8Array;
678
+ drawFunction: Uint8Array;
679
+ remapping: Uint8Array;
680
+ /** iscript.bin id of the image's animation script. */
681
+ iscript: Uint32Array;
682
+ /** 1-based images.tbl indices of the overlay position files (0 = none), in LO_KINDS order. */
683
+ lo: Uint32Array[];
684
+ }
685
+ export interface WeaponsDat {
686
+ damage: Uint16Array;
687
+ /** Added per upgrade level. */
688
+ bonus: Uint16Array;
689
+ }
690
+ export type Race = "zerg" | "terran" | "protoss" | null;
691
+ /** The columns Upgrade Settings shows as defaults: base cost and per-level factor for minerals, gas and time, plus the level cap. */
692
+ export interface UpgradesDat {
693
+ mineralCost: Uint16Array;
694
+ mineralFactor: Uint16Array;
695
+ vespeneCost: Uint16Array;
696
+ vespeneFactor: Uint16Array;
697
+ /** Game frames. */
698
+ timeCost: Uint16Array;
699
+ timeFactor: Uint16Array;
700
+ /** Highest level the upgrade goes to (3 for armour / weapons, 1 for the rest). */
701
+ maxRepeats: Uint8Array;
702
+ /** 1 for upgrades only Brood War has. */
703
+ broodWar: Uint8Array;
704
+ }
705
+ /** The columns Technology Settings shows as defaults. */
706
+ export interface TechdataDat {
707
+ mineralCost: Uint16Array;
708
+ vespeneCost: Uint16Array;
709
+ /** Game frames. */
710
+ researchTime: Uint16Array;
711
+ energyCost: Uint16Array;
712
+ /** 1 for abilities only Brood War has. */
713
+ broodWar: Uint8Array;
714
+ }
715
+ export interface SpriteGroup {
716
+ label: string;
717
+ ids: number[];
718
+ }
719
+ /**
720
+ * Plain types the chrome and the plugin API share: what the editor's layers, terrain
721
+ * modes, View ticks and notices are called. No state lives here — the atoms in
722
+ * `atoms/editorAtoms.ts` and `atoms/uiAtoms.ts` hold that and re-export these — so the
723
+ * plugin typings (`npm run build:plugin-types`) can name them without dragging Jotai in.
724
+ */
725
+ /** The classic SCMDraft layer combo; `"clipboard"` is Cut / Copy / Paste. */
726
+ export type EditorLayer = "terrain" | "doodads" | "units" | "sprites" | "locations" | "fog" | "clipboard";
727
+ export type TerrainMode = "isom" | "rect" | "tile" | "blend";
728
+ export interface ViewFlags {
729
+ grid: boolean;
730
+ locations: boolean;
731
+ locationNames: boolean;
732
+ units: boolean;
733
+ sprites: boolean;
734
+ /** Off draws the ground under the doodads (TILE) instead of the picture (MTXM). */
735
+ doodads: boolean;
736
+ fog: boolean;
737
+ /** Ground height per minitile, tinted (mid amber, high red). */
738
+ elevation: boolean;
739
+ /** Unbuildable tile groups, hatched in blue. */
740
+ buildability: boolean;
741
+ startLocations: boolean;
742
+ /** Cycle the palette so water and lava animate as they do in game. */
743
+ animateWater: boolean;
744
+ /** Run the units' iscript idle animations (turrets, pulsing buildings, fires, smoke). */
745
+ animateUnits: boolean;
746
+ }
747
+ /**
748
+ * A short notice over the map that leaves by itself — what a save says when it is done,
749
+ * since the status bar line is easy to miss and the menubar dot only stops glowing.
750
+ */
751
+ export interface Toast {
752
+ id: number;
753
+ kind: "ok" | "info" | "warn" | "error";
754
+ title: string;
755
+ detail?: string;
756
+ /** Milliseconds before it leaves on its own; 0 keeps it until dismissed. */
757
+ ttl: number;
758
+ /**
759
+ * One button beside the text, for a notice that is really a question — the update
760
+ * check's "Download". Pressing it dismisses the toast as well as running this.
761
+ */
762
+ action?: {
763
+ label: string;
764
+ run: () => void;
765
+ };
766
+ }
767
+ /**
768
+ * The colour and layout codes StarCraft reads out of a string, and what a string looks
769
+ * like once the game has read them.
770
+ *
771
+ * Bytes 0x01–0x1F are control codes: most set the text colour, a few move the text or
772
+ * hide it, and 0x09 / 0x0A / 0x0D are the ordinary whitespace `escapeControls` leaves
773
+ * literal. The table is the one the community documents (wiki.staredit.net/wiki/Color,
774
+ * cross-checked against the reference tables mirrored on staredit-network.fandom.com) —
775
+ * the twelve player colours in it are the classic Brood War player palette, which is what
776
+ * confirms the numbering. **The editor's older table was wrong from 0x12 up**: it had the
777
+ * alignment codes one byte early and called 0x18 black, when 0x18 is player 9's green.
778
+ *
779
+ * `RGB` values are the palette entries the game draws with; they are constants here rather
780
+ * than read from game data because the font palette is not one of the files `npm run
781
+ * extract` pulls out (only `tunit.pcx` lands in `public/game/`), and a swatch needs a
782
+ * colour whether or not the archives are present.
783
+ *
784
+ * ## Remastered
785
+ *
786
+ * `runsOf` models **Remastered's** rule, not 1.16.1's: a colour set on one line carries
787
+ * onto the next line of the same string. In 1.16.1 every newline reset the text to the
788
+ * default colour, so a string written before the remaster can render differently now —
789
+ * `bleedingLines` finds exactly that case, and the Repair plugin turns it into a finding
790
+ * offering to write the reset the old game supplied for free.
791
+ */
792
+ /** What a control byte does. Colours carry an `rgb`; the rest are layout or visibility. */
793
+ export type CodeEffect = "color" | "mimic" | "invisible" | "align" | "clip" | "nothing" | "space";
794
+ export interface TextCode {
795
+ byte: number;
796
+ /** `<0E>`, the way every StarCraft editor writes it. */
797
+ code: string;
798
+ label: string;
799
+ effect: CodeEffect;
800
+ /** `#rrggbb` for `effect: "color"`, else null. */
801
+ rgb: string | null;
802
+ /** Which player's colour this is, for the ones that are a player colour (1-based). */
803
+ player?: number;
804
+ }
805
+ export type Align = "left" | "right" | "center";
806
+ /** One stretch of text in a single colour. */
807
+ export interface TextRun {
808
+ text: string;
809
+ /** `#rrggbb`; the default colour until a code says otherwise. */
810
+ color: string;
811
+ /** After `<0B>` / `<14>`: the game draws nothing, but the text is still there. */
812
+ invisible: boolean;
813
+ /** After `<0C>` in the large font: the rest of the line is dropped. */
814
+ clipped: boolean;
815
+ }
816
+ export interface TextLine {
817
+ runs: TextRun[];
818
+ align: Align;
819
+ }
820
+ export interface RunOptions {
821
+ /**
822
+ * Reset to `DEFAULT_TEXT_COLOR` at every newline, the way 1.16.1 did. Remastered
823
+ * carries the colour across, which is the default here.
824
+ */
825
+ resetPerLine?: boolean;
826
+ /** What `<01>` mimics and what the string starts in. */
827
+ initialColor?: string;
828
+ }
829
+ export interface BleedingLine {
830
+ /** 0-based index of the line that inherits a colour it did not set. */
831
+ line: number;
832
+ /** The code Remastered carries onto it — the whole entry, so a caller can name it. */
833
+ carried: TextCode;
834
+ }
835
+ /** Every built-in dialog, by the id `openDialogAtom` takes; `DialogHost` maps each to its component. */
836
+ export type DialogId = "newMap" | "openMap" | "saveAs" | "exportImage" | "mapProperties" | "resizeMap" | "mapRevision" | "playerSettings" | "forceSettings" | "playerColors" | "unitSettings" | "upgradeSettings" | "techSettings" | "stringEditor" | "soundEditor" | "switches" | "locationList" | "unitProperties" | "locationProperties" | "spriteProperties" | "triggerEditor" | "textTriggerEditor" | "missionBriefing" | "cuwpEditor" | "replaceTerrain" | "autoStarts" | "testMap" | "symmetry" | "gridSettings" | "preferences" | "shortcuts" | "validateMap" | "statistics" | "importTriggers" | "exportTriggers" | "importStrings" | "exportStrings" | "find" | "about" | "confirmClose" | "plugins" | "confirmPlugin" | "pluginDialog" | "gameData" | "update";
837
+ export interface MapImageOptions {
838
+ /** Output pixels per map tile. 32 matches the game's art 1:1; 1 is a minimap. */
839
+ pixelsPerTile: number;
840
+ units: boolean;
841
+ sprites: boolean;
842
+ locations: boolean;
843
+ locationNames: boolean;
844
+ startLocations: boolean;
845
+ fog: boolean;
846
+ /** Whose fog is drawn (0-based player), when `fog` is on. */
847
+ fogPlayer: number;
848
+ /** Grid spacing in map pixels (32 = one tile); 0 for no grid. */
849
+ grid: number;
850
+ /**
851
+ * The part of the map to draw, in tiles (exclusive `x1` / `y1`). The whole map when
852
+ * absent. Everything is drawn in map coordinates and the canvas is translated, so a
853
+ * region looks exactly like that part of the full picture — a unit half outside the
854
+ * region still leans in.
855
+ */
856
+ rect?: Rect | null;
857
+ }
858
+ /** What the registry knows about a section name, sized for one map. */
859
+ export interface SectionKnowledge {
860
+ name: string;
861
+ /** "Placed units", "String table", … */
862
+ what: string;
863
+ /** How the game combines repeated occurrences. */
864
+ mode: CombineMode;
865
+ /** The fixed buffer the game reads the section into, for this map's size; null when the length varies. */
866
+ size: number | null;
867
+ /** The record length of a list section, or null. */
868
+ stride: number | null;
869
+ /** Whether the editor decodes the section into its model and re-encodes it on save. */
870
+ modelled: boolean;
871
+ }
872
+ /** One occurrence of a section in the file, as Save would write it. */
873
+ export interface SectionInfo {
874
+ /** Position in the file's section list — what the byte-level calls take. */
875
+ index: number;
876
+ /** The four characters as stored (`"VER "` keeps its space); junk in a protected map. */
877
+ name: string;
878
+ /** Byte offset of the eight-byte chunk header within the CHK. */
879
+ offset: number;
880
+ /** Payload length. */
881
+ size: number;
882
+ /** The length field as written; differs from `size` only when the file ended early. */
883
+ declaredSize: number;
884
+ truncated: boolean;
885
+ /** Which occurrence of this name it is, 0-based, and how many the file has. */
886
+ occurrence: number;
887
+ occurrences: number;
888
+ /** Whether the editor has unsaved changes it will encode into this section on save. */
889
+ dirty: boolean;
890
+ /** The registry entry, or null for a section the editor has never heard of. */
891
+ spec: SectionKnowledge | null;
892
+ }
893
+ /** What `rebuildSections` did: the parser's remarks and the sections actually re-encoded. */
894
+ export interface RebuildResult {
895
+ warnings: string[];
896
+ rebuilt: string[];
897
+ }
898
+ export type ArgKind = "player" | "unit" | "location" | "switch" | "comparison" | "switchState" | "switchAction" | "modifier" | "unitState" | "order" | "alliance" | "resource" | "score" | "aiScript" | "textFlags" | "text" | "wav" | "number" | "amount" | "count" | "duration" | "percent" | "cuwp" | "slot";
899
+ export type ConditionField = keyof ConditionRecord;
900
+ export type ActionField = keyof ActionRecord;
901
+ export interface ArgDef<F extends string> {
902
+ kind: ArgKind;
903
+ field: F;
904
+ label: string;
905
+ }
906
+ export interface ConditionDef {
907
+ type: number;
908
+ name: string;
909
+ args: ArgDef<ConditionField>[];
910
+ }
911
+ export interface ActionDef {
912
+ type: number;
913
+ name: string;
914
+ args: ArgDef<ActionField>[];
915
+ /** Text / Transmission: the `AlwaysDisplay` flag is an argument of its own. */
916
+ hasTextFlags?: boolean;
917
+ }
918
+ export interface Choice {
919
+ value: number;
920
+ label: string;
921
+ /** Extra spellings the text parser accepts. */
922
+ aliases?: string[];
923
+ }
924
+ /** How the text format names things it cannot know on its own. */
925
+ export interface TriggerNames {
926
+ /** Text of a string-table entry, null when unset. */
927
+ string(index: number): string | null;
928
+ /** Index for `text` — an existing identical entry or a new one. Only called while parsing. */
929
+ intern(text: string): number;
930
+ /** Display name of a 1-based location number. */
931
+ location(number: number): string;
932
+ /** 1-based number for a location name (or `Location N` / `Anywhere`), undefined when unknown. */
933
+ locationByName(name: string): number | undefined;
934
+ unit(id: number): string;
935
+ unitByName(name: string): number | undefined;
936
+ /** Display name of a 0-based switch. */
937
+ switch(index: number): string;
938
+ switchByName(name: string): number | undefined;
939
+ }
940
+ export interface TextTrigger {
941
+ trigger: TriggerRecord;
942
+ /** 1-based line the `Trigger(` header starts on. */
943
+ line: number;
944
+ }
945
+ export type IssueLevel = "error" | "warn" | "info";
946
+ export type IssueTarget = {
947
+ kind: "location";
948
+ index: number;
949
+ } | {
950
+ kind: "unit";
951
+ index: number;
952
+ } | {
953
+ kind: "trigger";
954
+ index: number;
955
+ } | {
956
+ kind: "dialog";
957
+ id: DialogId;
958
+ };
959
+ export interface Issue {
960
+ level: IssueLevel;
961
+ text: string;
962
+ where: string;
963
+ target?: IssueTarget;
964
+ }
965
+ export interface PlayerStatistics {
966
+ slot: number;
967
+ type: string;
968
+ race: string;
969
+ units: number;
970
+ /** Null when units.dat is not loaded. */
971
+ buildings: number | null;
972
+ startLocations: number;
973
+ }
974
+ export interface MapStatistics {
975
+ width: number;
976
+ height: number;
977
+ tileset: string;
978
+ revision: string;
979
+ sections: number;
980
+ strings: {
981
+ slots: number;
982
+ set: number;
983
+ extended: boolean;
984
+ };
985
+ players: PlayerStatistics[];
986
+ /** Units whose owner byte is past the twelve slots. */
987
+ unownedUnits: number;
988
+ units: {
989
+ total: number;
990
+ buildings: number | null;
991
+ top: {
992
+ id: number;
993
+ name: string;
994
+ count: number;
995
+ }[];
996
+ };
997
+ resources: {
998
+ minerals: number;
999
+ gas: number;
1000
+ fields: number;
1001
+ geysers: number;
1002
+ };
1003
+ doodads: number;
1004
+ sprites: {
1005
+ pure: number;
1006
+ unit: number;
1007
+ };
1008
+ locations: number;
1009
+ triggers: {
1010
+ count: number;
1011
+ conditions: number;
1012
+ actions: number;
1013
+ preserved: number;
1014
+ disabled: number;
1015
+ };
1016
+ /** MBRF: the briefing's records and the actions in them. */
1017
+ briefings: {
1018
+ count: number;
1019
+ actions: number;
1020
+ };
1021
+ switchesNamed: number;
1022
+ sounds: number;
1023
+ /** Tiles per terrain type ("Edges and cliffs" for the unnamed edge sets), most common first; null without the tileset graphics. */
1024
+ terrain: {
1025
+ name: string;
1026
+ tiles: number;
1027
+ }[] | null;
1028
+ }
1029
+ export type FindKind = "units" | "locations" | "sprites" | "doodads" | "strings" | "triggers" | "briefing";
1030
+ export interface FindResult {
1031
+ kind: FindKind;
1032
+ /** Index into the list the kind names (unit / sprite / trigger index, location slot, string index). */
1033
+ index: number;
1034
+ label: string;
1035
+ detail: string;
1036
+ /** Tile coordinates to centre on, where the thing has a position. */
1037
+ x?: number;
1038
+ y?: number;
1039
+ }
1040
+ export interface FindOptions {
1041
+ kind: FindKind;
1042
+ query: string;
1043
+ matchCase?: boolean;
1044
+ /** Display name of a sprite record (needs the game data); the id when omitted. */
1045
+ spriteName?: (r: SpriteRecord) => string;
1046
+ /** Display name of a doodad record (needs the tileset); the id when omitted. */
1047
+ doodadName?: (r: DoodadRecord) => string;
1048
+ limit?: number;
1049
+ }
1050
+ export type StringUsageKind = "name" | "description" | "force" | "location" | "unit" | "switch" | "trigger" | "briefing" | "wav";
1051
+ export interface StringUsage {
1052
+ kind: StringUsageKind;
1053
+ /** What the kind indexes: force / slot / unit id / switch / trigger index / WAV slot; 0 for name and description. */
1054
+ ref: number;
1055
+ label: string;
1056
+ }
1057
+ /**
1058
+ * Unit edits as invertible change lists, in the same spirit as terrain's `TileChange`.
1059
+ * `before`/`after` are whole records: null `before` is an insertion at `index`, null
1060
+ * `after` a removal, both set a replacement. Removals are listed highest index first so
1061
+ * that applying them in order keeps the remaining indices valid; undo walks the list
1062
+ * backwards and so re-inserts lowest first.
1063
+ */
1064
+ export interface UnitChange {
1065
+ index: number;
1066
+ before: UnitRecord | null;
1067
+ after: UnitRecord | null;
1068
+ }
1069
+ export interface PlacementOptions {
1070
+ /** Refuse to put a unit on top of another (ground units and buildings only). */
1071
+ checkCollision: boolean;
1072
+ /** Refuse unwalkable ground for units and unbuildable tiles for buildings. */
1073
+ checkTerrain: boolean;
1074
+ /** After a terrain edit, remove units the new terrain can no longer hold (the Terrain palette's toggle). */
1075
+ removeStranded: boolean;
1076
+ /** Buildings snap their placement box to the tile grid (StarEdit always does; SCMDraft lets you turn it off). */
1077
+ snapToGrid: boolean;
1078
+ }
1079
+ export type PlacementProblem = "terrain" | "collision";
1080
+ export interface PlacementVerdict {
1081
+ problem: PlacementProblem | null;
1082
+ /** The unit in the way, for collision problems. */
1083
+ blocker: number;
1084
+ /** The problem as a sentence fragment — "the ground is unwalkable", "it overlaps Terran Marine" — null when it fits. */
1085
+ reason: string | null;
1086
+ }
1087
+ export interface PlayerSlotView {
1088
+ /** 0-based; the chrome shows it as `slot + 1`. */
1089
+ slot: number;
1090
+ type: number;
1091
+ typeName: string;
1092
+ race: number;
1093
+ raceName: string;
1094
+ /** COLR index; null for the four unplayable slots. */
1095
+ color: number | null;
1096
+ /** The colour the chrome shows for the slot (CRGB-aware). */
1097
+ colorHex: string | null;
1098
+ /** The CRGB custom triple when one is in effect. */
1099
+ rgb: [
1100
+ number,
1101
+ number,
1102
+ number
1103
+ ] | null;
1104
+ /** 0-based force; null for the unplayable slots. */
1105
+ force: number | null;
1106
+ forceName: string | null;
1107
+ }
1108
+ export interface PlayerPatch {
1109
+ type?: number;
1110
+ race?: number;
1111
+ /** COLR index 0–15 (playable slots). */
1112
+ color?: number;
1113
+ /** A CRGB custom colour, or null to go back to the palette colour (playable slots). */
1114
+ rgb?: [
1115
+ number,
1116
+ number,
1117
+ number
1118
+ ] | null;
1119
+ /** 0-based force (playable slots). */
1120
+ force?: number;
1121
+ }
1122
+ export interface ForceView {
1123
+ /** 0-based. */
1124
+ force: number;
1125
+ name: string;
1126
+ flags: number;
1127
+ allied: boolean;
1128
+ alliedVictory: boolean;
1129
+ sharedVision: boolean;
1130
+ randomStart: boolean;
1131
+ /** 0-based playable slots in the force. */
1132
+ players: number[];
1133
+ }
1134
+ export interface ForcePatch {
1135
+ name?: string;
1136
+ allied?: boolean;
1137
+ alliedVictory?: boolean;
1138
+ sharedVision?: boolean;
1139
+ randomStart?: boolean;
1140
+ /** The whole flag word, applied before the booleans. */
1141
+ flags?: number;
1142
+ /** 0-based playable slots the force should contain (others keep theirs); moves them from their forces. */
1143
+ players?: number[];
1144
+ }
1145
+ export interface WeaponView {
1146
+ id: number;
1147
+ name: string;
1148
+ damage: number;
1149
+ bonus: number;
1150
+ }
1151
+ export interface UnitTypeView {
1152
+ id: number;
1153
+ /** The game's name (or the custom one when the map sets it). */
1154
+ name: string;
1155
+ /** The custom name the map sets, `""` for the default. */
1156
+ customName: string;
1157
+ useDefault: boolean;
1158
+ /** Whole hit points. */
1159
+ hitPoints: number;
1160
+ shields: number;
1161
+ armor: number;
1162
+ /** Game frames. */
1163
+ buildTime: number;
1164
+ mineralCost: number;
1165
+ gasCost: number;
1166
+ /** The type's ground and air weapons (a turreted vehicle's are its turret's), with the effective damage. */
1167
+ weapons: WeaponView[];
1168
+ /** units.dat / weapons.dat, null without the game data. */
1169
+ defaults: {
1170
+ hitPoints: number;
1171
+ shields: number;
1172
+ armor: number;
1173
+ buildTime: number;
1174
+ mineralCost: number;
1175
+ gasCost: number;
1176
+ weapons: WeaponView[];
1177
+ } | null;
1178
+ /** PUNI: whether the type can be built by default, and per player (`"default"` where the player follows the default). */
1179
+ availability: {
1180
+ defaultAvailable: boolean;
1181
+ players: (boolean | "default")[];
1182
+ };
1183
+ }
1184
+ export interface UnitTypePatch {
1185
+ /** Explicitly back to (or off) the dat defaults; setting any number below turns it off. */
1186
+ useDefault?: boolean;
1187
+ /** Custom name; `""` restores the default. */
1188
+ name?: string;
1189
+ hitPoints?: number;
1190
+ shields?: number;
1191
+ armor?: number;
1192
+ buildTime?: number;
1193
+ mineralCost?: number;
1194
+ gasCost?: number;
1195
+ weapons?: {
1196
+ id: number;
1197
+ damage?: number;
1198
+ bonus?: number;
1199
+ }[];
1200
+ /** PUNI: `player` 0-based or `"default"` (the default column); `value` true / false, or `"default"` to follow the default column again. */
1201
+ available?: {
1202
+ player: number | "default";
1203
+ value: boolean | "default";
1204
+ }[];
1205
+ }
1206
+ export interface UpgradeLevelsView {
1207
+ start: number;
1208
+ max: number;
1209
+ usesDefault: boolean;
1210
+ }
1211
+ export interface UpgradeView {
1212
+ id: number;
1213
+ name: string;
1214
+ useDefault: boolean;
1215
+ mineralCost: number;
1216
+ mineralFactor: number;
1217
+ gasCost: number;
1218
+ gasFactor: number;
1219
+ /** Game frames. */
1220
+ timeCost: number;
1221
+ timeFactor: number;
1222
+ /** upgrades.dat, null without the game data. */
1223
+ defaults: {
1224
+ mineralCost: number;
1225
+ mineralFactor: number;
1226
+ gasCost: number;
1227
+ gasFactor: number;
1228
+ timeCost: number;
1229
+ timeFactor: number;
1230
+ maxLevel: number;
1231
+ } | null;
1232
+ /** UPGR / PUPx: the default start and cap, and each of the 12 players' (effective, with `usesDefault`). */
1233
+ levels: {
1234
+ defaultStart: number;
1235
+ defaultMax: number;
1236
+ players: UpgradeLevelsView[];
1237
+ };
1238
+ }
1239
+ export interface UpgradePatch {
1240
+ useDefault?: boolean;
1241
+ mineralCost?: number;
1242
+ mineralFactor?: number;
1243
+ gasCost?: number;
1244
+ gasFactor?: number;
1245
+ timeCost?: number;
1246
+ timeFactor?: number;
1247
+ /** `player` 0-based or `"default"`; `useDefault: true` puts a player back on the default column. */
1248
+ levels?: {
1249
+ player: number | "default";
1250
+ start?: number;
1251
+ max?: number;
1252
+ useDefault?: boolean;
1253
+ }[];
1254
+ }
1255
+ export interface TechStateView {
1256
+ available: boolean;
1257
+ researched: boolean;
1258
+ usesDefault: boolean;
1259
+ }
1260
+ export interface TechView {
1261
+ id: number;
1262
+ name: string;
1263
+ useDefault: boolean;
1264
+ mineralCost: number;
1265
+ gasCost: number;
1266
+ /** Game frames. */
1267
+ researchTime: number;
1268
+ energyCost: number;
1269
+ defaults: {
1270
+ mineralCost: number;
1271
+ gasCost: number;
1272
+ researchTime: number;
1273
+ energyCost: number;
1274
+ } | null;
1275
+ /** PTEC / PTEx: the default column and each of the 12 players' effective state. */
1276
+ state: {
1277
+ defaultAvailable: boolean;
1278
+ defaultResearched: boolean;
1279
+ players: TechStateView[];
1280
+ };
1281
+ }
1282
+ export interface TechPatch {
1283
+ useDefault?: boolean;
1284
+ mineralCost?: number;
1285
+ gasCost?: number;
1286
+ researchTime?: number;
1287
+ energyCost?: number;
1288
+ /** `player` 0-based or `"default"`; `useDefault: true` puts a player back on the default column. */
1289
+ state?: {
1290
+ player: number | "default";
1291
+ available?: boolean;
1292
+ researched?: boolean;
1293
+ useDefault?: boolean;
1294
+ }[];
1295
+ }
1296
+ export interface MapVersionView {
1297
+ version: MapVersion;
1298
+ label: string;
1299
+ /** The VER word. */
1300
+ fileVersion: number;
1301
+ type: string;
1302
+ /** Whether the string table is STRx. */
1303
+ extendedStrings: boolean;
1304
+ /** The file extension StarEdit would give it. */
1305
+ extension: string;
1306
+ }
1307
+ export interface ResizeResult {
1308
+ dx: number;
1309
+ dy: number;
1310
+ unitsDropped: number;
1311
+ spritesDropped: number;
1312
+ doodadsDropped: number;
1313
+ locationsClamped: number;
1314
+ /** True when ISOM was reconstructed from the tiles; false when it is the flat fill's lattice. */
1315
+ isomRebuilt: boolean;
1316
+ }
1317
+ export interface SoundRow {
1318
+ slot: number;
1319
+ stringIndex: number;
1320
+ /** The path as the string table has it. */
1321
+ path: string;
1322
+ /** Whether the archive carries a member at that path. */
1323
+ present: boolean;
1324
+ /** Bytes in the archive, 0 when absent. */
1325
+ size: number;
1326
+ /** The archive member's name as stored (case may differ from the path). */
1327
+ member: string | null;
1328
+ usedBy: string[];
1329
+ }
1330
+ /** A slot patch the way the dialog and the plugin API both apply one: only the named fields move. */
1331
+ export interface CuwpSlotPatch {
1332
+ /** `null` leaves the field at the unit's default (clears its valid bit); a number sets it and the bit. */
1333
+ hitPointsPercent?: number | null;
1334
+ shieldsPercent?: number | null;
1335
+ energyPercent?: number | null;
1336
+ resources?: number | null;
1337
+ hangar?: number | null;
1338
+ /** `null` leaves the state alone; a boolean forces it. */
1339
+ cloaked?: boolean | null;
1340
+ burrowed?: boolean | null;
1341
+ inTransit?: boolean | null;
1342
+ hallucinated?: boolean | null;
1343
+ invincible?: boolean | null;
1344
+ }
1345
+ /** The slot as a patch-shaped view: null where the unit's default applies. */
1346
+ export interface CuwpSlotView {
1347
+ index: number;
1348
+ used: boolean;
1349
+ /** How many Create Unit with Properties actions name the slot. */
1350
+ references: number;
1351
+ hitPointsPercent: number | null;
1352
+ shieldsPercent: number | null;
1353
+ energyPercent: number | null;
1354
+ resources: number | null;
1355
+ hangar: number | null;
1356
+ cloaked: boolean | null;
1357
+ burrowed: boolean | null;
1358
+ inTransit: boolean | null;
1359
+ hallucinated: boolean | null;
1360
+ invincible: boolean | null;
1361
+ summary: string;
1362
+ }
1363
+ /** How the members of a map archive are compressed. */
1364
+ export type ArchiveCompression = "none" | "zlib" | "pkware";
1365
+ export type MapFormat = "scx" | "scm" | "chk";
1366
+ export interface SaveOptions {
1367
+ format: MapFormat;
1368
+ /** How every archive member is compressed; ignored for a bare .chk. */
1369
+ compression: ArchiveCompression;
1370
+ /** Encrypt the members as StarEdit does. Every StarCraft build reads it. */
1371
+ encrypt: boolean;
1372
+ /** Archive members left out of the file, by name. */
1373
+ omitExtras: string[];
1374
+ /** Leave out ISOM, TILE and DD2: the terrain-editing data the game never reads. */
1375
+ stripTerrainEditing: boolean;
1376
+ /** Leave out IVER, IVE2, IOWN, UPUS, SWNM and WAV: editor bookkeeping the game never reads. */
1377
+ stripBookkeeping: boolean;
1378
+ /** Leave out sections whose names the format reference does not know. */
1379
+ stripUnknown: boolean;
1380
+ /** Collapse a section that occurs more than once into the bytes the game would act on. */
1381
+ mergeRepeats: boolean;
1382
+ /** Drop bytes after the last section header the file could parse. */
1383
+ dropTrailing: boolean;
1384
+ }
1385
+ export interface ChangeTilesetResult {
1386
+ from: number;
1387
+ to: number;
1388
+ doodadsDropped: number;
1389
+ /** Overlay sprites that belonged to the dropped doodads. */
1390
+ spritesDropped: number;
1391
+ refilled: boolean;
1392
+ }
1393
+ export type SymmetryMode = "none" | "h" | "v" | "hv" | "rot180" | "rot90" | "diag" | "adiag";
1394
+ export interface DoodadChange {
1395
+ index: number;
1396
+ before: DoodadRecord | null;
1397
+ after: DoodadRecord | null;
1398
+ }
1399
+ export interface DoodadPlacementOptions {
1400
+ /** Skip StarEdit's ground check: any doodad goes on any terrain, even over another doodad. */
1401
+ placeAnywhere: boolean;
1402
+ /** Keep the left column on an even tile, as StarEdit always does. */
1403
+ snapToGrid: boolean;
1404
+ }
1405
+ export type StartLayout = "ring" | "corners";
1406
+ export interface StartPlacementResult {
1407
+ changes: UnitChange[];
1408
+ /** Per player (0-based), where the start location landed, or null when nothing within reach fit. */
1409
+ placed: ({
1410
+ x: number;
1411
+ y: number;
1412
+ } | null)[];
1413
+ removed: number;
1414
+ }
1415
+ export type Side = "left" | "top" | "right" | "bottom";
1416
+ export interface BlendCandidate {
1417
+ /** MTXM tile id to place. */
1418
+ id: number;
1419
+ megatile: number;
1420
+ /** `edgeDistance` between the anchor's side and this tile's opposite side. */
1421
+ distance: number;
1422
+ }
1423
+ export interface BlendOptions {
1424
+ /** Largest distance still listed. */
1425
+ maxDistance: number;
1426
+ /** Most candidates listed per side. */
1427
+ limit: number;
1428
+ /** Optional filter over tile ids (the palette's group-kind dropdown). */
1429
+ include?: (id: number) => boolean;
1430
+ }
1431
+ /** The change lists of one edit; `HistoryEntry` adds the label. */
1432
+ export interface HistoryEdit {
1433
+ changes: TileChange[];
1434
+ /** The isometric brush's changes to `scenario.isom`, undone together with the tiles. */
1435
+ isom?: TileChange[];
1436
+ /**
1437
+ * Set when the edit gave a map an ISOM section it did not have (Rebuild ISOM). Undo
1438
+ * removes the section again rather than leaving an all-zero one behind.
1439
+ */
1440
+ createdIsom?: Uint16Array;
1441
+ /** Unit placements, moves and deletions (see editor/units.ts). */
1442
+ units?: UnitChange[];
1443
+ /**
1444
+ * Doodad tiles stamped into or lifted off MTXM alone — TILE keeps the ground beneath
1445
+ * (see editor/doodads.ts). Applied after `changes`, so a terrain stroke that removes
1446
+ * the doodads it painted over restores their remaining cells on top of its own edit.
1447
+ */
1448
+ doodadTiles?: TileChange[];
1449
+ /** DD2 record insertions, removals and replacements. */
1450
+ doodads?: DoodadChange[];
1451
+ /** THG2 record changes: the Sprites layer's edits, and a doodad's overlay sprite coming and going with it. */
1452
+ sprites?: SpriteChange[];
1453
+ /** MRGN slot replacements — create, move, resize, rename, delete (see editor/locations.ts); a rename may carry a string. */
1454
+ locations?: LocationChange[];
1455
+ /** Fog of war edits to `scenario.mask` (see editor/fog.ts); `at` indexes the MASK byte. */
1456
+ fog?: TileChange[];
1457
+ /**
1458
+ * Set when the edit gave a map a MASK section it did not have (the first fog stroke
1459
+ * on such a map). Undo removes the section again.
1460
+ */
1461
+ createdMask?: Uint8Array;
1462
+ }
1463
+ export type ClipPart = "terrain" | "doodads" | "units" | "sprites" | "locations" | "fog";
1464
+ export type ClipParts = Record<ClipPart, boolean>;
1465
+ /** Whether a paste adds to what is in the target area or clears its objects first. */
1466
+ export type PasteMode = "merge" | "replace";
1467
+ export interface ClipLocation {
1468
+ /** Bounds in map pixels relative to the clip's origin (an inverted box keeps its inversion). */
1469
+ left: number;
1470
+ top: number;
1471
+ right: number;
1472
+ bottom: number;
1473
+ elevationFlags: number;
1474
+ name: string;
1475
+ }
1476
+ export interface Clip {
1477
+ /** Size in tiles. */
1478
+ width: number;
1479
+ height: number;
1480
+ /** ERA of the source map: tile ids mean nothing on another tileset. */
1481
+ era: number;
1482
+ /** MTXM ids, row-major, or null when terrain was not included. */
1483
+ tiles: Uint16Array | null;
1484
+ /** TILE ids (the ground under the doodads), alongside `tiles`. */
1485
+ ground: Uint16Array | null;
1486
+ /** Records with positions relative to the origin in map pixels. */
1487
+ doodads: DoodadRecord[];
1488
+ units: UnitRecord[];
1489
+ sprites: SpriteRecord[];
1490
+ locations: ClipLocation[];
1491
+ /** MASK bytes, row-major, or null when fog was not included. */
1492
+ fog: Uint8Array | null;
1493
+ }
1494
+ export interface PasteCounts {
1495
+ tiles: number;
1496
+ doodads: number;
1497
+ units: number;
1498
+ sprites: number;
1499
+ locations: number;
1500
+ fog: number;
1501
+ /** Objects the paste cleared out of the way (replace mode) or that a new tile stranded. */
1502
+ removed: number;
1503
+ }
1504
+ export interface PasteResult {
1505
+ edit: HistoryEdit;
1506
+ counts: PasteCounts;
1507
+ /** Things that could not be pasted, in words. */
1508
+ notes: string[];
1509
+ }
1510
+ export interface StringImport {
1511
+ entries: {
1512
+ index: number;
1513
+ text: string;
1514
+ }[];
1515
+ /** Lines that were not `N<TAB>text`, with their 1-based line numbers. */
1516
+ errors: {
1517
+ line: number;
1518
+ message: string;
1519
+ }[];
1520
+ }
1521
+ export interface Preferences {
1522
+ /** Show the splash while the game data loads; off starts straight on the editor. */
1523
+ splash: boolean;
1524
+ /** Ask before closing or replacing a map with unsaved changes. */
1525
+ confirmClose: boolean;
1526
+ /** What File ▸ New and the startup map start with. */
1527
+ newMap: {
1528
+ tileset: TilesetId;
1529
+ width: number;
1530
+ height: number;
1531
+ };
1532
+ /** Initial View ▸ Animate Water / Animate Units. */
1533
+ animateWater: boolean;
1534
+ animateUnits: boolean;
1535
+ /**
1536
+ * How fast the two animations run, as a multiple of the game's own speed (1 = the
1537
+ * game's "Fastest"). One of `ANIMATION_SPEEDS`; applied live, not just at startup.
1538
+ */
1539
+ animateWaterSpeed: number;
1540
+ animateUnitsSpeed: number;
1541
+ /**
1542
+ * Tools ▸ Test Map: whether to start the game after writing the map (desktop build), and
1543
+ * the game folder to use when the desktop build should not search ("" = search). The
1544
+ * browser's folder is a handle in IndexedDB (`services/handleStore.ts`), not here.
1545
+ */
1546
+ testMap: {
1547
+ launch: boolean;
1548
+ dir: string;
1549
+ };
1550
+ /**
1551
+ * The desktop build's in-app updates (`editor/updates.ts`, `desktop/updater.ts`); the
1552
+ * browser build shows none of this, having nothing to update. `checkOnStart` asks GitHub
1553
+ * for a newer version a few seconds after launch and raises a toast when there is one.
1554
+ * `nightly` follows the nightly channel instead of the numbered releases — one-way in
1555
+ * practice, since going back to a stable release is a downgrade the updater will not
1556
+ * offer.
1557
+ */
1558
+ updates: {
1559
+ checkOnStart: boolean;
1560
+ nightly: boolean;
1561
+ };
1562
+ }
1563
+ /**
1564
+ * The version a host provides; a manifest that asks for a newer one is refused. It stays
1565
+ * at 1 while the API is only used by the plugins in the scm-js organisation and grows
1566
+ * with them — the first incompatible change after outside plugins exist bumps it.
1567
+ */
1568
+ export declare const PLUGIN_API_VERSION = 1;
1569
+ export interface Disposable {
1570
+ dispose(): void;
1571
+ }
1572
+ /**
1573
+ * A transaction builder's return type: whatever it likes, so long as it is not a promise.
1574
+ *
1575
+ * Everything asynchronous in this API is a promise — `await` it. The two transaction
1576
+ * builders (`document.edit` and `document.update`) are the exception, and deliberately
1577
+ * so: their operations apply as they are called and one commit closes the transaction
1578
+ * when the builder returns, so an `async` builder would commit the part of its work that
1579
+ * ran before the first `await` and leave the rest to land outside the entry, breaking
1580
+ * undo without an error. Do the awaiting *before* the call:
1581
+ *
1582
+ * ```ts
1583
+ * await api.tileset.load(); // the async part, first
1584
+ * api.document.edit("Fill", tx => tx.stampTerrain(rect, id)); // then the write
1585
+ * ```
1586
+ *
1587
+ * TypeScript refuses an `async` builder because of this type; `runTransaction` and
1588
+ * `runUpdate` also catch one at runtime, for a plugin written in plain JavaScript.
1589
+ */
1590
+ export type Sync<T> = T extends PromiseLike<unknown> ? "a transaction builder must be synchronous: await before document.edit() / update(), not inside it" : T;
1591
+ /** `plugin.json`, next to the entry file. Only `name` is required. */
1592
+ export interface PluginManifest {
1593
+ name: string;
1594
+ /** Stable identifier (storage prefix, log prefix); derived from the name when absent. */
1595
+ id?: string;
1596
+ version?: string;
1597
+ description?: string;
1598
+ author?: string;
1599
+ homepage?: string;
1600
+ /** Entry file relative to the manifest; `plugin.ts` by default, then `plugin.js`. */
1601
+ entry?: string;
1602
+ /**
1603
+ * A built, self-contained JavaScript bundle of the plugin, relative to the manifest
1604
+ * (`"dist/plugin.js"`). When there is one the editor loads **it** and never `entry`:
1605
+ * one fetch, no TypeScript compiler, no import graph to walk — and the plugin may use
1606
+ * npm dependencies, which the source path cannot (`bundleModule` refuses a bare
1607
+ * specifier because it has no resolver).
1608
+ *
1609
+ * `entry` stays the source of truth and stays in the manifest: it is what a person
1610
+ * reads, what `npm run typecheck` checks, and what loads when there is no build. A
1611
+ * repository that publishes one builds it in CI at the tag it is loaded from — see
1612
+ * `docs/plugins.md`.
1613
+ */
1614
+ build?: string;
1615
+ /**
1616
+ * The plugin's face in Manage Plugins and on its own dialogs: an emoji (`"🗺️"`), a
1617
+ * `data:image/…` URI, an `https://…` image, or an image file beside the manifest
1618
+ * (`"icon.svg"`). Anything else is ignored — see `resolveIcon` in `loader.ts`.
1619
+ */
1620
+ icon?: string;
1621
+ /** The `PLUGIN_API_VERSION` the plugin was written against. */
1622
+ api?: number;
1623
+ }
1624
+ /** What `activate` may hand back: nothing, a cleanup function, or a Disposable. */
1625
+ export type Deactivate = void | (() => void) | Disposable;
1626
+ export type PluginActivate = (api: PluginApi) => Deactivate | Promise<Deactivate>;
1627
+ /**
1628
+ * The shape of the entry module: `export default function activate(api) {…}`, or
1629
+ * `export default { activate }`, or `export function activate(api) {…}`.
1630
+ */
1631
+ export interface PluginModule {
1632
+ activate: PluginActivate;
1633
+ }
1634
+ /** A manifest `icon` after resolution: a glyph to print, or an image to show. */
1635
+ export type PluginIcon = {
1636
+ kind: "text";
1637
+ text: string;
1638
+ } | {
1639
+ kind: "image";
1640
+ url: string;
1641
+ };
1642
+ export interface PluginInfo {
1643
+ /** Manifest id (or slug of the name). */
1644
+ id: string;
1645
+ name: string;
1646
+ /** The spec the plugin was installed from (`builtin:…`, `github:…`, a URL). */
1647
+ source: string;
1648
+ version?: string;
1649
+ /** The manifest's `icon`, resolved; absent when it declared none (or an unusable one). */
1650
+ icon?: PluginIcon;
1651
+ }
1652
+ export interface PluginApi {
1653
+ readonly apiVersion: number;
1654
+ readonly plugin: PluginInfo;
1655
+ readonly document: DocumentApi;
1656
+ /** The settings dialogs' tables, read-only; `document.update` writes them. */
1657
+ readonly settings: SettingsApi;
1658
+ readonly triggers: TriggersApi;
1659
+ readonly terrain: TerrainApi;
1660
+ readonly tileset: TilesetApi;
1661
+ readonly selection: SelectionApi;
1662
+ /** Cut / Copy / Paste: the clip, its parts, and pasting — what the clipboard layer does. */
1663
+ readonly clipboard: ClipboardApi;
1664
+ /** The file formats behind File ▸ Import / Export: `.trg` and the strings text. */
1665
+ readonly exchange: ExchangeApi;
1666
+ readonly palette: PaletteApi;
1667
+ readonly names: NamesApi;
1668
+ /** StarCraft's `<XX>` text control codes: what they mean, and what a string looks like drawn. */
1669
+ readonly text: TextApi;
1670
+ readonly query: QueryApi;
1671
+ readonly data: DataApi;
1672
+ readonly graphics: GraphicsApi;
1673
+ readonly view: ViewApi;
1674
+ readonly ui: UiApi;
1675
+ readonly menu: MenuApi;
1676
+ readonly contextMenu: ContextMenuApi;
1677
+ readonly hotkeys: HotkeyApi;
1678
+ readonly commands: CommandsApi;
1679
+ readonly events: EventsApi;
1680
+ readonly storage: StorageApi;
1681
+ /** `console.log` with the plugin's name in front. */
1682
+ log(...args: unknown[]): void;
1683
+ }
1684
+ export interface DocumentInfo {
1685
+ name: string;
1686
+ description: string;
1687
+ width: number;
1688
+ height: number;
1689
+ tileset: TilesetId;
1690
+ /** ERA as stored (the tileset is `era & 7`). */
1691
+ era: number;
1692
+ /** CHK VER: 59 original, 63 hybrid, 205 Brood War, 206 Remastered. */
1693
+ version: number;
1694
+ fileName: string | null;
1695
+ modified: boolean;
1696
+ }
1697
+ /** What one `document.edit` changed, per list. */
1698
+ export interface EditResult {
1699
+ changed: boolean;
1700
+ tiles: number;
1701
+ isom: number;
1702
+ units: number;
1703
+ sprites: number;
1704
+ doodads: number;
1705
+ locations: number;
1706
+ fog: number;
1707
+ notes: string[];
1708
+ }
1709
+ export type MapFileFormat = "scx" | "scm" | "chk";
1710
+ export interface ExportOptions {
1711
+ /** The container: the open file's (else `scx`) — `scx` / `scm` archives, `chk` the bare scenario. */
1712
+ format?: MapFileFormat;
1713
+ /** The file's name; defaults to the open file's, or the scenario name plus the format. */
1714
+ fileName?: string;
1715
+ /**
1716
+ * Compression, encryption and what is left out: over the options Save last used for this
1717
+ * map (or its defaults — PKWARE and encryption for a new map, the way it was opened for
1718
+ * an opened one), so the bytes are what Save would write unless you say otherwise.
1719
+ */
1720
+ saveOptions?: Partial<Omit<SaveOptions, "format">>;
1721
+ }
1722
+ /** `document.save` / `saveAs`. */
1723
+ export interface SaveDocumentOptions {
1724
+ /** Write a copy: the document keeps its own name, handle and clean state. */
1725
+ copy?: boolean;
1726
+ }
1727
+ export interface ChangeTilesetOptions {
1728
+ tileset: TilesetId;
1729
+ /** ISOM id of the terrain the map is refilled with; the tileset's default when omitted. */
1730
+ terrainId?: number;
1731
+ /** Keep the tile numbers and change only ERA (what SCMDraft's switch does). */
1732
+ keepTiles?: boolean;
1733
+ }
1734
+ /**
1735
+ * The files stored in the map archive next to `staredit\scenario.chk`: custom sounds,
1736
+ * graphics, and anything a plugin wants to keep with the map. Names are archive paths
1737
+ * with backslashes (`staredit\wav\hello.wav`). They are written on Save.
1738
+ */
1739
+ export interface ExtrasApi {
1740
+ list(): string[];
1741
+ get(name: string): Uint8Array | null;
1742
+ /** Add or replace a member; marks the map modified. */
1743
+ set(name: string, bytes: Uint8Array): void;
1744
+ /** Remove a member; true when there was one. */
1745
+ remove(name: string): boolean;
1746
+ }
1747
+ /** What `document.history()` answers. */
1748
+ export interface DocumentHistory {
1749
+ /** Label of the entry Undo would take back; null with nothing to undo. */
1750
+ undo: string | null;
1751
+ redo: string | null;
1752
+ undoDepth: number;
1753
+ redoDepth: number;
1754
+ }
1755
+ /** Scenario ▸ Resize / Crop Map's form. Width and height are clamped to 1…256, the anchor to 0…8. */
1756
+ export interface ResizeDocumentOptions {
1757
+ width: number;
1758
+ height: number;
1759
+ /** 3 × 3 grid, row-major: 0 top-left … 4 centre … 8 bottom-right. Default 4. */
1760
+ anchor?: number;
1761
+ /** ISOM id of the terrain for the new area (a `TerrainType.id`); the tileset's default when omitted. */
1762
+ terrainId?: number;
1763
+ /** Pull locations that hang past the new edge back inside; default true. */
1764
+ clampLocations?: boolean;
1765
+ }
1766
+ /** File ▸ New's form: size, tileset, and the two strings the dialog asks for. */
1767
+ export interface NewDocumentOptions {
1768
+ width: number;
1769
+ height: number;
1770
+ tileset: TilesetId;
1771
+ /** `Untitled Scenario` when omitted. */
1772
+ name?: string;
1773
+ description?: string;
1774
+ /** ISOM id of the terrain to fill with; the tileset's default ground when omitted. */
1775
+ terrainId?: number;
1776
+ }
1777
+ export interface DocumentApi {
1778
+ isOpen(): boolean;
1779
+ info(): DocumentInfo | null;
1780
+ /**
1781
+ * The live scenario, for reading. Writing to it directly bypasses undo, dirty
1782
+ * tracking and repaints — use `edit`.
1783
+ */
1784
+ scenario(): Scenario | null;
1785
+ /**
1786
+ * Run `build` against a transaction and record what it did as one undo entry.
1787
+ * Operations apply as they are called, so later ones see earlier ones' results.
1788
+ * Returns an all-zero result with `changed: false` when no map is open.
1789
+ *
1790
+ * `build` is synchronous — see `Sync`. Await what you need (graphics, a pick, a
1791
+ * fetch) before the call, then write in one go.
1792
+ */
1793
+ edit<R>(label: string, build: (tx: EditTransaction) => Sync<R>): EditResult;
1794
+ /**
1795
+ * The second kind of write: the tables and settings that live outside the undo model
1796
+ * — triggers, the string table, switch names, the scenario's own properties — as one
1797
+ * transaction, the way a settings dialog's OK applies its whole form at once.
1798
+ * Operations apply as they are called; the commit marks the map modified and bumps
1799
+ * what the chrome reads. There is no undo entry: keep your own if you need one.
1800
+ * `build` is synchronous, as `edit`'s is.
1801
+ */
1802
+ update<R>(label: string, build: (tx: UpdateTransaction) => Sync<R>): UpdateResult;
1803
+ undo(): string | null;
1804
+ redo(): string | null;
1805
+ /** The undo and redo stacks' tops — the labels the Edit menu shows — and their depths, without moving anything. */
1806
+ history(): DocumentHistory;
1807
+ /**
1808
+ * Open a map file (`.scx` / `.scm` / `.chk`) in place of the current one, the way
1809
+ * File ▸ Open does: when the open map has unsaved changes and Preferences say to ask,
1810
+ * the Close Scenario dialog comes first and the user may cancel. Resolves true once
1811
+ * the file is the open document, false when the user kept the current map or the
1812
+ * file could not be read (the status bar says why).
1813
+ */
1814
+ open(file: File | Blob | Uint8Array, fileName?: string): Promise<boolean>;
1815
+ /**
1816
+ * A blank map in place of the current one, the way File ▸ New makes one: flat ground
1817
+ * of the tileset's default terrain (or `terrainId`), an ISOM lattice to match, every
1818
+ * section a fresh map needs. Goes through the same unsaved-changes gate as `open`.
1819
+ * Resolves true once the new map is the open document, false when the user kept the
1820
+ * current one.
1821
+ */
1822
+ create(options: NewDocumentOptions): Promise<boolean>;
1823
+ /**
1824
+ * The open map as a file, as Save would write it — the remembered save options, the
1825
+ * archive extras included — unless `saveOptions` says otherwise. Null when no map is open.
1826
+ */
1827
+ export(options?: ExportOptions): Promise<File | null>;
1828
+ /**
1829
+ * File ▸ Save: write the map back where it came from with the options last confirmed
1830
+ * for it — into the file when the browser gave a handle, else through the browser's
1831
+ * save dialog or as a download; a map with no file yet goes through the Save dialog.
1832
+ * Resolves true once written, false when the user dismissed a dialog or the write failed
1833
+ * (the status bar and a toast say so).
1834
+ */
1835
+ save(options?: SaveDocumentOptions): Promise<boolean>;
1836
+ /** File ▸ Save As (or Save Copy As with `copy`): the Save dialog, resolving as `save` does. */
1837
+ saveAs(options?: SaveDocumentOptions): Promise<boolean>;
1838
+ /**
1839
+ * File ▸ Close: through the same unsaved-changes gate as `open`. Resolves true once the
1840
+ * map is closed (`isOpen()` is then false), false when the user kept it.
1841
+ */
1842
+ close(): Promise<boolean>;
1843
+ /**
1844
+ * A picture of the map as File ▸ Export ▸ Image draws it, as a PNG. `pixelsPerTile`
1845
+ * is the one dial (32 is the game's art 1:1, 1 is a minimap); the other options
1846
+ * default as the dialog's do. Needs the tileset graphics — without them, or without
1847
+ * a map, null.
1848
+ */
1849
+ renderImage(options?: Partial<MapImageOptions>): Promise<Blob | null>;
1850
+ /**
1851
+ * Scenario ▸ Resize / Crop Map: a transaction outside the undo model that drops both
1852
+ * history stacks (as the dialog does). Content keeps its place relative to `anchor`
1853
+ * (a 3 × 3 grid, row-major, 4 = centre, the default); the new ground is `terrainId`
1854
+ * (the tileset's default when omitted); objects outside the new bounds are dropped,
1855
+ * locations clamped unless `clampLocations` is false. Null with no map.
1856
+ */
1857
+ resize(options: ResizeDocumentOptions): ResizeResult | null;
1858
+ /**
1859
+ * Scenario ▸ Map Properties ▸ Tileset: change ERA and lay the terrain again with the
1860
+ * new tileset's terrain (tile numbers do not carry across tilesets; the doodads go with
1861
+ * them, everything else stays). Waits for the new graphics so the fill uses real tiles.
1862
+ * Like `resize`, a transaction outside the undo model. Null with no map.
1863
+ */
1864
+ changeTileset(options: ChangeTilesetOptions): Promise<ChangeTilesetResult | null>;
1865
+ readonly extras: ExtrasApi;
1866
+ readonly sections: SectionsApi;
1867
+ }
1868
+ /** What a raw section edit reported: the parser's remarks about the file it produced. */
1869
+ export interface RawEditResult {
1870
+ /** Warnings from parsing the edited file (a truncated section, no usable DIM, …); empty when it read cleanly. */
1871
+ warnings: string[];
1872
+ }
1873
+ /**
1874
+ * The scenario file at the byte level: every section occurrence in the order Save would
1875
+ * write them, with the bytes it would write — unsaved edits already encoded — and raw
1876
+ * edits to any of them.
1877
+ *
1878
+ * A raw edit is a different kind of transaction from `document.edit`: the edited file is
1879
+ * parsed again from scratch and installed as the open document, so the change reaches
1880
+ * every part of the editor whether or not it models the section — and, as with Resize,
1881
+ * the undo history is dropped and every selection cleared. The map is marked modified
1882
+ * and the `"document"` event fires. Keep your own undo if you need one.
1883
+ *
1884
+ * Indices are positions in `list()` and shift when a section is inserted or removed
1885
+ * before them; take a fresh `list()` after every edit.
1886
+ */
1887
+ export interface SectionsApi {
1888
+ /** Every occurrence in file order; empty without a map. */
1889
+ list(): SectionInfo[];
1890
+ /** A copy of one occurrence's payload. */
1891
+ bytes(index: number): Uint8Array;
1892
+ /**
1893
+ * The bytes the game acts on for a name — repeated occurrences combined the way the
1894
+ * game combines them (`SectionKnowledge.mode`) — or null when the file has none.
1895
+ */
1896
+ combined(name: string): Uint8Array | null;
1897
+ /** The whole CHK as Save would write it (the archive extras are `document.extras`). */
1898
+ file(): Uint8Array;
1899
+ /** What the editor knows about a section name, sized for the open map; null for an unknown name. */
1900
+ spec(name: string): SectionKnowledge | null;
1901
+ /** Every section the editor knows, sized for the open map. */
1902
+ known(): SectionKnowledge[];
1903
+ /** Replace one occurrence's payload. */
1904
+ write(index: number, bytes: Uint8Array): RawEditResult;
1905
+ /** Rename one occurrence (four characters; shorter names are padded with spaces). */
1906
+ rename(index: number, name: string): RawEditResult;
1907
+ /** Insert a section before `index` (`list().length` appends). */
1908
+ insert(index: number, name: string, bytes: Uint8Array): RawEditResult;
1909
+ remove(index: number): RawEditResult;
1910
+ /** Move the occurrence at `from` so that it sits at `to`. */
1911
+ move(from: number, to: number): RawEditResult;
1912
+ /** Replace the whole CHK, the way File ▸ Open reads one. */
1913
+ replaceFile(bytes: Uint8Array): RawEditResult;
1914
+ /**
1915
+ * Bytes after the last chunk the reader could parse — what follows a chunk header with
1916
+ * a negative length, say. Save writes them back as they are; a `replaceFile` without
1917
+ * them drops them. Null when the file ends cleanly.
1918
+ */
1919
+ trailing(): Uint8Array | null;
1920
+ /** 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). */
1921
+ required(): string[];
1922
+ /**
1923
+ * The bytes File ▸ New would write for a section on a map of this size, tileset and
1924
+ * revision — StarEdit's defaults for a settings table, the fixed VCOD, an empty list,
1925
+ * null terrain. Null for a section the editor cannot produce: one it does not model,
1926
+ * or an optional one a new map has no value for (CRGB, SWNM).
1927
+ */
1928
+ defaults(name: string): Uint8Array | null;
1929
+ /**
1930
+ * Re-encode sections from the editor's model, the way Save writes a dirty one, and
1931
+ * install the result like any other raw edit. This is what turns a protected file back
1932
+ * into a plain one: repeated occurrences collapse into one, a truncated or oversized
1933
+ * section comes back at the size the model encodes to, a string table whose offsets
1934
+ * point nowhere is rewritten with every string the editor could read. Names the editor
1935
+ * does not model, and modelled ones whose model is absent (no ISOM, no settings table),
1936
+ * are left as they are and missing from `rebuilt`; omit `names` for every modelled
1937
+ * section the map has a model for.
1938
+ */
1939
+ rebuild(names?: string[]): RebuildResult;
1940
+ }
1941
+ /** What `tx.rebuildIsom` did. */
1942
+ export interface IsomRebuildResult {
1943
+ /** The map had no usable ISOM, so one was created. */
1944
+ created: boolean;
1945
+ /** Lattice values that changed (every one of a created section). */
1946
+ changed: number;
1947
+ /** Diamonds the rebuild resolved from the tiles. */
1948
+ diamonds: number;
1949
+ /** Diamonds it had to guess — under doodads or off the edge. */
1950
+ unresolved: number;
1951
+ }
1952
+ /** Cells for the bulk terrain operations: a tile rect, or cell indices (`y * width + x`). */
1953
+ export type Cells = Rect | Iterable<number>;
1954
+ export interface EditTransaction {
1955
+ readonly scenario: Scenario;
1956
+ readonly width: number;
1957
+ readonly height: number;
1958
+ /** MTXM (what the game draws) at a cell. */
1959
+ tileAt(x: number, y: number): number;
1960
+ /** TILE (the ground without doodads) at a cell. */
1961
+ groundAt(x: number, y: number): number;
1962
+ /** One tile into both sections. */
1963
+ setTile(x: number, y: number, id: number): void;
1964
+ /** Many tiles into both sections; returns how many changed. */
1965
+ setTiles(cells: Cells, id: number): number;
1966
+ /**
1967
+ * The Rect brush: flat left/right pairs by column parity, one random variation per
1968
+ * pair (`variation` pins it). Needs the tileset graphics; returns tiles changed.
1969
+ */
1970
+ stampTerrain(cells: Cells, terrainId: number, variation?: number): number;
1971
+ /** Lay terrain the way a new map is laid over `rect`, ISOM lattice included when the map has one. */
1972
+ fillFlat(rect: Rect, terrainId: number): number;
1973
+ /**
1974
+ * The isometric brush on one diamond: sets its ISOM value and regenerates the tiles
1975
+ * around it, cliffs and shores included. Needs ISOM and the tileset; returns whether
1976
+ * the terrain could be painted there.
1977
+ */
1978
+ paintIsom(d: Diamond, terrainId: number, extent?: number): boolean;
1979
+ /**
1980
+ * Reconstruct the ISOM section from the tiles — for a map that arrived without one, or
1981
+ * whose lattice no longer matches after Rect / Tile edits: exact for terrain that was
1982
+ * laid down isometrically, a best guess under doodads and for hand-placed tiles. A
1983
+ * missing or wrongly sized ISOM is created (undo removes it again); an existing one gets
1984
+ * only the diamonds that differ. Needs the tileset graphics; null without them.
1985
+ */
1986
+ rebuildIsom(): IsomRebuildResult | null;
1987
+ /** A StarEdit-style unit record (fresh serial, valid/used masks) centred on map pixels. */
1988
+ makeUnit(unitId: number, owner: number, x: number, y: number): UnitRecord;
1989
+ addUnits(records: UnitRecord[]): number[];
1990
+ removeUnits(indices: number[]): number;
1991
+ updateUnits(indices: number[], patch: (u: UnitRecord) => Partial<UnitRecord>): number;
1992
+ /**
1993
+ * A unit the way the Units palette places one: a building snaps its placement box to
1994
+ * the tile grid (when the palette's *Snap to grid* is on), anything else lands where
1995
+ * you say, and nothing leaves the map. No placement checks — ask `canPlaceUnit` first
1996
+ * if you want them. Returns the record's index.
1997
+ */
1998
+ placeUnit(unitId: number, owner: number, x: number, y: number): number;
1999
+ /** Whether the Units palette's placement checks, with its current options, allow a unit of this type centred there. */
2000
+ canPlaceUnit(unitId: number, x: number, y: number): boolean;
2001
+ makeSprite(kind: SpriteKind, id: number, owner: number, x: number, y: number, opts?: {
2002
+ flipped?: boolean;
2003
+ disabled?: boolean;
2004
+ }): SpriteRecord;
2005
+ addSprites(records: SpriteRecord[]): number[];
2006
+ removeSprites(indices: number[]): number;
2007
+ /** `makeSprite` + `addSprites` in one, kept on the map; returns the record's index. */
2008
+ placeSprite(kind: SpriteKind, id: number, owner: number, x: number, y: number, opts?: {
2009
+ flipped?: boolean;
2010
+ disabled?: boolean;
2011
+ }): number;
2012
+ /** Stamp a doodad (a `dddata.bin` id) at a tile; returns its record index, or -1 when unknown or off the map. */
2013
+ placeDoodad(doodadId: number, tx: number, ty: number, owner?: number): number;
2014
+ removeDoodads(indices: number[]): number;
2015
+ /** A location in the lowest free slot (pixel bounds); returns the slot, or -1 when the table is full. */
2016
+ addLocation(bounds: Bounds, name?: string, elevationFlags?: number): number;
2017
+ editLocation(index: number, patch: LocationPatch): boolean;
2018
+ removeLocations(indices: number[]): number;
2019
+ /** Set (`"fog"`) or clear the `players` bits (bit n = player n + 1) over cells; creates MASK on first use. */
2020
+ setFog(cells: Cells, players: number, mode: FogMode): number;
2021
+ /**
2022
+ * Tools ▸ Replace Terrain: every tile matching `from` (a flat terrain by ISOM id, or
2023
+ * one exact tile) becomes `to`, over `rect` or the whole map; pairs are laid as the
2024
+ * Rect brush lays them. Returns tiles changed. Terrain picks need the graphics.
2025
+ */
2026
+ replaceTerrain(from: TerrainPick, to: TerrainPick, rect?: Rect): number;
2027
+ /**
2028
+ * The bucket fill: the 4-connected area around (x, y) of the same terrain type (`match:
2029
+ * "terrain"`, the Rect fill) or the same exact tile (`"tile"`), laid with `terrainId`
2030
+ * as the Rect brush would, or set to `tileId`. Returns tiles changed.
2031
+ */
2032
+ fillArea(x: number, y: number, fill: {
2033
+ terrainId: number;
2034
+ } | {
2035
+ tileId: number;
2036
+ }, match?: "terrain" | "tile"): number;
2037
+ /**
2038
+ * The Blend brush: `id` goes on the cell beside the anchor tile on `side`
2039
+ * (`terrain.blendCandidates` ranks what fits). Returns whether the cell was on the map.
2040
+ */
2041
+ placeBlend(x: number, y: number, side: Side, id: number): boolean;
2042
+ /**
2043
+ * What StarEdit does after an isometric edit: regenerate every tile from the ISOM
2044
+ * lattice (the reverse of `rebuildIsom`). Needs ISOM and the graphics; returns tiles
2045
+ * changed, or null without them.
2046
+ */
2047
+ tilesFromIsom(): number | null;
2048
+ /**
2049
+ * The cells and their images under the symmetry mode (Tools ▸ Symmetry…), each once —
2050
+ * what the built-in brushes paint over. With the mode off, the cells as given.
2051
+ */
2052
+ mirror(cells: Cells): number[];
2053
+ /** A map pixel and its images under the symmetry mode, the original first. */
2054
+ mirrorPoint(px: number, py: number): {
2055
+ x: number;
2056
+ y: number;
2057
+ }[];
2058
+ /** Shift units by a pixel delta; buildings re-snap to the grid when `snap` (the palette's option when omitted). Returns records changed. */
2059
+ moveUnits(indices: number[], dx: number, dy: number, snap?: boolean): number;
2060
+ /**
2061
+ * Tools ▸ Auto-place Start Locations: one per player on a ring or in the corners, each
2062
+ * moved to the nearest spot the placement checks accept; `replace` removes the existing
2063
+ * ones first. Players count from 1.
2064
+ */
2065
+ placeStartLocations(options: {
2066
+ players: number;
2067
+ layout?: StartLayout;
2068
+ margin?: number;
2069
+ replace?: boolean;
2070
+ }): StartPlacementResult;
2071
+ updateSprites(indices: number[], patch: (r: SpriteRecord) => Partial<SpriteRecord>): number;
2072
+ /** Shift sprites by a pixel delta, clamped to the map. Returns records changed. */
2073
+ moveSprites(indices: number[], dx: number, dy: number): number;
2074
+ /** Change a doodad's owner or disabled flag (its tiles stay). Returns records changed. */
2075
+ updateDoodads(indices: number[], patch: {
2076
+ owner?: number;
2077
+ disabled?: number;
2078
+ }): number;
2079
+ /** Put Anywhere (slot 63) back to the whole map; returns whether it had to move. */
2080
+ restoreAnywhere(): boolean;
2081
+ /** Flip every tile's fog bit for `players`; returns tiles changed. */
2082
+ invertFog(players: number): number;
2083
+ /** Copy one player's fog (0-based) onto the players in the `to` bit mask (bit n = player n + 1); returns tiles changed. */
2084
+ copyFog(from: number, to: number): number;
2085
+ /** Fog or clear the connected area around (x, y) that shares `player`'s fog state, for `players`; returns tiles changed. */
2086
+ floodFog(x: number, y: number, player: number, players: number, mode: FogMode): number;
2087
+ /** A line for the status bar, appended to the label. */
2088
+ note(text: string): void;
2089
+ }
2090
+ /**
2091
+ * What one `document.update` changed: every section it marked dirty, in the order the
2092
+ * operations touched them, and `changed` false when they were all no-ops.
2093
+ */
2094
+ export interface UpdateResult {
2095
+ changed: boolean;
2096
+ sections: string[];
2097
+ notes: string[];
2098
+ }
2099
+ /** TRIG or MBRF as a list. Operations apply as they are called, like `document.edit`'s. */
2100
+ export interface TriggerListUpdate {
2101
+ /** The list as it stands, cloned. */
2102
+ list(): TriggerRecord[];
2103
+ count(): number;
2104
+ /** Replace the whole list. */
2105
+ set(list: TriggerRecord[]): void;
2106
+ /** Insert one (at the end without `at`); returns its index. */
2107
+ add(trigger: TriggerRecord, at?: number): number;
2108
+ /** Replace one record; false when there is none at `index`. */
2109
+ replace(index: number, trigger: TriggerRecord): boolean;
2110
+ remove(indices: number[]): number;
2111
+ move(from: number, to: number): boolean;
2112
+ /**
2113
+ * Parse the text format (`triggers.text.print`'s inverse) and append the result, or
2114
+ * replace the list with `replace: true`. Strings the text names are interned as it
2115
+ * parses. Throws with the line number when the text does not parse.
2116
+ */
2117
+ fromText(source: string, options?: {
2118
+ replace?: boolean;
2119
+ }): number;
2120
+ }
2121
+ /**
2122
+ * The string table. Nothing is ever renumbered: `set` overwrites a slot (every record
2123
+ * pointing at it sees the new text) and `intern` appends rather than reusing a slot
2124
+ * something else may share.
2125
+ */
2126
+ export interface StringsUpdate {
2127
+ list(): (string | null)[];
2128
+ /** The index of `text`: an identical entry when there is one, else a new one. 0 for `""`. */
2129
+ intern(text: string): number;
2130
+ /** Overwrite one slot. */
2131
+ set(index: number, text: string): void;
2132
+ /** Install a whole table; unreferenced trailing blanks are dropped, other indices keep their place. */
2133
+ apply(list: (string | null)[]): void;
2134
+ /**
2135
+ * File ▸ Import ▸ Strings: `index<TAB>text` lines (`exchange.formatStrings` writes
2136
+ * them) set in place, indices past the end appended. Returns how many were replaced and added.
2137
+ */
2138
+ import(text: string): {
2139
+ replaced: number;
2140
+ added: number;
2141
+ };
2142
+ }
2143
+ export interface SwitchesUpdate {
2144
+ names(): string[];
2145
+ /** Name a switch (0-based); `""` clears the name. Creates SWNM on the first one. */
2146
+ setName(index: number, name: string): void;
2147
+ }
2148
+ /**
2149
+ * The second kind of write (see `document.update`): the tables and settings the editor's
2150
+ * own dialogs edit. Operations apply to the scenario as they are called — a string
2151
+ * interned on one line is there for the trigger added on the next — and the commit at
2152
+ * the end marks the map modified and tells the chrome to re-read.
2153
+ */
2154
+ export interface UpdateTransaction {
2155
+ readonly scenario: Scenario;
2156
+ /** TRIG. */
2157
+ readonly triggers: TriggerListUpdate;
2158
+ /** MBRF, the mission briefing's own list of the same records. */
2159
+ readonly briefing: TriggerListUpdate;
2160
+ readonly strings: StringsUpdate;
2161
+ /** SWNM. */
2162
+ readonly switches: SwitchesUpdate;
2163
+ /** SPRP: the scenario's name and description (`""` restores the file-name default). */
2164
+ properties(patch: {
2165
+ name?: string;
2166
+ description?: string;
2167
+ }): void;
2168
+ /** OWNR / SIDE / COLR / CRGB / FORC: the Player Settings and Player Colors dialogs. */
2169
+ readonly players: PlayersUpdate;
2170
+ /** FORC: the Force Settings dialog. */
2171
+ readonly forces: ForcesUpdate;
2172
+ /** UNIS / UNIx and PUNI: the Unit Settings dialog. */
2173
+ readonly unitTypes: UnitTypesUpdate;
2174
+ /** UPGS / UPGx and UPGR / PUPx: the Upgrade Settings dialog. */
2175
+ readonly upgrades: UpgradesUpdate;
2176
+ /** TECS / TECx and PTEC / PTEx: the Technology Settings dialog. */
2177
+ readonly techs: TechsUpdate;
2178
+ /** WAV and the archive's sound files: the Sound Editor. */
2179
+ readonly sounds: SoundsUpdate;
2180
+ /** UPRP / UPUS: the Create Unit with Properties slots (Triggers ▸ Unit Properties Slots…). */
2181
+ readonly cuwp: CuwpUpdate;
2182
+ /** Scenario ▸ Map Revision: VER / TYPE and, moving to or from Remastered, the string table's width. */
2183
+ setVersion(version: MapVersion, extendedStrings?: boolean): void;
2184
+ /** A line for the status bar, appended to the label. */
2185
+ note(text: string): void;
2186
+ }
2187
+ export interface PlayersUpdate {
2188
+ /** All 12 slots, 0-based, with the effective colour and force. */
2189
+ list(): PlayerSlotView[];
2190
+ /** Patch one slot; colours and forces apply to the eight playable slots only. */
2191
+ set(slot: number, patch: PlayerPatch): boolean;
2192
+ }
2193
+ export interface ForcesUpdate {
2194
+ list(): ForceView[];
2195
+ set(force: number, patch: ForcePatch): boolean;
2196
+ }
2197
+ export interface UnitTypesUpdate {
2198
+ /** The effective row for a units.dat id — the dat's numbers where the type is on "use default". */
2199
+ get(unitId: number): UnitTypeView;
2200
+ /**
2201
+ * Patch one type. Setting any number turns "use default" off for it (seeding the untouched
2202
+ * columns from the dat, as the dialog does); `useDefault: true` puts it back. Hit points are
2203
+ * whole points. `name` is the custom name (`""` restores the default); `available` edits PUNI.
2204
+ */
2205
+ set(unitId: number, patch: UnitTypePatch): boolean;
2206
+ }
2207
+ export interface UpgradesUpdate {
2208
+ get(upgradeId: number): UpgradeView;
2209
+ set(upgradeId: number, patch: UpgradePatch): boolean;
2210
+ }
2211
+ export interface TechsUpdate {
2212
+ get(techId: number): TechView;
2213
+ set(techId: number, patch: TechPatch): boolean;
2214
+ }
2215
+ export interface SoundsUpdate {
2216
+ /** The 512 WAV slots in use, joined with the archive (`present` says whether the file is there). */
2217
+ list(): SoundRow[];
2218
+ /**
2219
+ * Put `path` (`staredit\wav\name.wav`, or just `name.wav`) in the first free slot — the
2220
+ * existing slot when it is already listed — and, with `bytes`, store the file in the archive.
2221
+ * Returns the slot, or -1 when all 512 are taken.
2222
+ */
2223
+ add(path: string, bytes?: Uint8Array): number;
2224
+ /** Clear a slot; with `deleteFile`, remove the archive member too. */
2225
+ remove(slot: number, deleteFile?: boolean): boolean;
2226
+ }
2227
+ /**
2228
+ * The 64 Create Unit with Properties slots. A slot is addressed 0-based here; the action
2229
+ * that uses one stores the number 1-based (`target` = slot + 1), as `CuwpSlotView.index`
2230
+ * + 1. A view's field is `null` where the created units keep the type's default.
2231
+ */
2232
+ export interface CuwpUpdate {
2233
+ list(): CuwpSlotView[];
2234
+ get(index: number): CuwpSlotView | null;
2235
+ /**
2236
+ * Patch one slot: a number sets the field and its "applied" bit, `null` clears the bit;
2237
+ * a boolean forces a special state, `null` leaves it to the unit. `used` is StarEdit's
2238
+ * "in use" tick, on by itself once the slot sets anything.
2239
+ */
2240
+ set(index: number, patch: CuwpSlotPatch, used?: boolean): boolean;
2241
+ /** Back to an empty, unticked slot. */
2242
+ clear(index: number): boolean;
2243
+ }
2244
+ /**
2245
+ * What the settings dialogs show, read without a transaction: the same views
2246
+ * `document.update`'s `tx.players` … `tx.techs` hand out. Every list is empty and every
2247
+ * single read null when no map is open.
2248
+ */
2249
+ export interface SettingsApi {
2250
+ players(): PlayerSlotView[];
2251
+ player(slot: number): PlayerSlotView | null;
2252
+ forces(): ForceView[];
2253
+ unitType(unitId: number): UnitTypeView | null;
2254
+ /** Every type with a name, in id order. */
2255
+ unitTypes(): UnitTypeView[];
2256
+ upgrade(upgradeId: number): UpgradeView | null;
2257
+ upgrades(): UpgradeView[];
2258
+ tech(techId: number): TechView | null;
2259
+ techs(): TechView[];
2260
+ sounds(): SoundRow[];
2261
+ /** Whether a player (0-based) may build a unit type — PUNI's per-player byte resolved against its global default. */
2262
+ unitAvailable(player: number, unitId: number): boolean;
2263
+ /** The Create Unit with Properties slots, 0-based, with how many actions name each. */
2264
+ cuwpSlots(): CuwpSlotView[];
2265
+ cuwpSlot(index: number): CuwpSlotView | null;
2266
+ version(): MapVersionView | null;
2267
+ }
2268
+ /**
2269
+ * What each condition and action type takes: the argument list in the order StarEdit's
2270
+ * TrigEdit shows it, each naming the record field it lives in and the kind of value it
2271
+ * is (a player group, a unit id, a location, a comparison, …). Everything that displays
2272
+ * or edits a trigger reads this table, the editor's own dialogs included.
2273
+ */
2274
+ export interface TriggerDefsApi {
2275
+ conditions(): ConditionDef[];
2276
+ condition(type: number): ConditionDef | undefined;
2277
+ actions(briefing?: boolean): ActionDef[];
2278
+ action(type: number, briefing?: boolean): ActionDef | undefined;
2279
+ /** The values an enumerated argument kind can take, with their labels. */
2280
+ choices(kind: ArgKind): Choice[];
2281
+ choiceLabel(kind: ArgKind, value: number): string | undefined;
2282
+ /** The value behind a label or one of its aliases, for parsing. */
2283
+ choiceValue(kind: ArgKind, text: string): number | undefined;
2284
+ }
2285
+ /** Printing and parsing the text trigger format (File ▸ Import / Export ▸ Triggers). */
2286
+ export interface TriggerTextApi {
2287
+ print(triggers: TriggerRecord[], options?: {
2288
+ briefing?: boolean;
2289
+ }): string;
2290
+ /**
2291
+ * Parse text into records, resolving names against the open map (and interning the
2292
+ * strings it mentions). Throws a `TriggerTextError` carrying the line on bad input.
2293
+ */
2294
+ parse(source: string, options?: {
2295
+ briefing?: boolean;
2296
+ }): TextTrigger[];
2297
+ /** One trigger as its `Trigger(…)` block. */
2298
+ one(trigger: TriggerRecord, options?: {
2299
+ briefing?: boolean;
2300
+ }): string;
2301
+ }
2302
+ /**
2303
+ * Reading triggers, and the pure helpers that make them presentable. Writing goes
2304
+ * through `document.update`.
2305
+ */
2306
+ export interface TriggersApi {
2307
+ /** TRIG, cloned. */
2308
+ list(): TriggerRecord[];
2309
+ /** MBRF, cloned. */
2310
+ briefing(): TriggerRecord[];
2311
+ /** SWNM as names, StarEdit's `Switch N` where there is none. */
2312
+ switchNames(): string[];
2313
+ /** How many conditions and actions mention each switch. */
2314
+ switchUsage(): number[];
2315
+ /** The name context the text format resolves against: the map's locations, units, switches and strings. */
2316
+ names(): TriggerNames;
2317
+ readonly defs: TriggerDefsApi;
2318
+ readonly text: TriggerTextApi;
2319
+ /** An empty trigger owned by the given player groups (All Players by default). */
2320
+ newTrigger(players?: number[]): TriggerRecord;
2321
+ /** A condition of a type, with StarEdit's defaults for its arguments. */
2322
+ newCondition(type: number): ConditionRecord;
2323
+ newAction(type: number, briefing?: boolean): ActionRecord;
2324
+ isPreserved(trigger: TriggerRecord): boolean;
2325
+ setPreserved(trigger: TriggerRecord, on: boolean): TriggerRecord;
2326
+ /** The indices of the triggers any of these player groups own. */
2327
+ triggersFor(list: TriggerRecord[], groups: number[]): number[];
2328
+ /** The three lines the trigger list shows: players, conditions, actions. */
2329
+ summarize(trigger: TriggerRecord, briefing?: boolean): {
2330
+ players: string;
2331
+ conditions: string;
2332
+ actions: string;
2333
+ };
2334
+ /** A trigger's `Comment` action text, when it has one. */
2335
+ comment(trigger: TriggerRecord): string | null;
2336
+ /**
2337
+ * Tell the editor that a run of the trigger list is generated by this plugin. The
2338
+ * Trigger Editor badges those rows, locks them and offers the plugin's own editor in
2339
+ * place of the form; the Text Trigger Editor fences them in comments; Import Triggers
2340
+ * says what a replace would remove. The claim lives until `remove()` or the plugin's
2341
+ * deactivation; call `refresh()` after a rebuild so the editors ask `locate` again.
2342
+ */
2343
+ claim(spec: TriggerClaimSpec): TriggerClaimHandle;
2344
+ }
2345
+ /**
2346
+ * A run of triggers a plugin generates and owns. It is found by *content*: the
2347
+ * editors work on copies of the list with their own unsaved inserts and removals, so
2348
+ * `locate` is asked with whatever list an editor holds and answers where the run is
2349
+ * in it — by a hash of the records, say — or null when the records are not there.
2350
+ */
2351
+ export interface TriggerClaimSpec {
2352
+ /** What generated the run, in words, as a sentence would use it: `"the trigger script"`. */
2353
+ label: string;
2354
+ /** The word on the badge the trigger list shows on each row; the plugin's id by default. */
2355
+ badge?: string;
2356
+ /** Where the run sits in `list`, or null when it is not there (edited by hand, or gone). */
2357
+ locate(list: TriggerRecord[]): {
2358
+ start: number;
2359
+ count: number;
2360
+ } | null;
2361
+ /** A sentence about one trigger of the run (its index in `list`), shown in place of the editor's form. */
2362
+ describe?(index: number, list: TriggerRecord[]): string;
2363
+ /** Open the plugin's own editor on that trigger — the button under the sentence. */
2364
+ open?(index: number, list: TriggerRecord[]): void;
2365
+ /** That button's label; `Open <plugin name>` by default. */
2366
+ openLabel?: string;
2367
+ }
2368
+ export interface TriggerClaimHandle extends Disposable {
2369
+ /** The editors ask `locate` again (after a rebuild, or when the run moved). */
2370
+ refresh(): void;
2371
+ remove(): void;
2372
+ }
2373
+ /** A start location on the map: the record's index, its owner and where it sits. */
2374
+ export interface StartLocation {
2375
+ index: number;
2376
+ owner: number;
2377
+ /** Map pixels. */
2378
+ x: number;
2379
+ y: number;
2380
+ /** The tile it is centred on. */
2381
+ tx: number;
2382
+ ty: number;
2383
+ }
2384
+ /**
2385
+ * Reading the open map: what is under a point, what lies in a rectangle, and the
2386
+ * editor's own analyses — Check Map's issues, Tools ▸ Statistics, the Ctrl+F search and
2387
+ * the string usage map the String Editor is built on.
2388
+ *
2389
+ * Everything here is a read: nothing changes the map, and nothing throws without one
2390
+ * (an empty list, or null).
2391
+ */
2392
+ export interface QueryApi {
2393
+ /** The topmost unit whose sprite box covers a map pixel, or -1. */
2394
+ unitAt(px: number, py: number): number;
2395
+ /** Every unit whose centre lies in a tile rect. */
2396
+ unitsIn(rect: Rect): number[];
2397
+ /** Units owned by a player (0-based). */
2398
+ unitsOf(owner: number): number[];
2399
+ spriteAt(px: number, py: number): number;
2400
+ spritesIn(rect: Rect): number[];
2401
+ /** The doodad covering a tile, or -1. */
2402
+ doodadAt(tx: number, ty: number): number;
2403
+ /** The smallest location covering a map pixel (never Anywhere), or -1. */
2404
+ locationAt(px: number, py: number): number;
2405
+ /** Locations wholly inside a tile rect (never Anywhere). */
2406
+ locationsIn(rect: Rect): number[];
2407
+ /** The map's start locations, by player. */
2408
+ startLocations(): StartLocation[];
2409
+ /** Whether a unit type may be placed centred there, and what stops it; null with no map. */
2410
+ placement(unitId: number, x: number, y: number): PlacementVerdict | null;
2411
+ /** The MASK bits at a tile: bit n set = player n + 1 starts fogged there (every bit when the map has no MASK). */
2412
+ fogAt(tx: number, ty: number): number;
2413
+ /** The string table as it stands (index 0 is nothing); empty with no map. */
2414
+ strings(): (string | null)[];
2415
+ /** Check Map: every issue the editor knows how to spot, with a `target` to go to. */
2416
+ validate(): Issue[];
2417
+ /** Tools ▸ Statistics: tile, unit, resource and player counts. Null without a map. */
2418
+ statistics(): MapStatistics | null;
2419
+ /** The Ctrl+F search over units, locations, sprites, strings and triggers. */
2420
+ find(options: FindOptions): FindResult[];
2421
+ /** Every record that refers to each string index. */
2422
+ stringUsage(): Map<number, StringUsage[]>;
2423
+ /** String slots nothing refers to. */
2424
+ unusedStrings(): number[];
2425
+ }
2426
+ /** Where `view.goTo` should take the user. Issues from `query.validate` carry one. */
2427
+ export type GoTo = {
2428
+ kind: "tile";
2429
+ x: number;
2430
+ y: number;
2431
+ } | {
2432
+ kind: "unit";
2433
+ index: number;
2434
+ } | {
2435
+ kind: "sprite";
2436
+ index: number;
2437
+ } | {
2438
+ kind: "location";
2439
+ index: number;
2440
+ };
2441
+ /**
2442
+ * The map view: where the viewport is looking, how far in, and what it draws over the
2443
+ * terrain. A plugin that finds something needs this to show the user where it is.
2444
+ */
2445
+ export interface ViewApi {
2446
+ zoom(): number;
2447
+ /** 0.25 … 8, as the zoom control's steps. */
2448
+ setZoom(zoom: number): void;
2449
+ /** The tiles on screen. */
2450
+ visible(): Rect;
2451
+ /** Scroll so a tile is in the middle of the viewport. */
2452
+ center(x: number, y: number): void;
2453
+ /** Scroll to an object (and select it, for a unit, sprite or location). */
2454
+ goTo(target: GoTo): void;
2455
+ /** The tile under the pointer, as the status bar shows it. */
2456
+ cursorTile(): {
2457
+ x: number;
2458
+ y: number;
2459
+ };
2460
+ /** The View menu's ticks: grid, locations, units, sprites, doodads, fog, … */
2461
+ flags(): ViewFlags;
2462
+ setFlags(patch: Partial<ViewFlags>): void;
2463
+ /** Grid spacing in map pixels (32 = one tile). */
2464
+ gridSize(): number;
2465
+ setGridSize(size: 8 | 16 | 32 | 64 | 128): void;
2466
+ }
2467
+ export interface CommandSpec {
2468
+ /**
2469
+ * The command's id. One without a dot is namespaced under the plugin
2470
+ * (`"paint" → "paint-plugin.paint"`); one with a dot is taken as it is, so a plugin
2471
+ * can offer a stable name others call.
2472
+ */
2473
+ id: string;
2474
+ /** What a menu or a command list should call it. */
2475
+ title: string;
2476
+ enabled?: () => boolean;
2477
+ run: (...args: unknown[]) => unknown;
2478
+ }
2479
+ export interface CommandInfo {
2480
+ id: string;
2481
+ title: string;
2482
+ pluginId: string;
2483
+ enabled: boolean;
2484
+ }
2485
+ /**
2486
+ * Named things a plugin can do, so a menu item, a hotkey, a context entry and another
2487
+ * plugin all reach the same one. `menu.add`, `contextMenu.add` and `hotkeys.add` take a
2488
+ * `command` id in place of a `run`.
2489
+ */
2490
+ export interface CommandsApi {
2491
+ register(spec: CommandSpec): Disposable;
2492
+ /** Run one, whoever registered it; the command's own return value, or undefined when there is no such command. */
2493
+ run(id: string, ...args: unknown[]): unknown;
2494
+ has(id: string): boolean;
2495
+ /** Every command registered by every plugin. */
2496
+ list(): CommandInfo[];
2497
+ }
2498
+ /** A picture the editor already has: the canvas it draws from, and its size in pixels. */
2499
+ export interface PluginImage {
2500
+ image: HTMLCanvasElement;
2501
+ width: number;
2502
+ height: number;
2503
+ }
2504
+ /**
2505
+ * The pictures the viewport draws, for a plugin's own lists and previews: the same
2506
+ * cached canvases, so asking for one costs nothing after the first time. Everything is
2507
+ * null when the graphics it needs were never extracted — a plugin shows a name instead.
2508
+ */
2509
+ export interface GraphicsApi {
2510
+ /** Whether the tileset graphics and the unit tables are in memory. */
2511
+ ready(): {
2512
+ tileset: boolean;
2513
+ units: boolean;
2514
+ };
2515
+ /** Fetch both; resolves with what is available afterwards. */
2516
+ load(): Promise<{
2517
+ tileset: boolean;
2518
+ units: boolean;
2519
+ }>;
2520
+ /** A unit type in its editor pose, in a player's colours (`owner` 0 by default). */
2521
+ unitImage(unitId: number, options?: {
2522
+ owner?: number;
2523
+ }): PluginImage | null;
2524
+ /** A THG2 sprite: a sprites.dat image for `"pure"`, a unit for `"unit"`. */
2525
+ spriteImage(kind: SpriteKind, id: number, options?: {
2526
+ owner?: number;
2527
+ flipped?: boolean;
2528
+ }): PluginImage | null;
2529
+ /** One 32 × 32 megatile of the open map's tileset. */
2530
+ tileImage(tileId: number): PluginImage | null;
2531
+ /** A doodad drawn from its own tiles. */
2532
+ doodadImage(doodadId: number): PluginImage | null;
2533
+ /**
2534
+ * Part of the map as File ▸ Export ▸ Image draws it — the same render, cropped to a
2535
+ * tile rect. Everything it needs is loaded first; null without a map or a canvas.
2536
+ */
2537
+ renderRect(rect: Rect, options?: Partial<MapImageOptions>): Promise<Blob | null>;
2538
+ /** The colour a player's units are drawn in, `#rrggbb`. */
2539
+ playerColor(owner: number): string;
2540
+ /**
2541
+ * Start fetching the GRPs a unit type needs, and say whether they are already in
2542
+ * memory. Graphics load lazily, so the first `unitImage` for a type is often null —
2543
+ * ask for it here, redraw on `onImageLoaded`, and the list fills in.
2544
+ */
2545
+ requestUnit(unitId: number): boolean;
2546
+ requestSprite(kind: SpriteKind, id: number): boolean;
2547
+ /** A GRP finished loading: anything drawn from one may look different now. */
2548
+ onImageLoaded(listener: () => void): Disposable;
2549
+ }
2550
+ /**
2551
+ * The game's own tables, as the editor decoded them: `units.dat` and its neighbours.
2552
+ * `names` gives the labels, this gives the numbers — hit points, costs, build times,
2553
+ * weapons, flags, the sprite and image each unit draws through.
2554
+ *
2555
+ * Everything is null until the tables are loaded (`load`), and stays null when the game
2556
+ * data was never extracted.
2557
+ */
2558
+ export interface DataApi {
2559
+ ready(): boolean;
2560
+ load(): Promise<boolean>;
2561
+ units(): UnitsDat | null;
2562
+ weapons(): WeaponsDat | null;
2563
+ upgrades(): UpgradesDat | null;
2564
+ techs(): TechdataDat | null;
2565
+ sprites(): SpritesDat | null;
2566
+ flingy(): FlingyDat | null;
2567
+ images(): ImagesDat | null;
2568
+ /** Which race a unit type belongs to, from its flags. */
2569
+ race(unitId: number): Race;
2570
+ /** The GRP path an image id draws from, relative to `unit\`. */
2571
+ imagePath(imageId: number): string | null;
2572
+ }
2573
+ export interface ActiveBrush {
2574
+ mode: TerrainMode;
2575
+ /** The Rect / isometric brush's terrain id. */
2576
+ terrain: number;
2577
+ /** The Tile brush's MTXM id. */
2578
+ tile: number;
2579
+ brushSize: number;
2580
+ /** The Rect brush's variation slot, or -1 for a random one per pair. */
2581
+ variation: number;
2582
+ }
2583
+ export interface TerrainApi {
2584
+ /** Paintable flat terrains of the open map's tileset (empty without the graphics). */
2585
+ types(): TerrainType[];
2586
+ /** Terrain ids the isometric brush can paint on this tileset. */
2587
+ isomTypes(): number[];
2588
+ /** Whether the open map carries an ISOM section the isometric brush can work on. */
2589
+ hasIsom(): boolean;
2590
+ /**
2591
+ * How well the ISOM describes the tiles — `rects` measured, `mismatched` among them,
2592
+ * and `stale` when the share is past what the palette warns at — or null when the map
2593
+ * has no ISOM (`hasIsom`) or no map is open. Waits for the tileset graphics to load
2594
+ * and rejects when they are missing.
2595
+ */
2596
+ checkIsom(): Promise<IsomReport | null>;
2597
+ tileInfo(id: number): TileInfo | null;
2598
+ /**
2599
+ * The terrain a tile belongs to, as the id `types()` lists (a flat pair's CV5 index):
2600
+ * the tile's own group when it is flat ground, else — under a cliff, a shore or a
2601
+ * doodad — what the ISOM lattice says the diamond there is. Null off the map, without
2602
+ * the tileset graphics, or when neither tells.
2603
+ */
2604
+ terrainAt(tx: number, ty: number): number | null;
2605
+ /** The atlas average of a tile, packed `0xRRGGBB`, or null without graphics. */
2606
+ color(tileId: number): number | null;
2607
+ /** The mean colour of a terrain's common flat variations, packed `0xRRGGBB`. */
2608
+ terrainColor(terrainId: number): number | null;
2609
+ /** A flat terrain's height level (0 low, 1 high, 2 higher), or null when it is not one. */
2610
+ heightOf(terrainId: number): 0 | 1 | 2 | null;
2611
+ /** The lattice diamond under a map pixel. */
2612
+ diamondAt(px: number, py: number): Diamond;
2613
+ isDiamond(d: Diamond): boolean;
2614
+ /** Every in-bounds diamond whose centre tile lies in `rect`, row by row. */
2615
+ diamondsIn(rect: Rect): Diamond[];
2616
+ /** The 4-connected area around a tile of the same terrain type (`"terrain"`, the Rect fill's reading) or the same exact tile (`"tile"`), as flat indices. */
2617
+ floodRegion(x: number, y: number, match?: "terrain" | "tile"): number[];
2618
+ /**
2619
+ * The Blend brush's list: drawable tiles whose opposite edge meets the anchor tile's
2620
+ * `side`, nearest first, with the pixel distance (`0..255`, designed pairs 0.2–8).
2621
+ * Empty without the graphics.
2622
+ */
2623
+ blendCandidates(anchorTileId: number, side: Side, options?: Partial<BlendOptions>): BlendCandidate[];
2624
+ /** The Rect brush's terrain, in the tileset's own terms: the even CV5 group of a flat pair, or -1. */
2625
+ flatGroupOf(terrainId: number): number;
2626
+ /** Tools ▸ Symmetry…: the mode the brushes paint and the palettes place under. */
2627
+ symmetry(): SymmetryMode;
2628
+ setSymmetry(mode: SymmetryMode): void;
2629
+ /** Whether a mode can run on the open map (the rotations and diagonals need a square one). */
2630
+ symmetryAvailable(mode: SymmetryMode): boolean;
2631
+ /** Flat indices of `cells` and their images under the symmetry mode, each once. */
2632
+ mirror(cells: Rect | Iterable<number>): number[];
2633
+ /** A map pixel and its images under the symmetry mode, the original first. */
2634
+ mirrorPoint(px: number, py: number): {
2635
+ x: number;
2636
+ y: number;
2637
+ }[];
2638
+ active(): ActiveBrush;
2639
+ setActive(brush: Partial<ActiveBrush>): void;
2640
+ }
2641
+ export interface TilesetApi {
2642
+ id(): TilesetId | null;
2643
+ name(): string;
2644
+ isLoaded(): boolean;
2645
+ /** Fetch and decode the graphics; resolves false when they were never extracted. */
2646
+ load(): Promise<boolean>;
2647
+ raw(): LoadedTileset | null;
2648
+ }
2649
+ export interface SelectionApi {
2650
+ /** The Cut / Copy / Paste layer's marked rectangle, in tiles. */
2651
+ markedArea(): Rect | null;
2652
+ markArea(rect: Rect | null): void;
2653
+ units(): number[];
2654
+ setUnits(indices: number[]): void;
2655
+ sprites(): number[];
2656
+ setSprites(indices: number[]): void;
2657
+ doodads(): number[];
2658
+ setDoodads(indices: number[]): void;
2659
+ locations(): number[];
2660
+ setLocations(indices: number[]): void;
2661
+ layer(): EditorLayer;
2662
+ setLayer(layer: EditorLayer): void;
2663
+ /** Layers the Layers panel has locked: their tools refuse to change the map. */
2664
+ lockedLayers(): EditorLayer[];
2665
+ setLayerLocked(layer: EditorLayer, locked: boolean): void;
2666
+ }
2667
+ /** What `clipboard.copy` / `cut` take: the marked area (or any tile rect), or objects by index. */
2668
+ export type ClipSource = {
2669
+ rect: Rect;
2670
+ } | {
2671
+ units?: number[];
2672
+ sprites?: number[];
2673
+ doodads?: number[];
2674
+ locations?: number[];
2675
+ };
2676
+ export interface PasteOptionsSpec {
2677
+ /** Over what `clipboard.parts()` says. */
2678
+ parts?: Partial<ClipParts>;
2679
+ /** Over `clipboard.mode()`. */
2680
+ mode?: PasteMode;
2681
+ }
2682
+ /**
2683
+ * The Cut / Copy / Paste layer. The clip is self-contained (it outlives the map it came
2684
+ * from and pastes into another) and shared with the user's own clipboard, so a plugin
2685
+ * that copies here is copying for the user too.
2686
+ */
2687
+ export interface ClipboardApi {
2688
+ /** What is on the clipboard, or null. */
2689
+ clip(): Clip | null;
2690
+ /** Put a clip on the clipboard (one from `copy`, or built elsewhere); null clears it. */
2691
+ setClip(clip: Clip | null): void;
2692
+ /**
2693
+ * Copy a source — the marked area when omitted (or, on an object layer, that layer's
2694
+ * selection, as Ctrl+C does) — with the parts ticked in `parts()`. Null when there is
2695
+ * nothing to copy.
2696
+ */
2697
+ copy(source?: ClipSource): Clip | null;
2698
+ /** Copy, then remove the source's objects as one undo step (terrain and fog stay). Null when there was nothing. */
2699
+ cut(source?: ClipSource): Clip | null;
2700
+ /**
2701
+ * Stamp the clip with its top-left tile at (tx, ty), one undo step; the pasted area
2702
+ * becomes the marked one. Null when there is no clip or no map.
2703
+ */
2704
+ paste(tx: number, ty: number, options?: PasteOptionsSpec): PasteResult | null;
2705
+ parts(): ClipParts;
2706
+ setParts(patch: Partial<ClipParts>): void;
2707
+ mode(): PasteMode;
2708
+ setMode(mode: PasteMode): void;
2709
+ /** Whether the clipboard layer is armed: the next click on the map pastes. */
2710
+ pasting(): boolean;
2711
+ /** Arm pasting (switches to the clipboard layer) or stop. */
2712
+ setPasting(on: boolean): void;
2713
+ /** One line for a clip: `12 × 8 tiles · 3 units · 2 doodads`. */
2714
+ summary(clip: Clip): string;
2715
+ }
2716
+ /**
2717
+ * The file formats File ▸ Import / Export read and write, so a plugin can keep triggers
2718
+ * or strings outside the map in a form the editor (and SCMDraft) take back.
2719
+ */
2720
+ export interface ExchangeApi {
2721
+ /** Raw 2400-byte TRIG records, SCMDraft's `.trg`; string indices are the map's own. */
2722
+ encodeTrg(triggers: TriggerRecord[]): Uint8Array;
2723
+ decodeTrg(bytes: Uint8Array): TriggerRecord[];
2724
+ /** The whole string table as `index<TAB>text` lines, control bytes as `<XX>`. */
2725
+ formatStrings(): string;
2726
+ /** Parse that form (`errors` names the lines that were not `N<TAB>text`); `tx.strings.import` applies it. */
2727
+ parseStrings(text: string): StringImport;
2728
+ }
2729
+ /**
2730
+ * What the Units, Sprites, Doodads and Fog of War palettes have picked — the thing a
2731
+ * click on the map would place or paint. (The Terrain palette's pick is
2732
+ * `terrain.active()`.) Players are 0-based: `owner` 0 is Player 1.
2733
+ */
2734
+ export interface PaletteChoice {
2735
+ /** The player whose fog the Fog of War layer draws, 0-based. */
2736
+ fogViewPlayer: number;
2737
+ /** units.dat id the Units palette places. */
2738
+ unit: number;
2739
+ /** The player it places for, 0–11. */
2740
+ owner: number;
2741
+ spriteKind: SpriteKind;
2742
+ /** sprites.dat id, placed when `spriteKind` is `"pure"`. */
2743
+ sprite: number;
2744
+ /** units.dat id, placed when `spriteKind` is `"unit"` (doors and traps). */
2745
+ unitSprite: number;
2746
+ spriteFlipped: boolean;
2747
+ spriteDisabled: boolean;
2748
+ /** The Doodads palette's doodad id, or -1 before one was picked. */
2749
+ doodad: number;
2750
+ /** Bit mask of the players the fog brush paints for (bit n = player n + 1). */
2751
+ fogPlayers: number;
2752
+ fogMode: FogMode;
2753
+ }
2754
+ /** A unit type's footprint, from units.dat; a one-tile box without the tables. */
2755
+ export interface UnitSize {
2756
+ /** The placement box, in pixels. */
2757
+ width: number;
2758
+ height: number;
2759
+ building: boolean;
2760
+ flyer: boolean;
2761
+ }
2762
+ export interface DoodadInfo {
2763
+ id: number;
2764
+ /** "Trees #12" — the palette category and the id, which is the only name a doodad has. */
2765
+ name: string;
2766
+ category: string;
2767
+ /** Footprint in tiles. */
2768
+ width: number;
2769
+ height: number;
2770
+ }
2771
+ export interface PaletteApi {
2772
+ active(): PaletteChoice;
2773
+ setActive(choice: Partial<PaletteChoice>): void;
2774
+ /** The colour a player's units are shown in, `#rrggbb` (the same as `graphics.playerColor`). */
2775
+ playerColor(owner: number): string;
2776
+ /** The Units palette's placement checks and snapping — what `placeUnit`, `canPlaceUnit` and `query.placement` apply, and whether an edit removes stranded units. */
2777
+ placementOptions(): PlacementOptions;
2778
+ setPlacementOptions(patch: Partial<PlacementOptions>): void;
2779
+ /** The Doodads palette's rules: place anywhere, snap to grid. */
2780
+ doodadPlacement(): DoodadPlacementOptions;
2781
+ setDoodadPlacement(patch: Partial<DoodadPlacementOptions>): void;
2782
+ /** The Locations layer's snap step in pixels, 0 for off. */
2783
+ locationSnap(): number;
2784
+ setLocationSnap(step: 0 | 8 | 16 | 32 | 64): void;
2785
+ /** Every unit type, grouped the way the Units palette lists them. */
2786
+ unitGroups(): UnitGroup[];
2787
+ unitName(unitId: number): string;
2788
+ unitSize(unitId: number): UnitSize;
2789
+ /** What the Sprites palette lists, by group; empty until the unit tables are loaded. */
2790
+ spriteGroups(): SpriteGroup[];
2791
+ /** "Terran Marine" for a unit sprite, the unit or GRP the pure sprite draws otherwise. */
2792
+ spriteName(kind: SpriteKind, id: number): string;
2793
+ /** The open map's doodads by palette category; empty without the tileset graphics. */
2794
+ doodadCategories(): {
2795
+ name: string;
2796
+ doodads: DoodadInfo[];
2797
+ }[];
2798
+ doodadInfo(doodadId: number): DoodadInfo | null;
2799
+ }
2800
+ export interface NamedValue {
2801
+ value: number;
2802
+ label: string;
2803
+ }
2804
+ /**
2805
+ * The names behind the numbers a map stores, so a plugin that shows raw values need not
2806
+ * carry the game's tables itself. The per-map ones (`string`, `location`, `switch`,
2807
+ * `player`) read the open scenario and answer a placeholder without one; the rest are
2808
+ * the editor's own tables — the same names StarEdit shows.
2809
+ */
2810
+ export interface NamesApi {
2811
+ /** StarEdit's name for a units.dat id; `Any unit` / `Men` / `Buildings` / `Factories` for the trigger classes 228–231. */
2812
+ unit(id: number): string;
2813
+ units(): NamedValue[];
2814
+ upgrade(id: number): string;
2815
+ upgrades(): NamedValue[];
2816
+ tech(id: number): string;
2817
+ techs(): NamedValue[];
2818
+ weapon(id: number): string;
2819
+ weapons(): NamedValue[];
2820
+ /** An OWNR / IOWN controller byte: `Human`, `Computer`, `Neutral`, … */
2821
+ playerType(value: number): string;
2822
+ playerTypes(): NamedValue[];
2823
+ /** A SIDE race byte. */
2824
+ race(value: number): string;
2825
+ races(): NamedValue[];
2826
+ /** One of the 27 trigger player groups (`Player 1` … `Non Allied Victory Players`). */
2827
+ playerGroup(value: number): string;
2828
+ playerGroups(): NamedValue[];
2829
+ /** A TRIG condition type. */
2830
+ condition(type: number): string;
2831
+ conditions(): NamedValue[];
2832
+ /** A TRIG action type, or an MBRF one with `briefing`. */
2833
+ action(type: number, briefing?: boolean): string;
2834
+ actions(briefing?: boolean): NamedValue[];
2835
+ /** The script behind a Run AI Script code (the four characters as a little-endian u32); the code itself when unknown. */
2836
+ aiScript(code: number): string;
2837
+ /** The open map's string at an index; null for 0, out of range, or no map. */
2838
+ string(index: number): string | null;
2839
+ /** A location slot's name (0-based; 63 is Anywhere), or StarEdit's default for it. */
2840
+ location(index: number): string;
2841
+ /** A switch's name (0-based), or `Switch N`. */
2842
+ switch(index: number): string;
2843
+ /** `Player N` for a slot (0-based). */
2844
+ player(slot: number): string;
2845
+ /** The terrain a MTXM / TILE id belongs to (`Dirt`, `High Dirt`, a cliff, …), or null without the tileset graphics. */
2846
+ tile(id: number): string | null;
2847
+ }
2848
+ /**
2849
+ * Bytes 0x01–0x1F in a string are colour and layout codes. This is the editor's own table
2850
+ * of what each one does — the same one the String Editor's buttons and preview are drawn
2851
+ * from — plus the reading of a string that turns those bytes into what the game shows.
2852
+ *
2853
+ * A plugin that displays or rewrites map text should use this rather than carrying a copy:
2854
+ * the numbering is easy to get wrong (the editor's own table was, from 0x12 up, until it
2855
+ * was checked against the classic player palette).
2856
+ *
2857
+ * ## Remastered
2858
+ *
2859
+ * `runs` models Remastered's rule, where a colour set on one line carries onto the next.
2860
+ * StarCraft 1.16.1 reset the colour at every line break, so a string written before the
2861
+ * remaster can draw in colours its author never chose — `bleedingLines` finds exactly
2862
+ * those lines and `fixBleeding` writes the reset the old game supplied. Pass
2863
+ * `resetPerLine` to `runs` to see the old rendering.
2864
+ */
2865
+ export interface TextApi {
2866
+ /** Every byte the game gives a meaning, in order; `rgb` is set for the colours only. */
2867
+ codes(): TextCode[];
2868
+ /** One byte's meaning, or null for a byte the game ignores. */
2869
+ code(byte: number): TextCode | null;
2870
+ /** The codes worth offering as buttons: no tab, newlines or the do-nothing byte. */
2871
+ insertable(): TextCode[];
2872
+ /** `#rrggbb` — what the game starts a string in, and what 1.16.1 reset to at a line break. */
2873
+ defaultColor(): string;
2874
+ /** The string split into lines of coloured runs, the way the game draws it. */
2875
+ runs(text: string, options?: RunOptions): TextLine[];
2876
+ /** The text with every control byte removed — what the string actually says. */
2877
+ plain(text: string): string;
2878
+ /** `<0E>`, the way every StarCraft editor writes a control byte. */
2879
+ escape(byte: number): string;
2880
+ /**
2881
+ * The lines of `text` that Remastered draws in a colour 1.16.1 did not: a line that sets
2882
+ * no colour of its own, after one that left a colour set. Empty for a single-line string.
2883
+ */
2884
+ bleedingLines(text: string): BleedingLine[];
2885
+ /**
2886
+ * `text` with the default colour written at the head of every bleeding line, so both
2887
+ * games draw it alike. Idempotent, and never changes what the string says.
2888
+ */
2889
+ fixBleeding(text: string): string;
2890
+ }
2891
+ export type DialogSize = "sm" | "md" | "lg" | "xl" | "full";
2892
+ export interface DialogButton {
2893
+ label: string;
2894
+ primary?: boolean;
2895
+ /** Return `false` (or a promise of it) to keep the dialog open. */
2896
+ run?: (dialog: DialogHandle) => void | boolean | Promise<void | boolean>;
2897
+ /** Close after `run`; default true. */
2898
+ closes?: boolean;
2899
+ }
2900
+ /** What a paste or a drop brought into a dialog. */
2901
+ export interface DialogTransfer {
2902
+ files: File[];
2903
+ text: string;
2904
+ }
2905
+ export interface DialogSpec {
2906
+ title: string;
2907
+ size?: DialogSize;
2908
+ tall?: boolean;
2909
+ /** Fill `body` (an empty `<div>` inside the dialog); return a cleanup if you need one. */
2910
+ mount(body: HTMLElement, dialog: DialogHandle): void | (() => void);
2911
+ /** Footer buttons, left to right; a single Close when omitted. */
2912
+ buttons?: DialogButton[];
2913
+ /**
2914
+ * Ctrl+V anywhere in the dialog (while it is the topmost one). Files come from the
2915
+ * clipboard's items — a screenshot pastes as one `image/png` file — and `text` is the
2916
+ * plain-text part, so a copied image URL arrives here too.
2917
+ */
2918
+ onPaste?: (transfer: DialogTransfer, dialog: DialogHandle) => void;
2919
+ /** Something dropped onto the dialog body. */
2920
+ onDrop?: (transfer: DialogTransfer, dialog: DialogHandle) => void;
2921
+ /**
2922
+ * Escape normally closes the dialog. Answer true to keep it open for this press —
2923
+ * when the key was meant for something inside the dialog that handles Escape itself
2924
+ * (a code editor dismissing its own popups), judged by the element it landed on.
2925
+ */
2926
+ keepOpenOnEscape?: (target: EventTarget | null) => boolean;
2927
+ }
2928
+ export interface DialogHandle {
2929
+ close(): void;
2930
+ /** Whether the dialog is still on screen. */
2931
+ isOpen(): boolean;
2932
+ /** Change the title strip's text. */
2933
+ setTitle(title: string): void;
2934
+ }
2935
+ /**
2936
+ * A panel floats over the map and does not block it: the user keeps drawing, scrolling
2937
+ * and using hotkeys while it is open (except while typing in one of its fields). It
2938
+ * is dragged by its title bar and closed with the × or `close()`.
2939
+ */
2940
+ export interface PanelSpec {
2941
+ title: string;
2942
+ /** In CSS pixels; 260 by default. The panel is as tall as its content, up to the map's height. */
2943
+ width?: number;
2944
+ /** Fill `body` (an empty `<div>` inside the panel); return a cleanup if you need one. */
2945
+ mount(body: HTMLElement, panel: PanelHandle): void | (() => void);
2946
+ /** The panel closed, whichever way. */
2947
+ onClose?: () => void;
2948
+ }
2949
+ export interface PanelHandle {
2950
+ close(): void;
2951
+ isOpen(): boolean;
2952
+ setTitle(title: string): void;
2953
+ }
2954
+ /** The pointer over the map, in the map's own units. A tile is 32 × 32 pixels. */
2955
+ export interface MapPointer {
2956
+ /** Map pixels. Kept inside the map while a button is held, as the built-in brushes do. */
2957
+ px: number;
2958
+ py: number;
2959
+ /** The tile under the pointer. */
2960
+ tx: number;
2961
+ ty: number;
2962
+ /** False once the pointer has left the map with no button held. */
2963
+ inMap: boolean;
2964
+ /** Whether the primary button is held. */
2965
+ down: boolean;
2966
+ shift: boolean;
2967
+ ctrl: boolean;
2968
+ alt: boolean;
2969
+ }
2970
+ /** Map pixels to canvas pixels, for a tool's `draw`. */
2971
+ export interface MapView {
2972
+ zoom: number;
2973
+ /** Canvas pixels per tile. */
2974
+ tilePx: number;
2975
+ /** A map pixel's x on the canvas. */
2976
+ x(px: number): number;
2977
+ y(py: number): number;
2978
+ /** The tiles on screen. */
2979
+ visible: Rect;
2980
+ }
2981
+ export type MapToolStopReason =
2982
+ /** `handle.stop()`. */
2983
+ "stopped"
2984
+ /** Esc or a right-click, and `onCancel` did not keep the tool. */
2985
+ | "cancelled"
2986
+ /** The map was closed or replaced. */
2987
+ | "document"
2988
+ /** Another tool started. */
2989
+ | "replaced"
2990
+ /** The plugin was disabled. */
2991
+ | "disabled";
2992
+ /**
2993
+ * A tool owns the pointer over the map: the viewport hands it every press, move and
2994
+ * release ahead of the active layer's own tools, hides the layer's brush ghost, and
2995
+ * lets it draw an overlay. The map stays visible and scrollable, and a panel can stay
2996
+ * open beside it — which is how a plugin gets a drawing mode of its own.
2997
+ */
2998
+ export interface MapToolSpec {
2999
+ /** Shown in the viewport's HUD and the status bar while the tool runs. */
3000
+ name: string;
3001
+ /** After the name: `"drag to draw a line"`. */
3002
+ hint?: string;
3003
+ /** CSS cursor over the map; `"crosshair"` by default. */
3004
+ cursor?: string;
3005
+ onDown?(p: MapPointer): void;
3006
+ /** Every pointer move over the map, button held or not, and once with `inMap: false` when it leaves. */
3007
+ onMove?(p: MapPointer): void;
3008
+ onUp?(p: MapPointer): void;
3009
+ /** Esc or a right-click. Return `true` to keep the tool running (you cancelled a gesture of your own); otherwise it stops. */
3010
+ onCancel?(): boolean | void;
3011
+ /** Draw over the map, after everything else, each time the viewport repaints. */
3012
+ draw?(ctx: CanvasRenderingContext2D, view: MapView): void;
3013
+ /** The tool is no longer running, for whatever reason (once). */
3014
+ onStop?(reason: MapToolStopReason): void;
3015
+ }
3016
+ export interface MapToolHandle {
3017
+ stop(): void;
3018
+ isActive(): boolean;
3019
+ /** Repaint the viewport — and so call `draw` — now. Cheap; call it from `onMove`. */
3020
+ redraw(): void;
3021
+ }
3022
+ /** Where in the viewport's paint pass an overlay draws. */
3023
+ export type OverlayAbove =
3024
+ /** Over the terrain and grid, under doodad footprints, units, sprites and locations (the default). */
3025
+ "terrain"
3026
+ /** Over units, sprites and locations, under fog of war. */
3027
+ | "objects"
3028
+ /** Over everything but a running map tool's own drawing. */
3029
+ | "everything";
3030
+ /**
3031
+ * An overlay is a picture drawn over the map that the user can turn on and off: it
3032
+ * is listed under View ▸ Overlays and in the Layers panel, stays while the user works
3033
+ * on any layer, and never takes the pointer — clicks go to the active layer's tools
3034
+ * as usual. It hears the pointer through `onHover`, which is how a readout follows the
3035
+ * mouse while the user places units. Register one at activation and keep the handle;
3036
+ * it goes away with the plugin.
3037
+ */
3038
+ export interface OverlaySpec {
3039
+ /** Shown in View ▸ Overlays and the Layers panel. Unique per plugin. */
3040
+ name: string;
3041
+ /** Start visible; true by default. What the user last set for this name wins for the session. */
3042
+ visible?: boolean;
3043
+ /** Where the picture goes in the paint pass; `"terrain"` by default. */
3044
+ above?: OverlayAbove;
3045
+ /** Draw in canvas pixels through `view`, each time the viewport repaints while visible. */
3046
+ draw(ctx: CanvasRenderingContext2D, view: MapView): void;
3047
+ /**
3048
+ * The pointer over the map while the overlay is visible, on every layer and while a
3049
+ * map tool runs; `null` once when it leaves. Call `handle.redraw()` here to move a
3050
+ * hover mark. A press sets `down` but is never captured for you.
3051
+ */
3052
+ onHover?(p: MapPointer | null): void;
3053
+ /** The overlay was shown or hidden, by the user in the chrome or by your handle. */
3054
+ onToggle?(visible: boolean): void;
3055
+ }
3056
+ export interface OverlayHandle {
3057
+ show(): void;
3058
+ hide(): void;
3059
+ toggle(): void;
3060
+ isVisible(): boolean;
3061
+ /** Repaint the viewport — and so call `draw` — now. */
3062
+ redraw(): void;
3063
+ /** Take the overlay out of the chrome for good; `isVisible` is false from then on. */
3064
+ remove(): void;
3065
+ }
3066
+ export interface PickOptions {
3067
+ /** Shown in the viewport's HUD while the user picks; also the status line. */
3068
+ prompt?: string;
3069
+ }
3070
+ export interface PickFilesOptions {
3071
+ /** `accept` for the file input, e.g. `"image/*"` or `".png,.jpg"`. */
3072
+ accept?: string;
3073
+ multiple?: boolean;
3074
+ }
3075
+ export interface UiApi {
3076
+ /** Set the status bar line. */
3077
+ status(text: string): void;
3078
+ /** The status bar line as it stands. */
3079
+ statusText(): string;
3080
+ /** A short notice over the map that leaves by itself — how Save reports; `ttl` 0 keeps it until dismissed. */
3081
+ toast(toast: {
3082
+ kind?: Toast["kind"];
3083
+ title: string;
3084
+ detail?: string;
3085
+ ttl?: number;
3086
+ }): void;
3087
+ /**
3088
+ * Write a file to disk the way the editor's own exports do: into the file the browser's
3089
+ * save dialog picks, or as a download where there is no dialog. Resolves with where it
3090
+ * went, or null when the user dismissed the dialog.
3091
+ */
3092
+ saveFile(data: Blob | Uint8Array, fileName: string): Promise<{
3093
+ route: "picker" | "download";
3094
+ fileName: string;
3095
+ } | null>;
3096
+ dialog(spec: DialogSpec): DialogHandle;
3097
+ /** Open a floating panel over the map. As many as you like; each closes with the plugin. */
3098
+ panel(spec: PanelSpec): PanelHandle;
3099
+ /**
3100
+ * Take over the pointer on the map until `stop()`, Esc, a right-click, a map change
3101
+ * or another tool. One tool runs at a time — starting one stops the previous — and a
3102
+ * `pickArea` / `pickTile` in progress is served first.
3103
+ */
3104
+ mapTool(spec: MapToolSpec): MapToolHandle;
3105
+ /**
3106
+ * A picture over the map the user can switch on and off (View ▸ Overlays, the Layers
3107
+ * panel) and that stays while they work on any layer. It draws at every repaint and
3108
+ * hears the pointer, but never takes it. As many as you like; they go with the plugin.
3109
+ */
3110
+ overlay(spec: OverlaySpec): OverlayHandle;
3111
+ /** The browser's file picker; resolves with an empty list on cancel. */
3112
+ pickFiles(options?: PickFilesOptions): Promise<File[]>;
3113
+ /**
3114
+ * Let the user drag a rectangle on the map. The viewport switches to a crosshair, draws
3115
+ * the marquee and takes the gesture ahead of the active layer's tools; resolves with
3116
+ * the tile rect (exclusive `x1` / `y1`), or null when the user pressed Esc or
3117
+ * right-clicked, no map is open, the map was replaced, or the plugin was disabled.
3118
+ * Only one pick runs at a time — a new one cancels the previous. A modal dialog
3119
+ * covers the map, so close yours first and reopen it with the result.
3120
+ */
3121
+ pickArea(options?: PickOptions): Promise<Rect | null>;
3122
+ /** As `pickArea`, for a single click: the tile under it. */
3123
+ pickTile(options?: PickOptions): Promise<{
3124
+ x: number;
3125
+ y: number;
3126
+ } | null>;
3127
+ /**
3128
+ * Decode a picture: a `File` / `Blob`, a `data:` URL, or an `http(s)` URL (fetched, and
3129
+ * when the site refuses cross-origin reads, loaded through an `<img>` with
3130
+ * `crossOrigin` — a site that allows neither rejects with a message saying so).
3131
+ */
3132
+ loadImage(source: Blob | string): Promise<ImageBitmap>;
3133
+ /** The image on the system clipboard, if the browser lets the page read it (a permission prompt may appear); null otherwise. */
3134
+ readClipboardImage(): Promise<Blob | null>;
3135
+ /** Ask a yes/no question; resolves false on Cancel, Escape or the ×. */
3136
+ confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
3137
+ /** Say something with a single OK. */
3138
+ alert(message: string, options?: ConfirmOptions): Promise<void>;
3139
+ /** Ask for a line of text; null when the user cancelled. */
3140
+ prompt(message: string, options?: PromptOptions): Promise<string | null>;
3141
+ /** A progress bar over the map for long work, with an optional Cancel. */
3142
+ progress(label: string, options?: ProgressOptions): ProgressHandle;
3143
+ /** `el("div", { className: "row" }, …)`: the DOM helper the widgets are built from. */
3144
+ el<K extends keyof HTMLElementTagNameMap>(tag: K, props?: Record<string, unknown>, ...children: WidgetChild[]): HTMLElementTagNameMap[K];
3145
+ /** Buttons, fields, forms and lists in the editor's own styles. */
3146
+ readonly widgets: WidgetsApi;
3147
+ /** Open a built-in dialog (fire and forget; `ask` waits for one that answers). */
3148
+ open(dialog: DialogId, payload?: Record<string, unknown>): void;
3149
+ /**
3150
+ * Open a built-in dialog that answers — `"saveAs"`, `"confirmClose"`, `"newMap"` — and
3151
+ * resolve true when it went through, false when it was dismissed.
3152
+ */
3153
+ ask(dialog: DialogId, payload?: Record<string, unknown>): Promise<boolean>;
3154
+ /** Ask the viewport to repaint (a transaction does this by itself). Raises no event. */
3155
+ repaint(): void;
3156
+ }
3157
+ export interface ConfirmOptions {
3158
+ title?: string;
3159
+ /** The primary button; `OK` by default. */
3160
+ confirmLabel?: string;
3161
+ cancelLabel?: string;
3162
+ /** Paint the primary button as a destructive action. */
3163
+ danger?: boolean;
3164
+ }
3165
+ export interface PromptOptions extends ConfirmOptions {
3166
+ value?: string;
3167
+ placeholder?: string;
3168
+ /** A multi-line field. */
3169
+ multiline?: boolean;
3170
+ }
3171
+ export interface ProgressOptions {
3172
+ title?: string;
3173
+ /** Show a Cancel button; `cancelled()` answers true once it is pressed. */
3174
+ cancellable?: boolean;
3175
+ }
3176
+ /**
3177
+ * A progress panel over the map. It does not block the editor — the user can still
3178
+ * scroll and look — so a plugin doing long work should report often and check
3179
+ * `cancelled()` in its loop.
3180
+ */
3181
+ export interface ProgressHandle {
3182
+ /** How far along (0…1), and optionally a line under the bar. */
3183
+ report(fraction: number, text?: string): void;
3184
+ /** True once the user pressed Cancel or closed the panel. Poll it inside a loop. */
3185
+ cancelled(): boolean;
3186
+ /**
3187
+ * The same answer as a signal, for work that takes one: aborted when the user
3188
+ * cancels, so `fetch(url, { signal })` and anything built on `AbortSignal` stop
3189
+ * with the panel. Never aborted by `done()`.
3190
+ */
3191
+ readonly signal: AbortSignal;
3192
+ done(): void;
3193
+ isOpen(): boolean;
3194
+ }
3195
+ /** Anything the widget builders accept as a child: a node, text, or nothing. */
3196
+ export type WidgetChild = Node | string | number | null | undefined | false;
3197
+ export interface WidgetOptions {
3198
+ className?: string;
3199
+ title?: string;
3200
+ disabled?: boolean;
3201
+ }
3202
+ export interface ButtonOptions extends WidgetOptions {
3203
+ primary?: boolean;
3204
+ danger?: boolean;
3205
+ ghost?: boolean;
3206
+ onClick?: (event: MouseEvent) => void;
3207
+ }
3208
+ export interface CheckboxOptions extends WidgetOptions {
3209
+ value?: boolean;
3210
+ /** A radio button instead; give the group a `name`. */
3211
+ radio?: boolean;
3212
+ name?: string;
3213
+ onChange?: (value: boolean) => void;
3214
+ }
3215
+ /** The `<label>` a checkbox is, with its `<input>` on it so you can read or set the value. */
3216
+ export type CheckboxElement = HTMLLabelElement & {
3217
+ input: HTMLInputElement;
3218
+ };
3219
+ export interface TextFieldOptions extends WidgetOptions {
3220
+ value?: string;
3221
+ placeholder?: string;
3222
+ password?: boolean;
3223
+ onChange?: (value: string) => void;
3224
+ }
3225
+ export interface NumberFieldOptions extends WidgetOptions {
3226
+ value?: number;
3227
+ min?: number;
3228
+ max?: number;
3229
+ step?: number;
3230
+ onChange?: (value: number) => void;
3231
+ }
3232
+ export interface SelectOption {
3233
+ value: string | number;
3234
+ label: string;
3235
+ disabled?: boolean;
3236
+ }
3237
+ export interface SelectOptions extends WidgetOptions {
3238
+ value?: string | number;
3239
+ onChange?: (value: string) => void;
3240
+ }
3241
+ export interface FormRow {
3242
+ label: string;
3243
+ field: HTMLElement;
3244
+ }
3245
+ export interface ListItem<T> {
3246
+ label: string;
3247
+ value: T;
3248
+ /** Dimmed text at the end of the row. */
3249
+ hint?: string;
3250
+ /** A number in the row's gutter, as the editor's own lists show indices. */
3251
+ index?: number;
3252
+ title?: string;
3253
+ }
3254
+ export interface ListOptions<T> {
3255
+ /** Which row starts selected. */
3256
+ selected?: number;
3257
+ /** Maximum height in pixels; the list scrolls past it. */
3258
+ height?: number;
3259
+ className?: string;
3260
+ onPick?: (value: T, index: number) => void;
3261
+ }
3262
+ /**
3263
+ * Buttons, fields, forms and lists in the editor's own styles, as plain DOM. A plugin
3264
+ * dialog built from these looks like a built-in one; `el` is the escape hatch for
3265
+ * anything they do not cover.
3266
+ */
3267
+ export interface WidgetsApi {
3268
+ button(label: string, options?: ButtonOptions): HTMLButtonElement;
3269
+ checkbox(label: string, options?: CheckboxOptions): CheckboxElement;
3270
+ text(options?: TextFieldOptions): HTMLInputElement;
3271
+ number(options?: NumberFieldOptions): HTMLInputElement;
3272
+ select(items: SelectOption[], options?: SelectOptions): HTMLSelectElement;
3273
+ /** A two-column grid of labelled fields. */
3274
+ form(rows: (FormRow | null | undefined | false)[]): HTMLElement;
3275
+ /** A titled box (a `fieldset` with a legend, as the dialogs use). */
3276
+ group(title: string, ...children: WidgetChild[]): HTMLElement;
3277
+ row(...children: WidgetChild[]): HTMLDivElement;
3278
+ column(...children: WidgetChild[]): HTMLDivElement;
3279
+ /** Small dimmed explanatory text. */
3280
+ hint(text: string): HTMLElement;
3281
+ separator(): HTMLElement;
3282
+ list<T>(items: ListItem<T>[], options?: ListOptions<T>): HTMLElement;
3283
+ }
3284
+ export type TopMenu = "File" | "Edit" | "View" | "Layer" | "Scenario" | "Triggers" | "Tools" | "Plugins" | "Help";
3285
+ /**
3286
+ * A top-level menu, or a submenu by label: `"File/Import"`. A last segment that names no
3287
+ * submenu gets one of the plugin's own, at the end of the menu (`"Tools/AI"`).
3288
+ */
3289
+ export type MenuPath = TopMenu | `${TopMenu}/${string}`;
3290
+ export interface MenuItemSpec {
3291
+ label: string;
3292
+ /** Display only — bind the key with `hotkeys.add`. */
3293
+ shortcut?: string;
3294
+ /**
3295
+ * A mark in front of the label: `"plugin"` for the plugin's own icon (the manifest's),
3296
+ * or any `PluginIcon`. Use it when the item does something a built-in never would —
3297
+ * reach a network, say — so the user can tell at a glance.
3298
+ */
3299
+ icon?: "plugin" | PluginIcon;
3300
+ /**
3301
+ * Where in the menu: the label of the built-in item or submenu to sit directly under
3302
+ * (`"Open Recent"`). Without it, or when nothing has that label, the item goes to the
3303
+ * end of the menu after a separator.
3304
+ */
3305
+ after?: string;
3306
+ /** A separator line above this item — for grouping the items of a submenu of your own. */
3307
+ separator?: boolean;
3308
+ enabled?: () => boolean;
3309
+ /** What the item does. Give this or `command`. */
3310
+ run?: () => void;
3311
+ /** A registered command's id, instead of a `run` of its own. */
3312
+ command?: string;
3313
+ }
3314
+ export interface MenuApi {
3315
+ add(path: MenuPath, item: MenuItemSpec): Disposable;
3316
+ }
3317
+ export type ContextSurface = "viewport" | "terrainPalette";
3318
+ export interface ContextMenuContext {
3319
+ surface: ContextSurface;
3320
+ /** The tile under the pointer (viewport only). */
3321
+ tile: {
3322
+ x: number;
3323
+ y: number;
3324
+ } | null;
3325
+ /** The map pixel under the pointer (viewport only). */
3326
+ point: {
3327
+ px: number;
3328
+ py: number;
3329
+ } | null;
3330
+ layer: EditorLayer;
3331
+ terrainMode: TerrainMode;
3332
+ /** The palette's active terrain id. */
3333
+ terrain: number;
3334
+ markedArea: Rect | null;
3335
+ }
3336
+ export interface ContextItemSpec {
3337
+ label: string | ((ctx: ContextMenuContext) => string);
3338
+ enabled?: (ctx: ContextMenuContext) => boolean;
3339
+ visible?: (ctx: ContextMenuContext) => boolean;
3340
+ /** Give this or `command` (which is run with the context as its argument). */
3341
+ run?: (ctx: ContextMenuContext) => void;
3342
+ command?: string;
3343
+ }
3344
+ export interface ContextMenuApi {
3345
+ add(surface: ContextSurface, item: ContextItemSpec): Disposable;
3346
+ }
3347
+ export interface HotkeyApi {
3348
+ /** `"Ctrl+Shift+I"`, `"Alt+F9"`, `"F8"` — modifiers in any order, then a key name. */
3349
+ add(combo: string, run: (() => void) | {
3350
+ command: string;
3351
+ }): Disposable;
3352
+ }
3353
+ export type PluginEvent =
3354
+ /** A map was opened, closed or replaced. */
3355
+ "document"
3356
+ /** Any committed edit (every `document.edit`, stroke, undo and redo bumps it, terrain or not), and a fog edit. */
3357
+ | "terrain" | "units" | "doodads" | "locations" | "settings" | "triggers" | "layer"
3358
+ /** THG2 sprites — the same bump doodad edits make. */
3359
+ | "sprites" | "selection"
3360
+ /** The marked area or the clip on the clipboard changed. */
3361
+ | "clipboard"
3362
+ /** The viewport scrolled, zoomed, or a View menu tick moved. */
3363
+ | "view"
3364
+ /** A map tool or a map pick started or stopped. */
3365
+ | "tool"
3366
+ /** The map's unsaved-changes flag changed. */
3367
+ | "modified"
3368
+ /** A palette's pick changed: terrain brush, unit and owner, sprite, doodad, fog players. */
3369
+ | "palette"
3370
+ /** An editing option moved: symmetry, placement rules, doodad rules, location snap, the fog view player, clip parts and paste mode, locked layers, the grid look, Preferences. */
3371
+ | "options"
3372
+ /** The document's file changed: its name or handle after a Save, the save options, the archive extras, the recent list. */
3373
+ | "file"
3374
+ /** A plugin registered or removed a command — how a plugin learns that one it calls by id has arrived. */
3375
+ | "commands";
3376
+ /** Why the document changed. */
3377
+ export type DocumentChangeReason =
3378
+ /** File ▸ Open, drag-and-drop, `document.open`: a file the user chose. */
3379
+ "open"
3380
+ /** File ▸ New (the startup map included). */
3381
+ | "new"
3382
+ /** File ▸ Close: `document.isOpen()` is now false. */
3383
+ | "close"
3384
+ /** The open map parsed again from edited bytes — a `document.sections` write, by any plugin. */
3385
+ | "replace";
3386
+ export interface DocumentEvent {
3387
+ reason: DocumentChangeReason;
3388
+ fileName: string | null;
3389
+ }
3390
+ /**
3391
+ * Listeners are notifications: they run after the change, in the order the plugins were
3392
+ * activated, and cannot veto or reorder one another. A listener that rewrites the map in
3393
+ * response (through `document.sections`) simply raises a fresh `"document"` event with
3394
+ * reason `"replace"`, which every other listener sees in turn.
3395
+ */
3396
+ export interface EventsApi {
3397
+ on(event: "document", listener: (event: DocumentEvent) => void): Disposable;
3398
+ on(event: PluginEvent, listener: () => void): Disposable;
3399
+ }
3400
+ export interface StorageApi {
3401
+ get<T>(key: string, fallback: T): T;
3402
+ set(key: string, value: unknown): void;
3403
+ remove(key: string): void;
3404
+ }
3405
+ /** A slug for storage keys and log prefixes. */
3406
+ export declare function pluginIdOf(manifest: {
3407
+ id?: string;
3408
+ name: string;
3409
+ }): string;
3410
+
3411
+ export {};