@vespura/vmenu-plugin 0.0.0-stage → 0.0.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/dist/client.js ADDED
@@ -0,0 +1,1907 @@
1
+ // src/version.ts
2
+ var version = true ? "0.0.1" : "0.0.0-local";
3
+
4
+ // src/text.ts
5
+ var Text = class _Text {
6
+ constructor(value, isKey, args) {
7
+ this.value = value;
8
+ this.isKey = isKey;
9
+ this.args = args;
10
+ }
11
+ /** Text shown exactly as written, never translated. */
12
+ static literal(text) {
13
+ return new _Text(text, false);
14
+ }
15
+ /**
16
+ * A translation key, optionally with named placeholder values. In the translated string,
17
+ * `{name}` is replaced with the matching value, which can itself be a literal or another key.
18
+ */
19
+ static key(key, args) {
20
+ return new _Text(key, true, args);
21
+ }
22
+ /** @internal */
23
+ static toRef(text) {
24
+ if (text === null || text === void 0) {
25
+ return void 0;
26
+ }
27
+ if (typeof text === "string") {
28
+ return { text };
29
+ }
30
+ if (!text.isKey) {
31
+ return { text: text.value };
32
+ }
33
+ const reference = { key: text.value };
34
+ if (text.args) {
35
+ const args = {};
36
+ let any = false;
37
+ for (const [name, value] of Object.entries(text.args)) {
38
+ const argument = _Text.toRef(value);
39
+ if (argument) {
40
+ args[name] = argument;
41
+ any = true;
42
+ }
43
+ }
44
+ if (any) {
45
+ reference.args = args;
46
+ }
47
+ }
48
+ return reference;
49
+ }
50
+ /** @internal */
51
+ static toRefs(options) {
52
+ const refs = [];
53
+ for (const option of options) {
54
+ const reference = _Text.toRef(option);
55
+ if (reference) {
56
+ refs.push(reference);
57
+ }
58
+ }
59
+ return refs;
60
+ }
61
+ };
62
+
63
+ // src/client/gate.ts
64
+ var Gate = class _Gate {
65
+ constructor(node) {
66
+ this.node = node;
67
+ }
68
+ /** One of the permissions your server side declared, by its short name. */
69
+ static permission(shortName) {
70
+ return new _Gate({ permission: shortName });
71
+ }
72
+ /** One of your bool settings: the item is available while the convar reads true. */
73
+ static setting(setting) {
74
+ return new _Gate({ setting: typeof setting === "string" ? setting : setting.name });
75
+ }
76
+ /** Passes when every gate passes. */
77
+ static all(...gates) {
78
+ return new _Gate({ all: gates.map(_Gate.toNode) });
79
+ }
80
+ /** Passes when at least one gate passes. */
81
+ static any(...gates) {
82
+ return new _Gate({ any: gates.map(_Gate.toNode) });
83
+ }
84
+ /** Passes when this gate and the other one both pass. */
85
+ and(other) {
86
+ return _Gate.all(this, other);
87
+ }
88
+ /** Passes when this gate or the other one passes. */
89
+ or(other) {
90
+ return _Gate.any(this, other);
91
+ }
92
+ /** @internal */
93
+ static toNode(gate) {
94
+ return typeof gate === "string" ? { permission: gate } : gate.node;
95
+ }
96
+ };
97
+
98
+ // src/fivem.ts
99
+ var natives = globalThis;
100
+ var currentResource = () => natives.GetCurrentResourceName();
101
+ var emitLocal = (eventName, ...args) => natives.emit(eventName, ...args);
102
+ var onLocal = (eventName, handler) => natives.on(eventName, handler);
103
+ var getConvar = (name, fallback) => natives.GetConvar(name, fallback) ?? fallback;
104
+ var getKvp = (key) => natives.GetResourceKvpString(key) ?? void 0;
105
+ var setKvp = (key, value) => natives.SetResourceKvp(key, value);
106
+ var log = {
107
+ info: (resource, message) => console.log(`[${resource}] ${message}`),
108
+ warn: (resource, message) => console.warn(`[${resource}] ${message}`),
109
+ error: (resource, message) => console.error(`[${resource}] ${message}`)
110
+ };
111
+ function parseJson(json) {
112
+ if (typeof json !== "string") {
113
+ return void 0;
114
+ }
115
+ try {
116
+ return JSON.parse(json);
117
+ } catch {
118
+ return void 0;
119
+ }
120
+ }
121
+ function sanitizeId(resource) {
122
+ return resource.replace(/[^A-Za-z0-9]/g, "_");
123
+ }
124
+
125
+ // src/format.ts
126
+ function formatFloat(value) {
127
+ const text = value.toFixed(4).replace(/0+$/, "");
128
+ return text.endsWith(".") ? `${text}0` : text;
129
+ }
130
+
131
+ // src/protocol.ts
132
+ var PROTOCOL_VERSION = 2;
133
+ var VMENU_RESOURCE = "vMenu.Enhanced";
134
+ var PluginEvents = {
135
+ Probe: "vMenu.Enhanced:Plugins:Probe",
136
+ Register: "vMenu.Enhanced:Plugins:Register",
137
+ Unregister: "vMenu.Enhanced:Plugins:Unregister",
138
+ Update: "vMenu.Enhanced:Plugins:Update",
139
+ Notify: "vMenu.Enhanced:Plugins:Notify",
140
+ Prompt: "vMenu.Enhanced:Plugins:Prompt",
141
+ SetTheme: "vMenu.Enhanced:Plugins:SetTheme",
142
+ RegisterThemes: "vMenu.Enhanced:Plugins:RegisterThemes",
143
+ Ready: "vMenu.Enhanced:Plugins:Ready",
144
+ ServerProbe: "vMenu.Enhanced:Plugins:Server:Probe",
145
+ ServerRegister: "vMenu.Enhanced:Plugins:Server:Register",
146
+ ServerDenied: "vMenu.Enhanced:Plugins:Server:Denied",
147
+ ServerReady: "vMenu.Enhanced:Plugins:Server:Ready",
148
+ readyFor: (resource) => `vMenu.Enhanced:Plugins:${resource}:Ready`,
149
+ registerResultFor: (resource) => `vMenu.Enhanced:Plugins:${resource}:RegisterResult`,
150
+ eventFor: (resource) => `vMenu.Enhanced:Plugins:${resource}:Event`,
151
+ promptResultFor: (resource) => `vMenu.Enhanced:Plugins:${resource}:PromptResult`,
152
+ themesFor: (resource) => `vMenu.Enhanced:Plugins:${resource}:Themes`,
153
+ themesRegisteredFor: (resource) => `vMenu.Enhanced:Plugins:${resource}:ThemesRegistered`,
154
+ serverReadyFor: (resource) => `vMenu.Enhanced:Plugins:${resource}:Server:Ready`,
155
+ serverRegisterResultFor: (resource) => `vMenu.Enhanced:Plugins:${resource}:Server:RegisterResult`
156
+ };
157
+ var EntryTypes = {
158
+ Button: "button",
159
+ Checkbox: "checkbox",
160
+ List: "list",
161
+ Slider: "slider",
162
+ DynamicList: "dynamicList",
163
+ Submenu: "submenu",
164
+ Separator: "separator",
165
+ ConfirmButton: "confirmButton",
166
+ ConfirmList: "confirmList"
167
+ };
168
+ var NodeEvents = {
169
+ Opened: "opened",
170
+ Closed: "closed",
171
+ IndexChanged: "indexChanged",
172
+ Highlighted: "highlighted"
173
+ };
174
+ var SettingTypes = {
175
+ Bool: "bool",
176
+ Int: "int",
177
+ Float: "float",
178
+ String: "string"
179
+ };
180
+ var CallbackTypes = {
181
+ ItemSelected: "itemSelected",
182
+ CheckboxChanged: "checkboxChanged",
183
+ ListIndexChanged: "listIndexChanged",
184
+ ListSelected: "listSelected",
185
+ SliderMoved: "sliderMoved",
186
+ SliderSelected: "sliderSelected",
187
+ DynamicSelected: "dynamicSelected",
188
+ DynamicChanging: "dynamicChanging",
189
+ Confirmed: "confirmed",
190
+ ItemHighlighted: "itemHighlighted",
191
+ MenuOpened: "menuOpened",
192
+ MenuClosed: "menuClosed",
193
+ MenuIndexChanged: "menuIndexChanged",
194
+ PlayerActionSelected: "playerActionSelected",
195
+ PlayerActionConfirmed: "playerActionConfirmed",
196
+ PlayerActionListSelected: "playerActionListSelected",
197
+ KeyPressed: "keyPressed"
198
+ };
199
+ var UpdateOps = {
200
+ SetText: "setText",
201
+ SetDescription: "setDescription",
202
+ SetLabel: "setLabel",
203
+ SetLockedDescription: "setLockedDescription",
204
+ SetConfirmationDescription: "setConfirmationDescription",
205
+ SetIcons: "setIcons",
206
+ SetChecked: "setChecked",
207
+ SetOptions: "setOptions",
208
+ SetSelectedIndex: "setSelectedIndex",
209
+ SetSliderPosition: "setSliderPosition",
210
+ SetValue: "setValue",
211
+ SetVisible: "setVisible",
212
+ SetEnabled: "setEnabled",
213
+ SetGate: "setGate",
214
+ SetLog: "setLog",
215
+ SetBehaviour: "setBehaviour",
216
+ SetItemEvents: "setItemEvents",
217
+ AddItems: "addItems",
218
+ RemoveItems: "removeItems",
219
+ ClearMenu: "clearMenu",
220
+ MoveItem: "moveItem",
221
+ AddPlayerActions: "addPlayerActions",
222
+ SetMenuTitle: "setMenuTitle",
223
+ SetMenuSubtitle: "setMenuSubtitle",
224
+ OpenMenu: "openMenu",
225
+ CloseMenu: "closeMenu",
226
+ SelectItem: "selectItem",
227
+ SetMenuEvents: "setMenuEvents",
228
+ SetFilter: "setFilter",
229
+ ClearFilter: "clearFilter",
230
+ AddKeys: "addKeys",
231
+ SetKeyText: "setKeyText",
232
+ SetKeyEnabled: "setKeyEnabled",
233
+ SetKeyGate: "setKeyGate",
234
+ MergeTranslations: "mergeTranslations"
235
+ };
236
+ function normalizeResult(raw) {
237
+ if (!raw || typeof raw !== "object") {
238
+ return void 0;
239
+ }
240
+ return {
241
+ accepted: raw.accepted === true,
242
+ protocolVersion: typeof raw.protocolVersion === "number" ? raw.protocolVersion : 0,
243
+ errors: Array.isArray(raw.errors) ? raw.errors : [],
244
+ warnings: Array.isArray(raw.warnings) ? raw.warnings : []
245
+ };
246
+ }
247
+
248
+ // src/client/settings.ts
249
+ var PluginSetting = class {
250
+ /** @internal */
251
+ constructor(name, fullName, defaultValue) {
252
+ this.name = name;
253
+ this.fullName = fullName;
254
+ this.defaultValue = defaultValue;
255
+ }
256
+ raw(fallback) {
257
+ return getConvar(this.fullName, fallback);
258
+ }
259
+ };
260
+ var PluginBoolSetting = class extends PluginSetting {
261
+ get value() {
262
+ return this.raw(this.defaultValue ? "true" : "false").toLowerCase() === "true";
263
+ }
264
+ };
265
+ var PluginIntSetting = class extends PluginSetting {
266
+ get value() {
267
+ const raw = this.raw("").trim();
268
+ return /^[+-]?\d+$/.test(raw) ? Number.parseInt(raw, 10) : this.defaultValue;
269
+ }
270
+ };
271
+ var PluginFloatSetting = class extends PluginSetting {
272
+ get value() {
273
+ const raw = this.raw("").trim();
274
+ const parsed = Number(raw);
275
+ return raw.length > 0 && Number.isFinite(parsed) ? parsed : this.defaultValue;
276
+ }
277
+ };
278
+ var PluginStringSetting = class extends PluginSetting {
279
+ get value() {
280
+ const raw = this.raw(this.defaultValue);
281
+ return raw.length === 0 ? this.defaultValue : raw;
282
+ }
283
+ };
284
+ var PluginSettings = class {
285
+ /** @internal */
286
+ constructor(pluginId) {
287
+ /** @internal */
288
+ this.nodes = [];
289
+ this.prefix = `vMenu.Enhanced.Plugins.${pluginId}.`;
290
+ }
291
+ /** Declares an on or off setting. */
292
+ bool(name, defaultValue, description) {
293
+ this.declare(name, SettingTypes.Bool, defaultValue ? "true" : "false", description);
294
+ return new PluginBoolSetting(name, this.prefix + name, defaultValue);
295
+ }
296
+ /** Declares a whole number setting. */
297
+ int(name, defaultValue, description) {
298
+ const value = Math.trunc(defaultValue);
299
+ this.declare(name, SettingTypes.Int, String(value), description);
300
+ return new PluginIntSetting(name, this.prefix + name, value);
301
+ }
302
+ /** Declares a decimal number setting. */
303
+ float(name, defaultValue, description) {
304
+ this.declare(name, SettingTypes.Float, formatFloat(defaultValue), description);
305
+ return new PluginFloatSetting(name, this.prefix + name, defaultValue);
306
+ }
307
+ /** Declares a text setting. */
308
+ string(name, defaultValue, description) {
309
+ this.declare(name, SettingTypes.String, defaultValue, description);
310
+ return new PluginStringSetting(name, this.prefix + name, defaultValue);
311
+ }
312
+ declare(name, type, defaultText, description) {
313
+ this.nodes.push({ name, type, default: defaultText, description });
314
+ }
315
+ };
316
+
317
+ // src/signal.ts
318
+ var Signal = class {
319
+ constructor(resource, name) {
320
+ this.resource = resource;
321
+ this.name = name;
322
+ this.handlers = [];
323
+ }
324
+ get hasHandlers() {
325
+ return this.handlers.length > 0;
326
+ }
327
+ add(handler) {
328
+ this.handlers.push(handler);
329
+ return () => {
330
+ this.handlers = this.handlers.filter((existing) => existing !== handler);
331
+ };
332
+ }
333
+ fire(...args) {
334
+ for (const handler of [...this.handlers]) {
335
+ try {
336
+ handler(...args);
337
+ } catch (error) {
338
+ log.error(this.resource(), `A ${this.name} handler threw: ${describe(error)}`);
339
+ }
340
+ }
341
+ }
342
+ };
343
+ function describe(error) {
344
+ return error instanceof Error ? error.stack ?? error.message : String(error);
345
+ }
346
+
347
+ // src/client/diff.ts
348
+ function sameText(left, right) {
349
+ if (left === right) {
350
+ return true;
351
+ }
352
+ if (!left || !right) {
353
+ return false;
354
+ }
355
+ return left.text === right.text && left.key === right.key && sameArgs(left.args, right.args);
356
+ }
357
+ function sameTexts(left, right) {
358
+ if (left === right) {
359
+ return true;
360
+ }
361
+ if (!left || !right || left.length !== right.length) {
362
+ return false;
363
+ }
364
+ return left.every((value, index) => sameText(value, right[index]));
365
+ }
366
+ function sameGate(left, right) {
367
+ if (left === right) {
368
+ return true;
369
+ }
370
+ if (!left || !right) {
371
+ return false;
372
+ }
373
+ return left.permission === right.permission && left.setting === right.setting && sameGates(left.all, right.all) && sameGates(left.any, right.any);
374
+ }
375
+ function sameGates(left, right) {
376
+ if (left === right) {
377
+ return true;
378
+ }
379
+ if (!left || !right || left.length !== right.length) {
380
+ return false;
381
+ }
382
+ return left.every((value, index) => sameGate(value, right[index]));
383
+ }
384
+ function sameArgs(left, right) {
385
+ if (left === right) {
386
+ return true;
387
+ }
388
+ if (!left || !right) {
389
+ return false;
390
+ }
391
+ const keys = Object.keys(left);
392
+ if (keys.length !== Object.keys(right).length) {
393
+ return false;
394
+ }
395
+ return keys.every((key) => key in right && sameText(left[key], right[key]));
396
+ }
397
+
398
+ // src/client/item.ts
399
+ var PluginItem = class {
400
+ /** @internal */
401
+ constructor(node, text) {
402
+ this.node = node;
403
+ this.textValue = text;
404
+ this.highlighted = this.signal("highlighted");
405
+ }
406
+ /** The row's id, how vMenu refers to it. */
407
+ get id() {
408
+ return this.node.id;
409
+ }
410
+ /** The row's text. */
411
+ get text() {
412
+ return this.textValue;
413
+ }
414
+ set text(value) {
415
+ this.textValue = value;
416
+ this.setTextField("text", value, UpdateOps.SetText);
417
+ }
418
+ /** The line shown under the menu while the row is highlighted. */
419
+ get description() {
420
+ return this.descriptionValue;
421
+ }
422
+ set description(value) {
423
+ this.descriptionValue = value;
424
+ this.setTextField("description", value, UpdateOps.SetDescription);
425
+ }
426
+ /** Right aligned text. Ignored by rows whose label the menu draws itself. */
427
+ get label() {
428
+ return this.labelValue;
429
+ }
430
+ set label(value) {
431
+ this.labelValue = value;
432
+ this.setTextField("label", value, UpdateOps.SetLabel);
433
+ }
434
+ /** What the row says while its gate locks it. Empty uses vMenu's own wording. */
435
+ get lockedDescription() {
436
+ return this.lockedDescriptionValue;
437
+ }
438
+ set lockedDescription(value) {
439
+ this.lockedDescriptionValue = value;
440
+ this.setTextField("lockedDescription", value, UpdateOps.SetLockedDescription);
441
+ }
442
+ /** Decides whether the row is available, evaluated live by vMenu. */
443
+ get gate() {
444
+ return this.gateValue;
445
+ }
446
+ set gate(value) {
447
+ this.gateValue = value;
448
+ const gate = value === void 0 ? void 0 : Gate.toNode(value);
449
+ if (sameGate(this.node.gate, gate)) {
450
+ return;
451
+ }
452
+ this.node.gate = gate;
453
+ this.emit({ op: UpdateOps.SetGate, itemId: this.id, gate });
454
+ }
455
+ /** What a failing gate does to the row: greyed out with a lock, or gone entirely. */
456
+ get hideWhenLocked() {
457
+ return this.node.behaviour?.toLowerCase() === "hide";
458
+ }
459
+ set hideWhenLocked(value) {
460
+ const behaviour = value ? "hide" : "lock";
461
+ if (this.node.behaviour === behaviour) {
462
+ return;
463
+ }
464
+ this.node.behaviour = behaviour;
465
+ this.emit({ op: UpdateOps.SetBehaviour, itemId: this.id, value: behaviour });
466
+ }
467
+ /** Whether the row is shown at all. */
468
+ get visible() {
469
+ return this.node.visible !== false;
470
+ }
471
+ set visible(value) {
472
+ if (this.visible === value) {
473
+ return;
474
+ }
475
+ this.node.visible = value;
476
+ this.emit({ op: UpdateOps.SetVisible, itemId: this.id, flag: value });
477
+ }
478
+ /** A disabled row is greyed out but still visible. Independent of the gate. */
479
+ get enabled() {
480
+ return this.node.enabled !== false;
481
+ }
482
+ set enabled(value) {
483
+ if (this.enabled === value) {
484
+ return;
485
+ }
486
+ this.node.enabled = value;
487
+ this.emit({ op: UpdateOps.SetEnabled, itemId: this.id, flag: value });
488
+ }
489
+ /**
490
+ * Ask vMenu to log use of this row to the server owner's webhook. Does nothing on its own: the
491
+ * plugin's server half has to declare the same id with `addLoggedItem`.
492
+ */
493
+ get log() {
494
+ return this.node.log === true;
495
+ }
496
+ set log(value) {
497
+ if (this.log === value) {
498
+ return;
499
+ }
500
+ this.node.log = value;
501
+ this.emit({ op: UpdateOps.SetLog, itemId: this.id, flag: value });
502
+ }
503
+ /** Icon names from the vMenu icon set, for example "LOCK" or "STAR". */
504
+ setIcons(leftIcon, rightIcon) {
505
+ if (this.node.leftIcon === leftIcon && this.node.rightIcon === rightIcon) {
506
+ return;
507
+ }
508
+ this.node.leftIcon = leftIcon;
509
+ this.node.rightIcon = rightIcon;
510
+ this.emit({ op: UpdateOps.SetIcons, itemId: this.id, leftIcon, rightIcon });
511
+ }
512
+ /** Called while the player's cursor sits on this row. Chatty, subscribe deliberately. */
513
+ onHighlighted(handler) {
514
+ const unsubscribe = this.highlighted.add(handler);
515
+ this.subscribeNodeEvent(NodeEvents.Highlighted);
516
+ return unsubscribe;
517
+ }
518
+ /** @internal */
519
+ applyOptions(options) {
520
+ if (!options) {
521
+ return;
522
+ }
523
+ if (options.description !== void 0) this.description = options.description;
524
+ if (options.label !== void 0) this.label = options.label;
525
+ if (options.lockedDescription !== void 0) this.lockedDescription = options.lockedDescription;
526
+ if (options.gate !== void 0) this.gate = options.gate;
527
+ if (options.hideWhenLocked !== void 0) this.hideWhenLocked = options.hideWhenLocked;
528
+ if (options.visible !== void 0) this.visible = options.visible;
529
+ if (options.enabled !== void 0) this.enabled = options.enabled;
530
+ if (options.log !== void 0) this.log = options.log;
531
+ if (options.leftIcon !== void 0 || options.rightIcon !== void 0) this.setIcons(options.leftIcon, options.rightIcon);
532
+ }
533
+ /** @internal */
534
+ handle(callback) {
535
+ if (callback.type === CallbackTypes.ItemHighlighted) {
536
+ this.highlighted.fire();
537
+ }
538
+ }
539
+ signal(name) {
540
+ return new Signal(() => this.plugin?.resource ?? "", name);
541
+ }
542
+ subscribeNodeEvent(name) {
543
+ var _a;
544
+ (_a = this.node).events ?? (_a.events = []);
545
+ if (this.node.events.includes(name)) {
546
+ return;
547
+ }
548
+ this.node.events.push(name);
549
+ this.emit({ op: UpdateOps.SetItemEvents, itemId: this.id, events: [...this.node.events] });
550
+ }
551
+ emit(op) {
552
+ this.plugin?.emitOp(op);
553
+ }
554
+ setTextField(field, value, opName) {
555
+ const next = Text.toRef(value);
556
+ if (sameText(this.node[field], next)) {
557
+ return;
558
+ }
559
+ this.node[field] = next;
560
+ this.emit({ op: opName, itemId: this.id, textValue: next });
561
+ }
562
+ };
563
+
564
+ // src/client/preferences.ts
565
+ var KeyPrefix = "vmenu_plugin_pref_";
566
+ function readBool(itemId) {
567
+ const raw = getKvp(KeyPrefix + itemId);
568
+ if (raw === "true") {
569
+ return true;
570
+ }
571
+ if (raw === "false") {
572
+ return false;
573
+ }
574
+ return void 0;
575
+ }
576
+ function writeBool(itemId, value) {
577
+ setKvp(KeyPrefix + itemId, value ? "true" : "false");
578
+ }
579
+
580
+ // src/client/items.ts
581
+ var PluginButton = class extends PluginItem {
582
+ constructor() {
583
+ super(...arguments);
584
+ this.selected = this.signal("selected");
585
+ }
586
+ /** Called when the player presses the row. */
587
+ onSelected(handler) {
588
+ return this.selected.add(handler);
589
+ }
590
+ /** @internal */
591
+ handle(callback) {
592
+ super.handle(callback);
593
+ if (callback.type === CallbackTypes.ItemSelected) {
594
+ this.selected.fire();
595
+ }
596
+ }
597
+ };
598
+ var PluginConfirmButton = class extends PluginItem {
599
+ constructor() {
600
+ super(...arguments);
601
+ this.confirmed = this.signal("confirmed");
602
+ }
603
+ /** What the row asks before its second press. Empty uses vMenu's own wording. */
604
+ get confirmationDescription() {
605
+ return this.confirmationDescriptionValue;
606
+ }
607
+ set confirmationDescription(value) {
608
+ this.confirmationDescriptionValue = value;
609
+ this.setTextField("confirmationDescription", value, UpdateOps.SetConfirmationDescription);
610
+ }
611
+ /** Called on the confirming second press, never on the first. */
612
+ onConfirmed(handler) {
613
+ return this.confirmed.add(handler);
614
+ }
615
+ /** @internal */
616
+ handle(callback) {
617
+ super.handle(callback);
618
+ if (callback.type === CallbackTypes.Confirmed) {
619
+ this.confirmed.fire();
620
+ }
621
+ }
622
+ };
623
+ var PluginCheckbox = class extends PluginItem {
624
+ constructor() {
625
+ super(...arguments);
626
+ this.changed = this.signal("changed");
627
+ /** @internal */
628
+ this.persistedValue = false;
629
+ }
630
+ /** Whether the state is saved in this resource's key value store and restored on start. */
631
+ get persisted() {
632
+ return this.persistedValue;
633
+ }
634
+ /** Whether the box is ticked. */
635
+ get checked() {
636
+ return this.node.checked === true;
637
+ }
638
+ set checked(value) {
639
+ if (this.checked === value) {
640
+ return;
641
+ }
642
+ this.node.checked = value;
643
+ this.remember(value);
644
+ this.emit({ op: UpdateOps.SetChecked, itemId: this.id, flag: value });
645
+ }
646
+ /** Called when the player toggles the box. The new state is already in `checked`. */
647
+ onChanged(handler) {
648
+ return this.changed.add(handler);
649
+ }
650
+ /** @internal */
651
+ handle(callback) {
652
+ super.handle(callback);
653
+ if (callback.type === CallbackTypes.CheckboxChanged && typeof callback.checked === "boolean") {
654
+ this.node.checked = callback.checked;
655
+ this.remember(callback.checked);
656
+ this.changed.fire(callback.checked);
657
+ }
658
+ }
659
+ remember(state) {
660
+ if (this.persistedValue) {
661
+ writeBool(this.id, state);
662
+ }
663
+ }
664
+ };
665
+ var PluginList = class extends PluginItem {
666
+ constructor() {
667
+ super(...arguments);
668
+ this.indexChanged = this.signal("indexChanged");
669
+ this.selected = this.signal("selected");
670
+ }
671
+ /** The selected option, counted from 0. */
672
+ get selectedIndex() {
673
+ return this.node.selectedIndex ?? 0;
674
+ }
675
+ set selectedIndex(value) {
676
+ if (this.selectedIndex === value) {
677
+ return;
678
+ }
679
+ this.node.selectedIndex = value;
680
+ this.emit({ op: UpdateOps.SetSelectedIndex, itemId: this.id, index: value });
681
+ }
682
+ /** Replaces the options, optionally moving the selection at the same time. */
683
+ setOptions(options, selectedIndex) {
684
+ const refs = Text.toRefs(options);
685
+ if (sameTexts(this.node.options, refs) && (selectedIndex === void 0 || selectedIndex === this.selectedIndex)) {
686
+ return;
687
+ }
688
+ this.node.options = refs;
689
+ if (selectedIndex !== void 0) {
690
+ this.node.selectedIndex = selectedIndex;
691
+ }
692
+ this.emit({ op: UpdateOps.SetOptions, itemId: this.id, options: refs, index: selectedIndex });
693
+ }
694
+ /** Called when the player scrolls the value. The new index is already in `selectedIndex`. */
695
+ onIndexChanged(handler) {
696
+ return this.indexChanged.add(handler);
697
+ }
698
+ /** Called when the player presses the row, with the index they had selected. */
699
+ onSelected(handler) {
700
+ return this.selected.add(handler);
701
+ }
702
+ /** @internal */
703
+ handle(callback) {
704
+ super.handle(callback);
705
+ switch (callback.type) {
706
+ case CallbackTypes.ListIndexChanged:
707
+ if (typeof callback.newIndex === "number") {
708
+ this.node.selectedIndex = callback.newIndex;
709
+ this.indexChanged.fire(callback.oldIndex ?? 0, callback.newIndex);
710
+ }
711
+ break;
712
+ case CallbackTypes.ListSelected:
713
+ if (typeof callback.selectedIndex === "number") {
714
+ this.selected.fire(callback.selectedIndex);
715
+ }
716
+ break;
717
+ case CallbackTypes.Confirmed:
718
+ if (typeof callback.selectedIndex === "number") {
719
+ this.raiseConfirmed(callback.selectedIndex);
720
+ }
721
+ break;
722
+ }
723
+ }
724
+ raiseConfirmed(_index) {
725
+ }
726
+ };
727
+ var PluginConfirmList = class extends PluginList {
728
+ constructor() {
729
+ super(...arguments);
730
+ this.confirmed = this.signal("confirmed");
731
+ }
732
+ /** What the row asks before its second press. Empty uses vMenu's own wording. */
733
+ get confirmationDescription() {
734
+ return this.confirmationDescriptionValue;
735
+ }
736
+ set confirmationDescription(value) {
737
+ this.confirmationDescriptionValue = value;
738
+ this.setTextField("confirmationDescription", value, UpdateOps.SetConfirmationDescription);
739
+ }
740
+ /** Called on the confirming second press, with the index that was confirmed. */
741
+ onConfirmed(handler) {
742
+ return this.confirmed.add(handler);
743
+ }
744
+ raiseConfirmed(index) {
745
+ this.confirmed.fire(index);
746
+ }
747
+ };
748
+ var PluginSlider = class extends PluginItem {
749
+ constructor() {
750
+ super(...arguments);
751
+ this.moved = this.signal("moved");
752
+ this.selected = this.signal("selected");
753
+ }
754
+ /** The lowest position. */
755
+ get min() {
756
+ return this.node.min ?? 0;
757
+ }
758
+ /** The highest position. */
759
+ get max() {
760
+ return this.node.max ?? 0;
761
+ }
762
+ /** Where the bar sits. */
763
+ get position() {
764
+ return this.node.position ?? this.min;
765
+ }
766
+ set position(value) {
767
+ if (this.position === value) {
768
+ return;
769
+ }
770
+ this.node.position = value;
771
+ this.emit({ op: UpdateOps.SetSliderPosition, itemId: this.id, index: value });
772
+ }
773
+ /** Called while the player drags the bar. The new position is already in `position`. */
774
+ onMoved(handler) {
775
+ return this.moved.add(handler);
776
+ }
777
+ /** Called when the player presses the row, with the position it sat at. */
778
+ onSelected(handler) {
779
+ return this.selected.add(handler);
780
+ }
781
+ /** @internal */
782
+ handle(callback) {
783
+ super.handle(callback);
784
+ switch (callback.type) {
785
+ case CallbackTypes.SliderMoved:
786
+ if (typeof callback.newPosition === "number") {
787
+ this.node.position = callback.newPosition;
788
+ this.moved.fire(callback.oldPosition ?? 0, callback.newPosition);
789
+ }
790
+ break;
791
+ case CallbackTypes.SliderSelected:
792
+ if (typeof callback.position === "number") {
793
+ this.selected.fire(callback.position);
794
+ }
795
+ break;
796
+ }
797
+ }
798
+ };
799
+ var PluginDynamicList = class extends PluginItem {
800
+ constructor() {
801
+ super(...arguments);
802
+ this.selected = this.signal("selected");
803
+ }
804
+ /** The value the row shows. */
805
+ get value() {
806
+ return this.node.value ?? "";
807
+ }
808
+ set value(value) {
809
+ if (this.value === value) {
810
+ return;
811
+ }
812
+ this.node.value = value;
813
+ this.emit({ op: UpdateOps.SetValue, itemId: this.id, value });
814
+ }
815
+ /** Called when the player presses the row, with the value it showed. */
816
+ onSelected(handler) {
817
+ return this.selected.add(handler);
818
+ }
819
+ /** @internal */
820
+ handle(callback) {
821
+ super.handle(callback);
822
+ switch (callback.type) {
823
+ case CallbackTypes.DynamicChanging:
824
+ if (typeof callback.left === "boolean") {
825
+ const next = this.changeRequested?.(callback.currentValue ?? "", callback.left);
826
+ if (typeof next === "string") {
827
+ this.value = next;
828
+ }
829
+ }
830
+ break;
831
+ case CallbackTypes.DynamicSelected:
832
+ this.selected.fire(callback.value ?? "");
833
+ break;
834
+ }
835
+ }
836
+ };
837
+ var PluginSeparator = class extends PluginItem {
838
+ };
839
+ var PluginSubmenu = class extends PluginItem {
840
+ /** @internal */
841
+ constructor(node, text, menu) {
842
+ super(node, text);
843
+ this.menu = menu;
844
+ }
845
+ };
846
+
847
+ // src/client/key.ts
848
+ var PluginKey = class {
849
+ /** @internal */
850
+ constructor(plugin, node, text) {
851
+ this.plugin = plugin;
852
+ this.node = node;
853
+ this.textValue = text;
854
+ this.pressed = new Signal(() => plugin.resource, "pressed");
855
+ }
856
+ /** The key's id, which also names the binding in the player's key settings. */
857
+ get id() {
858
+ return this.node.id;
859
+ }
860
+ /** The instructional button's label. Empty hides the button, the key still works. */
861
+ get text() {
862
+ return this.textValue;
863
+ }
864
+ set text(value) {
865
+ this.textValue = value;
866
+ const text = Text.toRef(value);
867
+ if (sameText(this.node.text, text)) {
868
+ return;
869
+ }
870
+ this.node.text = text;
871
+ this.plugin.emitOp({ op: UpdateOps.SetKeyText, keyId: this.id, textValue: text });
872
+ }
873
+ /** A disabled key does nothing and shows no button. */
874
+ get enabled() {
875
+ return this.node.enabled !== false;
876
+ }
877
+ set enabled(value) {
878
+ if (this.enabled === value) {
879
+ return;
880
+ }
881
+ this.node.enabled = value;
882
+ this.plugin.emitOp({ op: UpdateOps.SetKeyEnabled, keyId: this.id, flag: value });
883
+ }
884
+ /** While the gate fails the key does nothing and shows no button. */
885
+ get gate() {
886
+ return this.gateValue;
887
+ }
888
+ set gate(value) {
889
+ this.gateValue = value;
890
+ const gate = value === void 0 ? void 0 : Gate.toNode(value);
891
+ if (sameGate(this.node.gate, gate)) {
892
+ return;
893
+ }
894
+ this.node.gate = gate;
895
+ this.plugin.emitOp({ op: UpdateOps.SetKeyGate, keyId: this.id, gate });
896
+ }
897
+ /** Called when the player presses the key, with the row the cursor was on. */
898
+ onPressed(handler) {
899
+ return this.pressed.add(handler);
900
+ }
901
+ /** @internal */
902
+ handle(press) {
903
+ this.pressed.fire(press);
904
+ }
905
+ };
906
+
907
+ // src/client/menu.ts
908
+ var PluginMenu = class _PluginMenu {
909
+ /** @internal */
910
+ constructor(plugin, node, title, subtitle) {
911
+ this.plugin = plugin;
912
+ this.itemList = [];
913
+ this.keyList = [];
914
+ this.node = node;
915
+ this.titleValue = title;
916
+ this.subtitleValue = subtitle;
917
+ this.opened = new Signal(() => plugin.resource, "opened");
918
+ this.closed = new Signal(() => plugin.resource, "closed");
919
+ this.indexChanged = new Signal(() => plugin.resource, "indexChanged");
920
+ }
921
+ /** The menu's id, how vMenu refers to it. */
922
+ get id() {
923
+ return this.node.id;
924
+ }
925
+ /** Every row, in order. */
926
+ get items() {
927
+ return this.itemList;
928
+ }
929
+ /** Every key of this menu. */
930
+ get keys() {
931
+ return this.keyList;
932
+ }
933
+ /** Whether `filter` is hiding rows right now. */
934
+ get isFiltered() {
935
+ return this.filterFn !== void 0;
936
+ }
937
+ /** The title in the menu's banner. */
938
+ get title() {
939
+ return this.titleValue;
940
+ }
941
+ set title(value) {
942
+ this.titleValue = value;
943
+ const title = Text.toRef(value);
944
+ if (sameText(this.node.title, title)) {
945
+ return;
946
+ }
947
+ this.node.title = title;
948
+ this.plugin.emitOp({ op: UpdateOps.SetMenuTitle, menuId: this.id, textValue: title });
949
+ }
950
+ /** The bar under the banner. */
951
+ get subtitle() {
952
+ return this.subtitleValue;
953
+ }
954
+ set subtitle(value) {
955
+ this.subtitleValue = value;
956
+ const subtitle = Text.toRef(value);
957
+ if (sameText(this.node.subtitle, subtitle)) {
958
+ return;
959
+ }
960
+ this.node.subtitle = subtitle;
961
+ this.plugin.emitOp({ op: UpdateOps.SetMenuSubtitle, menuId: this.id, textValue: subtitle });
962
+ }
963
+ /** Called when the player opens this menu. */
964
+ onOpened(handler) {
965
+ const unsubscribe = this.opened.add(handler);
966
+ this.subscribeMenuEvent(NodeEvents.Opened);
967
+ return unsubscribe;
968
+ }
969
+ /** Called when the player leaves this menu, including into a submenu. */
970
+ onClosed(handler) {
971
+ const unsubscribe = this.closed.add(handler);
972
+ this.subscribeMenuEvent(NodeEvents.Closed);
973
+ return unsubscribe;
974
+ }
975
+ /** Called when the cursor moves, with the old and new row index counted from 0. Chatty. */
976
+ onIndexChanged(handler) {
977
+ const unsubscribe = this.indexChanged.add(handler);
978
+ this.subscribeMenuEvent(NodeEvents.IndexChanged);
979
+ return unsubscribe;
980
+ }
981
+ /** Adds a row the player presses. */
982
+ addButton(text, options) {
983
+ return this.attach(new PluginButton(this.newNode(EntryTypes.Button, text, options), text), options);
984
+ }
985
+ /** Adds a row that asks for a second press before it does anything. */
986
+ addConfirmButton(text, options) {
987
+ const item = new PluginConfirmButton(this.newNode(EntryTypes.ConfirmButton, text, options), text);
988
+ if (options?.confirmationDescription !== void 0) {
989
+ item.confirmationDescription = options.confirmationDescription;
990
+ }
991
+ return this.attach(item, options);
992
+ }
993
+ /** Adds a row with a tick box. */
994
+ addCheckbox(text, options) {
995
+ const node = this.newNode(EntryTypes.Checkbox, text, options);
996
+ node.checked = options?.checked ?? false;
997
+ const checkbox = new PluginCheckbox(node, text);
998
+ if (options?.persist) {
999
+ checkbox.persistedValue = true;
1000
+ const stored = readBool(node.id);
1001
+ if (stored !== void 0) {
1002
+ node.checked = stored;
1003
+ }
1004
+ }
1005
+ return this.attach(checkbox, options);
1006
+ }
1007
+ /** Adds a row the player scrolls through a set of options on. */
1008
+ addList(text, values, options) {
1009
+ const node = this.newNode(EntryTypes.List, text, options);
1010
+ node.options = Text.toRefs(values);
1011
+ node.selectedIndex = options?.selectedIndex ?? 0;
1012
+ return this.attach(new PluginList(node, text), options);
1013
+ }
1014
+ /** Adds a list that asks for a second press before it does anything. */
1015
+ addConfirmList(text, values, options) {
1016
+ const node = this.newNode(EntryTypes.ConfirmList, text, options);
1017
+ node.options = Text.toRefs(values);
1018
+ node.selectedIndex = options?.selectedIndex ?? 0;
1019
+ const item = new PluginConfirmList(node, text);
1020
+ if (options?.confirmationDescription !== void 0) {
1021
+ item.confirmationDescription = options.confirmationDescription;
1022
+ }
1023
+ return this.attach(item, options);
1024
+ }
1025
+ /** Adds a row with a bar the player drags between `min` and `max`. */
1026
+ addSlider(text, min, max, position, options) {
1027
+ const node = this.newNode(EntryTypes.Slider, text, options);
1028
+ node.min = min;
1029
+ node.max = max;
1030
+ node.position = position;
1031
+ node.showDivider = options?.showDivider ?? false;
1032
+ return this.attach(new PluginSlider(node, text), options);
1033
+ }
1034
+ /** Adds a row whose value your code works out each time the player scrolls it, through `changeRequested`. */
1035
+ addDynamicList(text, initialValue, options) {
1036
+ const node = this.newNode(EntryTypes.DynamicList, text, options);
1037
+ node.value = initialValue;
1038
+ return this.attach(new PluginDynamicList(node, text), options);
1039
+ }
1040
+ /** Adds a row that only shows text, used to split a menu into sections. */
1041
+ addSeparator(text, options) {
1042
+ return this.attach(new PluginSeparator(this.newNode(EntryTypes.Separator, text, options), text), options);
1043
+ }
1044
+ /** Adds a row that opens a new menu, reached through the returned row's `menu`. */
1045
+ addSubmenu(text, options) {
1046
+ const node = this.newNode(EntryTypes.Submenu, text, options);
1047
+ const title = options?.title ?? text;
1048
+ node.menu = {
1049
+ id: this.plugin.nextMenuId(),
1050
+ title: Text.toRef(title),
1051
+ subtitle: Text.toRef(options?.subtitle),
1052
+ items: []
1053
+ };
1054
+ const menu = new _PluginMenu(this.plugin, node.menu, title, options?.subtitle);
1055
+ this.plugin.registerMenu(menu);
1056
+ return this.attach(new PluginSubmenu(node, text, menu), options);
1057
+ }
1058
+ /**
1059
+ * Adds a key that works while this menu is open, with an instructional button at the bottom of the
1060
+ * screen. Keep the id stable: it names the binding in the player's key settings, so changing it
1061
+ * loses a key they picked themselves.
1062
+ * @param id Letters, digits and underscores, unique within your plugin.
1063
+ * @param text The instructional button's label.
1064
+ * @param defaultKey A keyboard key name as the game knows it, for example "X" or "F5".
1065
+ */
1066
+ addKey(id, text, defaultKey, options) {
1067
+ var _a;
1068
+ const node = {
1069
+ id,
1070
+ text: Text.toRef(text),
1071
+ description: Text.toRef(options?.description),
1072
+ defaultKey,
1073
+ defaultButton: options?.defaultButton,
1074
+ shadowedControl: options?.shadowedControl
1075
+ };
1076
+ (_a = this.node).keys ?? (_a.keys = []);
1077
+ this.node.keys.push(node);
1078
+ const key = new PluginKey(this.plugin, node, text);
1079
+ this.keyList.push(key);
1080
+ this.plugin.registerKey(key);
1081
+ this.plugin.emitOp({ op: UpdateOps.AddKeys, menuId: this.id, keys: [node] });
1082
+ return key;
1083
+ }
1084
+ /**
1085
+ * Rows added inside `add` go in at `index`, counted from 0, one after another, instead of at the
1086
+ * bottom. Adding goes back to the bottom once `add` returns.
1087
+ */
1088
+ insertAt(index, add) {
1089
+ const previous = this.insertIndex;
1090
+ this.insertIndex = clamp(index, 0, this.itemList.length);
1091
+ try {
1092
+ add();
1093
+ } finally {
1094
+ this.insertIndex = previous;
1095
+ }
1096
+ }
1097
+ /** Moves a row of this menu to `index`, counted from 0. A submenu row keeps its menu, and the highlighted row stays highlighted. */
1098
+ move(item, index) {
1099
+ const from = this.itemList.indexOf(item);
1100
+ if (from < 0) {
1101
+ return;
1102
+ }
1103
+ const to = clamp(index, 0, this.itemList.length - 1);
1104
+ if (to === from) {
1105
+ return;
1106
+ }
1107
+ this.itemList.splice(from, 1);
1108
+ this.itemList.splice(to, 0, item);
1109
+ this.node.items.splice(from, 1);
1110
+ this.node.items.splice(to, 0, item.node);
1111
+ const before = to + 1 < this.itemList.length ? this.itemList[to + 1].id : void 0;
1112
+ this.plugin.emitOp({ op: UpdateOps.MoveItem, itemId: item.id, beforeItemId: before });
1113
+ }
1114
+ /**
1115
+ * Shows only the rows `keep` answers true for. Rows added later are checked too: inside a batch
1116
+ * when the batch ends, so properties set right after adding count, otherwise as they come in. Call
1117
+ * it again after changing what it looks at.
1118
+ */
1119
+ filter(keep) {
1120
+ this.filterFn = keep;
1121
+ this.plugin.filterChanged(this);
1122
+ }
1123
+ /** Shows every row again after `filter`. */
1124
+ clearFilter() {
1125
+ if (this.filterFn === void 0) {
1126
+ return;
1127
+ }
1128
+ this.filterFn = void 0;
1129
+ this.plugin.filterChanged(this);
1130
+ }
1131
+ /** Removes one row. For a submenu row, everything beneath it goes too. */
1132
+ remove(item) {
1133
+ if (!this.removeLocal(item)) {
1134
+ return;
1135
+ }
1136
+ this.plugin.emitOp({ op: UpdateOps.RemoveItems, itemIds: [item.id] });
1137
+ }
1138
+ /** Removes every row. */
1139
+ clear() {
1140
+ for (const item of this.itemList) {
1141
+ this.plugin.unregisterItem(item);
1142
+ }
1143
+ this.itemList.length = 0;
1144
+ this.node.items.length = 0;
1145
+ this.plugin.emitOp({ op: UpdateOps.ClearMenu, menuId: this.id });
1146
+ }
1147
+ /** Opens this menu on screen, closing whatever vMenu menu was open. */
1148
+ open() {
1149
+ this.plugin.emitOp({ op: UpdateOps.OpenMenu, menuId: this.id });
1150
+ }
1151
+ /** Closes this plugin's menu if one is open. */
1152
+ close() {
1153
+ this.plugin.emitOp({ op: UpdateOps.CloseMenu, menuId: this.id });
1154
+ }
1155
+ /**
1156
+ * Moves the cursor to a row of this menu, as if the player had moved there. Does nothing for a
1157
+ * hidden row or one from another menu.
1158
+ */
1159
+ select(item) {
1160
+ if (this.itemList.includes(item)) {
1161
+ this.plugin.emitOp({ op: UpdateOps.SelectItem, menuId: this.id, itemId: item.id });
1162
+ }
1163
+ }
1164
+ /** @internal */
1165
+ get hasFilter() {
1166
+ return this.filterFn !== void 0;
1167
+ }
1168
+ /** @internal */
1169
+ hides(item) {
1170
+ return this.filterFn !== void 0 && !this.filterFn(item);
1171
+ }
1172
+ /** @internal */
1173
+ filterOp() {
1174
+ const keep = this.filterFn;
1175
+ if (keep === void 0) {
1176
+ return { op: UpdateOps.ClearFilter, menuId: this.id };
1177
+ }
1178
+ return { op: UpdateOps.SetFilter, menuId: this.id, itemIds: this.itemList.filter((item) => !keep(item)).map((item) => item.id) };
1179
+ }
1180
+ /** @internal */
1181
+ handleMenu(callback) {
1182
+ switch (callback.type) {
1183
+ case CallbackTypes.MenuOpened:
1184
+ this.opened.fire();
1185
+ break;
1186
+ case CallbackTypes.MenuClosed:
1187
+ this.closed.fire();
1188
+ break;
1189
+ case CallbackTypes.MenuIndexChanged:
1190
+ if (typeof callback.newIndex === "number") {
1191
+ this.indexChanged.fire(callback.oldIndex ?? 0, callback.newIndex);
1192
+ }
1193
+ break;
1194
+ }
1195
+ }
1196
+ /** @internal */
1197
+ static asAdded(node) {
1198
+ if (!node.menu) {
1199
+ return node;
1200
+ }
1201
+ const { items: _items, ...menu } = node.menu;
1202
+ return { ...node, menu: { ...menu, items: [] } };
1203
+ }
1204
+ newNode(type, text, options) {
1205
+ return { id: options?.id ?? this.plugin.nextItemId(), type, text: Text.toRef(text) };
1206
+ }
1207
+ attach(item, options) {
1208
+ item.applyOptions(options);
1209
+ item.plugin = this.plugin;
1210
+ let before;
1211
+ if (this.insertIndex !== void 0 && this.insertIndex < this.itemList.length) {
1212
+ before = this.itemList[this.insertIndex].id;
1213
+ this.itemList.splice(this.insertIndex, 0, item);
1214
+ this.node.items.splice(this.insertIndex, 0, item.node);
1215
+ this.insertIndex++;
1216
+ } else {
1217
+ this.itemList.push(item);
1218
+ this.node.items.push(item.node);
1219
+ if (this.insertIndex !== void 0) {
1220
+ this.insertIndex = this.itemList.length;
1221
+ }
1222
+ }
1223
+ this.plugin.registerItem(item);
1224
+ this.plugin.emitAdd(this, item, { op: UpdateOps.AddItems, menuId: this.id, items: [item.node], beforeItemId: before });
1225
+ return item;
1226
+ }
1227
+ removeLocal(item) {
1228
+ const index = this.itemList.lastIndexOf(item);
1229
+ if (index < 0) {
1230
+ return false;
1231
+ }
1232
+ this.itemList.splice(index, 1);
1233
+ const nodeIndex = this.node.items.lastIndexOf(item.node);
1234
+ if (nodeIndex >= 0) {
1235
+ this.node.items.splice(nodeIndex, 1);
1236
+ }
1237
+ this.plugin.unregisterItem(item);
1238
+ return true;
1239
+ }
1240
+ subscribeMenuEvent(name) {
1241
+ var _a;
1242
+ (_a = this.node).events ?? (_a.events = []);
1243
+ if (this.node.events.includes(name)) {
1244
+ return;
1245
+ }
1246
+ this.node.events.push(name);
1247
+ this.plugin.emitOp({ op: UpdateOps.SetMenuEvents, menuId: this.id, events: [...this.node.events] });
1248
+ }
1249
+ };
1250
+ function clamp(value, min, max) {
1251
+ return Math.min(Math.max(value, min), max);
1252
+ }
1253
+
1254
+ // src/client/playerActions.ts
1255
+ function target(callback) {
1256
+ return typeof callback.targetServerId === "number" ? { serverId: callback.targetServerId, name: callback.targetName ?? "" } : void 0;
1257
+ }
1258
+ var PluginPlayerButton = class extends PluginItem {
1259
+ constructor() {
1260
+ super(...arguments);
1261
+ this.selected = this.signal("selected");
1262
+ }
1263
+ /** Called when the action is used on a player, with that player as the target. */
1264
+ onSelected(handler) {
1265
+ return this.selected.add(handler);
1266
+ }
1267
+ /** @internal */
1268
+ handle(callback) {
1269
+ super.handle(callback);
1270
+ const player = target(callback);
1271
+ if (callback.type === CallbackTypes.PlayerActionSelected && player) {
1272
+ this.selected.fire(player);
1273
+ }
1274
+ }
1275
+ };
1276
+ var PluginPlayerConfirmButton = class extends PluginItem {
1277
+ constructor() {
1278
+ super(...arguments);
1279
+ this.confirmed = this.signal("confirmed");
1280
+ }
1281
+ /** What the row asks before its second press. Empty uses vMenu's own wording. */
1282
+ get confirmationDescription() {
1283
+ return this.confirmationDescriptionValue;
1284
+ }
1285
+ set confirmationDescription(value) {
1286
+ this.confirmationDescriptionValue = value;
1287
+ this.node.confirmationDescription = Text.toRef(value);
1288
+ this.emit({
1289
+ op: UpdateOps.SetConfirmationDescription,
1290
+ itemId: this.id,
1291
+ textValue: this.node.confirmationDescription
1292
+ });
1293
+ }
1294
+ /** Called on the confirming second press, with the targeted player. */
1295
+ onConfirmed(handler) {
1296
+ return this.confirmed.add(handler);
1297
+ }
1298
+ /** @internal */
1299
+ handle(callback) {
1300
+ super.handle(callback);
1301
+ const player = target(callback);
1302
+ if (callback.type === CallbackTypes.PlayerActionConfirmed && player) {
1303
+ this.confirmed.fire(player);
1304
+ }
1305
+ }
1306
+ };
1307
+ var PluginPlayerList = class extends PluginItem {
1308
+ constructor() {
1309
+ super(...arguments);
1310
+ this.selected = this.signal("selected");
1311
+ }
1312
+ /** The current selection, counted from 0. Shared across every player the menu shows, since the same rows serve them all. */
1313
+ get selectedIndex() {
1314
+ return this.node.selectedIndex ?? 0;
1315
+ }
1316
+ set selectedIndex(value) {
1317
+ this.node.selectedIndex = value;
1318
+ this.emit({ op: UpdateOps.SetSelectedIndex, itemId: this.id, index: value });
1319
+ }
1320
+ /** Replaces the options, optionally moving the selection at the same time. */
1321
+ setOptions(options, selectedIndex) {
1322
+ this.node.options = Text.toRefs(options);
1323
+ if (selectedIndex !== void 0) {
1324
+ this.node.selectedIndex = selectedIndex;
1325
+ }
1326
+ this.emit({ op: UpdateOps.SetOptions, itemId: this.id, options: this.node.options, index: selectedIndex });
1327
+ }
1328
+ /** Called when the action is used on a player, with the target and the chosen index. */
1329
+ onSelected(handler) {
1330
+ return this.selected.add(handler);
1331
+ }
1332
+ /** @internal */
1333
+ handle(callback) {
1334
+ super.handle(callback);
1335
+ const player = target(callback);
1336
+ if (callback.type === CallbackTypes.PlayerActionListSelected && player) {
1337
+ if (typeof callback.selectedIndex === "number") {
1338
+ this.node.selectedIndex = callback.selectedIndex;
1339
+ }
1340
+ this.selected.fire(player, callback.selectedIndex ?? 0);
1341
+ }
1342
+ }
1343
+ };
1344
+ var PluginPlayerActions = class {
1345
+ /** @internal */
1346
+ constructor(plugin) {
1347
+ this.plugin = plugin;
1348
+ this.itemList = [];
1349
+ /** @internal */
1350
+ this.nodes = [];
1351
+ }
1352
+ /** Every action, in order. */
1353
+ get items() {
1354
+ return this.itemList;
1355
+ }
1356
+ /** Adds an action the player presses. */
1357
+ addButton(text, options) {
1358
+ return this.attach(new PluginPlayerButton(this.newNode(EntryTypes.Button, text, options), text), options);
1359
+ }
1360
+ /** Adds an action that asks for a second press. */
1361
+ addConfirmButton(text, options) {
1362
+ const item = new PluginPlayerConfirmButton(this.newNode(EntryTypes.ConfirmButton, text, options), text);
1363
+ if (options?.confirmationDescription !== void 0) {
1364
+ item.confirmationDescription = options.confirmationDescription;
1365
+ }
1366
+ return this.attach(item, options);
1367
+ }
1368
+ /** Adds an action with a set of options. */
1369
+ addList(text, values, options) {
1370
+ const node = this.newNode(EntryTypes.List, text, options);
1371
+ node.options = Text.toRefs(values);
1372
+ node.selectedIndex = options?.selectedIndex ?? 0;
1373
+ return this.attach(new PluginPlayerList(node, text), options);
1374
+ }
1375
+ /** Adds a row that only shows text. */
1376
+ addSeparator(text, options) {
1377
+ return this.attach(new PluginSeparator(this.newNode(EntryTypes.Separator, text, options), text), options);
1378
+ }
1379
+ /** Removes one action from every player's entry. */
1380
+ remove(item) {
1381
+ const index = this.itemList.indexOf(item);
1382
+ if (index < 0) {
1383
+ return;
1384
+ }
1385
+ this.itemList.splice(index, 1);
1386
+ const nodeIndex = this.nodes.indexOf(item.node);
1387
+ if (nodeIndex >= 0) {
1388
+ this.nodes.splice(nodeIndex, 1);
1389
+ }
1390
+ this.plugin.unregisterItem(item);
1391
+ this.plugin.emitOp({ op: UpdateOps.RemoveItems, itemIds: [item.id] });
1392
+ }
1393
+ newNode(type, text, options) {
1394
+ return { id: options?.id ?? this.plugin.nextItemId(), type, text: Text.toRef(text) };
1395
+ }
1396
+ attach(item, options) {
1397
+ item.applyOptions(options);
1398
+ item.plugin = this.plugin;
1399
+ this.itemList.push(item);
1400
+ this.nodes.push(item.node);
1401
+ this.plugin.registerItem(item);
1402
+ this.plugin.emitOp({ op: UpdateOps.AddPlayerActions, items: [item.node] });
1403
+ return item;
1404
+ }
1405
+ };
1406
+
1407
+ // src/client/themes.ts
1408
+ var PluginThemes = class {
1409
+ /** @internal */
1410
+ constructor(plugin) {
1411
+ this.plugin = plugin;
1412
+ this.availableValue = [];
1413
+ this.overriddenValue = false;
1414
+ this.changed = new Signal(() => plugin.resource, "themes changed");
1415
+ }
1416
+ /** Every theme vMenu offers, in the order it lists them. Empty until vMenu has sent them. */
1417
+ get available() {
1418
+ return this.availableValue;
1419
+ }
1420
+ /** The id of the theme on screen, undefined until vMenu has said what it is. */
1421
+ get currentId() {
1422
+ return this.currentIdValue;
1423
+ }
1424
+ /** The id the server's own setting asks for, which is where `reset` goes. */
1425
+ get configuredId() {
1426
+ return this.configuredIdValue;
1427
+ }
1428
+ /** Whether a plugin is overriding the server's setting for this player right now. */
1429
+ get isOverridden() {
1430
+ return this.overriddenValue;
1431
+ }
1432
+ /** Called whenever the list or the theme on screen changed, including when somebody else changed it. */
1433
+ onChanged(handler) {
1434
+ return this.changed.add(handler);
1435
+ }
1436
+ /** Puts vMenu's menus in a theme for this player. An id vMenu does not know is ignored. */
1437
+ set(themeId) {
1438
+ this.send(themeId);
1439
+ }
1440
+ /** Drops the override and goes back to the theme the server's setting asks for. */
1441
+ reset() {
1442
+ this.send(void 0);
1443
+ }
1444
+ /** @internal */
1445
+ handle(json) {
1446
+ const list = parseJson(json);
1447
+ if (!list) {
1448
+ return;
1449
+ }
1450
+ const current = list.current?.toLowerCase();
1451
+ this.availableValue = (list.themes ?? []).map((theme) => ({
1452
+ id: theme.id,
1453
+ name: theme.name,
1454
+ isCurrent: theme.id.toLowerCase() === current
1455
+ }));
1456
+ this.currentIdValue = list.current ?? void 0;
1457
+ this.configuredIdValue = list.configured ?? void 0;
1458
+ this.overriddenValue = list.overridden === true;
1459
+ this.changed.fire();
1460
+ }
1461
+ send(themeId) {
1462
+ if (!this.plugin.isConnected) {
1463
+ return;
1464
+ }
1465
+ emitLocal(PluginEvents.SetTheme, JSON.stringify({ theme: themeId ?? null }));
1466
+ }
1467
+ };
1468
+
1469
+ // src/client/translations.ts
1470
+ var PluginTranslations = class {
1471
+ /** @internal */
1472
+ constructor(plugin) {
1473
+ this.plugin = plugin;
1474
+ /** @internal */
1475
+ this.tables = {};
1476
+ }
1477
+ /** Adds or extends one language's table. Later entries win over earlier ones. */
1478
+ add(languageCode, entries) {
1479
+ var _a;
1480
+ const code = languageCode.trim().toLowerCase();
1481
+ const table = (_a = this.tables)[code] ?? (_a[code] = {});
1482
+ const merged = {};
1483
+ for (const [key, value] of Object.entries(entries)) {
1484
+ table[key] = value;
1485
+ merged[key] = value;
1486
+ }
1487
+ this.plugin.mergeTranslations(code, merged);
1488
+ }
1489
+ };
1490
+
1491
+ // src/client/plugin.ts
1492
+ var CarriedItemOps = /* @__PURE__ */ new Set([
1493
+ UpdateOps.SetText,
1494
+ UpdateOps.SetDescription,
1495
+ UpdateOps.SetLabel,
1496
+ UpdateOps.SetLockedDescription,
1497
+ UpdateOps.SetConfirmationDescription,
1498
+ UpdateOps.SetIcons,
1499
+ UpdateOps.SetChecked,
1500
+ UpdateOps.SetOptions,
1501
+ UpdateOps.SetSelectedIndex,
1502
+ UpdateOps.SetSliderPosition,
1503
+ UpdateOps.SetValue,
1504
+ UpdateOps.SetVisible,
1505
+ UpdateOps.SetEnabled,
1506
+ UpdateOps.SetGate,
1507
+ UpdateOps.SetLog,
1508
+ UpdateOps.SetBehaviour,
1509
+ UpdateOps.SetItemEvents
1510
+ ]);
1511
+ var CarriedMenuOps = /* @__PURE__ */ new Set([
1512
+ UpdateOps.SetMenuTitle,
1513
+ UpdateOps.SetMenuSubtitle,
1514
+ UpdateOps.SetMenuEvents,
1515
+ UpdateOps.AddKeys
1516
+ ]);
1517
+ var instance;
1518
+ var VMenuPlugin = class _VMenuPlugin {
1519
+ constructor(displayName) {
1520
+ this.displayName = displayName;
1521
+ this.itemsById = /* @__PURE__ */ new Map();
1522
+ this.menusById = /* @__PURE__ */ new Map();
1523
+ this.keysById = /* @__PURE__ */ new Map();
1524
+ this.pendingPrompts = /* @__PURE__ */ new Map();
1525
+ this.pendingItems = /* @__PURE__ */ new Set();
1526
+ this.pendingMenus = /* @__PURE__ */ new Set();
1527
+ this.dirtyFilters = [];
1528
+ this.batchDepth = 0;
1529
+ this.nextItem = 0;
1530
+ this.nextMenu = 0;
1531
+ this.nextPrompt = 0;
1532
+ this.handlersRegistered = false;
1533
+ this.connected = false;
1534
+ this.resource = currentResource();
1535
+ this.id = sanitizeId(this.resource);
1536
+ this.settings = new PluginSettings(this.id);
1537
+ this.themes = new PluginThemes(this);
1538
+ this.translations = new PluginTranslations(this);
1539
+ this.playerActions = new PluginPlayerActions(this);
1540
+ this.rootMenu = new PluginMenu(this, { id: "root", title: Text.toRef(displayName), items: [] }, displayName);
1541
+ this.registrationAnswered = new Signal(() => this.resource, "registration answered");
1542
+ this.disconnected = new Signal(() => this.resource, "disconnected");
1543
+ this.registerMenu(this.rootMenu);
1544
+ }
1545
+ /** Creates the plugin. One per resource: a second call returns the first instance. */
1546
+ static create(displayName) {
1547
+ if (instance) {
1548
+ log.warn(instance.resource, "VMenuPlugin.create was called twice, returning the first instance.");
1549
+ return instance;
1550
+ }
1551
+ instance = new _VMenuPlugin(displayName);
1552
+ return instance;
1553
+ }
1554
+ /** Whether vMenu currently has this plugin registered. */
1555
+ get isConnected() {
1556
+ return this.connected;
1557
+ }
1558
+ /** Called on every registration answer, including automatic re-registrations. */
1559
+ onRegistrationAnswered(handler) {
1560
+ return this.registrationAnswered.add(handler);
1561
+ }
1562
+ /** Called when vMenu stops, after which the plugin waits to register again. */
1563
+ onDisconnected(handler) {
1564
+ return this.disconnected.add(handler);
1565
+ }
1566
+ /**
1567
+ * Registers with vMenu. The promise resolves on vMenu's first answer, which can be a while when
1568
+ * vMenu starts later than your resource. It never rejects: a refusal arrives as a result with
1569
+ * `accepted` false.
1570
+ */
1571
+ connect() {
1572
+ this.firstResult ?? (this.firstResult = new Promise((resolve) => {
1573
+ this.resolveFirst = resolve;
1574
+ }));
1575
+ this.ensureHandlers();
1576
+ emitLocal(PluginEvents.Probe);
1577
+ return this.firstResult;
1578
+ }
1579
+ /** Shows a message through vMenu's notification area, credited to your resource. */
1580
+ notify(style, text, durationMs) {
1581
+ if (!this.connected) {
1582
+ return;
1583
+ }
1584
+ emitLocal(PluginEvents.Notify, JSON.stringify({ style, text: Text.toRef(text), durationMs }));
1585
+ }
1586
+ /** Asks the player for text through vMenu's input box. Resolves to null if they cancelled or the box was unavailable. */
1587
+ async getText(title, options) {
1588
+ const answers = await this.getTexts([{ title, ...options }]);
1589
+ return answers && answers.length > 0 ? answers[0] : null;
1590
+ }
1591
+ /** Asks several questions one after another. Resolves to null if the player cancelled any of them. */
1592
+ getTexts(prompts) {
1593
+ if (prompts.length === 0 || !this.connected) {
1594
+ return Promise.resolve(null);
1595
+ }
1596
+ const requestId = ++this.nextPrompt;
1597
+ const request = {
1598
+ requestId,
1599
+ prompts: prompts.map((prompt) => ({
1600
+ title: Text.toRef(prompt.title),
1601
+ maxLength: prompt.maxLength ?? 60,
1602
+ initial: prompt.initialValue ?? "",
1603
+ suggestions: prompt.suggestions && prompt.suggestions.length > 0 ? prompt.suggestions.map((suggestion) => ({ value: suggestion.value, description: suggestion.description })) : void 0
1604
+ }))
1605
+ };
1606
+ const pending = new Promise((resolve) => this.pendingPrompts.set(requestId, resolve));
1607
+ emitLocal(PluginEvents.Prompt, JSON.stringify(request));
1608
+ return pending.then((result) => result.cancelled || !Array.isArray(result.answers) ? null : [...result.answers]);
1609
+ }
1610
+ /**
1611
+ * Groups every change made inside `changes` into one update, so many small changes cost vMenu a
1612
+ * single repaint. Nesting is fine: only the outermost batch sends. The batch ends when `changes`
1613
+ * returns, so changes made after an `await` inside it are sent on their own.
1614
+ */
1615
+ batch(changes) {
1616
+ this.batchOps ?? (this.batchOps = []);
1617
+ this.batchDepth++;
1618
+ try {
1619
+ return changes();
1620
+ } finally {
1621
+ this.endBatch();
1622
+ }
1623
+ }
1624
+ /** @internal */
1625
+ nextItemId() {
1626
+ return `i${++this.nextItem}`;
1627
+ }
1628
+ /** @internal */
1629
+ nextMenuId() {
1630
+ return `m${++this.nextMenu}`;
1631
+ }
1632
+ /** @internal */
1633
+ registerMenu(menu) {
1634
+ this.menusById.set(menu.id, menu);
1635
+ }
1636
+ /** @internal */
1637
+ registerItem(item) {
1638
+ this.itemsById.set(item.id, item);
1639
+ }
1640
+ /** @internal */
1641
+ registerKey(key) {
1642
+ if (this.keysById.has(key.id)) {
1643
+ log.warn(this.resource, `Key id '${key.id}' is used twice, vMenu will skip the second one.`);
1644
+ return;
1645
+ }
1646
+ this.keysById.set(key.id, key);
1647
+ }
1648
+ /** @internal */
1649
+ unregisterItem(item) {
1650
+ this.itemsById.delete(item.id);
1651
+ if (!(item instanceof PluginSubmenu)) {
1652
+ return;
1653
+ }
1654
+ this.menusById.delete(item.menu.id);
1655
+ for (const key of item.menu.keys) {
1656
+ this.keysById.delete(key.id);
1657
+ }
1658
+ for (const child of item.menu.items) {
1659
+ this.unregisterItem(child);
1660
+ }
1661
+ }
1662
+ /** @internal */
1663
+ emitOp(op) {
1664
+ if (!this.connected) {
1665
+ return;
1666
+ }
1667
+ const batch = this.batchOps;
1668
+ if (!batch) {
1669
+ this.send([op]);
1670
+ return;
1671
+ }
1672
+ if (this.carriedByPendingAdd(op)) {
1673
+ return;
1674
+ }
1675
+ if (op.op === UpdateOps.AddItems) {
1676
+ for (const node of op.items ?? []) {
1677
+ this.pendingItems.add(node.id);
1678
+ if (node.menu) {
1679
+ this.pendingMenus.add(node.menu.id);
1680
+ }
1681
+ }
1682
+ }
1683
+ batch.push(op);
1684
+ }
1685
+ /** @internal */
1686
+ filterChanged(menu) {
1687
+ if (!this.connected) {
1688
+ return;
1689
+ }
1690
+ if (!this.batchOps) {
1691
+ this.send([menu.filterOp()]);
1692
+ return;
1693
+ }
1694
+ this.markFilterDirty(menu);
1695
+ }
1696
+ /** @internal */
1697
+ emitAdd(menu, item, add) {
1698
+ if (!this.connected) {
1699
+ return;
1700
+ }
1701
+ if (this.batchOps) {
1702
+ this.emitOp(add);
1703
+ if (menu.hasFilter) {
1704
+ this.markFilterDirty(menu);
1705
+ }
1706
+ return;
1707
+ }
1708
+ const ops = [add];
1709
+ if (menu.hides(item)) {
1710
+ ops.push({ op: UpdateOps.SetFilter, menuId: menu.id, itemIds: [item.id], flag: true });
1711
+ }
1712
+ this.send(ops);
1713
+ }
1714
+ /** @internal */
1715
+ mergeTranslations(code, entries) {
1716
+ if (this.connected) {
1717
+ this.emitOp({ op: UpdateOps.MergeTranslations, language: code, entries });
1718
+ }
1719
+ }
1720
+ endBatch() {
1721
+ if (--this.batchDepth > 0) {
1722
+ return;
1723
+ }
1724
+ const ops = this.batchOps;
1725
+ if (!ops) {
1726
+ return;
1727
+ }
1728
+ this.batchOps = void 0;
1729
+ this.pendingItems.clear();
1730
+ this.pendingMenus.clear();
1731
+ for (const menu of this.dirtyFilters) {
1732
+ ops.push(menu.filterOp());
1733
+ }
1734
+ this.dirtyFilters.length = 0;
1735
+ if (ops.length === 0 || !this.connected) {
1736
+ return;
1737
+ }
1738
+ this.send(ops);
1739
+ }
1740
+ markFilterDirty(menu) {
1741
+ if (!this.dirtyFilters.includes(menu)) {
1742
+ this.dirtyFilters.push(menu);
1743
+ }
1744
+ }
1745
+ carriedByPendingAdd(op) {
1746
+ if (CarriedItemOps.has(op.op)) {
1747
+ return op.itemId !== void 0 && this.pendingItems.has(op.itemId);
1748
+ }
1749
+ if (CarriedMenuOps.has(op.op)) {
1750
+ return op.menuId !== void 0 && this.pendingMenus.has(op.menuId);
1751
+ }
1752
+ return false;
1753
+ }
1754
+ send(ops) {
1755
+ const payload = ops.map(
1756
+ (op) => op.op === UpdateOps.AddItems && op.items ? { ...op, items: op.items.map(PluginMenu.asAdded) } : op
1757
+ );
1758
+ emitLocal(PluginEvents.Update, JSON.stringify({ ops: payload }));
1759
+ }
1760
+ ensureHandlers() {
1761
+ if (this.handlersRegistered) {
1762
+ return;
1763
+ }
1764
+ this.handlersRegistered = true;
1765
+ onLocal(PluginEvents.Ready, () => this.sendRegistration());
1766
+ onLocal(PluginEvents.readyFor(this.resource), () => this.sendRegistration());
1767
+ onLocal(PluginEvents.registerResultFor(this.resource), (json) => this.onRegisterResult(json));
1768
+ onLocal(PluginEvents.eventFor(this.resource), (json) => this.onCallback(json));
1769
+ onLocal(PluginEvents.promptResultFor(this.resource), (json) => this.onPromptResult(json));
1770
+ onLocal(PluginEvents.themesFor(this.resource), (json) => this.themes.handle(json));
1771
+ onLocal("onResourceStop", (stopped) => this.onResourceStop(stopped));
1772
+ }
1773
+ sendRegistration() {
1774
+ emitLocal(PluginEvents.Register, JSON.stringify(this.buildRequest()));
1775
+ }
1776
+ buildRequest() {
1777
+ const request = {
1778
+ protocolVersion: PROTOCOL_VERSION,
1779
+ displayName: Text.toRef(this.displayName),
1780
+ descriptionKey: this.descriptionKey,
1781
+ menu: this.rootMenu.node,
1782
+ playerActions: this.playerActions.nodes.length > 0 ? this.playerActions.nodes : void 0
1783
+ };
1784
+ if (Object.keys(this.translations.tables).length > 0) {
1785
+ request.translations = this.translations.tables;
1786
+ }
1787
+ if (this.settings.nodes.length > 0) {
1788
+ request.settings = [...this.settings.nodes];
1789
+ }
1790
+ return request;
1791
+ }
1792
+ onRegisterResult(json) {
1793
+ const result = normalizeResult(parseJson(json));
1794
+ if (!result) {
1795
+ log.warn(this.resource, "vMenu sent a registration answer that did not parse.");
1796
+ return;
1797
+ }
1798
+ for (const error of result.errors) {
1799
+ log.error(this.resource, `vMenu refused the plugin registration: ${error}`);
1800
+ }
1801
+ for (const warning of result.warnings) {
1802
+ log.warn(this.resource, `vMenu accepted the plugin registration with a note: ${warning}`);
1803
+ }
1804
+ this.connected = result.accepted;
1805
+ if (!this.connected) {
1806
+ this.cancelPendingPrompts();
1807
+ } else {
1808
+ this.batch(() => {
1809
+ for (const menu of this.menusById.values()) {
1810
+ if (menu.hasFilter) {
1811
+ this.filterChanged(menu);
1812
+ }
1813
+ }
1814
+ });
1815
+ }
1816
+ this.resolveFirst?.(result);
1817
+ this.resolveFirst = void 0;
1818
+ this.registrationAnswered.fire(result);
1819
+ }
1820
+ onCallback(json) {
1821
+ const callback = parseJson(json);
1822
+ if (!callback) {
1823
+ return;
1824
+ }
1825
+ try {
1826
+ switch (callback.type) {
1827
+ case CallbackTypes.MenuOpened:
1828
+ case CallbackTypes.MenuClosed:
1829
+ case CallbackTypes.MenuIndexChanged: {
1830
+ const menu = callback.menuId === void 0 ? void 0 : this.menusById.get(callback.menuId);
1831
+ menu?.handleMenu(callback);
1832
+ break;
1833
+ }
1834
+ case CallbackTypes.KeyPressed: {
1835
+ const key = callback.keyId === void 0 ? void 0 : this.keysById.get(callback.keyId);
1836
+ key?.handle({ item: this.itemOrUndefined(callback.itemId), disabledItem: this.itemOrUndefined(callback.disabledItemId) });
1837
+ break;
1838
+ }
1839
+ default: {
1840
+ const item = callback.itemId === void 0 ? void 0 : this.itemsById.get(callback.itemId);
1841
+ item?.handle(callback);
1842
+ break;
1843
+ }
1844
+ }
1845
+ } catch (error) {
1846
+ log.error(this.resource, `A menu callback handler threw: ${describe(error)}`);
1847
+ }
1848
+ }
1849
+ itemOrUndefined(id) {
1850
+ return id === void 0 || id === null ? void 0 : this.itemsById.get(id);
1851
+ }
1852
+ onPromptResult(json) {
1853
+ const result = parseJson(json);
1854
+ if (!result) {
1855
+ return;
1856
+ }
1857
+ const pending = this.pendingPrompts.get(result.requestId);
1858
+ if (pending) {
1859
+ this.pendingPrompts.delete(result.requestId);
1860
+ pending(result);
1861
+ }
1862
+ }
1863
+ cancelPendingPrompts() {
1864
+ for (const pending of this.pendingPrompts.values()) {
1865
+ pending({ requestId: 0, cancelled: true, busy: false });
1866
+ }
1867
+ this.pendingPrompts.clear();
1868
+ }
1869
+ onResourceStop(stopped) {
1870
+ if (typeof stopped !== "string" || stopped.toLowerCase() !== VMENU_RESOURCE.toLowerCase()) {
1871
+ return;
1872
+ }
1873
+ this.connected = false;
1874
+ this.cancelPendingPrompts();
1875
+ this.disconnected.fire();
1876
+ }
1877
+ };
1878
+ export {
1879
+ Gate,
1880
+ PluginBoolSetting,
1881
+ PluginButton,
1882
+ PluginCheckbox,
1883
+ PluginConfirmButton,
1884
+ PluginConfirmList,
1885
+ PluginDynamicList,
1886
+ PluginFloatSetting,
1887
+ PluginIntSetting,
1888
+ PluginItem,
1889
+ PluginKey,
1890
+ PluginList,
1891
+ PluginMenu,
1892
+ PluginPlayerActions,
1893
+ PluginPlayerButton,
1894
+ PluginPlayerConfirmButton,
1895
+ PluginPlayerList,
1896
+ PluginSeparator,
1897
+ PluginSetting,
1898
+ PluginSettings,
1899
+ PluginSlider,
1900
+ PluginStringSetting,
1901
+ PluginSubmenu,
1902
+ PluginThemes,
1903
+ PluginTranslations,
1904
+ Text,
1905
+ VMenuPlugin,
1906
+ version
1907
+ };