@whatever-engine/api 0.1.1 → 0.2.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/README.md +51 -14
- package/dist/index.d.ts +375 -0
- package/dist/index.js +600 -0
- package/package.json +15 -10
- package/index.d.ts +0 -144
- package/index.ts +0 -443
- package/tsconfig.json +0 -12
package/index.d.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/** Arbitrary JSON-serializable value used for inter-mod messages. */
|
|
2
|
-
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
3
|
-
[key: string]: JsonValue;
|
|
4
|
-
};
|
|
5
|
-
/** Metadata about a loaded mod, mirroring mod.toml. */
|
|
6
|
-
export type ModManifest = {
|
|
7
|
-
id: string;
|
|
8
|
-
name: string;
|
|
9
|
-
version: string;
|
|
10
|
-
description: string;
|
|
11
|
-
authors: string[];
|
|
12
|
-
license: string;
|
|
13
|
-
dependencies: Record<string, string>;
|
|
14
|
-
load_order: {
|
|
15
|
-
after: string[];
|
|
16
|
-
before: string[];
|
|
17
|
-
};
|
|
18
|
-
script?: {
|
|
19
|
-
entry: string;
|
|
20
|
-
runtime: string;
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
/** Payload types for each public event name. */
|
|
24
|
-
export type EventPayloads = {
|
|
25
|
-
/** Fired once after the engine has initialised and the window is ready. */
|
|
26
|
-
init: {
|
|
27
|
-
mod_id: string;
|
|
28
|
-
engine_version: string;
|
|
29
|
-
};
|
|
30
|
-
/** Fired when the game is shutting down. The process exits after all handlers return. */
|
|
31
|
-
exit: {
|
|
32
|
-
exit_code: number;
|
|
33
|
-
};
|
|
34
|
-
/** Fired every rendered frame. */
|
|
35
|
-
frame: {
|
|
36
|
-
delta_seconds: number;
|
|
37
|
-
frame_number: number;
|
|
38
|
-
};
|
|
39
|
-
/** Fired every frame with the current input state. */
|
|
40
|
-
input: {
|
|
41
|
-
keys_pressed: string[];
|
|
42
|
-
mouse_delta: [number, number];
|
|
43
|
-
};
|
|
44
|
-
/** Response to a prior `Assets.request` call. */
|
|
45
|
-
asset_response: {
|
|
46
|
-
request_id: string;
|
|
47
|
-
path: string;
|
|
48
|
-
data_base64: string | null;
|
|
49
|
-
error: string | null;
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Fired when another mod sends this mod a message via `Message.send`.
|
|
53
|
-
* If `request_id` is present the sender is awaiting a reply — call `Message.reply(request_id, data)`.
|
|
54
|
-
* Not fired for replies that arrive via the `Message.send(id, msg, timeout)` overload.
|
|
55
|
-
*/
|
|
56
|
-
mod_message: {
|
|
57
|
-
source_mod_id: string;
|
|
58
|
-
message: JsonValue;
|
|
59
|
-
request_id?: string;
|
|
60
|
-
};
|
|
61
|
-
};
|
|
62
|
-
export type EventName = keyof EventPayloads;
|
|
63
|
-
/** Core engine events and logging. */
|
|
64
|
-
export declare const Engine: {
|
|
65
|
-
/**
|
|
66
|
-
* Subscribe to an engine event. The handler is called each time the event fires.
|
|
67
|
-
* Registering a handler also sends a `Subscribe` message to the engine automatically
|
|
68
|
-
* (except for `mod_message`, which the engine routes unconditionally).
|
|
69
|
-
*/
|
|
70
|
-
on<E extends EventName>(event: E, handler: (payload: EventPayloads[E]) => void): void;
|
|
71
|
-
/** Log a message via the engine logger. Output includes timestamp and mod ID. */
|
|
72
|
-
log(level: "info" | "warn" | "error", message: string): void;
|
|
73
|
-
};
|
|
74
|
-
/** Window management. */
|
|
75
|
-
export declare const Window: {
|
|
76
|
-
/** Set the title of the active window. */
|
|
77
|
-
setTitle(title: string): void;
|
|
78
|
-
};
|
|
79
|
-
/** Sandboxed per-mod file I/O. Paths must not contain `..`. */
|
|
80
|
-
export declare const File: {
|
|
81
|
-
/** Write a UTF-8 string to a sandboxed file for this mod. */
|
|
82
|
-
write(path: string, data: string): Promise<void>;
|
|
83
|
-
/** Read a sandboxed file for this mod and return its contents as a UTF-8 string. */
|
|
84
|
-
read(path: string): Promise<string>;
|
|
85
|
-
/** Delete a sandboxed file for this mod. */
|
|
86
|
-
delete(path: string): Promise<void>;
|
|
87
|
-
};
|
|
88
|
-
/** Scene entity management. */
|
|
89
|
-
export declare const Scene: {
|
|
90
|
-
spawnSprite(entity_id: string, texture: string, position: [number, number, number], scale?: [number, number, number]): void;
|
|
91
|
-
moveEntity(entity_id: string, position: [number, number, number]): void;
|
|
92
|
-
destroyEntity(entity_id: string): void;
|
|
93
|
-
};
|
|
94
|
-
/** Asset requests (VFS paths: `mod_id://relative/path`). */
|
|
95
|
-
export declare const Assets: {
|
|
96
|
-
request(request_id: string, path: string): void;
|
|
97
|
-
};
|
|
98
|
-
/** Query information about loaded mods. */
|
|
99
|
-
export declare const Mods: {
|
|
100
|
-
/** Returns the manifests of all currently loaded mods in load order. */
|
|
101
|
-
list(): Promise<ModManifest[]>;
|
|
102
|
-
/** Returns the manifest for a specific mod by ID. Rejects if the mod is not loaded. */
|
|
103
|
-
get(id: string): Promise<ModManifest>;
|
|
104
|
-
};
|
|
105
|
-
interface _MessageNamespace {
|
|
106
|
-
/** Send a fire-and-forget message to another mod. */
|
|
107
|
-
sendAndForget<T extends JsonValue>(id: string, message: T): void;
|
|
108
|
-
/**
|
|
109
|
-
* Send a message to another mod and wait for a reply.
|
|
110
|
-
* The receiving mod's handler must return a non-null value within `timeout` ms.
|
|
111
|
-
* Rejects with a timeout error if no reply arrives in time.
|
|
112
|
-
*/
|
|
113
|
-
send<T extends JsonValue, U extends JsonValue>(id: string, message: T, timeout: number): Promise<U>;
|
|
114
|
-
/**
|
|
115
|
-
* Register a handler for incoming mod messages.
|
|
116
|
-
* Return a `JsonValue` to reply (only meaningful when the sender used `send`); return `null` to ignore.
|
|
117
|
-
* The `request_id` in the payload is an opaque engine token — do not inspect or store it.
|
|
118
|
-
*/
|
|
119
|
-
registerMessageHandler(handler: (payload: EventPayloads["mod_message"]) => JsonValue | null): void;
|
|
120
|
-
}
|
|
121
|
-
/** Inter-mod communication. */
|
|
122
|
-
export declare const Message: _MessageNamespace;
|
|
123
|
-
/** Public arg type for Console.register(). */
|
|
124
|
-
export type ArgType = "string" | "int" | "float" | "bool";
|
|
125
|
-
/** Argument specification for a command. */
|
|
126
|
-
export type ArgSpec = {
|
|
127
|
-
name: string;
|
|
128
|
-
type: ArgType;
|
|
129
|
-
required?: boolean;
|
|
130
|
-
description?: string;
|
|
131
|
-
};
|
|
132
|
-
/** A command or subcommand specification. */
|
|
133
|
-
export type CommandSpec = {
|
|
134
|
-
name: string;
|
|
135
|
-
description?: string;
|
|
136
|
-
subcommands?: CommandSpec[];
|
|
137
|
-
args?: ArgSpec[];
|
|
138
|
-
handler?: (args: Record<string, string | number | boolean>) => string | string[] | Promise<string | string[]>;
|
|
139
|
-
};
|
|
140
|
-
/** Register a command that users can invoke from the developer console. */
|
|
141
|
-
export declare const Console: {
|
|
142
|
-
register(spec: CommandSpec): void;
|
|
143
|
-
};
|
|
144
|
-
export {};
|
package/index.ts
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
import { createInterface } from "node:readline";
|
|
2
|
-
|
|
3
|
-
// Internal IPC types — match Rust serde tags exactly, not part of the public API.
|
|
4
|
-
type _EngineMsg =
|
|
5
|
-
| { type: "CommandInvoke"; request_id: string; command_path: string[]; args: JsonValue[] }
|
|
6
|
-
| { type: "Init"; mod_id: string; engine_version: string }
|
|
7
|
-
| { type: "Frame"; delta_seconds: number; frame_number: number }
|
|
8
|
-
| { type: "Input"; keys_pressed: string[]; mouse_delta: [number, number] }
|
|
9
|
-
| { type: "AssetResponse"; request_id: string; path: string; data_base64: string | null; error: string | null }
|
|
10
|
-
| { type: "FileResponse"; request_id: string; data_base64: string | null; error: string | null }
|
|
11
|
-
| { type: "ModListResponse"; request_id: string; mods: ModManifest[] }
|
|
12
|
-
| { type: "ModGetResponse"; request_id: string; manifest: ModManifest | null; error: string | null }
|
|
13
|
-
| { type: "ModMessageReceived"; source_mod_id: string; request_id: string | null; payload: JsonValue }
|
|
14
|
-
| { type: "ModMessageReplyDelivered"; request_id: string; payload: JsonValue }
|
|
15
|
-
| { type: "Shutdown"; exit_code: number };
|
|
16
|
-
|
|
17
|
-
type _ScriptMsg =
|
|
18
|
-
| { type: "RegisterCommand"; name: string; description: string; subcommands: _CommandNodeSpec[]; args: _ArgSpec[]; has_handler: boolean }
|
|
19
|
-
| { type: "CommandResponse"; request_id: string; output: string[]; error: string | null }
|
|
20
|
-
| { type: "Subscribe"; events: string[] }
|
|
21
|
-
| { type: "AssetRequest"; request_id: string; path: string }
|
|
22
|
-
| { type: "SpawnSprite"; entity_id: string; texture: string; position: [number, number, number]; scale: [number, number, number] }
|
|
23
|
-
| { type: "MoveEntity"; entity_id: string; position: [number, number, number] }
|
|
24
|
-
| { type: "DestroyEntity"; entity_id: string }
|
|
25
|
-
| { type: "Log"; level: "info" | "warn" | "error"; message: string }
|
|
26
|
-
| { type: "SetWindowTitle"; title: string }
|
|
27
|
-
| { type: "FileWrite"; request_id: string; path: string; data_base64: string }
|
|
28
|
-
| { type: "FileRead"; request_id: string; path: string }
|
|
29
|
-
| { type: "FileDelete"; request_id: string; path: string }
|
|
30
|
-
| { type: "ModListRequest"; request_id: string }
|
|
31
|
-
| { type: "ModGetRequest"; request_id: string; mod_id: string }
|
|
32
|
-
| { type: "ModMessageSend"; target_mod_id: string; request_id: string | null; payload: JsonValue }
|
|
33
|
-
| { type: "ModMessageReply"; request_id: string; payload: JsonValue };
|
|
34
|
-
|
|
35
|
-
/** Arbitrary JSON-serializable value used for inter-mod messages. */
|
|
36
|
-
export type JsonValue =
|
|
37
|
-
| string
|
|
38
|
-
| number
|
|
39
|
-
| boolean
|
|
40
|
-
| null
|
|
41
|
-
| JsonValue[]
|
|
42
|
-
| { [key: string]: JsonValue };
|
|
43
|
-
|
|
44
|
-
/** Metadata about a loaded mod, mirroring mod.toml. */
|
|
45
|
-
export type ModManifest = {
|
|
46
|
-
id: string;
|
|
47
|
-
name: string;
|
|
48
|
-
version: string;
|
|
49
|
-
description: string;
|
|
50
|
-
authors: string[];
|
|
51
|
-
license: string;
|
|
52
|
-
dependencies: Record<string, string>;
|
|
53
|
-
load_order: { after: string[]; before: string[] };
|
|
54
|
-
script?: { entry: string; runtime: string };
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/** Payload types for each public event name. */
|
|
58
|
-
export type EventPayloads = {
|
|
59
|
-
/** Fired once after the engine has initialised and the window is ready. */
|
|
60
|
-
init: { mod_id: string; engine_version: string };
|
|
61
|
-
/** Fired when the game is shutting down. The process exits after all handlers return. */
|
|
62
|
-
exit: { exit_code: number };
|
|
63
|
-
/** Fired every rendered frame. */
|
|
64
|
-
frame: { delta_seconds: number; frame_number: number };
|
|
65
|
-
/** Fired every frame with the current input state. */
|
|
66
|
-
input: { keys_pressed: string[]; mouse_delta: [number, number] };
|
|
67
|
-
/** Response to a prior `Assets.request` call. */
|
|
68
|
-
asset_response: { request_id: string; path: string; data_base64: string | null; error: string | null };
|
|
69
|
-
/**
|
|
70
|
-
* Fired when another mod sends this mod a message via `Message.send`.
|
|
71
|
-
* If `request_id` is present the sender is awaiting a reply — call `Message.reply(request_id, data)`.
|
|
72
|
-
* Not fired for replies that arrive via the `Message.send(id, msg, timeout)` overload.
|
|
73
|
-
*/
|
|
74
|
-
mod_message: { source_mod_id: string; message: JsonValue; request_id?: string };
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export type EventName = keyof EventPayloads;
|
|
78
|
-
|
|
79
|
-
// Maps public event names → internal Rust message types.
|
|
80
|
-
// mod_message is absent: it is dispatched specially and needs no Subscribe.
|
|
81
|
-
const _EVENT_TYPE: Partial<Record<EventName, _EngineMsg["type"]>> = {
|
|
82
|
-
init: "Init",
|
|
83
|
-
exit: "Shutdown",
|
|
84
|
-
frame: "Frame",
|
|
85
|
-
input: "Input",
|
|
86
|
-
asset_response: "AssetResponse",
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// --- Shared internal IPC state ---
|
|
90
|
-
|
|
91
|
-
const _handlers = new Map<EventName, Set<(payload: any) => void>>();
|
|
92
|
-
const _fileCallbacks = new Map<string, { resolve: (v: string | null) => void; reject: (e: Error) => void }>();
|
|
93
|
-
const _modListCallbacks = new Map<string, { resolve: (v: ModManifest[]) => void; reject: (e: Error) => void }>();
|
|
94
|
-
const _modGetCallbacks = new Map<string, { resolve: (v: ModManifest) => void; reject: (e: Error) => void }>();
|
|
95
|
-
const _msgCallbacks = new Map<string, { resolve: (v: JsonValue) => void; reject: (e: Error) => void }>();
|
|
96
|
-
let _reqCounter = 0;
|
|
97
|
-
|
|
98
|
-
function _send(msg: _ScriptMsg): void {
|
|
99
|
-
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function _dispatch(msg: _EngineMsg): void {
|
|
103
|
-
if (msg.type === "CommandInvoke") {
|
|
104
|
-
_handleCommandInvoke(msg);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (msg.type === "FileResponse") {
|
|
109
|
-
const cb = _fileCallbacks.get(msg.request_id);
|
|
110
|
-
if (cb) {
|
|
111
|
-
_fileCallbacks.delete(msg.request_id);
|
|
112
|
-
msg.error ? cb.reject(new Error(msg.error)) : cb.resolve(msg.data_base64);
|
|
113
|
-
}
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (msg.type === "ModListResponse") {
|
|
118
|
-
const cb = _modListCallbacks.get(msg.request_id);
|
|
119
|
-
if (cb) {
|
|
120
|
-
_modListCallbacks.delete(msg.request_id);
|
|
121
|
-
cb.resolve(msg.mods);
|
|
122
|
-
}
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (msg.type === "ModGetResponse") {
|
|
127
|
-
const cb = _modGetCallbacks.get(msg.request_id);
|
|
128
|
-
if (cb) {
|
|
129
|
-
_modGetCallbacks.delete(msg.request_id);
|
|
130
|
-
msg.error ? cb.reject(new Error(msg.error)) : cb.resolve(msg.manifest!);
|
|
131
|
-
}
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (msg.type === "ModMessageReplyDelivered") {
|
|
136
|
-
const cb = _msgCallbacks.get(msg.request_id);
|
|
137
|
-
if (cb) {
|
|
138
|
-
_msgCallbacks.delete(msg.request_id);
|
|
139
|
-
cb.resolve(msg.payload);
|
|
140
|
-
}
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (msg.type === "ModMessageReceived") {
|
|
145
|
-
const handlers = _handlers.get("mod_message");
|
|
146
|
-
if (handlers) {
|
|
147
|
-
const payload: EventPayloads["mod_message"] = {
|
|
148
|
-
source_mod_id: msg.source_mod_id,
|
|
149
|
-
message: msg.payload,
|
|
150
|
-
...(msg.request_id !== null && { request_id: msg.request_id }),
|
|
151
|
-
};
|
|
152
|
-
for (const fn_ of handlers) fn_(payload);
|
|
153
|
-
}
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
for (const [event, msgType] of Object.entries(_EVENT_TYPE) as [EventName, _EngineMsg["type"]][]) {
|
|
158
|
-
if (msg.type !== msgType) continue;
|
|
159
|
-
const handlers = _handlers.get(event);
|
|
160
|
-
if (handlers) {
|
|
161
|
-
for (const fn_ of handlers) fn_(msg);
|
|
162
|
-
}
|
|
163
|
-
if (event === "exit") {
|
|
164
|
-
process.exit((msg as Extract<_EngineMsg, { type: "Shutdown" }>).exit_code);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const _rl = createInterface({ input: process.stdin, terminal: false });
|
|
170
|
-
_rl.on("line", (line) => {
|
|
171
|
-
try {
|
|
172
|
-
_dispatch(JSON.parse(line) as _EngineMsg);
|
|
173
|
-
} catch {
|
|
174
|
-
// ignore malformed messages
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
_rl.on("close", () => process.exit(0));
|
|
178
|
-
|
|
179
|
-
// --- Public API namespaces ---
|
|
180
|
-
|
|
181
|
-
/** Core engine events and logging. */
|
|
182
|
-
export const Engine = {
|
|
183
|
-
/**
|
|
184
|
-
* Subscribe to an engine event. The handler is called each time the event fires.
|
|
185
|
-
* Registering a handler also sends a `Subscribe` message to the engine automatically
|
|
186
|
-
* (except for `mod_message`, which the engine routes unconditionally).
|
|
187
|
-
*/
|
|
188
|
-
on<E extends EventName>(event: E, handler: (payload: EventPayloads[E]) => void): void {
|
|
189
|
-
if (!_handlers.has(event)) _handlers.set(event, new Set());
|
|
190
|
-
_handlers.get(event)!.add(handler as (payload: any) => void);
|
|
191
|
-
const msgType = _EVENT_TYPE[event];
|
|
192
|
-
if (msgType) _send({ type: "Subscribe", events: [msgType] });
|
|
193
|
-
},
|
|
194
|
-
|
|
195
|
-
/** Log a message via the engine logger. Output includes timestamp and mod ID. */
|
|
196
|
-
log(level: "info" | "warn" | "error", message: string): void {
|
|
197
|
-
_send({ type: "Log", level, message });
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
/** Window management. */
|
|
202
|
-
export const Window = {
|
|
203
|
-
/** Set the title of the active window. */
|
|
204
|
-
setTitle(title: string): void {
|
|
205
|
-
_send({ type: "SetWindowTitle", title });
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
/** Sandboxed per-mod file I/O. Paths must not contain `..`. */
|
|
210
|
-
export const File = {
|
|
211
|
-
/** Write a UTF-8 string to a sandboxed file for this mod. */
|
|
212
|
-
write(path: string, data: string): Promise<void> {
|
|
213
|
-
return new Promise((resolve, reject) => {
|
|
214
|
-
const request_id = String(++_reqCounter);
|
|
215
|
-
_fileCallbacks.set(request_id, { resolve: () => resolve(), reject });
|
|
216
|
-
_send({ type: "FileWrite", request_id, path, data_base64: Buffer.from(data, "utf8").toString("base64") });
|
|
217
|
-
});
|
|
218
|
-
},
|
|
219
|
-
|
|
220
|
-
/** Read a sandboxed file for this mod and return its contents as a UTF-8 string. */
|
|
221
|
-
read(path: string): Promise<string> {
|
|
222
|
-
return new Promise((resolve, reject) => {
|
|
223
|
-
const request_id = String(++_reqCounter);
|
|
224
|
-
_fileCallbacks.set(request_id, {
|
|
225
|
-
resolve: (b64) => resolve(Buffer.from(b64!, "base64").toString("utf8")),
|
|
226
|
-
reject,
|
|
227
|
-
});
|
|
228
|
-
_send({ type: "FileRead", request_id, path });
|
|
229
|
-
});
|
|
230
|
-
},
|
|
231
|
-
|
|
232
|
-
/** Delete a sandboxed file for this mod. */
|
|
233
|
-
delete(path: string): Promise<void> {
|
|
234
|
-
return new Promise((resolve, reject) => {
|
|
235
|
-
const request_id = String(++_reqCounter);
|
|
236
|
-
_fileCallbacks.set(request_id, { resolve: () => resolve(), reject });
|
|
237
|
-
_send({ type: "FileDelete", request_id, path });
|
|
238
|
-
});
|
|
239
|
-
},
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
/** Scene entity management. */
|
|
243
|
-
export const Scene = {
|
|
244
|
-
spawnSprite(entity_id: string, texture: string, position: [number, number, number], scale: [number, number, number] = [1, 1, 1]): void {
|
|
245
|
-
_send({ type: "SpawnSprite", entity_id, texture, position, scale });
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
moveEntity(entity_id: string, position: [number, number, number]): void {
|
|
249
|
-
_send({ type: "MoveEntity", entity_id, position });
|
|
250
|
-
},
|
|
251
|
-
|
|
252
|
-
destroyEntity(entity_id: string): void {
|
|
253
|
-
_send({ type: "DestroyEntity", entity_id });
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
/** Asset requests (VFS paths: `mod_id://relative/path`). */
|
|
258
|
-
export const Assets = {
|
|
259
|
-
request(request_id: string, path: string): void {
|
|
260
|
-
_send({ type: "AssetRequest", request_id, path });
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
/** Query information about loaded mods. */
|
|
265
|
-
export const Mods = {
|
|
266
|
-
/** Returns the manifests of all currently loaded mods in load order. */
|
|
267
|
-
list(): Promise<ModManifest[]> {
|
|
268
|
-
return new Promise((resolve, reject) => {
|
|
269
|
-
const request_id = String(++_reqCounter);
|
|
270
|
-
_modListCallbacks.set(request_id, { resolve, reject });
|
|
271
|
-
_send({ type: "ModListRequest", request_id });
|
|
272
|
-
});
|
|
273
|
-
},
|
|
274
|
-
|
|
275
|
-
/** Returns the manifest for a specific mod by ID. Rejects if the mod is not loaded. */
|
|
276
|
-
get(id: string): Promise<ModManifest> {
|
|
277
|
-
return new Promise((resolve, reject) => {
|
|
278
|
-
const request_id = String(++_reqCounter);
|
|
279
|
-
_modGetCallbacks.set(request_id, { resolve, reject });
|
|
280
|
-
_send({ type: "ModGetRequest", request_id, mod_id: id });
|
|
281
|
-
});
|
|
282
|
-
},
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
interface _MessageNamespace {
|
|
286
|
-
/** Send a fire-and-forget message to another mod. */
|
|
287
|
-
sendAndForget<T extends JsonValue>(id: string, message: T): void;
|
|
288
|
-
/**
|
|
289
|
-
* Send a message to another mod and wait for a reply.
|
|
290
|
-
* The receiving mod's handler must return a non-null value within `timeout` ms.
|
|
291
|
-
* Rejects with a timeout error if no reply arrives in time.
|
|
292
|
-
*/
|
|
293
|
-
send<T extends JsonValue, U extends JsonValue>(id: string, message: T, timeout: number): Promise<U>;
|
|
294
|
-
/**
|
|
295
|
-
* Register a handler for incoming mod messages.
|
|
296
|
-
* Return a `JsonValue` to reply (only meaningful when the sender used `send`); return `null` to ignore.
|
|
297
|
-
* The `request_id` in the payload is an opaque engine token — do not inspect or store it.
|
|
298
|
-
*/
|
|
299
|
-
registerMessageHandler(handler: (payload: EventPayloads["mod_message"]) => JsonValue | null): void;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// --- Console command types (internal) ---
|
|
303
|
-
|
|
304
|
-
type _ArgSpec = {
|
|
305
|
-
name: string;
|
|
306
|
-
type: "string" | "int" | "float" | "bool";
|
|
307
|
-
required: boolean;
|
|
308
|
-
description: string;
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
type _CommandNodeSpec = {
|
|
312
|
-
name: string;
|
|
313
|
-
description: string;
|
|
314
|
-
subcommands: _CommandNodeSpec[];
|
|
315
|
-
args: _ArgSpec[];
|
|
316
|
-
has_handler: boolean;
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
// --- Inter-mod communication ---
|
|
320
|
-
|
|
321
|
-
/** Inter-mod communication. */
|
|
322
|
-
export const Message: _MessageNamespace = {
|
|
323
|
-
sendAndForget<T extends JsonValue>(id: string, message: T): void {
|
|
324
|
-
_send({ type: "ModMessageSend", target_mod_id: id, request_id: null, payload: message });
|
|
325
|
-
},
|
|
326
|
-
send<T extends JsonValue, U extends JsonValue>(id: string, message: T, timeout: number): Promise<U> {
|
|
327
|
-
return new Promise<U>((resolve, reject) => {
|
|
328
|
-
const request_id = String(++_reqCounter);
|
|
329
|
-
const timer = setTimeout(() => {
|
|
330
|
-
_msgCallbacks.delete(request_id);
|
|
331
|
-
reject(new Error(`Message to '${id}' timed out after ${timeout}ms`));
|
|
332
|
-
}, timeout);
|
|
333
|
-
_msgCallbacks.set(request_id, {
|
|
334
|
-
resolve: (v) => { clearTimeout(timer); resolve(v as U); },
|
|
335
|
-
reject,
|
|
336
|
-
});
|
|
337
|
-
_send({ type: "ModMessageSend", target_mod_id: id, request_id, payload: message });
|
|
338
|
-
});
|
|
339
|
-
},
|
|
340
|
-
registerMessageHandler(handler: (payload: EventPayloads["mod_message"]) => JsonValue | null): void {
|
|
341
|
-
Engine.on("mod_message", (payload) => {
|
|
342
|
-
const result = handler(payload);
|
|
343
|
-
if (payload.request_id !== undefined && result !== null) {
|
|
344
|
-
_send({ type: "ModMessageReply", request_id: payload.request_id, payload: result });
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
},
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
// --- Console command registration ---
|
|
351
|
-
|
|
352
|
-
/** Public arg type for Console.register(). */
|
|
353
|
-
export type ArgType = "string" | "int" | "float" | "bool";
|
|
354
|
-
|
|
355
|
-
/** Argument specification for a command. */
|
|
356
|
-
export type ArgSpec = {
|
|
357
|
-
name: string;
|
|
358
|
-
type: ArgType;
|
|
359
|
-
required?: boolean;
|
|
360
|
-
description?: string;
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
/** A command or subcommand specification. */
|
|
364
|
-
export type CommandSpec = {
|
|
365
|
-
name: string;
|
|
366
|
-
description?: string;
|
|
367
|
-
subcommands?: CommandSpec[];
|
|
368
|
-
args?: ArgSpec[];
|
|
369
|
-
handler?: (args: Record<string, string | number | boolean>) => string | string[] | Promise<string | string[]>;
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
// Handlers and their arg specs, keyed by dot-joined command path (e.g. "mycmd" or "mycmd.sub")
|
|
373
|
-
const _cmdHandlers = new Map<string, NonNullable<CommandSpec["handler"]>>();
|
|
374
|
-
const _cmdArgSpecs = new Map<string, _ArgSpec[]>();
|
|
375
|
-
|
|
376
|
-
function _specToInternal(spec: CommandSpec, pathPrefix: string): _CommandNodeSpec {
|
|
377
|
-
const path = pathPrefix ? `${pathPrefix}.${spec.name}` : spec.name;
|
|
378
|
-
const mappedArgs: _ArgSpec[] = (spec.args ?? []).map((a) => ({
|
|
379
|
-
name: a.name,
|
|
380
|
-
type: a.type,
|
|
381
|
-
required: a.required ?? false,
|
|
382
|
-
description: a.description ?? "",
|
|
383
|
-
}));
|
|
384
|
-
if (spec.handler) {
|
|
385
|
-
_cmdHandlers.set(path, spec.handler);
|
|
386
|
-
_cmdArgSpecs.set(path, mappedArgs);
|
|
387
|
-
}
|
|
388
|
-
return {
|
|
389
|
-
name: spec.name,
|
|
390
|
-
description: spec.description ?? "",
|
|
391
|
-
subcommands: (spec.subcommands ?? []).map((s) => _specToInternal(s, path)),
|
|
392
|
-
args: mappedArgs,
|
|
393
|
-
has_handler: !!spec.handler,
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Handle CommandInvoke from the engine
|
|
398
|
-
// command_path includes the root name, e.g. ["myfoo"] or ["myfoo", "sub"]
|
|
399
|
-
async function _handleCommandInvoke(msg: Extract<_EngineMsg, { type: "CommandInvoke" }>): Promise<void> {
|
|
400
|
-
const handlerKey = msg.command_path.join(".");
|
|
401
|
-
const handler = _cmdHandlers.get(handlerKey);
|
|
402
|
-
const argSpecs = _cmdArgSpecs.get(handlerKey) ?? [];
|
|
403
|
-
|
|
404
|
-
// Build args keyed by name (falling back to index if spec is missing)
|
|
405
|
-
const argsRecord: Record<string, string | number | boolean> = {};
|
|
406
|
-
msg.args.forEach((v, i) => {
|
|
407
|
-
const name = argSpecs[i]?.name ?? String(i);
|
|
408
|
-
argsRecord[name] = v as string | number | boolean;
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
if (!handler) {
|
|
412
|
-
_send({ type: "CommandResponse", request_id: msg.request_id, output: [], error: `no handler registered for '${handlerKey}'` });
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
try {
|
|
417
|
-
const result = await handler(argsRecord);
|
|
418
|
-
const lines = Array.isArray(result) ? result : [result];
|
|
419
|
-
_send({ type: "CommandResponse", request_id: msg.request_id, output: lines, error: null });
|
|
420
|
-
} catch(error: unknown) {
|
|
421
|
-
const message = error instanceof Error ? error.message + "\n" + error.stack : String(error);
|
|
422
|
-
_send({ type: "CommandResponse", request_id: msg.request_id, output: [], error: message });
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/** Register a command that users can invoke from the developer console. */
|
|
427
|
-
export const Console = {
|
|
428
|
-
register(spec: CommandSpec): void {
|
|
429
|
-
if (!/^[a-z_]+$/.test(spec.name)) {
|
|
430
|
-
throw new Error(`Console.register: command name '${spec.name}' must match [a-z_]+`);
|
|
431
|
-
}
|
|
432
|
-
const internal = _specToInternal(spec, "");
|
|
433
|
-
_send({
|
|
434
|
-
type: "RegisterCommand",
|
|
435
|
-
name: internal.name,
|
|
436
|
-
description: internal.description,
|
|
437
|
-
subcommands: internal.subcommands,
|
|
438
|
-
args: internal.args,
|
|
439
|
-
has_handler: internal.has_handler,
|
|
440
|
-
});
|
|
441
|
-
},
|
|
442
|
-
};
|
|
443
|
-
|
package/tsconfig.json
DELETED