@scm-js/plugin-api 1.40.0 → 1.42.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.
- package/index.d.ts +668 -95
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -297,6 +297,14 @@ export interface TriggerRecord {
|
|
|
297
297
|
/** The game's bookkeeping byte (offset 2399); StarEdit writes 0. */
|
|
298
298
|
currentAction: number;
|
|
299
299
|
}
|
|
300
|
+
/** A trigger that is only read: nothing on it, its conditions or its actions may be written. */
|
|
301
|
+
export interface ReadonlyTrigger {
|
|
302
|
+
readonly conditions: readonly Readonly<ConditionRecord>[];
|
|
303
|
+
readonly actions: readonly Readonly<ActionRecord>[];
|
|
304
|
+
readonly flags: number;
|
|
305
|
+
readonly players: readonly number[];
|
|
306
|
+
readonly currentAction: number;
|
|
307
|
+
}
|
|
300
308
|
declare const ConditionType: {
|
|
301
309
|
readonly None: 0;
|
|
302
310
|
readonly CountdownTimer: 1;
|
|
@@ -2193,6 +2201,29 @@ export declare const PLUGIN_API_VERSION = 1;
|
|
|
2193
2201
|
export interface Disposable {
|
|
2194
2202
|
dispose(): void;
|
|
2195
2203
|
}
|
|
2204
|
+
export type Primitive = string | number | boolean | bigint | symbol | null | undefined;
|
|
2205
|
+
/** A typed array without the calls that write to it, and with a read-only index. */
|
|
2206
|
+
export type ReadonlyTyped<T> = {
|
|
2207
|
+
readonly [K in keyof T as K extends "set" | "fill" | "copyWithin" | "reverse" | "sort" ? never : K]: T[K];
|
|
2208
|
+
};
|
|
2209
|
+
/**
|
|
2210
|
+
* `T` with every property, element and entry read-only, all the way down: arrays become
|
|
2211
|
+
* `readonly` arrays, a `Set` or `Map` its read-only form, a typed array `ReadonlyTyped`.
|
|
2212
|
+
*/
|
|
2213
|
+
export type DeepReadonly<T> = T extends Primitive | ((...args: never[]) => unknown) ? T : T extends Uint8Array | Uint16Array | Uint32Array ? ReadonlyTyped<T> : T extends Set<infer V> ? ReadonlySet<DeepReadonly<V>> : T extends Map<infer K, infer V> ? ReadonlyMap<K, DeepReadonly<V>> : T extends readonly (infer V)[] ? readonly DeepReadonly<V>[] : {
|
|
2214
|
+
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
2215
|
+
};
|
|
2216
|
+
/**
|
|
2217
|
+
* The open map as a plugin sees it: the editor's own live object, typed so that writing
|
|
2218
|
+
* to it does not compile. A direct write would skip undo, the file's dirty tracking, the
|
|
2219
|
+
* repaint and a shared map's other editors — every change goes through `document.edit`,
|
|
2220
|
+
* `document.update` or `document.sections`.
|
|
2221
|
+
*
|
|
2222
|
+
* The object is live, so a read after an edit sees the edit; copy what you need to keep
|
|
2223
|
+
* (`[...scn.units]`, `scn.tiles.slice()`). A copy of a record is an ordinary one:
|
|
2224
|
+
* `{ ...scn.units[0], x: 64 }` is a `UnitRecord` to hand to `tx.addUnits`.
|
|
2225
|
+
*/
|
|
2226
|
+
export type ReadonlyScenario = DeepReadonly<Scenario>;
|
|
2196
2227
|
/**
|
|
2197
2228
|
* A transaction builder's return type: whatever it likes, so long as it is not a promise.
|
|
2198
2229
|
*
|
|
@@ -2372,6 +2403,12 @@ export interface PluginApi {
|
|
|
2372
2403
|
* It is for a plugin that runs other code for a while and then takes it all back: the
|
|
2373
2404
|
* API Playground runs each snippet in one. The child speaks as the same plugin — same
|
|
2374
2405
|
* id, storage and log name.
|
|
2406
|
+
*
|
|
2407
|
+
* @example
|
|
2408
|
+
* // Register things for ten seconds, then take them all back.
|
|
2409
|
+
* const scope = api.scope();
|
|
2410
|
+
* scope.api.menu.add("Tools", { label: "Temporary item", run: () => scope.api.ui.status("Still here") });
|
|
2411
|
+
* setTimeout(() => scope.dispose(), 10_000);
|
|
2375
2412
|
*/
|
|
2376
2413
|
scope(): PluginScope;
|
|
2377
2414
|
}
|
|
@@ -2461,6 +2498,18 @@ export interface BuildStepInput {
|
|
|
2461
2498
|
* A step that throws, or that the user stops waiting for, does not stop a save: the map is
|
|
2462
2499
|
* written without the steps and a notice says why. Test Map stops instead, since a map
|
|
2463
2500
|
* without its built part is not the one to test. A bare `.chk` is never built.
|
|
2501
|
+
*
|
|
2502
|
+
* @example
|
|
2503
|
+
* api.document.buildSteps.add({
|
|
2504
|
+
* id: "stamp",
|
|
2505
|
+
* label: "Build stamp",
|
|
2506
|
+
* // Only maps that carry this file are built; every other Save is untouched.
|
|
2507
|
+
* applies: () => api.document.extras.get("my-plugin\\stamp.txt") !== null,
|
|
2508
|
+
* async run({ map, purpose }) {
|
|
2509
|
+
* api.log(`building ${map.length} bytes for ${purpose}`);
|
|
2510
|
+
* return map; // an archive in, an archive out
|
|
2511
|
+
* },
|
|
2512
|
+
* });
|
|
2464
2513
|
*/
|
|
2465
2514
|
export interface BuildStepSpec {
|
|
2466
2515
|
/** Unique within the plugin; the file remembers `plugin id/id`. */
|
|
@@ -2512,6 +2561,14 @@ export interface BuildStepsApi {
|
|
|
2512
2561
|
* The files stored in the map archive next to `staredit\scenario.chk`: custom sounds,
|
|
2513
2562
|
* graphics, and anything a plugin wants to keep with the map. Names are archive paths
|
|
2514
2563
|
* with backslashes (`staredit\wav\hello.wav`). They are written on Save.
|
|
2564
|
+
*
|
|
2565
|
+
* @example
|
|
2566
|
+
* // Keep a small JSON file inside the map, in a folder of the plugin's own.
|
|
2567
|
+
* const name = "my-plugin\\notes.json";
|
|
2568
|
+
* const bytes = api.document.extras.get(name);
|
|
2569
|
+
* const notes = bytes ? JSON.parse(new TextDecoder().decode(bytes)) : { opened: 0 };
|
|
2570
|
+
* notes.opened++;
|
|
2571
|
+
* api.document.extras.set(name, new TextEncoder().encode(JSON.stringify(notes)));
|
|
2515
2572
|
*/
|
|
2516
2573
|
export interface ExtrasApi {
|
|
2517
2574
|
list(): string[];
|
|
@@ -2586,10 +2643,17 @@ export interface DocumentApi {
|
|
|
2586
2643
|
isOpen(): boolean;
|
|
2587
2644
|
info(): DocumentInfo | null;
|
|
2588
2645
|
/**
|
|
2589
|
-
* The live scenario, for reading
|
|
2590
|
-
*
|
|
2646
|
+
* The live scenario, for reading — see `ReadonlyScenario`. To change the map, use
|
|
2647
|
+
* `edit`, `update` or `sections`.
|
|
2648
|
+
*
|
|
2649
|
+
* @example
|
|
2650
|
+
* const scn = api.document.scenario();
|
|
2651
|
+
* if (scn) {
|
|
2652
|
+
* const marines = scn.units.filter((u) => u.unitId === 0);
|
|
2653
|
+
* api.ui.status(`${marines.length} marines on a ${scn.width} × ${scn.height} map`);
|
|
2654
|
+
* }
|
|
2591
2655
|
*/
|
|
2592
|
-
scenario():
|
|
2656
|
+
scenario(): ReadonlyScenario | null;
|
|
2593
2657
|
/**
|
|
2594
2658
|
* Run `build` against a transaction and record what it did as one undo entry.
|
|
2595
2659
|
* Operations apply as they are called, so later ones see earlier ones' results.
|
|
@@ -2604,9 +2668,10 @@ export interface DocumentApi {
|
|
|
2604
2668
|
*
|
|
2605
2669
|
* @example
|
|
2606
2670
|
* // One undo entry called "Fill", however many operations it takes.
|
|
2607
|
-
*
|
|
2671
|
+
* await api.tileset.load(); // the async part first
|
|
2672
|
+
* const [, ground] = api.terrain.types(); // the tileset's second flat terrain; none without the graphics
|
|
2608
2673
|
* const result = api.document.edit("Fill", (tx) => {
|
|
2609
|
-
* tx.stampTerrain({ x0: 0, y0: 0, x1: 8, y1: 8 }, ground);
|
|
2674
|
+
* if (ground) tx.stampTerrain({ x0: 0, y0: 0, x1: 8, y1: 8 }, ground.id);
|
|
2610
2675
|
* tx.placeUnit(api.consts.unit.startLocation, 0, 4 * api.consts.tile, 4 * api.consts.tile);
|
|
2611
2676
|
* });
|
|
2612
2677
|
* api.ui.status(`${result.tiles} tiles, ${result.units} units`);
|
|
@@ -2632,7 +2697,13 @@ export interface DocumentApi {
|
|
|
2632
2697
|
update<R>(label: string, build: (tx: UpdateTransaction) => Sync<R>): UpdateResult;
|
|
2633
2698
|
undo(): string | null;
|
|
2634
2699
|
redo(): string | null;
|
|
2635
|
-
/**
|
|
2700
|
+
/**
|
|
2701
|
+
* The undo and redo stacks' tops — the labels the Edit menu shows — and their depths, without moving anything.
|
|
2702
|
+
*
|
|
2703
|
+
* @example
|
|
2704
|
+
* const { undo } = api.document.history();
|
|
2705
|
+
* if (undo && (await api.ui.confirm(`Undo "${undo}"?`))) api.document.undo();
|
|
2706
|
+
*/
|
|
2636
2707
|
history(): DocumentHistory;
|
|
2637
2708
|
/**
|
|
2638
2709
|
* The id of the map in front — the one `info()`, `scenario()` and every write are
|
|
@@ -2644,6 +2715,9 @@ export interface DocumentApi {
|
|
|
2644
2715
|
* Every open map, in the order the editor lists them, the one in front marked `active`.
|
|
2645
2716
|
* Empty with no map. The list changes only through `open`, `create`, `activate` and
|
|
2646
2717
|
* `close` (by any plugin or the user), each of which fires the `"document"` event.
|
|
2718
|
+
*
|
|
2719
|
+
* @example
|
|
2720
|
+
* for (const map of api.document.list()) api.log(map.id, map.name, map.active ? "(in front)" : "");
|
|
2647
2721
|
*/
|
|
2648
2722
|
list(): OpenDocumentInfo[];
|
|
2649
2723
|
/**
|
|
@@ -2661,6 +2735,10 @@ export interface DocumentApi {
|
|
|
2661
2735
|
* Scenario dialog comes first and the user may cancel. Resolves true once the file is
|
|
2662
2736
|
* the map in front, false when the user kept the current map or the file could not be
|
|
2663
2737
|
* read (the status bar says why).
|
|
2738
|
+
*
|
|
2739
|
+
* @example
|
|
2740
|
+
* const [file] = await api.ui.pickFiles({ accept: ".scx,.scm,.chk" });
|
|
2741
|
+
* if (file) await api.document.open(file, file.name, { into: "new" });
|
|
2664
2742
|
*/
|
|
2665
2743
|
open(file: File | Blob | Uint8Array, fileName?: string, options?: OpenDocumentOptions): Promise<boolean>;
|
|
2666
2744
|
/**
|
|
@@ -2669,11 +2747,19 @@ export interface DocumentApi {
|
|
|
2669
2747
|
* the open map or in place of it as `open` decides, through the same unsaved-changes
|
|
2670
2748
|
* gate. Resolves true once the new map is in front, false when the user kept the
|
|
2671
2749
|
* current one.
|
|
2750
|
+
*
|
|
2751
|
+
* @example
|
|
2752
|
+
* await api.document.create({ width: 128, height: 128, tileset: "jungle", name: "Sketch", startLocations: 4 });
|
|
2672
2753
|
*/
|
|
2673
2754
|
create(options: NewDocumentOptions): Promise<boolean>;
|
|
2674
2755
|
/**
|
|
2675
2756
|
* The open map as a file, as Save would write it — the remembered save options, the
|
|
2676
2757
|
* archive extras included — unless `saveOptions` says otherwise. Null when no map is open.
|
|
2758
|
+
*
|
|
2759
|
+
* @example
|
|
2760
|
+
* // The bytes Save would write, without saving: ready to upload or hand to another tool.
|
|
2761
|
+
* const file = await api.document.export({ format: "scx" });
|
|
2762
|
+
* if (file) api.ui.status(`${file.name}: ${file.size} bytes`);
|
|
2677
2763
|
*/
|
|
2678
2764
|
export(options?: ExportOptions): Promise<File | null>;
|
|
2679
2765
|
/**
|
|
@@ -2699,6 +2785,11 @@ export interface DocumentApi {
|
|
|
2699
2785
|
* is the one dial (32 is the game's art 1:1, 1 is a minimap); the other options
|
|
2700
2786
|
* default as the dialog's do. Needs the tileset graphics — without them, or without
|
|
2701
2787
|
* a map, null.
|
|
2788
|
+
*
|
|
2789
|
+
* @example
|
|
2790
|
+
* // A minimap-sized picture of the whole map, four pixels to a tile.
|
|
2791
|
+
* const png = await api.document.renderImage({ pixelsPerTile: 4 });
|
|
2792
|
+
* if (png) await api.ui.saveFile(png, "minimap.png");
|
|
2702
2793
|
*/
|
|
2703
2794
|
renderImage(options?: Partial<MapImageOptions>): Promise<Blob | null>;
|
|
2704
2795
|
/**
|
|
@@ -2750,6 +2841,11 @@ export interface RawEditResult {
|
|
|
2750
2841
|
*
|
|
2751
2842
|
* Indices are positions in `list()` and shift when a section is inserted or removed
|
|
2752
2843
|
* before them; take a fresh `list()` after every edit.
|
|
2844
|
+
*
|
|
2845
|
+
* @example
|
|
2846
|
+
* // The file's sections, largest first.
|
|
2847
|
+
* const sections = api.document.sections.list().sort((a, b) => b.size - a.size);
|
|
2848
|
+
* for (const s of sections) api.log(s.name, `${s.size} bytes`, s.spec ? "" : "(unknown to the editor)");
|
|
2753
2849
|
*/
|
|
2754
2850
|
export interface SectionsApi {
|
|
2755
2851
|
/** Every occurrence in file order; empty without a map. */
|
|
@@ -2809,7 +2905,7 @@ export interface SectionsApi {
|
|
|
2809
2905
|
* are left as they are and missing from `rebuilt`; omit `names` for every modelled
|
|
2810
2906
|
* section the map has a model for.
|
|
2811
2907
|
*/
|
|
2812
|
-
rebuild(names?: string[]): RebuildResult;
|
|
2908
|
+
rebuild(names?: readonly string[]): RebuildResult;
|
|
2813
2909
|
}
|
|
2814
2910
|
/** What `tx.rebuildIsom` did. */
|
|
2815
2911
|
export interface IsomRebuildResult {
|
|
@@ -2825,7 +2921,7 @@ export interface IsomRebuildResult {
|
|
|
2825
2921
|
/** Cells for the bulk terrain operations: a tile rect, or cell indices (`y * width + x`). */
|
|
2826
2922
|
export type Cells = Rect | Iterable<number>;
|
|
2827
2923
|
export interface EditTransaction {
|
|
2828
|
-
readonly scenario:
|
|
2924
|
+
readonly scenario: ReadonlyScenario;
|
|
2829
2925
|
readonly width: number;
|
|
2830
2926
|
readonly height: number;
|
|
2831
2927
|
/** MTXM (what the game draws) at a cell. */
|
|
@@ -2834,7 +2930,13 @@ export interface EditTransaction {
|
|
|
2834
2930
|
groundAt(x: number, y: number): number;
|
|
2835
2931
|
/** One tile into both sections. */
|
|
2836
2932
|
setTile(x: number, y: number, id: number): void;
|
|
2837
|
-
/**
|
|
2933
|
+
/**
|
|
2934
|
+
* Many tiles into both sections; returns how many changed.
|
|
2935
|
+
*
|
|
2936
|
+
* @example
|
|
2937
|
+
* // Copy the tile in the top-left corner over a 4 × 4 square beside it.
|
|
2938
|
+
* api.document.edit("Repeat a tile", (tx) => tx.setTiles({ x0: 1, y0: 0, x1: 5, y1: 4 }, tx.tileAt(0, 0)));
|
|
2939
|
+
*/
|
|
2838
2940
|
setTiles(cells: Cells, id: number): number;
|
|
2839
2941
|
/**
|
|
2840
2942
|
* The Rect brush: flat left/right pairs by column parity, one random variation per
|
|
@@ -2847,6 +2949,15 @@ export interface EditTransaction {
|
|
|
2847
2949
|
* The isometric brush on one diamond: sets its ISOM value and regenerates the tiles
|
|
2848
2950
|
* around it, cliffs and shores included. Needs ISOM and the tileset; returns whether
|
|
2849
2951
|
* the terrain could be painted there.
|
|
2952
|
+
*
|
|
2953
|
+
* @example
|
|
2954
|
+
* await api.tileset.load();
|
|
2955
|
+
* const [terrain] = api.terrain.isomTypes();
|
|
2956
|
+
* if (terrain !== undefined) {
|
|
2957
|
+
* api.document.edit("Paint a patch", (tx) => {
|
|
2958
|
+
* for (const d of api.terrain.diamondsIn({ x0: 8, y0: 8, x1: 16, y1: 16 })) tx.paintIsom(d, terrain);
|
|
2959
|
+
* });
|
|
2960
|
+
* }
|
|
2850
2961
|
*/
|
|
2851
2962
|
paintIsom(d: Diamond, terrainId: number, extent?: number): boolean;
|
|
2852
2963
|
/**
|
|
@@ -2859,9 +2970,15 @@ export interface EditTransaction {
|
|
|
2859
2970
|
rebuildIsom(): IsomRebuildResult | null;
|
|
2860
2971
|
/** A StarEdit-style unit record (fresh serial, valid/used masks) centred on map pixels. */
|
|
2861
2972
|
makeUnit(unitId: number, owner: number, x: number, y: number): UnitRecord;
|
|
2862
|
-
addUnits(records: UnitRecord[]): number[];
|
|
2863
|
-
removeUnits(indices: number[]): number;
|
|
2864
|
-
|
|
2973
|
+
addUnits(records: readonly UnitRecord[]): number[];
|
|
2974
|
+
removeUnits(indices: readonly number[]): number;
|
|
2975
|
+
/**
|
|
2976
|
+
* @example
|
|
2977
|
+
* // Give every selected unit to Player 2.
|
|
2978
|
+
* const selected = api.selection.units();
|
|
2979
|
+
* api.document.edit("Give to Player 2", (tx) => tx.updateUnits(selected, () => ({ owner: 1 })));
|
|
2980
|
+
*/
|
|
2981
|
+
updateUnits(indices: readonly number[], patch: (u: UnitRecord) => Partial<UnitRecord>): number;
|
|
2865
2982
|
/**
|
|
2866
2983
|
* A unit the way the Units palette places one: a building snaps its placement box to
|
|
2867
2984
|
* the tile grid (when the palette's *Snap to grid* is on), anything else lands where
|
|
@@ -2869,14 +2986,25 @@ export interface EditTransaction {
|
|
|
2869
2986
|
* if you want them. Returns the record's index.
|
|
2870
2987
|
*/
|
|
2871
2988
|
placeUnit(unitId: number, owner: number, x: number, y: number): number;
|
|
2872
|
-
/**
|
|
2989
|
+
/**
|
|
2990
|
+
* Whether the Units palette's placement checks, with its current options, allow a unit of this type centred there.
|
|
2991
|
+
*
|
|
2992
|
+
* @example
|
|
2993
|
+
* const marine = 0;
|
|
2994
|
+
* const x = 10 * api.consts.tile;
|
|
2995
|
+
* const y = 10 * api.consts.tile;
|
|
2996
|
+
* api.document.edit("Place a marine", (tx) => {
|
|
2997
|
+
* if (tx.canPlaceUnit(marine, x, y)) tx.placeUnit(marine, 0, x, y);
|
|
2998
|
+
* else tx.note("no room there");
|
|
2999
|
+
* });
|
|
3000
|
+
*/
|
|
2873
3001
|
canPlaceUnit(unitId: number, x: number, y: number): boolean;
|
|
2874
3002
|
makeSprite(kind: SpriteKind, id: number, owner: number, x: number, y: number, opts?: {
|
|
2875
3003
|
flipped?: boolean;
|
|
2876
3004
|
disabled?: boolean;
|
|
2877
3005
|
}): SpriteRecord;
|
|
2878
|
-
addSprites(records: SpriteRecord[]): number[];
|
|
2879
|
-
removeSprites(indices: number[]): number;
|
|
3006
|
+
addSprites(records: readonly SpriteRecord[]): number[];
|
|
3007
|
+
removeSprites(indices: readonly number[]): number;
|
|
2880
3008
|
/** `makeSprite` + `addSprites` in one, kept on the map; returns the record's index. */
|
|
2881
3009
|
placeSprite(kind: SpriteKind, id: number, owner: number, x: number, y: number, opts?: {
|
|
2882
3010
|
flipped?: boolean;
|
|
@@ -2884,14 +3012,14 @@ export interface EditTransaction {
|
|
|
2884
3012
|
}): number;
|
|
2885
3013
|
/** Stamp a doodad (a `dddata.bin` id) at a tile; returns its record index, or -1 when unknown or off the map. */
|
|
2886
3014
|
placeDoodad(doodadId: number, tx: number, ty: number, owner?: number): number;
|
|
2887
|
-
removeDoodads(indices: number[]): number;
|
|
3015
|
+
removeDoodads(indices: readonly number[]): number;
|
|
2888
3016
|
/**
|
|
2889
3017
|
* Make doodads plain terrain: their records go, their tiles stay (in TILE as well as
|
|
2890
3018
|
* MTXM, so the cells are ground to every tool from here on) and an overlay stays as an
|
|
2891
3019
|
* ordinary sprite. The way to touch up a ramp or a cliff piece tile by tile.
|
|
2892
3020
|
* Returns records converted.
|
|
2893
3021
|
*/
|
|
2894
|
-
convertDoodads(indices: number[]): number;
|
|
3022
|
+
convertDoodads(indices: readonly number[]): number;
|
|
2895
3023
|
/**
|
|
2896
3024
|
* Stamp a `Clip` with its top-left tile at (tx, ty) inside this transaction — the
|
|
2897
3025
|
* clipboard's paste without the clipboard: the clip can come from anywhere (`clipboard.copy`,
|
|
@@ -2900,13 +3028,38 @@ export interface EditTransaction {
|
|
|
2900
3028
|
* unless `parts` says otherwise; `mode` is `"merge"` unless given. Terrain and doodads
|
|
2901
3029
|
* from another tileset are refused, as a paste refuses them, and anything off the map is
|
|
2902
3030
|
* skipped, both said in the result's `notes`.
|
|
3031
|
+
*
|
|
3032
|
+
* @example
|
|
3033
|
+
* // Stamp the marked area again ten tiles to the right, as one undo step.
|
|
3034
|
+
* const area = api.selection.markedArea();
|
|
3035
|
+
* const clip = area && api.clipboard.capture({ rect: area });
|
|
3036
|
+
* if (area && clip) api.document.edit("Stamp", (tx) => { tx.paste(clip, area.x0 + 10, area.y0); });
|
|
2903
3037
|
*/
|
|
2904
3038
|
paste(clip: Clip, tx: number, ty: number, options?: PasteOptionsSpec): PasteResult;
|
|
2905
|
-
/**
|
|
3039
|
+
/**
|
|
3040
|
+
* A location in the lowest free slot (pixel bounds); returns the slot, or -1 when the table is full.
|
|
3041
|
+
*
|
|
3042
|
+
* @example
|
|
3043
|
+
* const t = api.consts.tile;
|
|
3044
|
+
* api.document.edit("Add a location", (tx) => {
|
|
3045
|
+
* const slot = tx.addLocation({ left: 4 * t, top: 4 * t, right: 8 * t, bottom: 8 * t }, "Beacon Alpha");
|
|
3046
|
+
* if (slot < 0) tx.note("the location table is full");
|
|
3047
|
+
* });
|
|
3048
|
+
*/
|
|
2906
3049
|
addLocation(bounds: Bounds, name?: string, elevationFlags?: number): number;
|
|
2907
3050
|
editLocation(index: number, patch: LocationPatch): boolean;
|
|
2908
|
-
removeLocations(indices: number[]): number;
|
|
2909
|
-
/**
|
|
3051
|
+
removeLocations(indices: readonly number[]): number;
|
|
3052
|
+
/**
|
|
3053
|
+
* Set (`"fog"`) or clear the `players` bits (bit n = player n + 1) over cells; creates MASK on first use.
|
|
3054
|
+
*
|
|
3055
|
+
* @example
|
|
3056
|
+
* // Fog the left half of the map for Player 1 and Player 2.
|
|
3057
|
+
* const info = api.document.info();
|
|
3058
|
+
* if (info) {
|
|
3059
|
+
* const half = { x0: 0, y0: 0, x1: info.width >> 1, y1: info.height };
|
|
3060
|
+
* api.document.edit("Fog the west", (tx) => tx.setFog(half, 0b11, "fog"));
|
|
3061
|
+
* }
|
|
3062
|
+
*/
|
|
2910
3063
|
setFog(cells: Cells, players: number, mode: FogMode): number;
|
|
2911
3064
|
/**
|
|
2912
3065
|
* Tools ▸ Replace Terrain: every tile matching `from` (a flat terrain by ISOM id, or
|
|
@@ -2940,17 +3093,31 @@ export interface EditTransaction {
|
|
|
2940
3093
|
* what the built-in brushes paint over. With the mode off, the cells as given.
|
|
2941
3094
|
*/
|
|
2942
3095
|
mirror(cells: Cells): number[];
|
|
2943
|
-
/**
|
|
3096
|
+
/**
|
|
3097
|
+
* A map pixel and its images under the symmetry mode, the original first.
|
|
3098
|
+
*
|
|
3099
|
+
* @example
|
|
3100
|
+
* // Follow Tools ▸ Symmetry: one marine at the point and one at each of its images.
|
|
3101
|
+
* api.document.edit("Marines", (tx) => {
|
|
3102
|
+
* for (const p of tx.mirrorPoint(320, 320)) tx.placeUnit(0, 0, p.x, p.y);
|
|
3103
|
+
* });
|
|
3104
|
+
*/
|
|
2944
3105
|
mirrorPoint(px: number, py: number): {
|
|
2945
3106
|
x: number;
|
|
2946
3107
|
y: number;
|
|
2947
3108
|
}[];
|
|
2948
3109
|
/** Shift units by a pixel delta; buildings re-snap to the grid when `snap` (the palette's option when omitted). Returns records changed. */
|
|
2949
|
-
moveUnits(indices: number[], dx: number, dy: number, snap?: boolean): number;
|
|
3110
|
+
moveUnits(indices: readonly number[], dx: number, dy: number, snap?: boolean): number;
|
|
2950
3111
|
/**
|
|
2951
3112
|
* Tools ▸ Auto-place Start Locations: one per player on a ring or in the corners, each
|
|
2952
3113
|
* moved to the nearest spot the placement checks accept; `replace` removes the existing
|
|
2953
3114
|
* ones first. Players count from 1.
|
|
3115
|
+
*
|
|
3116
|
+
* @example
|
|
3117
|
+
* api.document.edit("Four starts", (tx) => {
|
|
3118
|
+
* const { placed } = tx.placeStartLocations({ players: 4, replace: true });
|
|
3119
|
+
* tx.note(`${placed.filter(Boolean).length} of 4 placed`);
|
|
3120
|
+
* });
|
|
2954
3121
|
*/
|
|
2955
3122
|
placeStartLocations(options: {
|
|
2956
3123
|
players: number;
|
|
@@ -2958,11 +3125,11 @@ export interface EditTransaction {
|
|
|
2958
3125
|
margin?: number;
|
|
2959
3126
|
replace?: boolean;
|
|
2960
3127
|
}): StartPlacementResult;
|
|
2961
|
-
updateSprites(indices: number[], patch: (r: SpriteRecord) => Partial<SpriteRecord>): number;
|
|
3128
|
+
updateSprites(indices: readonly number[], patch: (r: SpriteRecord) => Partial<SpriteRecord>): number;
|
|
2962
3129
|
/** Shift sprites by a pixel delta, clamped to the map. Returns records changed. */
|
|
2963
|
-
moveSprites(indices: number[], dx: number, dy: number): number;
|
|
3130
|
+
moveSprites(indices: readonly number[], dx: number, dy: number): number;
|
|
2964
3131
|
/** Change a doodad's owner or disabled flag (its tiles stay). Returns records changed. */
|
|
2965
|
-
updateDoodads(indices: number[], patch: {
|
|
3132
|
+
updateDoodads(indices: readonly number[], patch: {
|
|
2966
3133
|
owner?: number;
|
|
2967
3134
|
disabled?: number;
|
|
2968
3135
|
}): number;
|
|
@@ -2997,12 +3164,22 @@ export interface TriggerListUpdate {
|
|
|
2997
3164
|
add(trigger: TriggerRecord, at?: number): number;
|
|
2998
3165
|
/** Replace one record; false when there is none at `index`. */
|
|
2999
3166
|
replace(index: number, trigger: TriggerRecord): boolean;
|
|
3000
|
-
remove(indices: number[]): number;
|
|
3167
|
+
remove(indices: readonly number[]): number;
|
|
3001
3168
|
move(from: number, to: number): boolean;
|
|
3002
3169
|
/**
|
|
3003
3170
|
* Parse the text format (`triggers.text.print`'s inverse) and append the result, or
|
|
3004
3171
|
* replace the list with `replace: true`. Strings the text names are interned as it
|
|
3005
3172
|
* parses. Throws with the line number when the text does not parse.
|
|
3173
|
+
*
|
|
3174
|
+
* @example
|
|
3175
|
+
* const source = `
|
|
3176
|
+
* Trigger("Player 1"){
|
|
3177
|
+
* Conditions:
|
|
3178
|
+
* Always();
|
|
3179
|
+
* Actions:
|
|
3180
|
+
* Display Text Message(Always Display, "Welcome.");
|
|
3181
|
+
* }`;
|
|
3182
|
+
* api.document.update("Add a greeting", (tx) => { tx.triggers.fromText(source); });
|
|
3006
3183
|
*/
|
|
3007
3184
|
fromText(source: string, options?: {
|
|
3008
3185
|
replace?: boolean;
|
|
@@ -3015,7 +3192,18 @@ export interface TriggerListUpdate {
|
|
|
3015
3192
|
*/
|
|
3016
3193
|
export interface StringsUpdate {
|
|
3017
3194
|
list(): (string | null)[];
|
|
3018
|
-
/**
|
|
3195
|
+
/**
|
|
3196
|
+
* The index of `text`: an identical entry when there is one, else a new one. 0 for `""`.
|
|
3197
|
+
*
|
|
3198
|
+
* @example
|
|
3199
|
+
* // Point the first trigger's first action at a new string, without touching the old one.
|
|
3200
|
+
* api.document.update("Reword", (tx) => {
|
|
3201
|
+
* const [trigger] = tx.triggers.list();
|
|
3202
|
+
* if (!trigger) return;
|
|
3203
|
+
* trigger.actions[0].text = tx.strings.intern("Hold the line!");
|
|
3204
|
+
* tx.triggers.replace(0, trigger);
|
|
3205
|
+
* });
|
|
3206
|
+
*/
|
|
3019
3207
|
intern(text: string): number;
|
|
3020
3208
|
/** Overwrite one slot. */
|
|
3021
3209
|
set(index: number, text: string): void;
|
|
@@ -3032,7 +3220,12 @@ export interface StringsUpdate {
|
|
|
3032
3220
|
}
|
|
3033
3221
|
export interface SwitchesUpdate {
|
|
3034
3222
|
names(): string[];
|
|
3035
|
-
/**
|
|
3223
|
+
/**
|
|
3224
|
+
* Name a switch (0-based); `""` clears the name. Creates SWNM on the first one.
|
|
3225
|
+
*
|
|
3226
|
+
* @example
|
|
3227
|
+
* api.document.update("Name a switch", (tx) => tx.switches.setName(0, "Gate is open"));
|
|
3228
|
+
*/
|
|
3036
3229
|
setName(index: number, name: string): void;
|
|
3037
3230
|
}
|
|
3038
3231
|
/**
|
|
@@ -3042,7 +3235,7 @@ export interface SwitchesUpdate {
|
|
|
3042
3235
|
* the end marks the map modified and tells the chrome to re-read.
|
|
3043
3236
|
*/
|
|
3044
3237
|
export interface UpdateTransaction {
|
|
3045
|
-
readonly scenario:
|
|
3238
|
+
readonly scenario: ReadonlyScenario;
|
|
3046
3239
|
/** TRIG. */
|
|
3047
3240
|
readonly triggers: TriggerListUpdate;
|
|
3048
3241
|
/** MBRF, the mission briefing's own list of the same records. */
|
|
@@ -3080,6 +3273,15 @@ export interface UpdateTransaction {
|
|
|
3080
3273
|
/** A line for the status bar, appended to the label. */
|
|
3081
3274
|
note(text: string): void;
|
|
3082
3275
|
}
|
|
3276
|
+
/**
|
|
3277
|
+
* @example
|
|
3278
|
+
* // Make Player 2 a Zerg computer.
|
|
3279
|
+
* const computer = api.names.playerTypes().find((t) => t.label === "Computer");
|
|
3280
|
+
* const zerg = api.names.races().find((r) => r.label === "Zerg");
|
|
3281
|
+
* if (computer && zerg) {
|
|
3282
|
+
* api.document.update("Player 2: Zerg computer", (tx) => { tx.players.set(1, { type: computer.value, race: zerg.value }); });
|
|
3283
|
+
* }
|
|
3284
|
+
*/
|
|
3083
3285
|
export interface PlayersUpdate {
|
|
3084
3286
|
/** All 12 slots, 0-based, with the effective colour and force. */
|
|
3085
3287
|
list(): PlayerSlotView[];
|
|
@@ -3097,6 +3299,10 @@ export interface UnitTypesUpdate {
|
|
|
3097
3299
|
* Patch one type. Setting any number turns "use default" off for it (seeding the untouched
|
|
3098
3300
|
* columns from the dat, as the dialog does); `useDefault: true` puts it back. Hit points are
|
|
3099
3301
|
* whole points. `name` is the custom name (`""` restores the default); `available` edits PUNI.
|
|
3302
|
+
*
|
|
3303
|
+
* @example
|
|
3304
|
+
* // Marines with 80 hit points and a name of their own.
|
|
3305
|
+
* api.document.update("Tougher marines", (tx) => { tx.unitTypes.set(0, { hitPoints: 80, name: "Veteran Marine" }); });
|
|
3100
3306
|
*/
|
|
3101
3307
|
set(unitId: number, patch: UnitTypePatch): boolean;
|
|
3102
3308
|
}
|
|
@@ -3141,6 +3347,11 @@ export interface CuwpUpdate {
|
|
|
3141
3347
|
* What the settings dialogs show, read without a transaction: the same views
|
|
3142
3348
|
* `document.update`'s `tx.players` … `tx.techs` hand out. Every list is empty and every
|
|
3143
3349
|
* single read null when no map is open.
|
|
3350
|
+
*
|
|
3351
|
+
* @example
|
|
3352
|
+
* for (const p of api.settings.players().slice(0, 8)) {
|
|
3353
|
+
* api.log(`Player ${p.slot + 1}: ${p.typeName}, ${p.raceName}, ${p.forceName ?? "no force"}`);
|
|
3354
|
+
* }
|
|
3144
3355
|
*/
|
|
3145
3356
|
export interface SettingsApi {
|
|
3146
3357
|
players(): PlayerSlotView[];
|
|
@@ -3166,6 +3377,12 @@ export interface SettingsApi {
|
|
|
3166
3377
|
* TrigEdit shows it, each naming the record field it lives in and the kind of value it
|
|
3167
3378
|
* is (a player group, a unit id, a location, a comparison, …). Everything that displays
|
|
3168
3379
|
* or edits a trigger reads this table, the editor's own dialogs included.
|
|
3380
|
+
*
|
|
3381
|
+
* @example
|
|
3382
|
+
* // What the first action of the first trigger is, with the name and kind of each argument.
|
|
3383
|
+
* const action = api.triggers.list()[0]?.actions[0];
|
|
3384
|
+
* const def = action && api.triggers.defs.action(action.type);
|
|
3385
|
+
* if (def) api.log(def.name, def.args.map((a) => `${a.label} (${a.kind}) in .${a.field}`));
|
|
3169
3386
|
*/
|
|
3170
3387
|
export interface TriggerDefsApi {
|
|
3171
3388
|
conditions(): ConditionDef[];
|
|
@@ -3178,9 +3395,16 @@ export interface TriggerDefsApi {
|
|
|
3178
3395
|
/** The value behind a label or one of its aliases, for parsing. */
|
|
3179
3396
|
choiceValue(kind: ArgKind, text: string): number | undefined;
|
|
3180
3397
|
}
|
|
3181
|
-
/**
|
|
3398
|
+
/**
|
|
3399
|
+
* Printing and parsing the text trigger format (File ▸ Import / Export ▸ Triggers).
|
|
3400
|
+
*
|
|
3401
|
+
* @example
|
|
3402
|
+
* // Save the triggers as text, the form File ▸ Export ▸ Triggers writes.
|
|
3403
|
+
* const text = api.triggers.text.print(api.triggers.list());
|
|
3404
|
+
* await api.ui.saveFile(new Blob([text], { type: "text/plain" }), "triggers.txt");
|
|
3405
|
+
*/
|
|
3182
3406
|
export interface TriggerTextApi {
|
|
3183
|
-
print(triggers:
|
|
3407
|
+
print(triggers: readonly ReadonlyTrigger[], options?: {
|
|
3184
3408
|
briefing?: boolean;
|
|
3185
3409
|
}): string;
|
|
3186
3410
|
/**
|
|
@@ -3191,7 +3415,7 @@ export interface TriggerTextApi {
|
|
|
3191
3415
|
briefing?: boolean;
|
|
3192
3416
|
}): TextTrigger[];
|
|
3193
3417
|
/** One trigger as its `Trigger(…)` block. */
|
|
3194
|
-
one(trigger:
|
|
3418
|
+
one(trigger: ReadonlyTrigger, options?: {
|
|
3195
3419
|
briefing?: boolean;
|
|
3196
3420
|
}): string;
|
|
3197
3421
|
}
|
|
@@ -3213,22 +3437,30 @@ export interface TriggersApi {
|
|
|
3213
3437
|
readonly defs: TriggerDefsApi;
|
|
3214
3438
|
readonly text: TriggerTextApi;
|
|
3215
3439
|
/** An empty trigger owned by the given player groups (All Players by default). */
|
|
3216
|
-
newTrigger(players?: number[]): TriggerRecord;
|
|
3440
|
+
newTrigger(players?: readonly number[]): TriggerRecord;
|
|
3217
3441
|
/** A condition of a type, with StarEdit's defaults for its arguments. */
|
|
3218
3442
|
newCondition(type: number): ConditionRecord;
|
|
3219
3443
|
newAction(type: number, briefing?: boolean): ActionRecord;
|
|
3220
|
-
isPreserved(trigger:
|
|
3221
|
-
setPreserved(trigger:
|
|
3444
|
+
isPreserved(trigger: ReadonlyTrigger): boolean;
|
|
3445
|
+
setPreserved(trigger: ReadonlyTrigger, on: boolean): TriggerRecord;
|
|
3222
3446
|
/** The indices of the triggers any of these player groups own. */
|
|
3223
|
-
triggersFor(list:
|
|
3224
|
-
/**
|
|
3225
|
-
|
|
3447
|
+
triggersFor(list: readonly ReadonlyTrigger[], groups: readonly number[]): number[];
|
|
3448
|
+
/**
|
|
3449
|
+
* The three lines the trigger list shows: players, conditions, actions.
|
|
3450
|
+
*
|
|
3451
|
+
* @example
|
|
3452
|
+
* for (const trigger of api.triggers.list()) {
|
|
3453
|
+
* const line = api.triggers.summarize(trigger);
|
|
3454
|
+
* api.log(`${line.players}: ${line.conditions} → ${line.actions}`);
|
|
3455
|
+
* }
|
|
3456
|
+
*/
|
|
3457
|
+
summarize(trigger: ReadonlyTrigger, briefing?: boolean): {
|
|
3226
3458
|
players: string;
|
|
3227
3459
|
conditions: string;
|
|
3228
3460
|
actions: string;
|
|
3229
3461
|
};
|
|
3230
3462
|
/** A trigger's `Comment` action text, when it has one. */
|
|
3231
|
-
comment(trigger:
|
|
3463
|
+
comment(trigger: ReadonlyTrigger): string | null;
|
|
3232
3464
|
/**
|
|
3233
3465
|
* EUD: the player value that reaches a memory address through the deaths table
|
|
3234
3466
|
* (`(address − consts.triggers.deathsTable) / 4`, the address rounded down to a dword)
|
|
@@ -3244,15 +3476,21 @@ export interface TriggersApi {
|
|
|
3244
3476
|
* finds its run by (`TriggerClaimSpec.locate`), and what an editor keeps a selection
|
|
3245
3477
|
* by across a list that changed under it.
|
|
3246
3478
|
*/
|
|
3247
|
-
fingerprint(trigger:
|
|
3479
|
+
fingerprint(trigger: ReadonlyTrigger): string;
|
|
3248
3480
|
/**
|
|
3249
3481
|
* Every death-counter cell (`[player, unit]`) and switch the triggers read or write —
|
|
3250
3482
|
* TRIG when `list` is omitted. A player group counts as every slot it can resolve to
|
|
3251
3483
|
* (`Current Player` as the trigger's owners); a class unit (*Any unit*, *Men*, …)
|
|
3252
3484
|
* counts for nothing, and an EUD player is an address, not a cell. What a plugin that
|
|
3253
3485
|
* allocates counters or switches of its own must keep clear of.
|
|
3486
|
+
*
|
|
3487
|
+
* @example
|
|
3488
|
+
* // A switch no trigger touches, for a plugin that needs one of its own.
|
|
3489
|
+
* const taken = new Set(api.triggers.usage().switches);
|
|
3490
|
+
* const free = Array.from({ length: 256 }, (_, i) => i).find((i) => !taken.has(i));
|
|
3491
|
+
* api.log(free === undefined ? "every switch is in use" : `${api.names.switch(free)} is free`);
|
|
3254
3492
|
*/
|
|
3255
|
-
usage(list?:
|
|
3493
|
+
usage(list?: readonly ReadonlyTrigger[]): {
|
|
3256
3494
|
cells: [
|
|
3257
3495
|
player: number,
|
|
3258
3496
|
unit: number
|
|
@@ -3278,7 +3516,7 @@ export interface TriggersApi {
|
|
|
3278
3516
|
* const set = new Set(refs.filter((r) => r.access === "write").map((r) => r.id));
|
|
3279
3517
|
* const never = [...new Set(refs.filter((r) => r.access === "read" && !set.has(r.id)).map((r) => r.id))];
|
|
3280
3518
|
*/
|
|
3281
|
-
references(list?:
|
|
3519
|
+
references(list?: readonly ReadonlyTrigger[], options?: {
|
|
3282
3520
|
briefing?: boolean;
|
|
3283
3521
|
}): TriggerRefs[];
|
|
3284
3522
|
/**
|
|
@@ -3288,8 +3526,8 @@ export interface TriggersApi {
|
|
|
3288
3526
|
* settled while the game runs (*Foes*, *Allies*, *Neutral Players*, *Non Allied Victory
|
|
3289
3527
|
* Players*); `players` is then every slot it could name.
|
|
3290
3528
|
*/
|
|
3291
|
-
resolvePlayers(group: number, owners?: number[]): {
|
|
3292
|
-
players: number[];
|
|
3529
|
+
resolvePlayers(group: number, owners?: readonly number[]): {
|
|
3530
|
+
players: readonly number[];
|
|
3293
3531
|
approximate: boolean;
|
|
3294
3532
|
};
|
|
3295
3533
|
/**
|
|
@@ -3298,6 +3536,25 @@ export interface TriggersApi {
|
|
|
3298
3536
|
* place of the form; the Text Trigger Editor fences them in comments; Import Triggers
|
|
3299
3537
|
* says what a replace would remove. The claim lives until `remove()` or the plugin's
|
|
3300
3538
|
* deactivation; call `refresh()` after a rebuild so the editors ask `locate` again.
|
|
3539
|
+
*
|
|
3540
|
+
* @example
|
|
3541
|
+
* // Fingerprints of the triggers this plugin generated, kept as it generates them.
|
|
3542
|
+
* const mine = new Set<string>();
|
|
3543
|
+
* const isMine = (t: Parameters<typeof api.triggers.fingerprint>[0]) => mine.has(api.triggers.fingerprint(t));
|
|
3544
|
+
* const claim = api.triggers.claim({
|
|
3545
|
+
* label: "the wave generator",
|
|
3546
|
+
* badge: "waves",
|
|
3547
|
+
* locate(list) {
|
|
3548
|
+
* const start = list.findIndex(isMine);
|
|
3549
|
+
* if (start < 0) return null;
|
|
3550
|
+
* let count = 1;
|
|
3551
|
+
* while (start + count < list.length && isMine(list[start + count])) count++;
|
|
3552
|
+
* return { start, count };
|
|
3553
|
+
* },
|
|
3554
|
+
* describe: () => "Made by the wave generator. Change the waves there, not here.",
|
|
3555
|
+
* });
|
|
3556
|
+
* // After regenerating: update `mine`, then
|
|
3557
|
+
* claim.refresh();
|
|
3301
3558
|
*/
|
|
3302
3559
|
claim(spec: TriggerClaimSpec): TriggerClaimHandle;
|
|
3303
3560
|
/**
|
|
@@ -3305,7 +3562,7 @@ export interface TriggersApi {
|
|
|
3305
3562
|
* run first — what an editor of the trigger list needs to fence or lock the generated
|
|
3306
3563
|
* runs. A claim whose `locate` finds nothing, or throws, is left out.
|
|
3307
3564
|
*/
|
|
3308
|
-
claims(list?:
|
|
3565
|
+
claims(list?: readonly ReadonlyTrigger[]): TriggerClaimRange[];
|
|
3309
3566
|
}
|
|
3310
3567
|
/** One claimed run as `triggers.claims` located it. */
|
|
3311
3568
|
export interface TriggerClaimRange {
|
|
@@ -3334,14 +3591,14 @@ export interface TriggerClaimSpec {
|
|
|
3334
3591
|
/** The word on the badge the trigger list shows on each row; the plugin's id by default. */
|
|
3335
3592
|
badge?: string;
|
|
3336
3593
|
/** Where the run sits in `list`, or null when it is not there (edited by hand, or gone). */
|
|
3337
|
-
locate(list:
|
|
3594
|
+
locate(list: readonly ReadonlyTrigger[]): {
|
|
3338
3595
|
start: number;
|
|
3339
3596
|
count: number;
|
|
3340
3597
|
} | null;
|
|
3341
3598
|
/** A sentence about one trigger of the run (its index in `list`), shown in place of the editor's form. */
|
|
3342
|
-
describe?(index: number, list:
|
|
3599
|
+
describe?(index: number, list: readonly ReadonlyTrigger[]): string;
|
|
3343
3600
|
/** Open the plugin's own editor on that trigger — the button under the sentence. */
|
|
3344
|
-
open?(index: number, list:
|
|
3601
|
+
open?(index: number, list: readonly ReadonlyTrigger[]): void;
|
|
3345
3602
|
/** That button's label; `Open <plugin name>` by default. */
|
|
3346
3603
|
openLabel?: string;
|
|
3347
3604
|
}
|
|
@@ -3368,6 +3625,14 @@ export interface StartLocation {
|
|
|
3368
3625
|
*
|
|
3369
3626
|
* Everything here is a read: nothing changes the map, and nothing throws without one
|
|
3370
3627
|
* (an empty list, or null).
|
|
3628
|
+
*
|
|
3629
|
+
* @example
|
|
3630
|
+
* // Check Map's findings in the log, and the view taken to the first one that has a place.
|
|
3631
|
+
* const issues = api.query.validate();
|
|
3632
|
+
* for (const issue of issues) api.log(issue.level, issue.where, issue.text);
|
|
3633
|
+
* const target = issues.find((i) => i.target)?.target;
|
|
3634
|
+
* if (target?.kind === "dialog") api.ui.open(target.id);
|
|
3635
|
+
* else if (target) api.view.goTo(target);
|
|
3371
3636
|
*/
|
|
3372
3637
|
export interface QueryApi {
|
|
3373
3638
|
/** The topmost unit whose sprite box covers a map pixel, or -1. */
|
|
@@ -3386,7 +3651,13 @@ export interface QueryApi {
|
|
|
3386
3651
|
locationsIn(rect: Rect): number[];
|
|
3387
3652
|
/** The map's start locations, by player. */
|
|
3388
3653
|
startLocations(): StartLocation[];
|
|
3389
|
-
/**
|
|
3654
|
+
/**
|
|
3655
|
+
* Whether a unit type may be placed centred there, and what stops it; null with no map.
|
|
3656
|
+
*
|
|
3657
|
+
* @example
|
|
3658
|
+
* const verdict = api.query.placement(106, 640, 640); // a Terran Command Center
|
|
3659
|
+
* api.ui.status(verdict?.problem ? `Blocked: ${verdict.reason}` : "It fits.");
|
|
3660
|
+
*/
|
|
3390
3661
|
placement(unitId: number, x: number, y: number): PlacementVerdict | null;
|
|
3391
3662
|
/**
|
|
3392
3663
|
* StarEdit's ground check for a doodad with its top-left tile at (tx, ty): `ok`, or the
|
|
@@ -3402,13 +3673,32 @@ export interface QueryApi {
|
|
|
3402
3673
|
strings(): (string | null)[];
|
|
3403
3674
|
/** Check Map: every issue the editor knows how to spot, with a `target` to go to. */
|
|
3404
3675
|
validate(): Issue[];
|
|
3405
|
-
/**
|
|
3676
|
+
/**
|
|
3677
|
+
* Tools ▸ Statistics: tile, unit, resource and player counts. Null without a map.
|
|
3678
|
+
*
|
|
3679
|
+
* @example
|
|
3680
|
+
* const stats = api.query.statistics();
|
|
3681
|
+
* if (stats) api.ui.status(`${stats.units.total} units, ${stats.resources.fields} mineral fields, ${stats.triggers.count} triggers`);
|
|
3682
|
+
*/
|
|
3406
3683
|
statistics(): MapStatistics | null;
|
|
3407
|
-
/**
|
|
3684
|
+
/**
|
|
3685
|
+
* The Ctrl+F search over units, locations, sprites, strings and triggers.
|
|
3686
|
+
*
|
|
3687
|
+
* @example
|
|
3688
|
+
* // Select every unit whose name contains "marine".
|
|
3689
|
+
* const hits = api.query.find({ kind: "units", query: "marine" });
|
|
3690
|
+
* api.selection.setLayer("units");
|
|
3691
|
+
* api.selection.setUnits(hits.map((h) => h.index));
|
|
3692
|
+
*/
|
|
3408
3693
|
find(options: FindOptions): FindResult[];
|
|
3409
3694
|
/** Every record that refers to each string index. */
|
|
3410
3695
|
stringUsage(): Map<number, StringUsage[]>;
|
|
3411
|
-
/**
|
|
3696
|
+
/**
|
|
3697
|
+
* String slots nothing refers to.
|
|
3698
|
+
*
|
|
3699
|
+
* @example
|
|
3700
|
+
* for (const index of api.query.unusedStrings()) api.log(index, api.names.string(index));
|
|
3701
|
+
*/
|
|
3412
3702
|
unusedStrings(): number[];
|
|
3413
3703
|
}
|
|
3414
3704
|
/** Where `view.goTo` should take the user. Issues from `query.validate` carry one. */
|
|
@@ -3452,10 +3742,18 @@ export interface ViewApi {
|
|
|
3452
3742
|
* own work around the map takes that as its cue to stop.
|
|
3453
3743
|
*
|
|
3454
3744
|
* @example
|
|
3455
|
-
*
|
|
3745
|
+
* const shown = await api.view.reveal({ x0: 0, y0: 0, x1: 16, y1: 16 }, { fit: true });
|
|
3746
|
+
* if (!shown) api.log("the user moved the view first");
|
|
3456
3747
|
*/
|
|
3457
3748
|
reveal(rect: Rect, options?: RevealOptions): Promise<boolean>;
|
|
3458
|
-
/**
|
|
3749
|
+
/**
|
|
3750
|
+
* Scroll to an object (and select it, for a unit, sprite or location).
|
|
3751
|
+
*
|
|
3752
|
+
* @example
|
|
3753
|
+
* // Player 1's start location, selected and in the middle of the view.
|
|
3754
|
+
* const start = api.query.startLocations().find((s) => s.owner === 0);
|
|
3755
|
+
* if (start) api.view.goTo({ kind: "unit", index: start.index });
|
|
3756
|
+
*/
|
|
3459
3757
|
goTo(target: GoTo): void;
|
|
3460
3758
|
/** The tile under the pointer, as the status bar shows it. */
|
|
3461
3759
|
cursorTile(): {
|
|
@@ -3464,6 +3762,10 @@ export interface ViewApi {
|
|
|
3464
3762
|
};
|
|
3465
3763
|
/** The View menu's ticks: grid, locations, units, sprites, doodads, fog, … */
|
|
3466
3764
|
flags(): ViewFlags;
|
|
3765
|
+
/**
|
|
3766
|
+
* @example
|
|
3767
|
+
* api.view.setFlags({ grid: !api.view.flags().grid });
|
|
3768
|
+
*/
|
|
3467
3769
|
setFlags(patch: Partial<ViewFlags>): void;
|
|
3468
3770
|
/** Grid spacing in map pixels (32 = one tile). */
|
|
3469
3771
|
gridSize(): number;
|
|
@@ -3474,8 +3776,9 @@ export interface ViewApi {
|
|
|
3474
3776
|
* clean up.
|
|
3475
3777
|
*
|
|
3476
3778
|
* @example
|
|
3477
|
-
*
|
|
3478
|
-
* const
|
|
3779
|
+
* // Mark the top-left corner for Player 1's fog, and show where that was.
|
|
3780
|
+
* const rect = { x0: 0, y0: 0, x1: 8, y1: 8 };
|
|
3781
|
+
* const r = api.document.edit("Fog a corner", (tx) => tx.setFog(rect, 0b1, "fog"));
|
|
3479
3782
|
* if (r.changed) api.view.flash({ rect });
|
|
3480
3783
|
*/
|
|
3481
3784
|
flash(target: FlashTarget): void;
|
|
@@ -3511,6 +3814,13 @@ export interface CommandInfo {
|
|
|
3511
3814
|
* Named things a plugin can do, so a menu item, a hotkey, a context entry and another
|
|
3512
3815
|
* plugin all reach the same one. `menu.add`, `contextMenu.add` and `hotkeys.add` take a
|
|
3513
3816
|
* `command` id in place of a `run`.
|
|
3817
|
+
*
|
|
3818
|
+
* @example
|
|
3819
|
+
* api.commands.register({ id: "count", title: "Count units", run: () => api.document.scenario()?.units.length ?? 0 });
|
|
3820
|
+
* api.menu.add("Tools", { label: "Count units", command: "count" });
|
|
3821
|
+
* api.hotkeys.add("Ctrl+Alt+U", { command: "count" });
|
|
3822
|
+
* // Any plugin can run it by its full id, and gets what it returns.
|
|
3823
|
+
* api.log(api.commands.run(`${api.plugin.id}.count`));
|
|
3514
3824
|
*/
|
|
3515
3825
|
export interface CommandsApi {
|
|
3516
3826
|
register(spec: CommandSpec): Disposable;
|
|
@@ -3542,11 +3852,12 @@ export interface ServiceOptions {
|
|
|
3542
3852
|
* the plugin (`"account"` → `"scmjs-dev.account"`); one with a dot is taken as it is.
|
|
3543
3853
|
*
|
|
3544
3854
|
* @example
|
|
3545
|
-
*
|
|
3546
|
-
*
|
|
3547
|
-
*
|
|
3548
|
-
*
|
|
3549
|
-
*
|
|
3855
|
+
* interface Clock { now(): number }
|
|
3856
|
+
* // The provider. The name becomes "<plugin id>.clock".
|
|
3857
|
+
* api.services.provide<Clock>("clock", { now: () => Date.now() }, { version: 1 });
|
|
3858
|
+
* // A consumer, in any plugin, whichever of the two started first.
|
|
3859
|
+
* api.services.watch<Clock>(`${api.plugin.id}.clock`, (clock, info) => {
|
|
3860
|
+
* api.log(clock ? `clock v${info?.version}: ${clock.now()}` : "no clock yet");
|
|
3550
3861
|
* });
|
|
3551
3862
|
*/
|
|
3552
3863
|
export interface ServicesApi {
|
|
@@ -3576,6 +3887,27 @@ export interface RenderClipOptions {
|
|
|
3576
3887
|
* The pictures the viewport draws, for a plugin's own lists and previews: the same
|
|
3577
3888
|
* cached canvases, so asking for one costs nothing after the first time. Everything is
|
|
3578
3889
|
* null when the graphics it needs were never extracted — a plugin shows a name instead.
|
|
3890
|
+
*
|
|
3891
|
+
* @example
|
|
3892
|
+
* // A marine in Player 2's colours. The picture is the editor's own cached canvas, so draw
|
|
3893
|
+
* // from it instead of moving it into your page.
|
|
3894
|
+
* await api.graphics.load();
|
|
3895
|
+
* api.ui.panel({
|
|
3896
|
+
* title: "Marine",
|
|
3897
|
+
* mount(body) {
|
|
3898
|
+
* const show = () => {
|
|
3899
|
+
* const picture = api.graphics.unitImage(0, { owner: 1 });
|
|
3900
|
+
* if (!picture) { body.textContent = api.names.unit(0); return; } // not loaded, or no graphics
|
|
3901
|
+
* const canvas = api.ui.el("canvas", { width: picture.width, height: picture.height });
|
|
3902
|
+
* canvas.getContext("2d")?.drawImage(picture.image, 0, 0);
|
|
3903
|
+
* body.replaceChildren(canvas);
|
|
3904
|
+
* };
|
|
3905
|
+
* api.graphics.requestUnit(0);
|
|
3906
|
+
* const loaded = api.graphics.onImageLoaded(show);
|
|
3907
|
+
* show();
|
|
3908
|
+
* return () => loaded.dispose();
|
|
3909
|
+
* },
|
|
3910
|
+
* });
|
|
3579
3911
|
*/
|
|
3580
3912
|
export interface GraphicsApi {
|
|
3581
3913
|
/** Whether the tileset graphics and the unit tables are in memory. */
|
|
@@ -3637,6 +3969,15 @@ export interface GraphicsApi {
|
|
|
3637
3969
|
*
|
|
3638
3970
|
* Everything is null until the tables are loaded (`load`), and stays null when the game
|
|
3639
3971
|
* data was never extracted.
|
|
3972
|
+
*
|
|
3973
|
+
* @example
|
|
3974
|
+
* if (await api.data.load()) {
|
|
3975
|
+
* const units = api.data.units()!;
|
|
3976
|
+
* // units.dat keeps hit points in 256ths.
|
|
3977
|
+
* api.log(`Marine: ${units.hitPoints[0] / 256} HP, ${units.mineralCost[0]} minerals, ${api.data.race(0)}`);
|
|
3978
|
+
* } else {
|
|
3979
|
+
* api.log("no game data installed");
|
|
3980
|
+
* }
|
|
3640
3981
|
*/
|
|
3641
3982
|
export interface DataApi {
|
|
3642
3983
|
ready(): boolean;
|
|
@@ -3709,8 +4050,8 @@ export interface GameDataFiles {
|
|
|
3709
4050
|
* // A mod plugin: install its files once, then draw with them.
|
|
3710
4051
|
* const sets = await api.gameData.profiles();
|
|
3711
4052
|
* if (!sets.some((p) => p.id === "my-mod")) {
|
|
3712
|
-
* const files = await api.ui.pickFiles({ multiple: true });
|
|
3713
|
-
* if (files) await api.gameData.install({ id: "my-mod", name: "My Mod" }, { archives: files.map((f) => ({ name: f.name, data: f })) });
|
|
4053
|
+
* const files = await api.ui.pickFiles({ multiple: true }); // an empty list when the user cancels
|
|
4054
|
+
* if (files.length > 0) await api.gameData.install({ id: "my-mod", name: "My Mod" }, { archives: files.map((f) => ({ name: f.name, data: f })) });
|
|
3714
4055
|
* }
|
|
3715
4056
|
* await api.gameData.select("my-mod");
|
|
3716
4057
|
*/
|
|
@@ -3755,6 +4096,16 @@ export interface GameDataApi {
|
|
|
3755
4096
|
* type-checks and is then undefined. Anything a plugin needs while it runs has to come
|
|
3756
4097
|
* off `api`. Without this a plugin writes the hex itself, which is how a sprite's kind
|
|
3757
4098
|
* came to be read as `flags & 0x1000` in three places.
|
|
4099
|
+
*
|
|
4100
|
+
* @example
|
|
4101
|
+
* // Make the selected units invincible: the state bit, and the two masks that say it is set.
|
|
4102
|
+
* const { state, valid, used } = api.consts.unit;
|
|
4103
|
+
* const selected = api.selection.units();
|
|
4104
|
+
* api.document.edit("Invincible", (tx) => tx.updateUnits(selected, (u) => ({
|
|
4105
|
+
* stateFlags: u.stateFlags | state.Invincible,
|
|
4106
|
+
* validProperties: u.validProperties | valid.Invincible,
|
|
4107
|
+
* validStates: u.validStates | used.State,
|
|
4108
|
+
* })));
|
|
3758
4109
|
*/
|
|
3759
4110
|
export interface ConstsApi {
|
|
3760
4111
|
/** Map pixels to a tile. UNIT and THG2 store pixels; MTXM, MRGN and the brushes count tiles. */
|
|
@@ -3859,6 +4210,13 @@ export interface LocationConsts {
|
|
|
3859
4210
|
* Generating a whole run of triggers is usually better done through
|
|
3860
4211
|
* `tx.triggers.fromText`, which resolves names against the open map; these are for
|
|
3861
4212
|
* editing a field of an existing record, and for reading one back.
|
|
4213
|
+
*
|
|
4214
|
+
* @example
|
|
4215
|
+
* // "At most 30 seconds on the countdown timer", written field by field.
|
|
4216
|
+
* const { condition, comparison } = api.consts.triggers;
|
|
4217
|
+
* const timer = api.triggers.newCondition(condition.CountdownTimer);
|
|
4218
|
+
* timer.comparison = comparison.AtMost;
|
|
4219
|
+
* timer.amount = 30;
|
|
3862
4220
|
*/
|
|
3863
4221
|
export interface TriggerConsts {
|
|
3864
4222
|
/** Condition `type`. */
|
|
@@ -3912,7 +4270,13 @@ export interface ActiveBrush {
|
|
|
3912
4270
|
variation: number;
|
|
3913
4271
|
}
|
|
3914
4272
|
export interface TerrainApi {
|
|
3915
|
-
/**
|
|
4273
|
+
/**
|
|
4274
|
+
* Paintable flat terrains of the open map's tileset (empty without the graphics).
|
|
4275
|
+
*
|
|
4276
|
+
* @example
|
|
4277
|
+
* await api.tileset.load();
|
|
4278
|
+
* for (const t of api.terrain.types()) api.log(t.id, t.name, `height ${t.height}`, t.buildable ? "buildable" : "");
|
|
4279
|
+
*/
|
|
3916
4280
|
types(): TerrainType[];
|
|
3917
4281
|
/** Terrain ids the isometric brush can paint on this tileset. */
|
|
3918
4282
|
isomTypes(): number[];
|
|
@@ -3931,7 +4295,7 @@ export interface TerrainApi {
|
|
|
3931
4295
|
*
|
|
3932
4296
|
* @example
|
|
3933
4297
|
* const r = await api.terrain.checkIsom();
|
|
3934
|
-
* if (r?.stale)
|
|
4298
|
+
* if (r?.stale) api.document.edit("Rebuild ISOM", (tx) => { tx.rebuildIsom(); });
|
|
3935
4299
|
*/
|
|
3936
4300
|
checkIsom(): Promise<IsomReport | null>;
|
|
3937
4301
|
tileInfo(id: number): TileInfo | null;
|
|
@@ -3940,6 +4304,12 @@ export interface TerrainApi {
|
|
|
3940
4304
|
* the tile's own group when it is flat ground, else — under a cliff, a shore or a
|
|
3941
4305
|
* doodad — what the ISOM lattice says the diamond there is. Null off the map, without
|
|
3942
4306
|
* the tileset graphics, or when neither tells.
|
|
4307
|
+
*
|
|
4308
|
+
* @example
|
|
4309
|
+
* // Name the terrain under the pointer.
|
|
4310
|
+
* const { x, y } = api.view.cursorTile();
|
|
4311
|
+
* const id = api.terrain.terrainAt(x, y);
|
|
4312
|
+
* api.ui.status(api.terrain.types().find((t) => t.id === id)?.name ?? "unknown terrain");
|
|
3943
4313
|
*/
|
|
3944
4314
|
terrainAt(tx: number, ty: number): number | null;
|
|
3945
4315
|
/** The atlas average of a tile, packed `0xRRGGBB`, or null without graphics. */
|
|
@@ -3965,6 +4335,10 @@ export interface TerrainApi {
|
|
|
3965
4335
|
flatGroupOf(terrainId: number): number;
|
|
3966
4336
|
/** Tools ▸ Symmetry…: the mode the brushes paint and the palettes place under. */
|
|
3967
4337
|
symmetry(): SymmetryMode;
|
|
4338
|
+
/**
|
|
4339
|
+
* @example
|
|
4340
|
+
* if (api.terrain.symmetryAvailable("rot180")) api.terrain.setSymmetry("rot180");
|
|
4341
|
+
*/
|
|
3968
4342
|
setSymmetry(mode: SymmetryMode): void;
|
|
3969
4343
|
/** Whether a mode can run on the open map (the rotations and diagonals need a square one). */
|
|
3970
4344
|
symmetryAvailable(mode: SymmetryMode): boolean;
|
|
@@ -3988,6 +4362,11 @@ export interface TilesetApi {
|
|
|
3988
4362
|
* tileset changes nothing about the map: it is for `graphics.renderClip` drawing a clip
|
|
3989
4363
|
* that came from a map on that tileset (a stamp, a recording), which answers null until
|
|
3990
4364
|
* its tileset's graphics are loaded.
|
|
4365
|
+
*
|
|
4366
|
+
* @example
|
|
4367
|
+
* if (!(await api.tileset.load())) {
|
|
4368
|
+
* api.ui.toast({ kind: "warn", title: "No tileset graphics", detail: "Install them from Help ▸ Game Data…" });
|
|
4369
|
+
* }
|
|
3991
4370
|
*/
|
|
3992
4371
|
load(tileset?: TilesetId): Promise<boolean>;
|
|
3993
4372
|
raw(): LoadedTileset | null;
|
|
@@ -3997,13 +4376,13 @@ export interface SelectionApi {
|
|
|
3997
4376
|
markedArea(): Rect | null;
|
|
3998
4377
|
markArea(rect: Rect | null): void;
|
|
3999
4378
|
units(): number[];
|
|
4000
|
-
setUnits(indices: number[]): void;
|
|
4379
|
+
setUnits(indices: readonly number[]): void;
|
|
4001
4380
|
sprites(): number[];
|
|
4002
|
-
setSprites(indices: number[]): void;
|
|
4381
|
+
setSprites(indices: readonly number[]): void;
|
|
4003
4382
|
doodads(): number[];
|
|
4004
|
-
setDoodads(indices: number[]): void;
|
|
4383
|
+
setDoodads(indices: readonly number[]): void;
|
|
4005
4384
|
locations(): number[];
|
|
4006
|
-
setLocations(indices: number[]): void;
|
|
4385
|
+
setLocations(indices: readonly number[]): void;
|
|
4007
4386
|
layer(): EditorLayer;
|
|
4008
4387
|
setLayer(layer: EditorLayer): void;
|
|
4009
4388
|
/** Layers the Layers panel has locked: their tools refuse to change the map. */
|
|
@@ -4029,6 +4408,14 @@ export interface PasteOptionsSpec {
|
|
|
4029
4408
|
* The Cut / Copy / Paste layer. The clip is self-contained (it outlives the map it came
|
|
4030
4409
|
* from and pastes into another) and shared with the user's own clipboard, so a plugin
|
|
4031
4410
|
* that copies here is copying for the user too.
|
|
4411
|
+
*
|
|
4412
|
+
* @example
|
|
4413
|
+
* // Copy the top-left 8 × 8 tiles and paste them 16 tiles to the right.
|
|
4414
|
+
* const clip = api.clipboard.copy({ rect: { x0: 0, y0: 0, x1: 8, y1: 8 } });
|
|
4415
|
+
* if (clip) {
|
|
4416
|
+
* api.ui.status(api.clipboard.summary(clip));
|
|
4417
|
+
* api.clipboard.paste(16, 0);
|
|
4418
|
+
* }
|
|
4032
4419
|
*/
|
|
4033
4420
|
export interface ClipboardApi {
|
|
4034
4421
|
/** What is on the clipboard, or null. */
|
|
@@ -4070,10 +4457,20 @@ export interface ClipboardApi {
|
|
|
4070
4457
|
/**
|
|
4071
4458
|
* The file formats File ▸ Import / Export read and write, so a plugin can keep triggers
|
|
4072
4459
|
* or strings outside the map in a form the editor (and SCMDraft) take back.
|
|
4460
|
+
*
|
|
4461
|
+
* @example
|
|
4462
|
+
* // The string table out to a text file, and back in after editing it elsewhere.
|
|
4463
|
+
* await api.ui.saveFile(new Blob([api.exchange.formatStrings()], { type: "text/plain" }), "strings.txt");
|
|
4464
|
+
* const [file] = await api.ui.pickFiles({ accept: ".txt" });
|
|
4465
|
+
* if (file) {
|
|
4466
|
+
* const text = await file.text();
|
|
4467
|
+
* const { errors } = api.exchange.parseStrings(text);
|
|
4468
|
+
* if (errors.length === 0) api.document.update("Import strings", (tx) => { tx.strings.import(text); });
|
|
4469
|
+
* }
|
|
4073
4470
|
*/
|
|
4074
4471
|
export interface ExchangeApi {
|
|
4075
4472
|
/** Raw 2400-byte TRIG records, SCMDraft's `.trg`; string indices are the map's own. */
|
|
4076
|
-
encodeTrg(triggers:
|
|
4473
|
+
encodeTrg(triggers: readonly ReadonlyTrigger[]): Uint8Array;
|
|
4077
4474
|
decodeTrg(bytes: Uint8Array): TriggerRecord[];
|
|
4078
4475
|
/** The whole string table as `index<TAB>text` lines, control bytes as `<XX>`. */
|
|
4079
4476
|
formatStrings(): string;
|
|
@@ -4135,6 +4532,16 @@ export interface DoodadInfo {
|
|
|
4135
4532
|
*/
|
|
4136
4533
|
required: number[];
|
|
4137
4534
|
}
|
|
4535
|
+
/**
|
|
4536
|
+
* @example
|
|
4537
|
+
* // Place whatever the Units palette has picked, for the player it has picked.
|
|
4538
|
+
* const { unit, owner } = api.palette.active();
|
|
4539
|
+
* const at = await api.ui.pickTile({ prompt: `Click where the ${api.palette.unitName(unit)} goes` });
|
|
4540
|
+
* if (at) {
|
|
4541
|
+
* const t = api.consts.tile;
|
|
4542
|
+
* api.document.edit("Place", (tx) => { tx.placeUnit(unit, owner, at.x * t + t / 2, at.y * t + t / 2); });
|
|
4543
|
+
* }
|
|
4544
|
+
*/
|
|
4138
4545
|
export interface PaletteApi {
|
|
4139
4546
|
active(): PaletteChoice;
|
|
4140
4547
|
setActive(choice: Partial<PaletteChoice>): void;
|
|
@@ -4173,6 +4580,11 @@ export interface NamedValue {
|
|
|
4173
4580
|
* carry the game's tables itself. The per-map ones (`string`, `location`, `switch`,
|
|
4174
4581
|
* `player`) read the open scenario and answer a placeholder without one; the rest are
|
|
4175
4582
|
* the editor's own tables — the same names StarEdit shows.
|
|
4583
|
+
*
|
|
4584
|
+
* @example
|
|
4585
|
+
* api.log(api.names.unit(0), api.names.playerGroup(api.consts.triggers.player.Player1), api.names.location(api.consts.location.anywhere));
|
|
4586
|
+
* // The list forms are ready for a drop-down.
|
|
4587
|
+
* const upgrade = api.ui.widgets.select(api.names.upgrades());
|
|
4176
4588
|
*/
|
|
4177
4589
|
export interface NamesApi {
|
|
4178
4590
|
/** StarEdit's name for a units.dat id; `Any unit` / `Men` / `Buildings` / `Factories` for the trigger classes 229–232 (228 is the game's `None`). */
|
|
@@ -4233,6 +4645,21 @@ export interface NamesApi {
|
|
|
4233
4645
|
* alignment code on a line, stacking the pieces between them in one place: `stackedLines`
|
|
4234
4646
|
* finds those lines and `flattenStacks` lays them out left to right. `runs` is one of the
|
|
4235
4647
|
* renderers that cannot show them — `TextLine.align` is one alignment for the whole line.
|
|
4648
|
+
*
|
|
4649
|
+
* @example
|
|
4650
|
+
* const text = "\x07Mission\nKill them all"; // byte 0x07 colours the first line
|
|
4651
|
+
* api.log(api.text.plain(text)); // "Mission\nKill them all"
|
|
4652
|
+
* api.log(api.text.bleedingLines(text).map((l) => l.line)); // the second line inherits the colour
|
|
4653
|
+
* api.log(api.text.fixBleeding(text) === text); // false: a reset was written
|
|
4654
|
+
*
|
|
4655
|
+
* @example
|
|
4656
|
+
* // Repair every string in the map that has the problem.
|
|
4657
|
+
* const strings = api.query.strings();
|
|
4658
|
+
* api.document.update("Fix carried colours", (tx) => {
|
|
4659
|
+
* strings.forEach((s, i) => {
|
|
4660
|
+
* if (s && api.text.bleedingLines(s).length > 0) tx.strings.set(i, api.text.fixBleeding(s));
|
|
4661
|
+
* });
|
|
4662
|
+
* });
|
|
4236
4663
|
*/
|
|
4237
4664
|
export interface TextApi {
|
|
4238
4665
|
/** Every byte the game gives a meaning, in order; `rgb` is set for the colours only. */
|
|
@@ -4287,6 +4714,19 @@ export interface DialogTransfer {
|
|
|
4287
4714
|
files: File[];
|
|
4288
4715
|
text: string;
|
|
4289
4716
|
}
|
|
4717
|
+
/**
|
|
4718
|
+
* @example
|
|
4719
|
+
* const name = api.ui.widgets.text({ value: api.document.info()?.name ?? "" });
|
|
4720
|
+
* api.ui.dialog({
|
|
4721
|
+
* title: "Rename the map",
|
|
4722
|
+
* size: "sm",
|
|
4723
|
+
* mount: (body) => body.append(api.ui.widgets.form([{ label: "Name", field: name }])),
|
|
4724
|
+
* buttons: [
|
|
4725
|
+
* { label: "Cancel" },
|
|
4726
|
+
* { label: "Rename", primary: true, run: () => { api.document.update("Rename", (tx) => tx.properties({ name: name.value })); } },
|
|
4727
|
+
* ],
|
|
4728
|
+
* });
|
|
4729
|
+
*/
|
|
4290
4730
|
export interface DialogSpec {
|
|
4291
4731
|
title: string;
|
|
4292
4732
|
size?: DialogSize;
|
|
@@ -4347,6 +4787,19 @@ export interface DialogHandle {
|
|
|
4347
4787
|
* A panel floats over the map and does not block it: the user keeps drawing, scrolling
|
|
4348
4788
|
* and using hotkeys while it is open (except while typing in one of its fields). It
|
|
4349
4789
|
* is dragged by its title bar and closed with the × or `close()`.
|
|
4790
|
+
*
|
|
4791
|
+
* @example
|
|
4792
|
+
* // A unit count that follows the map, docked under the built-in panels.
|
|
4793
|
+
* api.ui.panel({
|
|
4794
|
+
* title: "Unit count",
|
|
4795
|
+
* dock: "right",
|
|
4796
|
+
* mount(body) {
|
|
4797
|
+
* const show = () => { body.textContent = `${api.document.scenario()?.units.length ?? 0} units`; };
|
|
4798
|
+
* show();
|
|
4799
|
+
* const sub = api.events.on("units", show);
|
|
4800
|
+
* return () => sub.dispose();
|
|
4801
|
+
* },
|
|
4802
|
+
* });
|
|
4350
4803
|
*/
|
|
4351
4804
|
export interface PanelSpec {
|
|
4352
4805
|
title: string;
|
|
@@ -4388,6 +4841,14 @@ export interface PanelHandle {
|
|
|
4388
4841
|
* One cell in the status bar (`ui.statusItem`): a line of text with the plugin's icon,
|
|
4389
4842
|
* a spinner while `busy`, and a click. It is how a plugin that works in the background
|
|
4390
4843
|
* stays visible without a panel — "AI · working 12 s", "3 problems", "Synced".
|
|
4844
|
+
*
|
|
4845
|
+
* @example
|
|
4846
|
+
* const count = () => api.query.validate().filter((i) => i.level !== "info").length;
|
|
4847
|
+
* const cell = api.ui.statusItem({ text: `${count()} problems`, onClick: () => api.ui.open("validateMap") });
|
|
4848
|
+
* api.events.on("commit", () => {
|
|
4849
|
+
* const n = count();
|
|
4850
|
+
* cell.set({ text: `${n} problems`, warn: n > 0 });
|
|
4851
|
+
* });
|
|
4391
4852
|
*/
|
|
4392
4853
|
export interface StatusItemSpec {
|
|
4393
4854
|
text: string;
|
|
@@ -4405,7 +4866,17 @@ export interface StatusItemHandle {
|
|
|
4405
4866
|
remove(): void;
|
|
4406
4867
|
isShown(): boolean;
|
|
4407
4868
|
}
|
|
4408
|
-
/**
|
|
4869
|
+
/**
|
|
4870
|
+
* A button on the map itself (`ui.mapButton`), in the row at its bottom-right corner.
|
|
4871
|
+
*
|
|
4872
|
+
* @example
|
|
4873
|
+
* let notes = 3;
|
|
4874
|
+
* const button = api.ui.mapButton({
|
|
4875
|
+
* label: "Notes",
|
|
4876
|
+
* badge: notes,
|
|
4877
|
+
* onClick: () => { notes = 0; button.set({ badge: null }); },
|
|
4878
|
+
* });
|
|
4879
|
+
*/
|
|
4409
4880
|
export interface MapButtonSpec {
|
|
4410
4881
|
/** A word or two; the button is as wide as it. */
|
|
4411
4882
|
label: string;
|
|
@@ -4474,13 +4945,16 @@ export interface DialogSlotSpec {
|
|
|
4474
4945
|
* which is the simpler shape when nothing needs undoing on Cancel.
|
|
4475
4946
|
*
|
|
4476
4947
|
* @example
|
|
4948
|
+
* const settings = api.storage.get("settings", { dock: "float" });
|
|
4949
|
+
* let pending: (() => void) | null = null;
|
|
4477
4950
|
* api.ui.preferencesPage({
|
|
4478
4951
|
* mount(body) {
|
|
4479
4952
|
* const w = api.ui.widgets;
|
|
4480
4953
|
* const dock = w.select([{ value: "float", label: "Floating" }, { value: "right", label: "Docked" }], { value: settings.dock });
|
|
4481
4954
|
* body.append(w.form([{ label: "Panel", field: dock }]), w.hint("Where the panel opens."));
|
|
4482
|
-
* pending = () =>
|
|
4955
|
+
* pending = () => api.storage.set("settings", { dock: dock.value });
|
|
4483
4956
|
* },
|
|
4957
|
+
* // OK and Apply write it; Cancel leaves the stored value alone.
|
|
4484
4958
|
* apply: () => pending?.(),
|
|
4485
4959
|
* });
|
|
4486
4960
|
*/
|
|
@@ -4568,6 +5042,23 @@ export type MapToolStopReason =
|
|
|
4568
5042
|
* release ahead of the active layer's own tools, hides the layer's brush ghost, and
|
|
4569
5043
|
* lets it draw an overlay. The map stays visible and scrollable, and a panel can stay
|
|
4570
5044
|
* open beside it — which is how a plugin gets a drawing mode of its own.
|
|
5045
|
+
*
|
|
5046
|
+
* @example
|
|
5047
|
+
* // Click to drop a marine, with a ring under the pointer. Esc or a right-click ends the tool.
|
|
5048
|
+
* let at: { px: number; py: number } | null = null;
|
|
5049
|
+
* const tool = api.ui.mapTool({
|
|
5050
|
+
* name: "Drop marines",
|
|
5051
|
+
* hint: "click to place, Esc to stop",
|
|
5052
|
+
* onMove(p) { at = p.inMap ? p : null; tool.redraw(); },
|
|
5053
|
+
* onDown(p) { api.document.edit("Drop a marine", (tx) => { tx.placeUnit(0, 0, p.px, p.py); }); },
|
|
5054
|
+
* draw(ctx, view) {
|
|
5055
|
+
* if (!at) return;
|
|
5056
|
+
* ctx.strokeStyle = "#ffd24a";
|
|
5057
|
+
* ctx.beginPath();
|
|
5058
|
+
* ctx.arc(view.x(at.px), view.y(at.py), view.tilePx / 2, 0, Math.PI * 2);
|
|
5059
|
+
* ctx.stroke();
|
|
5060
|
+
* },
|
|
5061
|
+
* });
|
|
4571
5062
|
*/
|
|
4572
5063
|
export interface MapToolSpec {
|
|
4573
5064
|
/** Shown in the viewport's HUD and the status bar while the tool runs. */
|
|
@@ -4608,6 +5099,23 @@ export type OverlayAbove =
|
|
|
4608
5099
|
* as usual. It hears the pointer through `onHover`, which is how a readout follows the
|
|
4609
5100
|
* mouse while the user places units. Register one at activation and keep the handle;
|
|
4610
5101
|
* it goes away with the plugin.
|
|
5102
|
+
*
|
|
5103
|
+
* @example
|
|
5104
|
+
* // A ring twelve tiles wide round every start location, listed under View ▸ Overlays.
|
|
5105
|
+
* const overlay = api.ui.overlay({
|
|
5106
|
+
* name: "Start rings",
|
|
5107
|
+
* above: "objects",
|
|
5108
|
+
* draw(ctx, view) {
|
|
5109
|
+
* ctx.strokeStyle = "#ffd24a";
|
|
5110
|
+
* ctx.lineWidth = 2;
|
|
5111
|
+
* for (const start of api.query.startLocations()) {
|
|
5112
|
+
* ctx.beginPath();
|
|
5113
|
+
* ctx.arc(view.x(start.x), view.y(start.y), 12 * view.tilePx, 0, Math.PI * 2);
|
|
5114
|
+
* ctx.stroke();
|
|
5115
|
+
* }
|
|
5116
|
+
* },
|
|
5117
|
+
* });
|
|
5118
|
+
* api.events.on("units", () => overlay.redraw());
|
|
4611
5119
|
*/
|
|
4612
5120
|
export interface OverlaySpec {
|
|
4613
5121
|
/** Shown in View ▸ Overlays and the Layers panel. Unique per plugin. In English, like a menu label: shown through the plugin's registered catalogues. */
|
|
@@ -4667,7 +5175,12 @@ export interface UiApi {
|
|
|
4667
5175
|
status(text: string): void;
|
|
4668
5176
|
/** The status bar line as it stands. */
|
|
4669
5177
|
statusText(): string;
|
|
4670
|
-
/**
|
|
5178
|
+
/**
|
|
5179
|
+
* A short notice over the map that leaves by itself — how Save reports; `ttl` 0 keeps it until dismissed.
|
|
5180
|
+
*
|
|
5181
|
+
* @example
|
|
5182
|
+
* api.ui.toast({ kind: "ok", title: "Export finished", detail: "12 triggers written." });
|
|
5183
|
+
*/
|
|
4671
5184
|
toast(toast: {
|
|
4672
5185
|
kind?: Toast["kind"];
|
|
4673
5186
|
title: string;
|
|
@@ -4710,10 +5223,11 @@ export interface UiApi {
|
|
|
4710
5223
|
* `dispose()` or the plugin.
|
|
4711
5224
|
*
|
|
4712
5225
|
* @example
|
|
5226
|
+
* // A button in Map Properties that tidies the name in the form. The user still presses OK.
|
|
4713
5227
|
* api.ui.dialogSlot("mapProperties", {
|
|
4714
5228
|
* mount(body, dlg) {
|
|
4715
|
-
* body.append(api.ui.widgets.button("
|
|
4716
|
-
* dlg.fields.name.set(
|
|
5229
|
+
* body.append(api.ui.widgets.button("Tidy name", { onClick: () => {
|
|
5230
|
+
* dlg.fields.name.set(dlg.fields.name.get().trim().replace(/\s+/g, " "));
|
|
4717
5231
|
* } }));
|
|
4718
5232
|
* },
|
|
4719
5233
|
* });
|
|
@@ -4757,6 +5271,11 @@ export interface UiApi {
|
|
|
4757
5271
|
* As `pickTile`, for an object: the user clicks a unit or a location. The viewport
|
|
4758
5272
|
* outlines and names what is under the pointer as it moves; a click on nothing keeps
|
|
4759
5273
|
* picking. Resolves with the kind and the record's index, or null as `pickArea` does.
|
|
5274
|
+
*
|
|
5275
|
+
* @example
|
|
5276
|
+
* const picked = await api.ui.pickObject({ prompt: "Click a unit", kinds: ["unit"] });
|
|
5277
|
+
* const unit = picked && api.document.scenario()?.units[picked.index];
|
|
5278
|
+
* if (unit) await api.ui.alert(`${api.names.unit(unit.unitId)}, owned by ${api.names.player(unit.owner)}`);
|
|
4760
5279
|
*/
|
|
4761
5280
|
pickObject(options?: PickObjectOptions): Promise<PickedObject | null>;
|
|
4762
5281
|
/**
|
|
@@ -4771,9 +5290,29 @@ export interface UiApi {
|
|
|
4771
5290
|
confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
4772
5291
|
/** Say something with a single OK. */
|
|
4773
5292
|
alert(message: string, options?: ConfirmOptions): Promise<void>;
|
|
4774
|
-
/**
|
|
5293
|
+
/**
|
|
5294
|
+
* Ask for a line of text; null when the user cancelled.
|
|
5295
|
+
*
|
|
5296
|
+
* @example
|
|
5297
|
+
* const name = await api.ui.prompt("Name the map", { value: api.document.info()?.name, confirmLabel: "Rename" });
|
|
5298
|
+
* if (name) api.document.update("Rename", (tx) => tx.properties({ name }));
|
|
5299
|
+
*/
|
|
4775
5300
|
prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
4776
|
-
/**
|
|
5301
|
+
/**
|
|
5302
|
+
* A progress bar over the map for long work, with an optional Cancel.
|
|
5303
|
+
*
|
|
5304
|
+
* @example
|
|
5305
|
+
* const triggers = api.triggers.list();
|
|
5306
|
+
* const job = api.ui.progress("Reading triggers", { cancellable: true });
|
|
5307
|
+
* try {
|
|
5308
|
+
* for (let i = 0; i < triggers.length && !job.cancelled(); i++) {
|
|
5309
|
+
* job.report(i / triggers.length, `Trigger ${i + 1} of ${triggers.length}`);
|
|
5310
|
+
* await new Promise((next) => setTimeout(next, 20)); // the slow part of your work goes here
|
|
5311
|
+
* }
|
|
5312
|
+
* } finally {
|
|
5313
|
+
* job.done();
|
|
5314
|
+
* }
|
|
5315
|
+
*/
|
|
4777
5316
|
progress(label: string, options?: ProgressOptions): ProgressHandle;
|
|
4778
5317
|
/** `el("div", { className: "row" }, …)`: the DOM helper the widgets are built from. */
|
|
4779
5318
|
el<K extends keyof HTMLElementTagNameMap>(tag: K, props?: Record<string, unknown>, ...children: WidgetChild[]): HTMLElementTagNameMap[K];
|
|
@@ -5072,6 +5611,18 @@ export interface WidgetsApi {
|
|
|
5072
5611
|
/** Small dimmed explanatory text. */
|
|
5073
5612
|
hint(text: string): HTMLElement;
|
|
5074
5613
|
separator(): HTMLElement;
|
|
5614
|
+
/**
|
|
5615
|
+
* @example
|
|
5616
|
+
* // Player 1's units in a dialog; picking one closes it and goes there.
|
|
5617
|
+
* const units = api.document.scenario()?.units ?? [];
|
|
5618
|
+
* const items = api.query.unitsOf(0).map((index) => ({ label: api.names.unit(units[index].unitId), value: index, index }));
|
|
5619
|
+
* api.ui.dialog({
|
|
5620
|
+
* title: "Player 1's units",
|
|
5621
|
+
* mount(body, dialog) {
|
|
5622
|
+
* body.append(api.ui.widgets.list(items, { height: 320, onPick: (index) => { dialog.close(); api.view.goTo({ kind: "unit", index }); } }));
|
|
5623
|
+
* },
|
|
5624
|
+
* });
|
|
5625
|
+
*/
|
|
5075
5626
|
list<T>(items: ListItem<T>[], options?: ListOptions<T>): HTMLElement;
|
|
5076
5627
|
/** A turning ring: on its own to put beside your own text, or with a `label` beside it. */
|
|
5077
5628
|
spinner(options?: SpinnerOptions): HTMLElement;
|
|
@@ -5141,18 +5692,21 @@ export interface MenuItemSpec {
|
|
|
5141
5692
|
}
|
|
5142
5693
|
export interface MenuApi {
|
|
5143
5694
|
/**
|
|
5144
|
-
*
|
|
5695
|
+
* Add an item to the editor's menu bar.
|
|
5145
5696
|
*
|
|
5146
5697
|
* @example
|
|
5147
5698
|
* api.menu.add("Tools", {
|
|
5148
|
-
* label: "Count units
|
|
5699
|
+
* label: "Count units…",
|
|
5149
5700
|
* icon: "plugin",
|
|
5701
|
+
* enabled: () => api.document.isOpen(),
|
|
5150
5702
|
* run: () => api.ui.alert(`Player 1 has ${api.query.unitsOf(0).length} units.`),
|
|
5151
5703
|
* });
|
|
5152
5704
|
*
|
|
5153
5705
|
* @example
|
|
5154
5706
|
* // A path whose last segment names no submenu makes one, at the end of that menu.
|
|
5155
|
-
* api.menu.add("Tools/My plugin", { label: "
|
|
5707
|
+
* api.menu.add("Tools/My plugin", { label: "About…", run: () => api.ui.alert(`${api.plugin.name} ${api.plugin.version ?? ""}`) });
|
|
5708
|
+
* // `after` puts an item directly under a built-in one, named by its English label.
|
|
5709
|
+
* api.menu.add("File", { label: "Open from My Site…", after: "Open Recent", run: () => api.ui.status("…") });
|
|
5156
5710
|
*/
|
|
5157
5711
|
add(path: MenuPath, item: MenuItemSpec): Disposable;
|
|
5158
5712
|
}
|
|
@@ -5188,12 +5742,23 @@ export interface ContextItemSpec {
|
|
|
5188
5742
|
command?: string;
|
|
5189
5743
|
}
|
|
5190
5744
|
export interface ContextMenuApi {
|
|
5745
|
+
/**
|
|
5746
|
+
* @example
|
|
5747
|
+
* // Right-click the map: copy the tile's position.
|
|
5748
|
+
* api.contextMenu.add("viewport", {
|
|
5749
|
+
* label: (ctx) => (ctx.tile ? `Copy position (${ctx.tile.x}, ${ctx.tile.y})` : "Copy position"),
|
|
5750
|
+
* enabled: (ctx) => ctx.tile !== null,
|
|
5751
|
+
* run: (ctx) => { if (ctx.tile) void navigator.clipboard.writeText(`${ctx.tile.x}, ${ctx.tile.y}`); },
|
|
5752
|
+
* });
|
|
5753
|
+
*/
|
|
5191
5754
|
add(surface: ContextSurface, item: ContextItemSpec): Disposable;
|
|
5192
5755
|
}
|
|
5193
5756
|
export interface HotkeyApi {
|
|
5194
|
-
/**
|
|
5757
|
+
/**
|
|
5758
|
+
* `"Ctrl+Shift+I"`, `"Alt+F9"`, `"F8"` — modifiers in any order, then a key name.
|
|
5759
|
+
*
|
|
5195
5760
|
* @example
|
|
5196
|
-
* api.hotkeys.add("Ctrl+
|
|
5761
|
+
* api.hotkeys.add("Ctrl+Alt+G", () => api.view.setFlags({ grid: !api.view.flags().grid }));
|
|
5197
5762
|
*/
|
|
5198
5763
|
add(combo: string, run: (() => void) | {
|
|
5199
5764
|
command: string;
|
|
@@ -5241,9 +5806,11 @@ export type PluginEvent =
|
|
|
5241
5806
|
* closing and switching maps are not commits — the `"document"` event says those.
|
|
5242
5807
|
*
|
|
5243
5808
|
* @example
|
|
5809
|
+
* // Flash where each change landed, and log what kind of change it was.
|
|
5244
5810
|
* api.events.on("commit", (e) => {
|
|
5245
|
-
* if (e.
|
|
5246
|
-
*
|
|
5811
|
+
* if (e.area) api.view.flash({ rect: e.area, kind: "attention" });
|
|
5812
|
+
* const parts = Object.entries(e.parts).filter(([, changed]) => changed).map(([part]) => part);
|
|
5813
|
+
* api.log(e.reason, e.label, parts);
|
|
5247
5814
|
* });
|
|
5248
5815
|
*/
|
|
5249
5816
|
export interface CommitEvent {
|
|
@@ -5365,9 +5932,15 @@ export interface DocumentEvent {
|
|
|
5365
5932
|
* `{x, select, …}` and, for Korean, `{name|을}` (the particle agrees with the value).
|
|
5366
5933
|
*
|
|
5367
5934
|
* @example
|
|
5368
|
-
* api.i18n.register({ ko: { "Count units
|
|
5369
|
-
*
|
|
5370
|
-
* api.
|
|
5935
|
+
* api.i18n.register({ ko: { "Count units…": "유닛 세기…", "{n} units": "유닛 {n}개" } });
|
|
5936
|
+
* // A menu label stays English: the menu shows it through the catalogue above.
|
|
5937
|
+
* api.menu.add("Tools", {
|
|
5938
|
+
* label: "Count units…",
|
|
5939
|
+
* run: () => api.ui.alert(api.i18n.t("{n} units", { n: api.document.scenario()?.units.length ?? 0 })),
|
|
5940
|
+
* });
|
|
5941
|
+
* // Anything you drew yourself is yours to redraw when the language changes.
|
|
5942
|
+
* const cell = api.ui.statusItem({ text: api.i18n.t("{n} units", { n: 0 }) });
|
|
5943
|
+
* api.events.on("language", () => cell.set({ text: api.i18n.t("{n} units", { n: 0 }) }));
|
|
5371
5944
|
*/
|
|
5372
5945
|
export interface I18nApi {
|
|
5373
5946
|
/** The editor's language, a BCP 47 primary tag: `"en"`, `"ko"`. */
|
|
@@ -5388,12 +5961,12 @@ export interface I18nApi {
|
|
|
5388
5961
|
export interface EventsApi {
|
|
5389
5962
|
/**
|
|
5390
5963
|
* @example
|
|
5391
|
-
* // Notifications, in activation order; a listener never intercepts what it hears.
|
|
5392
5964
|
* api.events.on("document", (e) => {
|
|
5393
|
-
* if (e.reason === "open")
|
|
5965
|
+
* if (e.reason === "open") api.ui.status(`Opened ${e.fileName ?? "a map"}`);
|
|
5394
5966
|
* });
|
|
5395
|
-
* api.events.on("
|
|
5396
|
-
*
|
|
5967
|
+
* const selection = api.events.on("selection", () => api.log(`${api.selection.units().length} units selected`));
|
|
5968
|
+
* // Stop listening early; otherwise the listener goes when the plugin is turned off.
|
|
5969
|
+
* setTimeout(() => selection.dispose(), 60_000);
|
|
5397
5970
|
*/
|
|
5398
5971
|
on(event: "document", listener: (event: DocumentEvent) => void): Disposable;
|
|
5399
5972
|
on(event: "commit", listener: (event: CommitEvent) => void): Disposable;
|
|
@@ -5401,12 +5974,12 @@ export interface EventsApi {
|
|
|
5401
5974
|
}
|
|
5402
5975
|
export interface StorageApi {
|
|
5403
5976
|
/**
|
|
5404
|
-
*
|
|
5405
|
-
* storage and listed in Preferences \u25b8 Browser storage.
|
|
5977
|
+
* Read a value, or `fallback` when the key was never set.
|
|
5406
5978
|
*
|
|
5407
5979
|
* @example
|
|
5408
|
-
* const
|
|
5409
|
-
* api.storage.set("options", { ...
|
|
5980
|
+
* const options = api.storage.get("options", { showGrid: true });
|
|
5981
|
+
* const kept = api.storage.set("options", { ...options, showGrid: false });
|
|
5982
|
+
* if (!kept) api.ui.toast({ kind: "error", title: "Could not save the options", detail: "The browser's storage is full." });
|
|
5410
5983
|
*/
|
|
5411
5984
|
get<T>(key: string, fallback: T): T;
|
|
5412
5985
|
/**
|