@sculptr/sdk 0.0.0-stage → 2.17.1
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/README.md +23 -2
- package/dist/content/index.d.ts +54 -0
- package/dist/decompiled-gml.d.ts +117 -0
- package/dist/host/index.d.ts +160 -0
- package/dist/image/index.d.ts +16 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +663 -0
- package/dist/keyboard/index.d.ts +42 -0
- package/dist/resources/object.d.ts +109 -0
- package/dist/resources/script.d.ts +184 -0
- package/dist/resources/sprite.d.ts +142 -0
- package/dist/ui/index.d.ts +232 -0
- package/dist/utils.d.ts +4 -0
- package/package.json +47 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Engine, EventName, HostApi, Identity } from "../host";
|
|
2
|
+
/**
|
|
3
|
+
* The added GML variables.
|
|
4
|
+
*/
|
|
5
|
+
export type ObjectVars<Name extends string> = Record<Name, string>;
|
|
6
|
+
/**
|
|
7
|
+
* What is given to help create a GML block.
|
|
8
|
+
*/
|
|
9
|
+
export interface ObjectContext<Name extends string> {
|
|
10
|
+
/**
|
|
11
|
+
* Useful if your code needs to differ by GM version
|
|
12
|
+
*/
|
|
13
|
+
engine: Engine;
|
|
14
|
+
/**
|
|
15
|
+
* List of modded vars for this GML
|
|
16
|
+
*/
|
|
17
|
+
v: ObjectVars<Name>;
|
|
18
|
+
/**
|
|
19
|
+
* Sends an event to JavaScript:
|
|
20
|
+
* ```ts
|
|
21
|
+
* mod.resource.object.create({
|
|
22
|
+
* name: "myObject",
|
|
23
|
+
* events: {
|
|
24
|
+
* create: ({ send }) => send("ping", `"Hello from GML"`),
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
send(event: string, expression?: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Calls a script you previously defined:
|
|
32
|
+
* ```ts
|
|
33
|
+
* mod.resource.script.create({
|
|
34
|
+
* name: "myScript",
|
|
35
|
+
* // ...
|
|
36
|
+
* });
|
|
37
|
+
* mod.resource.object.create({
|
|
38
|
+
* name: "myObject",
|
|
39
|
+
* events: {
|
|
40
|
+
* create: ({ call }) => `${call("myScript", "1")};`,
|
|
41
|
+
* },
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
call(script: string, ...args: string[]): string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Same as @see ObjectContext but with the extra receive payload
|
|
49
|
+
*/
|
|
50
|
+
export interface ObjectReceiveContext<Name extends string> extends ObjectContext<Name> {
|
|
51
|
+
/**
|
|
52
|
+
* The GML variable holding this message's payload.
|
|
53
|
+
*/
|
|
54
|
+
payload: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* GML factory
|
|
58
|
+
*/
|
|
59
|
+
export type ObjectGml<Name extends string> = (context: ObjectContext<Name>) => string;
|
|
60
|
+
/**
|
|
61
|
+
* GML factory for a receive event
|
|
62
|
+
*/
|
|
63
|
+
export type ObjectReceiveGml<Name extends string> = (context: ObjectReceiveContext<Name>) => string;
|
|
64
|
+
export interface ObjectSpec<Name extends string> {
|
|
65
|
+
/**
|
|
66
|
+
* The object name can be simple as it will be mangled.
|
|
67
|
+
*/
|
|
68
|
+
name: string;
|
|
69
|
+
vars?: readonly Name[];
|
|
70
|
+
events?: Partial<Record<EventName, ObjectGml<Name>>>;
|
|
71
|
+
/**
|
|
72
|
+
* GML to run when a given event is received:
|
|
73
|
+
* ```ts
|
|
74
|
+
* mod.resource.object.create({
|
|
75
|
+
* name: "myObject",
|
|
76
|
+
* receive: {
|
|
77
|
+
* pong: ({ payload }) => `show_message(${payload});`,
|
|
78
|
+
* },
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
receive?: Record<string, ObjectReceiveGml<Name>>;
|
|
83
|
+
depth?: number;
|
|
84
|
+
persistent?: boolean;
|
|
85
|
+
visible?: boolean;
|
|
86
|
+
}
|
|
87
|
+
export interface ObjectActions {
|
|
88
|
+
/**
|
|
89
|
+
* Creates a new object.
|
|
90
|
+
*/
|
|
91
|
+
create<const Name extends string = never>(spec: ObjectSpec<Name>): void;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Installs the mod's objects, plus the inbox object that sorts incoming events into queues.
|
|
95
|
+
*/
|
|
96
|
+
export declare const registerObjects: (data: {
|
|
97
|
+
identity: Identity;
|
|
98
|
+
hostApi: HostApi;
|
|
99
|
+
prefix: string;
|
|
100
|
+
objects: ObjectSpec<string>[];
|
|
101
|
+
/** GML the inbox runs for each of the library's own events. */
|
|
102
|
+
internalReceive: Record<string, string>;
|
|
103
|
+
/** GML appended to the inbox's Create. */
|
|
104
|
+
internalCreate: string;
|
|
105
|
+
normalizeEventName: (event: string) => string;
|
|
106
|
+
context: (spec: ObjectSpec<string>) => ObjectContext<string>;
|
|
107
|
+
queue: (event: string) => string;
|
|
108
|
+
scratch: (name: string) => string;
|
|
109
|
+
}) => void;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { type GameContent } from "../content";
|
|
2
|
+
import type { At, Stmt } from "../decompiled-gml";
|
|
3
|
+
import type { Engine, HostApi, Identity } from "../host";
|
|
4
|
+
/**
|
|
5
|
+
* What is given to help create the GML block.
|
|
6
|
+
*/
|
|
7
|
+
export interface ScriptContext<Arg extends string> {
|
|
8
|
+
engine: Engine;
|
|
9
|
+
/** This script's arguments, by the names it declared: `a.value` is `argument0`. */
|
|
10
|
+
a: Record<Arg, string>;
|
|
11
|
+
/**
|
|
12
|
+
* Sends an event to JavaScript:
|
|
13
|
+
* ```ts
|
|
14
|
+
* mod.resource.script.create({
|
|
15
|
+
* name: "myScript",
|
|
16
|
+
* body: ({ send }) => send("ping", `"Hello from GML"`),
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
send(event: string, expression?: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Calls a script you previously defined:
|
|
23
|
+
* ```ts
|
|
24
|
+
* mod.resource.script.create({
|
|
25
|
+
* name: "myScript",
|
|
26
|
+
* body: ({ call }) => `return ${call("myOtherScript", "1")};`,
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
call(script: string, ...args: string[]): string;
|
|
31
|
+
}
|
|
32
|
+
export interface ScriptSpec<Arg extends string> {
|
|
33
|
+
/**
|
|
34
|
+
* The script name can be simple as it will be mangled.
|
|
35
|
+
*/
|
|
36
|
+
name: string;
|
|
37
|
+
/**
|
|
38
|
+
* Script argument names:
|
|
39
|
+
* ```ts
|
|
40
|
+
* mod.resource.script.create({
|
|
41
|
+
* name: "sum",
|
|
42
|
+
* args: ["x", "y"],
|
|
43
|
+
* body: ({ a }) => `return ${a.x} + ${a.y};`,
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
args?: readonly Arg[];
|
|
48
|
+
body: (context: ScriptContext<Arg>) => string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The script a hook replaced.
|
|
52
|
+
*/
|
|
53
|
+
export interface OriginalScript {
|
|
54
|
+
/**
|
|
55
|
+
* Calls the original. With no arguments it forwards the ones this call received.
|
|
56
|
+
*/
|
|
57
|
+
call(...args: string[]): string;
|
|
58
|
+
/**
|
|
59
|
+
* Argument `index` of this call, as GML.
|
|
60
|
+
*/
|
|
61
|
+
arg(index: number): string;
|
|
62
|
+
/**
|
|
63
|
+
* How many arguments this call received, as GML.
|
|
64
|
+
*/
|
|
65
|
+
readonly argumentCount: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Same as @see ScriptContext but with the original instead of the arguments.
|
|
69
|
+
*/
|
|
70
|
+
export interface HookContext {
|
|
71
|
+
engine: Engine;
|
|
72
|
+
original: OriginalScript;
|
|
73
|
+
send(event: string, expression?: string): string;
|
|
74
|
+
call(script: string, ...args: string[]): string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* GML factory for a hook
|
|
78
|
+
*/
|
|
79
|
+
export type HookWrap = (context: HookContext) => string;
|
|
80
|
+
/**
|
|
81
|
+
* Same as @see HookContext but with no original: an insertion runs beside the game's code, not
|
|
82
|
+
* instead of it.
|
|
83
|
+
*/
|
|
84
|
+
export interface InsertContext {
|
|
85
|
+
engine: Engine;
|
|
86
|
+
send(event: string, expression?: string): string;
|
|
87
|
+
call(script: string, ...args: string[]): string;
|
|
88
|
+
}
|
|
89
|
+
export interface InsertSpec {
|
|
90
|
+
/**
|
|
91
|
+
* The statement to run around, from `mod.content`. One the engine could not place is refused.
|
|
92
|
+
*/
|
|
93
|
+
at: Stmt;
|
|
94
|
+
/**
|
|
95
|
+
* `"before"` it starts, or `"after"` it finishes -- once, on every way out of it. For a loop that
|
|
96
|
+
* is once when the loop ends; point at the last statement of its body to run per iteration.
|
|
97
|
+
*
|
|
98
|
+
* `"instead"` replaces it: the body runs and the statement does not.
|
|
99
|
+
*/
|
|
100
|
+
place: "before" | "after" | "instead";
|
|
101
|
+
/**
|
|
102
|
+
* The GML to run there.
|
|
103
|
+
*/
|
|
104
|
+
body: (context: InsertContext) => string;
|
|
105
|
+
}
|
|
106
|
+
export interface Hook {
|
|
107
|
+
script: string;
|
|
108
|
+
wrap: HookWrap;
|
|
109
|
+
}
|
|
110
|
+
export declare const registerHooks: (data: {
|
|
111
|
+
identity: Identity;
|
|
112
|
+
hooks: Hook[];
|
|
113
|
+
content: GameContent;
|
|
114
|
+
prefix: string;
|
|
115
|
+
hostApi: HostApi;
|
|
116
|
+
send(event: string, expression?: string | undefined): string;
|
|
117
|
+
call(script: string, ...args: string[]): string;
|
|
118
|
+
}) => void;
|
|
119
|
+
export interface Insert {
|
|
120
|
+
at: At;
|
|
121
|
+
place: "before" | "after" | "instead";
|
|
122
|
+
body: (context: InsertContext) => string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The statement's token, refusing what only this side can see. The host checks the token, and the
|
|
126
|
+
* engine checks the address.
|
|
127
|
+
*/
|
|
128
|
+
export declare function insertToken(modId: string, spec: InsertSpec): At;
|
|
129
|
+
/**
|
|
130
|
+
* Declares each body as a script, then the insertion calling it. Scripts first, so the call resolves.
|
|
131
|
+
*/
|
|
132
|
+
export declare const registerInserts: (data: {
|
|
133
|
+
identity: Identity;
|
|
134
|
+
inserts: Insert[];
|
|
135
|
+
prefix: string;
|
|
136
|
+
hostApi: HostApi;
|
|
137
|
+
send(event: string, expression?: string): string;
|
|
138
|
+
call(script: string, ...args: string[]): string;
|
|
139
|
+
}) => void;
|
|
140
|
+
export interface ScriptActions {
|
|
141
|
+
/**
|
|
142
|
+
* Creates a new script.
|
|
143
|
+
*/
|
|
144
|
+
create<const Arg extends string = never>(spec: ScriptSpec<Arg>): void;
|
|
145
|
+
/**
|
|
146
|
+
* Replaces one of the game's scripts, keeping the original callable:
|
|
147
|
+
* ```ts
|
|
148
|
+
* mod.resource.script.hook("saveExe", ({ original, call }) => `
|
|
149
|
+
* var result = ${original.call()};
|
|
150
|
+
* ${call("afterLoad")};
|
|
151
|
+
* return result;
|
|
152
|
+
* `);
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* Only the game's own scripts: an object event has no caller to wrap.
|
|
156
|
+
*/
|
|
157
|
+
hook(name: string, wrap: HookWrap): void;
|
|
158
|
+
/**
|
|
159
|
+
* Runs GML at one statement inside the game's code -- or in place of it -- leaving the rest
|
|
160
|
+
* alone:
|
|
161
|
+
* ```ts
|
|
162
|
+
* mod.resource.script.insert({
|
|
163
|
+
* at: statement,
|
|
164
|
+
* place: "after",
|
|
165
|
+
* body: ({ send }) => send("saved", "string(room)"),
|
|
166
|
+
* });
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* The body sees `self`, `other`, globals and builtins, but not the host script's `var` locals or
|
|
170
|
+
* its `argument0...`. Several insertions at one statement run in declaration order.
|
|
171
|
+
*
|
|
172
|
+
* Statements of the game's scripts and of its object events (`mod.content.events`). A statement
|
|
173
|
+
* the engine could not place carries no `at` and is refused here, by name.
|
|
174
|
+
*/
|
|
175
|
+
insert(spec: InsertSpec): void;
|
|
176
|
+
}
|
|
177
|
+
export declare const registerScripts: (data: {
|
|
178
|
+
identity: Identity;
|
|
179
|
+
scripts: ScriptSpec<string>[];
|
|
180
|
+
prefix: string;
|
|
181
|
+
hostApi: HostApi;
|
|
182
|
+
send(event: string, expression?: string | undefined): string;
|
|
183
|
+
call(script: string, ...args: string[]): string;
|
|
184
|
+
}) => void;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Engine, HostApi, Identity } from "../host";
|
|
2
|
+
import type { EventHandler } from "../utils";
|
|
3
|
+
/** How long to wait for the game to confirm a picture before giving up on it. */
|
|
4
|
+
export declare const SPRITE_TIMEOUT_MS = 15000;
|
|
5
|
+
/** How long to give the game to say where its files live, before writing anyway. */
|
|
6
|
+
export declare const WHERE_WAIT_MS = 2000;
|
|
7
|
+
export interface SpriteOptions {
|
|
8
|
+
frames?: number;
|
|
9
|
+
origin?: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Shrink any frame longer than this on its longest side before sending it.
|
|
15
|
+
*/
|
|
16
|
+
maxSize?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface SpriteSize {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The picture itself.
|
|
24
|
+
* Either a URL to fetch, a `data:` URI, or bytes.
|
|
25
|
+
*/
|
|
26
|
+
export type SpriteSource = string | Uint8Array | ArrayBuffer;
|
|
27
|
+
/**
|
|
28
|
+
* A sprite this mod can fill with a picture while the game runs.
|
|
29
|
+
*
|
|
30
|
+
* Must be declared immediately but can be loaded anytime.
|
|
31
|
+
*/
|
|
32
|
+
export interface Sprite {
|
|
33
|
+
/**
|
|
34
|
+
* The sprite index, or `-1` if unloaded.
|
|
35
|
+
* ```
|
|
36
|
+
* draw: () => `if (${mySprite.id} >= 0) draw_sprite(${mySprite.id}, 0, x, y);`
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
readonly id: string;
|
|
40
|
+
/**
|
|
41
|
+
* Every frame must be the same size.
|
|
42
|
+
*
|
|
43
|
+
* @returns the sprite size
|
|
44
|
+
*/
|
|
45
|
+
load(frames: SpriteSource[]): Promise<SpriteSize>;
|
|
46
|
+
}
|
|
47
|
+
export interface SpriteActions {
|
|
48
|
+
/**
|
|
49
|
+
* Creates a new sprite.
|
|
50
|
+
*/
|
|
51
|
+
create(name: string, options?: SpriteOptions): Sprite;
|
|
52
|
+
}
|
|
53
|
+
export interface ImageKind {
|
|
54
|
+
extension: string;
|
|
55
|
+
type: string;
|
|
56
|
+
}
|
|
57
|
+
export declare const getImageKind: (bytes: Uint8Array) => ImageKind | null;
|
|
58
|
+
/**
|
|
59
|
+
* The game restarted, and a newer load of the same pictures is on its way.
|
|
60
|
+
*/
|
|
61
|
+
export declare class SupersededTransfer extends Error {
|
|
62
|
+
constructor();
|
|
63
|
+
}
|
|
64
|
+
export declare const readBytes: (source: SpriteSource) => Promise<Uint8Array>;
|
|
65
|
+
/**
|
|
66
|
+
* Lays several pictures out left to right as one PNG.
|
|
67
|
+
*
|
|
68
|
+
* Every frame has to be the same size.
|
|
69
|
+
*/
|
|
70
|
+
export declare const composeStrip: (images: Uint8Array[], maxSize?: number) => Promise<Uint8Array>;
|
|
71
|
+
export declare const toPng: (bytes: Uint8Array, type: string, maxSize?: number) => Promise<Uint8Array>;
|
|
72
|
+
/**
|
|
73
|
+
* The directory the game reads its mod files from, as a GML *value* it can report.
|
|
74
|
+
*
|
|
75
|
+
* The host writes the picture; what it needs back is the string the game puts in front of a
|
|
76
|
+
* filename, and only the game can say. `game_save_id` rather than `working_directory` for
|
|
77
|
+
* GameMaker Studio, and the difference is the whole point: a Studio runner re-roots every path a
|
|
78
|
+
* game opens into its save area, so a file written next to the game is looked for somewhere else
|
|
79
|
+
* entirely and reads as missing.
|
|
80
|
+
*/
|
|
81
|
+
export declare const scratchHere: (engine: Engine) => string;
|
|
82
|
+
/** Everything the two sides say to each other about pictures. */
|
|
83
|
+
export declare const SPR: {
|
|
84
|
+
/** host -> game: the picture is on the filesystem; add it. */
|
|
85
|
+
readonly put: "__spr_put";
|
|
86
|
+
/** game -> host: added, with its size. */
|
|
87
|
+
readonly ok: "__spr_ok";
|
|
88
|
+
/** game -> host: could not. */
|
|
89
|
+
readonly failed: "__spr_failed";
|
|
90
|
+
/** game -> host: I have (re)started, and hold no pictures. */
|
|
91
|
+
readonly hello: "__spr_hello";
|
|
92
|
+
/** game -> host: this is where I read files from. */
|
|
93
|
+
readonly where: "__spr_where";
|
|
94
|
+
};
|
|
95
|
+
export declare const getSpriteLoader: (data: {
|
|
96
|
+
identity: Identity;
|
|
97
|
+
hostApi: HostApi;
|
|
98
|
+
sendInternal: (event: string, payload?: string) => void;
|
|
99
|
+
}) => {
|
|
100
|
+
loadSprite: (slot: string, options: SpriteOptions, source: SpriteSource | SpriteSource[], stillWanted?: () => boolean) => Promise<SpriteSize>;
|
|
101
|
+
where: (dir: string) => void;
|
|
102
|
+
done: (slot: string, size: SpriteSize) => void;
|
|
103
|
+
failed: (slot: string, reason: string) => void;
|
|
104
|
+
abandon: () => void | undefined;
|
|
105
|
+
};
|
|
106
|
+
export type SpriteLoader = ReturnType<typeof getSpriteLoader>;
|
|
107
|
+
export interface DeclaredSprite {
|
|
108
|
+
name: string;
|
|
109
|
+
options: SpriteOptions;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Installs the GML half of the sprite transfer.
|
|
113
|
+
*
|
|
114
|
+
* @returns the GML handling each internal event, keyed by event name
|
|
115
|
+
*/
|
|
116
|
+
export declare const registerSprites: (data: {
|
|
117
|
+
hostApi: HostApi;
|
|
118
|
+
prefix: string;
|
|
119
|
+
sprites: DeclaredSprite[];
|
|
120
|
+
payload: string;
|
|
121
|
+
sendInternalGml: (event: string, expression?: string) => string;
|
|
122
|
+
}) => Record<string, string>;
|
|
123
|
+
/**
|
|
124
|
+
* The inbox object's Create: no sprites yet, and a word to the host.
|
|
125
|
+
*
|
|
126
|
+
* Runs again after a `game_restart()`, which frees every added sprite and wipes every global --
|
|
127
|
+
* hence the hello, which means "I hold nothing; send it all again". That costs a write and a
|
|
128
|
+
* message per picture, which is why nothing here tries to be clever about putting them back.
|
|
129
|
+
*/
|
|
130
|
+
export declare const spriteInboxCreate: (data: {
|
|
131
|
+
engine: Engine;
|
|
132
|
+
prefix: string;
|
|
133
|
+
sprites: DeclaredSprite[];
|
|
134
|
+
sendInternalGml: (event: string, expression?: string) => string;
|
|
135
|
+
}) => string;
|
|
136
|
+
export declare const registerSpriteHandlers: (data: {
|
|
137
|
+
identity: Identity;
|
|
138
|
+
sprites: DeclaredSprite[];
|
|
139
|
+
shown: Map<string, SpriteSource[]>;
|
|
140
|
+
spriteLoader: SpriteLoader;
|
|
141
|
+
handlers: Map<string, EventHandler[]>;
|
|
142
|
+
}) => void;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mod's own panel.
|
|
3
|
+
*
|
|
4
|
+
* The host shows it beside the running game, and in its own window for an installed one. The mod
|
|
5
|
+
* describes it once; after that it never renders again, it just writes to the values it declared and
|
|
6
|
+
* whatever showed them updates.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const channel = mod.ui.state("", { persist: "channel" });
|
|
10
|
+
* const lines = mod.ui.state<string[]>([]);
|
|
11
|
+
*
|
|
12
|
+
* mod.ui.panel({ title: "Chat" }, (ui) => [
|
|
13
|
+
* ui.input({ label: "Channel", value: channel }),
|
|
14
|
+
* ui.scroll([ui.each(lines, (line) => ui.text(line), { key: (line) => line })], {
|
|
15
|
+
* grow: true,
|
|
16
|
+
* stickToBottom: true,
|
|
17
|
+
* }),
|
|
18
|
+
* ]);
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* These types mirror the loader's own, structurally. The SDK is a published package that must build
|
|
22
|
+
* on its own, so it states the shape it is written against rather than importing it -- the same way
|
|
23
|
+
* `HostApi` is declared here rather than depended on.
|
|
24
|
+
*/
|
|
25
|
+
/** A value the panel can show and the player can change. Read and write `.value`. */
|
|
26
|
+
export interface Ref<T> {
|
|
27
|
+
value: T;
|
|
28
|
+
}
|
|
29
|
+
/** A value derived from other values. Rendered, never written to. */
|
|
30
|
+
export interface ReadonlyRef<T> {
|
|
31
|
+
readonly value: T;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Anything a node property accepts.
|
|
35
|
+
*
|
|
36
|
+
* A plain value never changes. A `Ref` changes when you write to it. A function is read like a
|
|
37
|
+
* `computed` -- handy for a label built out of other values, without naming it first.
|
|
38
|
+
*/
|
|
39
|
+
export type Bindable<T> = T | Ref<T> | ReadonlyRef<T> | (() => T);
|
|
40
|
+
/** What a render function returns, at any depth: one node, or several. */
|
|
41
|
+
export type UiSpec = UiNodeSpec | readonly UiNodeSpec[];
|
|
42
|
+
/** One node. Opaque -- build them with the `ui` builder, never by hand. */
|
|
43
|
+
export interface UiNodeSpec {
|
|
44
|
+
readonly t: "element" | "dynamic" | "each";
|
|
45
|
+
}
|
|
46
|
+
/** How much a piece of text is trying to say. Never a colour: the app decides what these look like,
|
|
47
|
+
* and looks different in the dark panel beside a game than it would on a page. */
|
|
48
|
+
export type UiTone = "normal" | "muted" | "ok" | "warning" | "danger";
|
|
49
|
+
/** A button's weight. */
|
|
50
|
+
export type UiVariant = "normal" | "primary" | "danger" | "ghost";
|
|
51
|
+
export type UiGap = "none" | "small" | "normal" | "large";
|
|
52
|
+
export type UiAlign = "start" | "center" | "end" | "between";
|
|
53
|
+
export interface UiSelectOption {
|
|
54
|
+
readonly value: string;
|
|
55
|
+
readonly label: string;
|
|
56
|
+
}
|
|
57
|
+
export interface UiSectionOptions {
|
|
58
|
+
title?: Bindable<string>;
|
|
59
|
+
/** Lets the player fold it away. The fold state belongs to the app, not to the mod. */
|
|
60
|
+
collapsible?: boolean;
|
|
61
|
+
grow?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface UiRowOptions {
|
|
64
|
+
align?: UiAlign;
|
|
65
|
+
/** Defaults to true. The panel is narrow, and how narrow is not something a mod can know. */
|
|
66
|
+
wrap?: boolean;
|
|
67
|
+
grow?: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface UiColOptions {
|
|
70
|
+
gap?: UiGap;
|
|
71
|
+
grow?: boolean;
|
|
72
|
+
}
|
|
73
|
+
export interface UiScrollOptions {
|
|
74
|
+
grow?: boolean;
|
|
75
|
+
/** Keep the newest line in view as they arrive -- unless the player has scrolled up to read. */
|
|
76
|
+
stickToBottom?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* A fixed height in pixels, rather than however tall its contents happen to be.
|
|
79
|
+
*
|
|
80
|
+
* For a box whose whole job is to hold a growing list -- a chat log, a run of events -- where the
|
|
81
|
+
* panel jumping about as lines arrive is worse than the space an empty box costs. The box is the
|
|
82
|
+
* same height holding nothing as holding a hundred lines.
|
|
83
|
+
*/
|
|
84
|
+
height?: number;
|
|
85
|
+
}
|
|
86
|
+
export interface UiTextOptions {
|
|
87
|
+
tone?: UiTone;
|
|
88
|
+
strong?: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface UiButtonOptions {
|
|
91
|
+
label: Bindable<string>;
|
|
92
|
+
onClick?: () => void;
|
|
93
|
+
variant?: UiVariant;
|
|
94
|
+
disabled?: Bindable<boolean>;
|
|
95
|
+
grow?: boolean;
|
|
96
|
+
}
|
|
97
|
+
export interface UiToggleOptions {
|
|
98
|
+
label: Bindable<string>;
|
|
99
|
+
/** A `Ref` here is two-way: the player flipping the switch writes it, like `v-model`. */
|
|
100
|
+
value: Bindable<boolean>;
|
|
101
|
+
onChange?: (value: boolean) => void;
|
|
102
|
+
disabled?: Bindable<boolean>;
|
|
103
|
+
}
|
|
104
|
+
export interface UiInputOptions {
|
|
105
|
+
/** A `Ref` here is two-way: what the player types is written straight into it. */
|
|
106
|
+
value: Bindable<string>;
|
|
107
|
+
label?: Bindable<string>;
|
|
108
|
+
placeholder?: Bindable<string>;
|
|
109
|
+
multiline?: boolean;
|
|
110
|
+
onChange?: (value: string) => void;
|
|
111
|
+
/** Enter in a single-line field. */
|
|
112
|
+
onSubmit?: () => void;
|
|
113
|
+
disabled?: Bindable<boolean>;
|
|
114
|
+
grow?: boolean;
|
|
115
|
+
}
|
|
116
|
+
export interface UiSelectOptions {
|
|
117
|
+
value: Bindable<string>;
|
|
118
|
+
options: Bindable<readonly UiSelectOption[]>;
|
|
119
|
+
label?: Bindable<string>;
|
|
120
|
+
onChange?: (value: string) => void;
|
|
121
|
+
disabled?: Bindable<boolean>;
|
|
122
|
+
}
|
|
123
|
+
export interface UiKeybindOptions {
|
|
124
|
+
/**
|
|
125
|
+
* A `KeyboardEvent.code` -- `"KeyH"`, `"ArrowLeft"`, `"Space"` -- or `""` for unbound. Two-way
|
|
126
|
+
* when it is a `Ref`, like every other control's value.
|
|
127
|
+
*
|
|
128
|
+
* The *physical* key, not the letter printed on it, so a binding made on AZERTY stays under the
|
|
129
|
+
* same finger. `gameMakerKey` turns one into the number GML checks.
|
|
130
|
+
*/
|
|
131
|
+
value: Bindable<string>;
|
|
132
|
+
label?: Bindable<string>;
|
|
133
|
+
onChange?: (code: string) => void;
|
|
134
|
+
disabled?: Bindable<boolean>;
|
|
135
|
+
}
|
|
136
|
+
export interface UiEachOptions<T> {
|
|
137
|
+
/**
|
|
138
|
+
* Stable identity for one item.
|
|
139
|
+
*
|
|
140
|
+
* Required rather than defaulted to the position, because the position is wrong in exactly the
|
|
141
|
+
* case this exists for: add a line at the top and every position shifts, so a position-keyed list
|
|
142
|
+
* redraws itself entirely instead of inserting one row.
|
|
143
|
+
*/
|
|
144
|
+
key: (item: T, index: number) => string | number;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Everything a panel is made of.
|
|
148
|
+
*
|
|
149
|
+
* Layout is expressed as intent, never as measurement: say which child should absorb the leftover
|
|
150
|
+
* room with `grow`, and let the app decide what that means at the width it happens to have. There
|
|
151
|
+
* are no pixels, no percentages and no styles here, which is what lets one panel work in a narrow
|
|
152
|
+
* sidebar, on a phone, and in a desktop window without its author having thought about any of them.
|
|
153
|
+
*/
|
|
154
|
+
export interface UiBuilder {
|
|
155
|
+
/** The primary grouping: a titled card. Vertical inside. */
|
|
156
|
+
section(options: UiSectionOptions, children: UiSpec): UiNodeSpec;
|
|
157
|
+
/** Horizontal, wrapping by default. */
|
|
158
|
+
row(children: UiSpec, options?: UiRowOptions): UiNodeSpec;
|
|
159
|
+
col(children: UiSpec, options?: UiColOptions): UiNodeSpec;
|
|
160
|
+
grid(children: UiSpec, options: {
|
|
161
|
+
columns: number;
|
|
162
|
+
grow?: boolean;
|
|
163
|
+
}): UiNodeSpec;
|
|
164
|
+
/** The one container that absorbs leftover height and scrolls. */
|
|
165
|
+
scroll(children: UiSpec, options?: UiScrollOptions): UiNodeSpec;
|
|
166
|
+
text(text: Bindable<string>, options?: UiTextOptions): UiNodeSpec;
|
|
167
|
+
badge(label: Bindable<string>, options?: {
|
|
168
|
+
tone?: UiTone;
|
|
169
|
+
}): UiNodeSpec;
|
|
170
|
+
separator(): UiNodeSpec;
|
|
171
|
+
spacer(): UiNodeSpec;
|
|
172
|
+
/** 0..1. */
|
|
173
|
+
progress(value: Bindable<number>, options?: {
|
|
174
|
+
label?: Bindable<string>;
|
|
175
|
+
}): UiNodeSpec;
|
|
176
|
+
button(options: UiButtonOptions): UiNodeSpec;
|
|
177
|
+
toggle(options: UiToggleOptions): UiNodeSpec;
|
|
178
|
+
input(options: UiInputOptions): UiNodeSpec;
|
|
179
|
+
select(options: UiSelectOptions): UiNodeSpec;
|
|
180
|
+
/**
|
|
181
|
+
* A key the player can rebind.
|
|
182
|
+
*
|
|
183
|
+
* **The one control whose value you cannot bake into your GML.** A mod's code is written before
|
|
184
|
+
* the player has said anything, so `keyboard_check_pressed(ord("H"))` fixes that key forever.
|
|
185
|
+
* Keep the key in an instance variable, send the new one down when it changes, and the binding
|
|
186
|
+
* follows. See `gameMakerKey` for the translation and a worked example.
|
|
187
|
+
*/
|
|
188
|
+
keybind(options: UiKeybindOptions): UiNodeSpec;
|
|
189
|
+
/**
|
|
190
|
+
* A region that re-runs when any value it reads changes. For anything whose *shape* depends on
|
|
191
|
+
* state -- a conditional, a different set of controls once connected.
|
|
192
|
+
*
|
|
193
|
+
* Keep it synchronous: a value read after an `await` is read outside the region, so it is not
|
|
194
|
+
* noticed and the region will not re-run for it.
|
|
195
|
+
*/
|
|
196
|
+
dynamic(fn: () => UiSpec): UiNodeSpec;
|
|
197
|
+
/**
|
|
198
|
+
* A keyed list.
|
|
199
|
+
*
|
|
200
|
+
* The item function runs per item and does not subscribe the list to what it reads, so one item's
|
|
201
|
+
* value moving updates that item alone rather than redrawing everything.
|
|
202
|
+
*/
|
|
203
|
+
each<T>(source: Bindable<readonly T[]>, render: (item: T, index: number) => UiSpec, options: UiEachOptions<T>): UiNodeSpec;
|
|
204
|
+
}
|
|
205
|
+
export interface UiStateOptions {
|
|
206
|
+
/**
|
|
207
|
+
* Keep this value between sessions, under this name.
|
|
208
|
+
*
|
|
209
|
+
* Stored by the host against the player's account when they are signed in, and in the browser
|
|
210
|
+
* when they are not. It is an opaque bag as far as the app is concerned -- it stores what the mod
|
|
211
|
+
* puts there and never reads into it -- which is what lets a mod have settings without the host
|
|
212
|
+
* having to know what they are.
|
|
213
|
+
*/
|
|
214
|
+
persist?: string;
|
|
215
|
+
}
|
|
216
|
+
/** `mod.ui`. */
|
|
217
|
+
export interface ModUi {
|
|
218
|
+
/** A value the panel can show and the player can change. */
|
|
219
|
+
state<T>(initial: T, options?: UiStateOptions): Ref<T>;
|
|
220
|
+
/** A value derived from other values. */
|
|
221
|
+
computed<T>(fn: () => T): ReadonlyRef<T>;
|
|
222
|
+
/**
|
|
223
|
+
* Declares a panel. Only while the mod is being built, like every other thing it declares.
|
|
224
|
+
*
|
|
225
|
+
* The render function runs once. What keeps the panel live afterwards are the values it read, so
|
|
226
|
+
* there is nothing to call again and nothing for a mod to know about when its panel is on screen.
|
|
227
|
+
*/
|
|
228
|
+
panel(options: {
|
|
229
|
+
id?: string;
|
|
230
|
+
title: string;
|
|
231
|
+
}, render: (ui: UiBuilder) => UiSpec): void;
|
|
232
|
+
}
|
package/dist/utils.d.ts
ADDED