@scm-js/plugin-api 1.29.0 → 1.31.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 (2) hide show
  1. package/index.d.ts +291 -28
  2. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -1694,7 +1694,6 @@ export interface ChangeTilesetResult {
1694
1694
  spritesDropped: number;
1695
1695
  refilled: boolean;
1696
1696
  }
1697
- export type SymmetryMode = "none" | "h" | "v" | "hv" | "rot180" | "rot90" | "diag" | "adiag";
1698
1697
  export interface DoodadChange {
1699
1698
  index: number;
1700
1699
  before: DoodadRecord | null;
@@ -1715,32 +1714,6 @@ export interface DoodadVerdict {
1715
1714
  /** Cell indices (row-major) whose ground fails the check, for the ghost to mark red. */
1716
1715
  bad: number[];
1717
1716
  }
1718
- export type StartLayout = "ring" | "corners";
1719
- export interface StartPlacementResult {
1720
- changes: UnitChange[];
1721
- /** Per player (0-based), where the start location landed, or null when nothing within reach fit. */
1722
- placed: ({
1723
- x: number;
1724
- y: number;
1725
- } | null)[];
1726
- removed: number;
1727
- }
1728
- export type Side = "left" | "top" | "right" | "bottom";
1729
- export interface BlendCandidate {
1730
- /** MTXM tile id to place. */
1731
- id: number;
1732
- megatile: number;
1733
- /** `edgeDistance` between the anchor's side and this tile's opposite side. */
1734
- distance: number;
1735
- }
1736
- export interface BlendOptions {
1737
- /** Largest distance still listed. */
1738
- maxDistance: number;
1739
- /** Most candidates listed per side. */
1740
- limit: number;
1741
- /** Optional filter over tile ids (the palette's group-kind dropdown). */
1742
- include?: (id: number) => boolean;
1743
- }
1744
1717
  /** The change lists of one edit; `HistoryEntry` adds the label. */
1745
1718
  export interface HistoryEdit {
1746
1719
  changes: TileChange[];
@@ -1780,6 +1753,128 @@ export interface HistoryEdit {
1780
1753
  */
1781
1754
  createdMask?: Uint8Array;
1782
1755
  }
1756
+ export interface WireListChange<T> {
1757
+ index: number;
1758
+ before: T | null;
1759
+ after: T | null;
1760
+ }
1761
+ export interface WireLocationChange {
1762
+ index: number;
1763
+ after: LocationRecord;
1764
+ /** The name as text (`after.nameIndex` is resolved again where it lands); null for none. */
1765
+ name: string | null;
1766
+ /** The slot was empty before: a new location, which moves to a free slot if someone else took this one. */
1767
+ created: boolean;
1768
+ }
1769
+ /**
1770
+ * One undo step's worth of terrain and objects. The grids are flat number lists: terrain
1771
+ * `[at, mtxm, tile, …]`, the others `[at, value, …]`. `reverse` marks the undo of an edit:
1772
+ * its parts go in the reverse of the usual order, as undo walks them.
1773
+ */
1774
+ export interface SyncEditOp {
1775
+ kind: "edit";
1776
+ label: string;
1777
+ /** The map size the cells index; a grid part is skipped on a map of another size. */
1778
+ width: number;
1779
+ height: number;
1780
+ reverse?: boolean;
1781
+ terrain?: number[];
1782
+ isom?: number[];
1783
+ doodadTiles?: number[];
1784
+ fog?: number[];
1785
+ /** The whole ISOM lattice (base64 of the u16 cells) the edit gave the map, or null for taking it away. */
1786
+ isomWhole?: string | null;
1787
+ maskWhole?: string | null;
1788
+ units?: WireListChange<UnitRecord>[];
1789
+ doodads?: WireListChange<DoodadRecord>[];
1790
+ sprites?: WireListChange<SpriteRecord>[];
1791
+ locations?: WireLocationChange[];
1792
+ }
1793
+ declare const SYNC_FIELDS: readonly [
1794
+ "type",
1795
+ "fileVersion",
1796
+ "nameIndex",
1797
+ "descriptionIndex",
1798
+ "playerTypes",
1799
+ "editorPlayerTypes",
1800
+ "playerRaces",
1801
+ "playerColors",
1802
+ "playerRgb",
1803
+ "forces",
1804
+ "unitSettings",
1805
+ "unitAvailability",
1806
+ "upgradeSettings",
1807
+ "upgradeRestrictions",
1808
+ "techSettings",
1809
+ "techRestrictions",
1810
+ "wavs",
1811
+ "cuwp",
1812
+ "cuwpUsed",
1813
+ "triggers",
1814
+ "briefing",
1815
+ "switchNames"
1816
+ ];
1817
+ export type SyncField = (typeof SYNC_FIELDS)[number];
1818
+ export interface SyncFieldsOp {
1819
+ kind: "fields";
1820
+ /** Each changed field's new value, packed (`pack`). */
1821
+ set: Partial<Record<SyncField, unknown>>;
1822
+ /** String slots that changed: `[index, before, after]`. */
1823
+ strings?: [
1824
+ number,
1825
+ string | null,
1826
+ string | null
1827
+ ][];
1828
+ /** The table's length afterwards (trailing blank slots are dropped). */
1829
+ stringsLength?: number;
1830
+ /** The table's width and text encoding, when they changed. */
1831
+ stringsFormat?: {
1832
+ extended: boolean;
1833
+ encoding: TextEncoding;
1834
+ };
1835
+ }
1836
+ export interface SyncExtrasOp {
1837
+ kind: "extras";
1838
+ /** `[member name, base64 bytes]`, or null bytes for a member taken out. */
1839
+ set: [
1840
+ string,
1841
+ string | null
1842
+ ][];
1843
+ }
1844
+ export interface SyncResetOp {
1845
+ kind: "reset";
1846
+ label: string;
1847
+ /** The whole scenario, base64. */
1848
+ chk: string;
1849
+ }
1850
+ export type SyncOp = SyncEditOp | SyncFieldsOp | SyncExtrasOp | SyncResetOp;
1851
+ export type SymmetryMode = "none" | "h" | "v" | "hv" | "rot180" | "rot90" | "diag" | "adiag";
1852
+ export type StartLayout = "ring" | "corners";
1853
+ export interface StartPlacementResult {
1854
+ changes: UnitChange[];
1855
+ /** Per player (0-based), where the start location landed, or null when nothing within reach fit. */
1856
+ placed: ({
1857
+ x: number;
1858
+ y: number;
1859
+ } | null)[];
1860
+ removed: number;
1861
+ }
1862
+ export type Side = "left" | "top" | "right" | "bottom";
1863
+ export interface BlendCandidate {
1864
+ /** MTXM tile id to place. */
1865
+ id: number;
1866
+ megatile: number;
1867
+ /** `edgeDistance` between the anchor's side and this tile's opposite side. */
1868
+ distance: number;
1869
+ }
1870
+ export interface BlendOptions {
1871
+ /** Largest distance still listed. */
1872
+ maxDistance: number;
1873
+ /** Most candidates listed per side. */
1874
+ limit: number;
1875
+ /** Optional filter over tile ids (the palette's group-kind dropdown). */
1876
+ include?: (id: number) => boolean;
1877
+ }
1783
1878
  export type ClipPart = "terrain" | "doodads" | "units" | "sprites" | "locations" | "fog";
1784
1879
  export type ClipParts = Record<ClipPart, boolean>;
1785
1880
  /** Whether a paste adds to what is in the target area or clears its objects first. */
@@ -1855,11 +1950,55 @@ export interface Preferences {
1855
1950
  * is replaced by the first map opened rather than kept beside it.
1856
1951
  */
1857
1952
  multipleMaps: boolean;
1858
- /** What File ▸ New and the startup map start with. */
1953
+ /**
1954
+ * What File ▸ New and the startup map start with. `version` is the file's revision:
1955
+ * Brood War (VER 205, what every build reads) or Remastered (206, 32-bit strings).
1956
+ */
1859
1957
  newMap: {
1860
1958
  tileset: TilesetId;
1861
1959
  width: number;
1862
1960
  height: number;
1961
+ version: NewMapVersion;
1962
+ };
1963
+ /**
1964
+ * Startup: `reopenLast` opens the most recent file again in place of the blank map
1965
+ * (the desktop app straight away; a browser needs a click first, so it asks in a
1966
+ * notice), and `recents` is how many File ▸ Open Recent keeps.
1967
+ */
1968
+ startup: {
1969
+ reopenLast: boolean;
1970
+ recents: number;
1971
+ };
1972
+ /**
1973
+ * What the Save dialog starts from: the options the file was opened with
1974
+ * (`"asOpened"`, the default), or one of its presets. `compression` is for a map with
1975
+ * no origin — new, or opened from a bare .chk — where there is nothing to follow;
1976
+ * `"asOpened"` there means StarEdit's PKWARE.
1977
+ */
1978
+ save: {
1979
+ start: "asOpened" | "everything" | "smallest";
1980
+ compression: "asOpened" | ArchiveCompression;
1981
+ };
1982
+ /** How many edits Undo keeps per map (SCMDraft keeps 200). */
1983
+ undoLevels: number;
1984
+ /**
1985
+ * The mouse wheel over the map: scrolling, with Ctrl+wheel zooming (the default), or
1986
+ * zooming, with Shift+wheel scrolling sideways. `zoomToCursor` keeps the tile under the
1987
+ * pointer in place when the wheel zooms; the menu and keyboard keep the centre.
1988
+ */
1989
+ view: {
1990
+ wheel: "scroll" | "zoom";
1991
+ zoomToCursor: boolean;
1992
+ };
1993
+ /**
1994
+ * What the palettes start on: the owner of placed units and sprites (0 = player 1),
1995
+ * the brush size (1–7), and the size of a location the Locations palette's New makes,
1996
+ * in tiles.
1997
+ */
1998
+ placement: {
1999
+ owner: number;
2000
+ brushSize: number;
2001
+ locationTiles: number;
1863
2002
  };
1864
2003
  /** Initial View ▸ Animate Water / Animate Units. */
1865
2004
  animateWater: boolean;
@@ -1912,6 +2051,8 @@ export interface Preferences {
1912
2051
  }
1913
2052
  /** See `Preferences.plugins.updates`. */
1914
2053
  export type PluginUpdateMode = "notify" | "manual" | "auto";
2054
+ /** The revisions a new map can start on; the two older ones are for opening old files, not making new ones. */
2055
+ export type NewMapVersion = Extract<MapVersion, "broodwar" | "remastered">;
1915
2056
  /**
1916
2057
  * The version a host provides; a manifest that asks for a newer one is refused. It stays
1917
2058
  * at 1 while the API is only used by the plugins in the scm-js organisation and grows
@@ -2075,6 +2216,13 @@ export interface PluginApi {
2075
2216
  /** The plugin's words in the editor's language: its own catalogues, and a `t` over them. */
2076
2217
  readonly i18n: I18nApi;
2077
2218
  readonly events: EventsApi;
2219
+ /**
2220
+ * Editing the map in front together with other editors: every change to it is handed out
2221
+ * as an op for the plugin to carry to them, and theirs come back in, in the order a
2222
+ * server puts them. The scmjs.dev plugin's shared maps are built on it; the transport,
2223
+ * the server and who may join are the plugin's.
2224
+ */
2225
+ readonly sync: SyncApi;
2078
2226
  /**
2079
2227
  * A small key-value store of the plugin's own, kept in the browser's local storage under
2080
2228
  * the plugin's id and listed with everything else in Preferences ▸ Browser storage.
@@ -4077,6 +4225,40 @@ export interface DialogSlotHost {
4077
4225
  export interface DialogSlotSpec {
4078
4226
  mount(body: HTMLElement, host: DialogSlotHost): void | (() => void);
4079
4227
  }
4228
+ /**
4229
+ * A page of the plugin's own in Edit ▸ Preferences, listed under Plugins by the plugin's
4230
+ * name — where a user looks for a setting, instead of a menu item per plugin. One page per
4231
+ * plugin; registering again replaces it. `mount` runs the first time the page is shown
4232
+ * while the dialog is open and the page then stays mounted until the dialog closes, so
4233
+ * what the user changed is still there when OK or Apply calls `apply`; the cleanup runs
4234
+ * on close. A page with no `apply` writes its settings as they change (`api.storage`),
4235
+ * which is the simpler shape when nothing needs undoing on Cancel.
4236
+ *
4237
+ * @example
4238
+ * api.ui.preferencesPage({
4239
+ * mount(body) {
4240
+ * const w = api.ui.widgets;
4241
+ * const dock = w.select([{ value: "float", label: "Floating" }, { value: "right", label: "Docked" }], { value: settings.dock });
4242
+ * body.append(w.form([{ label: "Panel", field: dock }]), w.hint("Where the panel opens."));
4243
+ * pending = () => save({ dock: dock.value });
4244
+ * },
4245
+ * apply: () => pending?.(),
4246
+ * });
4247
+ */
4248
+ export interface PreferencesPageSpec {
4249
+ /** Fill `body` with the page's controls; return a cleanup if you need one. */
4250
+ mount(body: HTMLElement, page: PreferencesPageHost): void | (() => void);
4251
+ /** Called on OK and Apply while the dialog is open and the page has been shown. */
4252
+ apply?(): void;
4253
+ /** Called by Reset to defaults while the page is the one showing. */
4254
+ reset?(): void;
4255
+ }
4256
+ /** What a preferences page's `mount` is handed. */
4257
+ export interface PreferencesPageHost {
4258
+ readonly plugin: PluginInfo;
4259
+ /** Close the Preferences dialog. */
4260
+ close(): void;
4261
+ }
4080
4262
  /**
4081
4263
  * What `view.flash` highlights: a tile rect, or units / locations by index. The
4082
4264
  * highlight fades over `ms` (600 by default) and never blocks or takes the pointer; it is
@@ -4237,6 +4419,11 @@ export interface PickFilesOptions {
4237
4419
  multiple?: boolean;
4238
4420
  }
4239
4421
  export interface UiApi {
4422
+ /**
4423
+ * The dialogs open now, bottom to top, by id: the built-in ones by their `DialogId`, a
4424
+ * plugin's as `"pluginDialog"`. What a shared map shows others ("in Player Settings").
4425
+ */
4426
+ openDialogs(): string[];
4240
4427
  /** Set the status bar line. */
4241
4428
  status(text: string): void;
4242
4429
  /** The status bar line as it stands. */
@@ -4286,6 +4473,12 @@ export interface UiApi {
4286
4473
  * });
4287
4474
  */
4288
4475
  dialogSlot(dialog: DialogSlotId, spec: DialogSlotSpec): Disposable;
4476
+ /**
4477
+ * A page of the plugin's own in Edit ▸ Preferences, under Plugins — see
4478
+ * `PreferencesPageSpec`. `ui.open("preferences", { page: "plugin:<id>" })` opens it.
4479
+ * Returns the registration; it leaves with `dispose()` or the plugin.
4480
+ */
4481
+ preferencesPage(spec: PreferencesPageSpec): Disposable;
4289
4482
  /**
4290
4483
  * Take over the pointer on the map until `stop()`, Esc, a right-click, a map change
4291
4484
  * or another tool. One tool runs at a time — starting one stops the previous — and a
@@ -4778,8 +4971,78 @@ export type PluginEvent =
4778
4971
  | "commands"
4779
4972
  /** A plugin provided or withdrew a service — `services.watch` is the usual way to hear this for one name. */
4780
4973
  | "services"
4974
+ /** A dialog opened or closed — `ui.openDialogs()` says which are open. */
4975
+ | "dialogs"
4781
4976
  /** The game data source changed: installed, switched to another data set, or a copy removed. `gameData.source()` says what it is now. */
4782
4977
  | "gameData";
4978
+ /** Why other people's changes are waiting: a stroke under way on the map, a dialog that edits the map open, another map in front. */
4979
+ export type SyncHold = "stroke" | "dialog" | "behind";
4980
+ export interface SyncReport {
4981
+ /** Other people's ops applied. */
4982
+ ops: number;
4983
+ /** Parts of them that could not be applied — the unit they moved was deleted, the map was resized… */
4984
+ dropped: number;
4985
+ /** Parts of this editor's unconfirmed changes that no longer applied on top of theirs. */
4986
+ lost: number;
4987
+ }
4988
+ export interface SyncStartOptions {
4989
+ /**
4990
+ * Every change made to the shared map, as a plain object JSON can carry, in the order
4991
+ * made: commits, undo and redo, the dialogs' writes, archive files, and a resize or a
4992
+ * tileset change (the whole scenario). Each must reach the server, and every other
4993
+ * editor, in this order.
4994
+ */
4995
+ send(op: SyncOp): void;
4996
+ /** The session ended: the shared map was closed or replaced by another, or `stop` was called. */
4997
+ onEnd?(reason: "closed" | "stopped"): void;
4998
+ /** Other people's changes were applied to the map. */
4999
+ onApplied?(report: SyncReport): void;
5000
+ }
5001
+ /**
5002
+ * One shared map. The server confirms each op this editor sent (`confirm`, in order) and
5003
+ * relays everyone else's (`receive`, in the order it gave them); the session applies
5004
+ * them to the map when nothing is in the way (`holding`) and rebases this editor's
5005
+ * unconfirmed changes on top, so every editor that has seen the same ops has the same map.
5006
+ *
5007
+ * @example
5008
+ * const session = api.sync.start({ send: (op) => socket.send(JSON.stringify({ type: "op", op })) });
5009
+ * socket.onmessage = (m) => {
5010
+ * const msg = JSON.parse(m.data);
5011
+ * if (msg.type === "ack") session?.confirm();
5012
+ * if (msg.type === "op") session?.receive(msg.op);
5013
+ * };
5014
+ */
5015
+ export interface SyncSession {
5016
+ /** The shared map's `document.id()`. */
5017
+ readonly documentId: number;
5018
+ /** Another editor's op, in the server's order. False, and nothing done, for a value that is not an op. */
5019
+ receive(op: unknown): boolean;
5020
+ /** The server confirmed the oldest op this editor sent. */
5021
+ confirm(): void;
5022
+ /** Ops sent and not yet confirmed. */
5023
+ pending(): number;
5024
+ /** Ops and confirmations received and not yet applied. */
5025
+ waiting(): number;
5026
+ /**
5027
+ * The shared map as a file (`.scx` / `.scm`, not built), copied at this moment — for a
5028
+ * server to hand people joining, labelled with the last op this editor applied. Null
5029
+ * while anything is pending or waiting (the copy would not match any point in the
5030
+ * server's order) or while another map is in front. The copy is taken before the first
5031
+ * `await`, so a change made while the file is written is not in it.
5032
+ */
5033
+ snapshot(): Promise<Uint8Array | null>;
5034
+ holding(): SyncHold | null;
5035
+ stop(): void;
5036
+ }
5037
+ export interface SyncApi {
5038
+ /**
5039
+ * Share the map in front. Null when no map is open or another session is running (one
5040
+ * at a time). A session a plugin started stops when the plugin is turned off.
5041
+ */
5042
+ start(options: SyncStartOptions): SyncSession | null;
5043
+ /** The running session, whichever plugin started it. */
5044
+ current(): SyncSession | null;
5045
+ }
4783
5046
  /** Why the document changed. */
4784
5047
  export type DocumentChangeReason =
4785
5048
  /** File ▸ Open, drag-and-drop, `document.open`: a file the user chose. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scm-js/plugin-api",
3
- "version": "1.29.0",
3
+ "version": "1.31.0",
4
4
  "description": "Type declarations for the scmJS plugin API",
5
5
  "license": "MIT",
6
6
  "types": "index.d.ts",