@oss-idle/addon-sdk 0.0.0-stage → 0.6.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/LICENSE +661 -0
- package/README.md +262 -2
- package/bin/cli.mjs +137 -0
- package/bin/scaffold.mjs +157 -0
- package/dist/bridge/beatmaps.d.ts +57 -0
- package/dist/bridge/beatmaps.d.ts.map +1 -0
- package/dist/bridge/beatmaps.js +65 -0
- package/dist/bridge/character.d.ts +91 -0
- package/dist/bridge/character.d.ts.map +1 -0
- package/dist/bridge/character.js +107 -0
- package/dist/bridge/db.d.ts +44 -0
- package/dist/bridge/db.d.ts.map +1 -0
- package/dist/bridge/db.js +83 -0
- package/dist/bridge/playlists.d.ts +55 -0
- package/dist/bridge/playlists.d.ts.map +1 -0
- package/dist/bridge/playlists.js +85 -0
- package/dist/bridge/scores.d.ts +28 -0
- package/dist/bridge/scores.d.ts.map +1 -0
- package/dist/bridge/scores.js +43 -0
- package/dist/build/index.d.ts +73 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +126 -0
- package/dist/build/plugins.d.ts +95 -0
- package/dist/build/plugins.d.ts.map +1 -0
- package/dist/build/plugins.js +240 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/overlay.d.ts +56 -0
- package/dist/react/overlay.d.ts.map +1 -0
- package/dist/react/overlay.js +63 -0
- package/dist/react/reactAddon.d.ts +37 -0
- package/dist/react/reactAddon.d.ts.map +1 -0
- package/dist/react/reactAddon.js +51 -0
- package/dist/react/useSettings.d.ts +16 -0
- package/dist/react/useSettings.d.ts.map +1 -0
- package/dist/react/useSettings.js +30 -0
- package/dist/runtime/addon.d.ts +94 -0
- package/dist/runtime/addon.d.ts.map +1 -0
- package/dist/runtime/addon.js +129 -0
- package/dist/runtime/buildMeta.d.ts +11 -0
- package/dist/runtime/buildMeta.d.ts.map +1 -0
- package/dist/runtime/buildMeta.js +1 -0
- package/dist/runtime/define.d.ts +26 -0
- package/dist/runtime/define.d.ts.map +1 -0
- package/dist/runtime/define.js +40 -0
- package/dist/runtime/host.d.ts +113 -0
- package/dist/runtime/host.d.ts.map +1 -0
- package/dist/runtime/host.js +8 -0
- package/dist/runtime/input.d.ts +58 -0
- package/dist/runtime/input.d.ts.map +1 -0
- package/dist/runtime/input.js +119 -0
- package/dist/runtime/settings.d.ts +36 -0
- package/dist/runtime/settings.d.ts.map +1 -0
- package/dist/runtime/settings.js +74 -0
- package/package.json +83 -4
- package/templates/common/README.md +50 -0
- package/templates/common/addon.config.json +7 -0
- package/templates/common/eslint.config.js +54 -0
- package/templates/common/gitignore +3 -0
- package/templates/common/src/env.d.ts +17 -0
- package/templates/common/tsconfig.json +14 -0
- package/templates/plain/src/index.ts +32 -0
- package/templates/react/src/Addon.tsx +44 -0
- package/templates/react/src/Panel.tsx +41 -0
- package/templates/react/src/index.ts +12 -0
- package/templates/react/src/styles.css +61 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A React add-on.
|
|
3
|
+
*
|
|
4
|
+
* Bundles the two things a React overlay always needs - a shadow-rooted host
|
|
5
|
+
* and a React 18 root - and ties both to the add-on lifecycle, so disabling the
|
|
6
|
+
* add-on unmounts the tree and removes the host in one step.
|
|
7
|
+
*
|
|
8
|
+
* React itself is bundled into the add-on's single emitted file. Two add-ons
|
|
9
|
+
* each carrying their own React is fine: they render into separate roots inside
|
|
10
|
+
* separate shadow trees and never share a reconciler.
|
|
11
|
+
*/
|
|
12
|
+
import type { ReactNode } from 'react';
|
|
13
|
+
import { Addon, type AddonMeta } from '../runtime/addon.js';
|
|
14
|
+
import { HotkeyManager } from '../runtime/input.js';
|
|
15
|
+
export type ReactAddonMeta = AddonMeta & {
|
|
16
|
+
/** Host element id. Defaults to a slug of the add-on name. */
|
|
17
|
+
overlayId?: string;
|
|
18
|
+
/** Stylesheet for the shadow tree. */
|
|
19
|
+
styles?: string;
|
|
20
|
+
};
|
|
21
|
+
export declare abstract class ReactAddon extends Addon {
|
|
22
|
+
private overlay?;
|
|
23
|
+
private root?;
|
|
24
|
+
/** Global shortcuts, bound ahead of the game's own handlers. */
|
|
25
|
+
protected readonly hotkeys: HotkeyManager;
|
|
26
|
+
private readonly reactMeta;
|
|
27
|
+
constructor(meta: ReactAddonMeta);
|
|
28
|
+
/** The add-on's root component. Re-rendered only when {@link refresh} is called. */
|
|
29
|
+
protected abstract render(): ReactNode;
|
|
30
|
+
protected onMount(): Promise<void>;
|
|
31
|
+
/** Hook for work that needs the overlay to exist (shortcuts, data loading). */
|
|
32
|
+
protected onReady(): void | Promise<void>;
|
|
33
|
+
/** Re-render the tree - for state the add-on holds outside React. */
|
|
34
|
+
protected refresh(): void;
|
|
35
|
+
protected onUnmount(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=reactAddon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reactAddon.d.ts","sourceRoot":"","sources":["../../src/react/reactAddon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAE,KAAK,EAAE,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpD,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG;IACxC,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,8BAAsB,UAAW,SAAQ,KAAK;IAE7C,OAAO,CAAC,OAAO,CAAC,CAAU;IAC1B,OAAO,CAAC,IAAI,CAAC,CAAO;IAEpB,gEAAgE;IAChE,SAAS,CAAC,QAAQ,CAAC,OAAO,gBAAuB;IAEjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiB;gBAE/B,IAAI,EAAE,cAAc;IAKhC,oFAAoF;IACpF,SAAS,CAAC,QAAQ,CAAC,MAAM,IAAI,SAAS;cAEb,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAYjD,+EAA+E;IAC/E,SAAS,CAAC,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAEzC,qEAAqE;IACrE,SAAS,CAAC,OAAO,IAAI,IAAI;cAIA,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CASnD"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A React add-on.
|
|
3
|
+
*
|
|
4
|
+
* Bundles the two things a React overlay always needs - a shadow-rooted host
|
|
5
|
+
* and a React 18 root - and ties both to the add-on lifecycle, so disabling the
|
|
6
|
+
* add-on unmounts the tree and removes the host in one step.
|
|
7
|
+
*
|
|
8
|
+
* React itself is bundled into the add-on's single emitted file. Two add-ons
|
|
9
|
+
* each carrying their own React is fine: they render into separate roots inside
|
|
10
|
+
* separate shadow trees and never share a reconciler.
|
|
11
|
+
*/
|
|
12
|
+
import { createRoot } from 'react-dom/client';
|
|
13
|
+
import { Addon } from '../runtime/addon.js';
|
|
14
|
+
import { HotkeyManager } from '../runtime/input.js';
|
|
15
|
+
import { createOverlay } from './overlay.js';
|
|
16
|
+
export class ReactAddon extends Addon {
|
|
17
|
+
overlay;
|
|
18
|
+
root;
|
|
19
|
+
/** Global shortcuts, bound ahead of the game's own handlers. */
|
|
20
|
+
hotkeys = new HotkeyManager();
|
|
21
|
+
reactMeta;
|
|
22
|
+
constructor(meta) {
|
|
23
|
+
super(meta);
|
|
24
|
+
this.reactMeta = meta;
|
|
25
|
+
}
|
|
26
|
+
async onMount() {
|
|
27
|
+
const id = this.reactMeta.overlayId
|
|
28
|
+
?? `addon-${this.reactMeta.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;
|
|
29
|
+
this.overlay = createOverlay({ id, styles: this.reactMeta.styles });
|
|
30
|
+
this.root = createRoot(this.overlay.container);
|
|
31
|
+
this.root.render(this.render());
|
|
32
|
+
this.own(() => this.hotkeys.dispose());
|
|
33
|
+
await this.onReady();
|
|
34
|
+
}
|
|
35
|
+
/** Hook for work that needs the overlay to exist (shortcuts, data loading). */
|
|
36
|
+
onReady() { }
|
|
37
|
+
/** Re-render the tree - for state the add-on holds outside React. */
|
|
38
|
+
refresh() {
|
|
39
|
+
this.root?.render(this.render());
|
|
40
|
+
}
|
|
41
|
+
async onUnmount() {
|
|
42
|
+
// React 18 warns if a root is unmounted while it is rendering, which is
|
|
43
|
+
// possible here because the runtime can disable an add-on at any moment.
|
|
44
|
+
const root = this.root;
|
|
45
|
+
this.root = undefined;
|
|
46
|
+
if (root)
|
|
47
|
+
await Promise.resolve().then(() => root.unmount());
|
|
48
|
+
this.overlay?.dispose();
|
|
49
|
+
this.overlay = undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React binding for a {@link SettingsStore}.
|
|
3
|
+
*
|
|
4
|
+
* Loads once on mount and writes back whenever a setting changes, so the values
|
|
5
|
+
* a player picked are there the next time the overlay opens.
|
|
6
|
+
*/
|
|
7
|
+
import type { SettingsStore } from '../runtime/settings.js';
|
|
8
|
+
export type SettingsHandle<T extends object> = [
|
|
9
|
+
settings: T,
|
|
10
|
+
/** Merge a patch into the settings and persist. */
|
|
11
|
+
update: (patch: Partial<T>) => void,
|
|
12
|
+
/** Restore the defaults. */
|
|
13
|
+
reset: () => void
|
|
14
|
+
];
|
|
15
|
+
export declare const usePersistedSettings: <T extends object>(store: SettingsStore<T>, defaults: T) => SettingsHandle<T>;
|
|
16
|
+
//# sourceMappingURL=useSettings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSettings.d.ts","sourceRoot":"","sources":["../../src/react/useSettings.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE5D,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,IAAI;IAC9C,QAAQ,EAAE,CAAC;IACX,mDAAmD;IACnD,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI;IACnC,4BAA4B;IAC5B,KAAK,EAAE,MAAM,IAAI;CACjB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EACpD,OAAO,aAAa,CAAC,CAAC,CAAC,EACvB,UAAU,CAAC,KACT,cAAc,CAAC,CAAC,CA2BlB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React binding for a {@link SettingsStore}.
|
|
3
|
+
*
|
|
4
|
+
* Loads once on mount and writes back whenever a setting changes, so the values
|
|
5
|
+
* a player picked are there the next time the overlay opens.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
8
|
+
export const usePersistedSettings = (store, defaults) => {
|
|
9
|
+
// Loaded in the initialiser so the first render already has the stored
|
|
10
|
+
// values - a later effect would flash the defaults first.
|
|
11
|
+
const [settings, setSettings] = useState(() => store.load());
|
|
12
|
+
const latest = useRef(settings);
|
|
13
|
+
latest.current = settings;
|
|
14
|
+
const update = useCallback((patch) => {
|
|
15
|
+
const next = { ...latest.current, ...patch };
|
|
16
|
+
latest.current = next;
|
|
17
|
+
setSettings(next);
|
|
18
|
+
store.save(next);
|
|
19
|
+
}, [store]);
|
|
20
|
+
const reset = useCallback(() => {
|
|
21
|
+
const next = { ...defaults };
|
|
22
|
+
latest.current = next;
|
|
23
|
+
setSettings(next);
|
|
24
|
+
store.clear();
|
|
25
|
+
}, [store, defaults]);
|
|
26
|
+
// Persist whatever the component settled on when it goes away, covering a
|
|
27
|
+
// value changed through some path other than `update`.
|
|
28
|
+
useEffect(() => () => store.save(latest.current), [store]);
|
|
29
|
+
return [settings, update, reset];
|
|
30
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The add-on contract.
|
|
3
|
+
*
|
|
4
|
+
* oss!idle loads an add-on by turning its `source` into a Blob URL and
|
|
5
|
+
* `import()`ing it in the page realm, then calling the module's `mount` /
|
|
6
|
+
* `unmount` exports - that pair *is* the contract. `mount` is handed the game's
|
|
7
|
+
* host (its React, the editor for add-ons of the Editor type: {@link host});
|
|
8
|
+
* the rest (the game's database, its beatmap store, the DOM) an add-on reaches
|
|
9
|
+
* for itself, because it runs unsandboxed on the page.
|
|
10
|
+
*
|
|
11
|
+
* Two consequences shape this class:
|
|
12
|
+
*
|
|
13
|
+
* - **Nothing resolves at load time.** The module is a Blob, so it has no base
|
|
14
|
+
* URL and no import map: every dependency, React included, must already be
|
|
15
|
+
* inside the single emitted file. That is the bundler's job (`./build`).
|
|
16
|
+
* - **`unmount` must actually undo everything.** Add-ons are enabled, disabled,
|
|
17
|
+
* updated and uninstalled without a page reload, so a listener or timer left
|
|
18
|
+
* behind leaks into a game the player keeps playing. {@link Addon} therefore
|
|
19
|
+
* hands out registration helpers that record their own teardown, and reverses
|
|
20
|
+
* them for you.
|
|
21
|
+
*/
|
|
22
|
+
import { type AddonHost, type EditorApi } from './host.js';
|
|
23
|
+
/** A teardown returned by a registration; running it undoes that registration. */
|
|
24
|
+
export type Disposer = () => void;
|
|
25
|
+
export type AddonMeta = {
|
|
26
|
+
/** Human name, used in log lines and error surfaces. */
|
|
27
|
+
name: string;
|
|
28
|
+
/** The add-on's own semver. Defaults to the version the bundler stamped in. */
|
|
29
|
+
version?: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Base class for an add-on. Subclass it, implement {@link onMount}, and export
|
|
33
|
+
* the pair the runtime wants with {@link defineAddon}.
|
|
34
|
+
*
|
|
35
|
+
* Anything registered through `this.own`, `this.on`, `this.interval` or
|
|
36
|
+
* `this.timeout` is torn down in reverse order on unmount, so `onUnmount` is
|
|
37
|
+
* only for work those helpers cannot express.
|
|
38
|
+
*/
|
|
39
|
+
export declare abstract class Addon {
|
|
40
|
+
protected readonly meta: AddonMeta;
|
|
41
|
+
/** Registered teardowns, newest first when reversed at unmount. */
|
|
42
|
+
private disposers;
|
|
43
|
+
private mounted;
|
|
44
|
+
constructor(meta: AddonMeta);
|
|
45
|
+
/** Runs when the player enables the add-on (and once per boot thereafter). */
|
|
46
|
+
protected abstract onMount(): void | Promise<void>;
|
|
47
|
+
/** Optional extra teardown, run *before* the registered disposers unwind. */
|
|
48
|
+
protected onUnmount(): void | Promise<void>;
|
|
49
|
+
/** Record a teardown to run at unmount. Returns it, so it can also be run early. */
|
|
50
|
+
protected own<T extends Disposer>(dispose: T): T;
|
|
51
|
+
/**
|
|
52
|
+
* Add a DOM listener that is removed at unmount. Defaults to the capture
|
|
53
|
+
* phase, which is what an overlay wants: the game binds its own handlers on
|
|
54
|
+
* `window` in the bubble phase, so capturing lets an overlay claim an event
|
|
55
|
+
* before gameplay ever sees it.
|
|
56
|
+
*/
|
|
57
|
+
protected on<K extends keyof WindowEventMap>(target: Window, type: K, handler: (event: WindowEventMap[K]) => void, options?: AddEventListenerOptions): Disposer;
|
|
58
|
+
protected on(target: EventTarget, type: string, handler: EventListenerOrEventListenerObject, options?: AddEventListenerOptions): Disposer;
|
|
59
|
+
/** `setInterval` that is cleared at unmount. */
|
|
60
|
+
protected interval(handler: () => void, ms: number): Disposer;
|
|
61
|
+
/** `setTimeout` that is cleared at unmount if it has not fired yet. */
|
|
62
|
+
protected timeout(handler: () => void, ms: number): Disposer;
|
|
63
|
+
/** Prefixed console logger, so a player's console says which add-on spoke. */
|
|
64
|
+
protected get log(): {
|
|
65
|
+
info: (...args: unknown[]) => void;
|
|
66
|
+
warn: (...args: unknown[]) => void;
|
|
67
|
+
error: (...args: unknown[]) => void;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Internal: called by {@link defineAddon}, not by add-on code.
|
|
71
|
+
*
|
|
72
|
+
* Public only because `defineAddon` lives outside the class and TypeScript
|
|
73
|
+
* has no package-private visibility; the underscore marks it as off-limits.
|
|
74
|
+
*/
|
|
75
|
+
_mount(host?: AddonHost): Promise<void>;
|
|
76
|
+
private handed?;
|
|
77
|
+
/** The game's host: React, the editor. Undefined on a game too old to hand one. */
|
|
78
|
+
protected get host(): AddonHost | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Work with the editor while it is open (an add-on of the Editor type):
|
|
81
|
+
* `onOpen` runs each time it opens, with its API. What is registered through
|
|
82
|
+
* that API goes when the editor closes or this add-on unmounts.
|
|
83
|
+
*/
|
|
84
|
+
protected editor(onOpen: (editor: EditorApi) => void, onClose?: () => void): void;
|
|
85
|
+
/**
|
|
86
|
+
* Internal: called by {@link defineAddon}, not by add-on code.
|
|
87
|
+
*
|
|
88
|
+
* Every teardown runs even if an earlier one throws - a half-unmounted
|
|
89
|
+
* add-on that keeps a key handler alive would break the game far more
|
|
90
|
+
* visibly than a logged error here.
|
|
91
|
+
*/
|
|
92
|
+
_unmount(): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=addon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addon.d.ts","sourceRoot":"","sources":["../../src/runtime/addon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAEN,KAAK,SAAS,EACd,KAAK,SAAS,EACd,MAAM,WAAW,CAAC;AAEnB,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC;AAElC,MAAM,MAAM,SAAS,GAAG;IACvB,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;GAOG;AACH,8BAAsB,KAAK;IAE1B,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAEnC,mEAAmE;IACnE,OAAO,CAAC,SAAS,CAAkB;IAEnC,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,SAAS;IAI3B,8EAA8E;IAC9E,SAAS,CAAC,QAAQ,CAAC,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAElD,6EAA6E;IAC7E,SAAS,CAAC,SAAS,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAE3C,oFAAoF;IACpF,SAAS,CAAC,GAAG,CAAC,CAAC,SAAS,QAAQ,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC;IAKhD;;;;;OAKG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,SAAS,MAAM,cAAc,EAC1C,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI,EAC3C,OAAO,CAAC,EAAE,uBAAuB,GAC/B,QAAQ;IACX,SAAS,CAAC,EAAE,CACX,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,kCAAkC,EAC3C,OAAO,CAAC,EAAE,uBAAuB,GAC/B,QAAQ;IAWX,gDAAgD;IAChD,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,QAAQ;IAK7D,uEAAuE;IACvE,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,QAAQ;IAK5D,8EAA8E;IAC9E,SAAS,KAAK,GAAG;wBAGC,OAAO,EAAE;wBACT,OAAO,EAAE;yBACR,OAAO,EAAE;MAE3B;IAED;;;;;OAKG;IACG,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAO7C,OAAO,CAAC,MAAM,CAAC,CAAY;IAE3B,mFAAmF;IACnF,SAAS,KAAK,IAAI,IAAI,SAAS,GAAG,SAAS,CAE1C;IAED;;;;OAIG;IACH,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI;IASjF;;;;;;OAMG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAkB/B"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The add-on contract.
|
|
3
|
+
*
|
|
4
|
+
* oss!idle loads an add-on by turning its `source` into a Blob URL and
|
|
5
|
+
* `import()`ing it in the page realm, then calling the module's `mount` /
|
|
6
|
+
* `unmount` exports - that pair *is* the contract. `mount` is handed the game's
|
|
7
|
+
* host (its React, the editor for add-ons of the Editor type: {@link host});
|
|
8
|
+
* the rest (the game's database, its beatmap store, the DOM) an add-on reaches
|
|
9
|
+
* for itself, because it runs unsandboxed on the page.
|
|
10
|
+
*
|
|
11
|
+
* Two consequences shape this class:
|
|
12
|
+
*
|
|
13
|
+
* - **Nothing resolves at load time.** The module is a Blob, so it has no base
|
|
14
|
+
* URL and no import map: every dependency, React included, must already be
|
|
15
|
+
* inside the single emitted file. That is the bundler's job (`./build`).
|
|
16
|
+
* - **`unmount` must actually undo everything.** Add-ons are enabled, disabled,
|
|
17
|
+
* updated and uninstalled without a page reload, so a listener or timer left
|
|
18
|
+
* behind leaks into a game the player keeps playing. {@link Addon} therefore
|
|
19
|
+
* hands out registration helpers that record their own teardown, and reverses
|
|
20
|
+
* them for you.
|
|
21
|
+
*/
|
|
22
|
+
import { buildMeta } from './buildMeta.js';
|
|
23
|
+
import { gameHost, } from './host.js';
|
|
24
|
+
/**
|
|
25
|
+
* Base class for an add-on. Subclass it, implement {@link onMount}, and export
|
|
26
|
+
* the pair the runtime wants with {@link defineAddon}.
|
|
27
|
+
*
|
|
28
|
+
* Anything registered through `this.own`, `this.on`, `this.interval` or
|
|
29
|
+
* `this.timeout` is torn down in reverse order on unmount, so `onUnmount` is
|
|
30
|
+
* only for work those helpers cannot express.
|
|
31
|
+
*/
|
|
32
|
+
export class Addon {
|
|
33
|
+
meta;
|
|
34
|
+
/** Registered teardowns, newest first when reversed at unmount. */
|
|
35
|
+
disposers = [];
|
|
36
|
+
mounted = false;
|
|
37
|
+
constructor(meta) {
|
|
38
|
+
this.meta = { ...meta, version: meta.version ?? buildMeta().version };
|
|
39
|
+
}
|
|
40
|
+
/** Optional extra teardown, run *before* the registered disposers unwind. */
|
|
41
|
+
onUnmount() { }
|
|
42
|
+
/** Record a teardown to run at unmount. Returns it, so it can also be run early. */
|
|
43
|
+
own(dispose) {
|
|
44
|
+
this.disposers.push(dispose);
|
|
45
|
+
return dispose;
|
|
46
|
+
}
|
|
47
|
+
on(target, type, handler, options = { capture: true }) {
|
|
48
|
+
target.addEventListener(type, handler, options);
|
|
49
|
+
return this.own(() => target.removeEventListener(type, handler, options));
|
|
50
|
+
}
|
|
51
|
+
/** `setInterval` that is cleared at unmount. */
|
|
52
|
+
interval(handler, ms) {
|
|
53
|
+
const id = window.setInterval(handler, ms);
|
|
54
|
+
return this.own(() => window.clearInterval(id));
|
|
55
|
+
}
|
|
56
|
+
/** `setTimeout` that is cleared at unmount if it has not fired yet. */
|
|
57
|
+
timeout(handler, ms) {
|
|
58
|
+
const id = window.setTimeout(handler, ms);
|
|
59
|
+
return this.own(() => window.clearTimeout(id));
|
|
60
|
+
}
|
|
61
|
+
/** Prefixed console logger, so a player's console says which add-on spoke. */
|
|
62
|
+
get log() {
|
|
63
|
+
const tag = `[${this.meta.name}]`;
|
|
64
|
+
return {
|
|
65
|
+
info: (...args) => console.log(tag, ...args),
|
|
66
|
+
warn: (...args) => console.warn(tag, ...args),
|
|
67
|
+
error: (...args) => console.error(tag, ...args),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Internal: called by {@link defineAddon}, not by add-on code.
|
|
72
|
+
*
|
|
73
|
+
* Public only because `defineAddon` lives outside the class and TypeScript
|
|
74
|
+
* has no package-private visibility; the underscore marks it as off-limits.
|
|
75
|
+
*/
|
|
76
|
+
async _mount(host) {
|
|
77
|
+
if (this.mounted)
|
|
78
|
+
return;
|
|
79
|
+
this.mounted = true;
|
|
80
|
+
this.handed = host;
|
|
81
|
+
await this.onMount();
|
|
82
|
+
}
|
|
83
|
+
handed;
|
|
84
|
+
/** The game's host: React, the editor. Undefined on a game too old to hand one. */
|
|
85
|
+
get host() {
|
|
86
|
+
return this.handed ?? gameHost();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Work with the editor while it is open (an add-on of the Editor type):
|
|
90
|
+
* `onOpen` runs each time it opens, with its API. What is registered through
|
|
91
|
+
* that API goes when the editor closes or this add-on unmounts.
|
|
92
|
+
*/
|
|
93
|
+
editor(onOpen, onClose) {
|
|
94
|
+
const host = this.host;
|
|
95
|
+
if (!host) {
|
|
96
|
+
this.log.warn('this game hands add-ons no editor: update oss!idle');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.own(host.editor.watch(onOpen, onClose));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Internal: called by {@link defineAddon}, not by add-on code.
|
|
103
|
+
*
|
|
104
|
+
* Every teardown runs even if an earlier one throws - a half-unmounted
|
|
105
|
+
* add-on that keeps a key handler alive would break the game far more
|
|
106
|
+
* visibly than a logged error here.
|
|
107
|
+
*/
|
|
108
|
+
async _unmount() {
|
|
109
|
+
if (!this.mounted)
|
|
110
|
+
return;
|
|
111
|
+
this.mounted = false;
|
|
112
|
+
try {
|
|
113
|
+
await this.onUnmount();
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
this.log.error('onUnmount failed', e);
|
|
117
|
+
}
|
|
118
|
+
const pending = this.disposers.reverse();
|
|
119
|
+
this.disposers = [];
|
|
120
|
+
for (const dispose of pending) {
|
|
121
|
+
try {
|
|
122
|
+
dispose();
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
this.log.error('teardown failed', e);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type BuildMeta = {
|
|
2
|
+
name?: string;
|
|
3
|
+
version?: string;
|
|
4
|
+
/** The oss!idle release the bundle was built against. */
|
|
5
|
+
gameVersion?: string;
|
|
6
|
+
/** The @oss-idle/addon-sdk that built it. */
|
|
7
|
+
sdkVersion?: string;
|
|
8
|
+
license?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const buildMeta: () => BuildMeta;
|
|
11
|
+
//# sourceMappingURL=buildMeta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildMeta.d.ts","sourceRoot":"","sources":["../../src/runtime/buildMeta.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,SAAS,GAAG;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,SAAS,QAAO,SACuC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const buildMeta = () => typeof __OSS_IDLE_ADDON__ === 'undefined' ? {} : __OSS_IDLE_ADDON__;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Addon } from './addon.js';
|
|
2
|
+
import type { AddonHost } from './host.js';
|
|
3
|
+
/** The module shape oss!idle's add-on runtime imports and drives. */
|
|
4
|
+
export type AddonModule = {
|
|
5
|
+
mount: (meta?: unknown, host?: AddonHost) => void;
|
|
6
|
+
unmount: () => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Turn an {@link Addon} subclass into the `mount` / `unmount` pair the runtime
|
|
10
|
+
* expects:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* export const { mount, unmount } = defineAddon(() => new MyAddon());
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The runtime calls `mount` synchronously and ignores what it returns, so an
|
|
17
|
+
* async `onMount` is started and left to run - its failure is reported here
|
|
18
|
+
* rather than surfacing as an unhandled rejection in the player's console.
|
|
19
|
+
* `unmount` returns its teardown, which the runtime awaits before mounting an
|
|
20
|
+
* updated version.
|
|
21
|
+
*
|
|
22
|
+
* A mount still settling when `unmount` arrives (disable clicked mid-boot) is
|
|
23
|
+
* awaited first, so teardown never races a half-built overlay.
|
|
24
|
+
*/
|
|
25
|
+
export declare const defineAddon: (create: () => Addon) => AddonModule;
|
|
26
|
+
//# sourceMappingURL=define.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../../src/runtime/define.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,qEAAqE;AACrE,MAAM,MAAM,WAAW,GAAG;IACzB,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,KAAK,IAAI,CAAC;IAClD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,KAAK,KAAG,WAsBjD,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn an {@link Addon} subclass into the `mount` / `unmount` pair the runtime
|
|
3
|
+
* expects:
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* export const { mount, unmount } = defineAddon(() => new MyAddon());
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* The runtime calls `mount` synchronously and ignores what it returns, so an
|
|
10
|
+
* async `onMount` is started and left to run - its failure is reported here
|
|
11
|
+
* rather than surfacing as an unhandled rejection in the player's console.
|
|
12
|
+
* `unmount` returns its teardown, which the runtime awaits before mounting an
|
|
13
|
+
* updated version.
|
|
14
|
+
*
|
|
15
|
+
* A mount still settling when `unmount` arrives (disable clicked mid-boot) is
|
|
16
|
+
* awaited first, so teardown never races a half-built overlay.
|
|
17
|
+
*/
|
|
18
|
+
export const defineAddon = (create) => {
|
|
19
|
+
let instance;
|
|
20
|
+
let settling = Promise.resolve();
|
|
21
|
+
return {
|
|
22
|
+
mount: (_meta, host) => {
|
|
23
|
+
instance = create();
|
|
24
|
+
const addon = instance;
|
|
25
|
+
settling = addon._mount(host).catch((e) => {
|
|
26
|
+
console.error('[addon] mount failed', e);
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
unmount: () => {
|
|
30
|
+
const addon = instance;
|
|
31
|
+
if (!addon)
|
|
32
|
+
return settling;
|
|
33
|
+
instance = undefined;
|
|
34
|
+
settling = settling
|
|
35
|
+
.then(() => addon._unmount())
|
|
36
|
+
.catch((e) => console.error('[addon] unmount failed', e));
|
|
37
|
+
return settling;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the game hands an add-on: `mount(meta, host)`, and the same object on
|
|
3
|
+
* `globalThis.ossIdle` for code that runs before `mount` (the `sharedReact`
|
|
4
|
+
* build reads React there). Mirrors the game's `addons/host.ts` and
|
|
5
|
+
* `editor/editorApi.ts`; an older game hands none.
|
|
6
|
+
*/
|
|
7
|
+
/** A teardown; running it undoes what returned it. */
|
|
8
|
+
type Disposer = () => void;
|
|
9
|
+
/** A note as the editor holds it; `end` makes it a long note. */
|
|
10
|
+
export type EditorNote = {
|
|
11
|
+
time: number;
|
|
12
|
+
column: number;
|
|
13
|
+
end?: number;
|
|
14
|
+
/** the .osu hitSound bits, as written */
|
|
15
|
+
hitSound: string;
|
|
16
|
+
/** the .osu sample fields, as written */
|
|
17
|
+
sample: string;
|
|
18
|
+
};
|
|
19
|
+
export type EditorTimingPoint = {
|
|
20
|
+
time: number;
|
|
21
|
+
/** ms per beat for a BPM point; -100 / the multiplier for a scroll speed one */
|
|
22
|
+
beatLength: number;
|
|
23
|
+
meter: number;
|
|
24
|
+
sound: string;
|
|
25
|
+
effects: string;
|
|
26
|
+
};
|
|
27
|
+
export type EditorMetadata = {
|
|
28
|
+
title: string;
|
|
29
|
+
titleUnicode: string;
|
|
30
|
+
artist: string;
|
|
31
|
+
artistUnicode: string;
|
|
32
|
+
creator: string;
|
|
33
|
+
version: string;
|
|
34
|
+
source: string;
|
|
35
|
+
tags: string;
|
|
36
|
+
previewTime: number;
|
|
37
|
+
};
|
|
38
|
+
/** Draws into `container`; what it returns runs when it goes. */
|
|
39
|
+
export type EditorRender = (container: HTMLElement) => void | (() => void);
|
|
40
|
+
/**
|
|
41
|
+
* The open editor. Notes and points are the editor's own objects: read them,
|
|
42
|
+
* never change them, and hand new ones to `edit`. Everything registered here
|
|
43
|
+
* goes when the editor closes or the add-on stops.
|
|
44
|
+
*/
|
|
45
|
+
export type EditorApi = {
|
|
46
|
+
readonly keys: number;
|
|
47
|
+
notes(): readonly EditorNote[];
|
|
48
|
+
timing(): readonly EditorTimingPoint[];
|
|
49
|
+
scroll(): readonly EditorTimingPoint[];
|
|
50
|
+
metadata(): Readonly<EditorMetadata>;
|
|
51
|
+
bookmarks(): readonly number[];
|
|
52
|
+
text(): string;
|
|
53
|
+
edit(change: {
|
|
54
|
+
notes?: EditorNote[];
|
|
55
|
+
timing?: EditorTimingPoint[];
|
|
56
|
+
scroll?: EditorTimingPoint[];
|
|
57
|
+
bookmarks?: number[];
|
|
58
|
+
metadata?: Partial<EditorMetadata>;
|
|
59
|
+
}, select?: EditorNote[]): void;
|
|
60
|
+
undo(): void;
|
|
61
|
+
redo(): void;
|
|
62
|
+
save(): void;
|
|
63
|
+
selection(): EditorNote[];
|
|
64
|
+
select(notes: EditorNote[]): void;
|
|
65
|
+
now(): number;
|
|
66
|
+
seek(ms: number): void;
|
|
67
|
+
play(): void;
|
|
68
|
+
pause(): void;
|
|
69
|
+
isPlaying(): boolean;
|
|
70
|
+
snap(ms: number): number;
|
|
71
|
+
divisor(): number;
|
|
72
|
+
setDivisor(divisor: number): void;
|
|
73
|
+
onChange(callback: () => void): Disposer;
|
|
74
|
+
addAction(action: {
|
|
75
|
+
id: string;
|
|
76
|
+
label: string;
|
|
77
|
+
keys?: string[];
|
|
78
|
+
run: () => void;
|
|
79
|
+
}): Disposer;
|
|
80
|
+
addTab(tab: {
|
|
81
|
+
id: string;
|
|
82
|
+
label: string;
|
|
83
|
+
render: EditorRender;
|
|
84
|
+
}): Disposer;
|
|
85
|
+
addPanel(panel: {
|
|
86
|
+
id: string;
|
|
87
|
+
title?: string;
|
|
88
|
+
render: EditorRender;
|
|
89
|
+
}): Disposer;
|
|
90
|
+
openWindow(window: {
|
|
91
|
+
title: string;
|
|
92
|
+
width?: number;
|
|
93
|
+
height?: number;
|
|
94
|
+
render: EditorRender;
|
|
95
|
+
onClose?: () => void;
|
|
96
|
+
}): {
|
|
97
|
+
close(): void;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
export type AddonHost = {
|
|
101
|
+
/** the game's version */
|
|
102
|
+
version: string;
|
|
103
|
+
/** the game's React modules by import path */
|
|
104
|
+
react: Record<string, unknown>;
|
|
105
|
+
editor: {
|
|
106
|
+
/** `onOpen` runs whenever the editor opens (now, if it is open), `onClose` when it closes. */
|
|
107
|
+
watch(onOpen: (editor: EditorApi) => void, onClose?: () => void): Disposer;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
/** The game's host, if it hands one out (older games do not). */
|
|
111
|
+
export declare const gameHost: () => AddonHost | undefined;
|
|
112
|
+
export {};
|
|
113
|
+
//# sourceMappingURL=host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/runtime/host.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the game hands an add-on: `mount(meta, host)`, and the same object on
|
|
3
|
+
* `globalThis.ossIdle` for code that runs before `mount` (the `sharedReact`
|
|
4
|
+
* build reads React there). Mirrors the game's `addons/host.ts` and
|
|
5
|
+
* `editor/editorApi.ts`; an older game hands none.
|
|
6
|
+
*/
|
|
7
|
+
/** The game's host, if it hands one out (older games do not). */
|
|
8
|
+
export const gameHost = () => globalThis.ossIdle;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping an overlay's input out of the game.
|
|
3
|
+
*
|
|
4
|
+
* The client binds its gameplay controls as **bubble-phase listeners on
|
|
5
|
+
* `window`** (`input/Controls.ts`), and the same is true of the scene-level
|
|
6
|
+
* shortcuts and the wheel handlers that drive volume and the song carousel.
|
|
7
|
+
* Nothing is bound in the capture phase, which is what makes an overlay able to
|
|
8
|
+
* coexist with it:
|
|
9
|
+
*
|
|
10
|
+
* - **Inside the overlay**, {@link isolateInput} stops these events at the
|
|
11
|
+
* overlay's root while they bubble. The event still reached its target, so a
|
|
12
|
+
* text field types normally, but it never continues up to `window` - so typing
|
|
13
|
+
* "speed" into a filter box cannot press four gameplay keys, and scrolling a
|
|
14
|
+
* results list cannot change the game's volume.
|
|
15
|
+
* - **Outside the overlay**, {@link HotkeyManager} listens in the capture phase,
|
|
16
|
+
* ahead of every game handler, so a shortcut can claim its combo before
|
|
17
|
+
* gameplay interprets it.
|
|
18
|
+
*/
|
|
19
|
+
import type { Disposer } from './addon.js';
|
|
20
|
+
/**
|
|
21
|
+
* Stop the game from seeing input that belongs to `root`.
|
|
22
|
+
*
|
|
23
|
+
* Bubble phase on purpose: the event has already done its job inside the
|
|
24
|
+
* overlay by the time it is stopped here. Capturing instead would take the
|
|
25
|
+
* keystroke away from the very field the player is typing into.
|
|
26
|
+
*/
|
|
27
|
+
export declare const isolateInput: (root: HTMLElement) => Disposer;
|
|
28
|
+
export type Hotkey = {
|
|
29
|
+
/** `KeyboardEvent.key`, compared case-insensitively. */
|
|
30
|
+
key: string;
|
|
31
|
+
ctrl: boolean;
|
|
32
|
+
shift: boolean;
|
|
33
|
+
alt: boolean;
|
|
34
|
+
meta: boolean;
|
|
35
|
+
};
|
|
36
|
+
/** Parse `"Ctrl+Shift+F"` into a {@link Hotkey}. Order and case do not matter. */
|
|
37
|
+
export declare const parseHotkey: (combo: string) => Hotkey;
|
|
38
|
+
/**
|
|
39
|
+
* Global shortcuts that win against the game.
|
|
40
|
+
*
|
|
41
|
+
* One capture-phase listener on `window` serves every binding. A claimed combo
|
|
42
|
+
* is stopped with `stopImmediatePropagation` and `preventDefault`, so neither
|
|
43
|
+
* the game's handlers nor the browser's own act on it.
|
|
44
|
+
*/
|
|
45
|
+
export declare class HotkeyManager {
|
|
46
|
+
private readonly bindings;
|
|
47
|
+
private readonly onKeyDown;
|
|
48
|
+
constructor();
|
|
49
|
+
/**
|
|
50
|
+
* Bind a combo. `allowInText` lets it fire even from a focused field, which
|
|
51
|
+
* is what a close/escape shortcut wants.
|
|
52
|
+
*/
|
|
53
|
+
register(combo: string, handler: (event: KeyboardEvent) => void, { allowInText }?: {
|
|
54
|
+
allowInText?: boolean;
|
|
55
|
+
}): Disposer;
|
|
56
|
+
dispose(): void;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/runtime/input.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAS3C;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,WAAW,KAAG,QAUhD,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG;IACpB,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,EAAE,OAAO,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;CACd,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MAY3C,CAAC;AAqBF;;;;;;GAMG;AACH,qBAAa,aAAa;IAEzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAE/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAYxB;;IAMF;;;OAGG;IACH,QAAQ,CACP,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,EACvC,EAAE,WAAmB,EAAE,GAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAA;KAAO,GACrD,QAAQ;IAMX,OAAO,IAAI,IAAI;CAIf"}
|