@oss-idle/addon-sdk 0.0.0-stage → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +222 -2
  3. package/bin/cli.mjs +136 -0
  4. package/bin/scaffold.mjs +154 -0
  5. package/dist/bridge/beatmaps.d.ts +57 -0
  6. package/dist/bridge/beatmaps.d.ts.map +1 -0
  7. package/dist/bridge/beatmaps.js +65 -0
  8. package/dist/bridge/character.d.ts +91 -0
  9. package/dist/bridge/character.d.ts.map +1 -0
  10. package/dist/bridge/character.js +107 -0
  11. package/dist/bridge/db.d.ts +44 -0
  12. package/dist/bridge/db.d.ts.map +1 -0
  13. package/dist/bridge/db.js +83 -0
  14. package/dist/bridge/playlists.d.ts +55 -0
  15. package/dist/bridge/playlists.d.ts.map +1 -0
  16. package/dist/bridge/playlists.js +85 -0
  17. package/dist/bridge/scores.d.ts +28 -0
  18. package/dist/bridge/scores.d.ts.map +1 -0
  19. package/dist/bridge/scores.js +43 -0
  20. package/dist/build/index.d.ts +60 -0
  21. package/dist/build/index.d.ts.map +1 -0
  22. package/dist/build/index.js +123 -0
  23. package/dist/build/plugins.d.ts +88 -0
  24. package/dist/build/plugins.d.ts.map +1 -0
  25. package/dist/build/plugins.js +213 -0
  26. package/dist/index.d.ts +11 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +10 -0
  29. package/dist/react/index.d.ts +4 -0
  30. package/dist/react/index.d.ts.map +1 -0
  31. package/dist/react/index.js +3 -0
  32. package/dist/react/overlay.d.ts +56 -0
  33. package/dist/react/overlay.d.ts.map +1 -0
  34. package/dist/react/overlay.js +63 -0
  35. package/dist/react/reactAddon.d.ts +37 -0
  36. package/dist/react/reactAddon.d.ts.map +1 -0
  37. package/dist/react/reactAddon.js +51 -0
  38. package/dist/react/useSettings.d.ts +16 -0
  39. package/dist/react/useSettings.d.ts.map +1 -0
  40. package/dist/react/useSettings.js +30 -0
  41. package/dist/runtime/addon.d.ts +83 -0
  42. package/dist/runtime/addon.d.ts.map +1 -0
  43. package/dist/runtime/addon.js +108 -0
  44. package/dist/runtime/buildMeta.d.ts +11 -0
  45. package/dist/runtime/buildMeta.d.ts.map +1 -0
  46. package/dist/runtime/buildMeta.js +1 -0
  47. package/dist/runtime/define.d.ts +23 -0
  48. package/dist/runtime/define.d.ts.map +1 -0
  49. package/dist/runtime/define.js +37 -0
  50. package/dist/runtime/input.d.ts +58 -0
  51. package/dist/runtime/input.d.ts.map +1 -0
  52. package/dist/runtime/input.js +119 -0
  53. package/dist/runtime/settings.d.ts +36 -0
  54. package/dist/runtime/settings.d.ts.map +1 -0
  55. package/dist/runtime/settings.js +74 -0
  56. package/package.json +83 -4
  57. package/templates/common/README.md +31 -0
  58. package/templates/common/addon.config.json +6 -0
  59. package/templates/common/gitignore +3 -0
  60. package/templates/common/src/env.d.ts +17 -0
  61. package/templates/common/tsconfig.json +14 -0
  62. package/templates/plain/src/index.ts +32 -0
  63. package/templates/react/src/Addon.tsx +44 -0
  64. package/templates/react/src/Panel.tsx +41 -0
  65. package/templates/react/src/index.ts +12 -0
  66. package/templates/react/src/styles.css +61 -0
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Persisted add-on settings.
3
+ *
4
+ * An add-on is remounted on every boot and on every enable/disable, so anything
5
+ * held only in component state is re-entered by the player each time. Settings
6
+ * belong in `localStorage`, which is the same store the client keeps its own
7
+ * options in and survives reloads without touching the game's database.
8
+ *
9
+ * **Keys are namespaced deliberately.** The client writes its settings under
10
+ * bare, unprefixed keys (`db/settings.ts`), and reads them back with a bare
11
+ * `JSON.parse`. An add-on writing a colliding key would corrupt a game setting,
12
+ * so everything here lives under `oss-idle-addon:<namespace>`.
13
+ *
14
+ * Stored values are merged **over** the defaults on load, so adding a setting in
15
+ * a later version does not invalidate what a player already saved, and only keys
16
+ * the defaults still declare are kept - a removed setting cleans itself up.
17
+ */
18
+ const PREFIX = 'oss-idle-addon:';
19
+ /** {@link PREFIX} before the rename to oss!idle: still read, so saved settings carry over. */
20
+ const LEGACY_PREFIX = 'osu-idle-addon:';
21
+ /**
22
+ * A namespaced settings store.
23
+ *
24
+ * Every operation is guarded: `localStorage` can be unavailable (private
25
+ * browsing, storage disabled) or full, and losing settings must never take the
26
+ * add-on - or the game - down with it.
27
+ */
28
+ export const createSettings = (namespace, defaults) => {
29
+ const key = `${PREFIX}${namespace}`;
30
+ return {
31
+ key,
32
+ load() {
33
+ try {
34
+ const raw = localStorage.getItem(key) ?? localStorage.getItem(`${LEGACY_PREFIX}${namespace}`);
35
+ if (!raw)
36
+ return { ...defaults };
37
+ const stored = JSON.parse(raw);
38
+ if (!stored || typeof stored !== 'object')
39
+ return { ...defaults };
40
+ // Take only keys the defaults still declare, and only when the
41
+ // stored value has the same shape - a hand-edited or stale entry
42
+ // must not put a string where a number is expected.
43
+ const merged = { ...defaults };
44
+ for (const name of Object.keys(defaults)) {
45
+ const value = stored[name];
46
+ if (value === undefined)
47
+ continue;
48
+ if (typeof value !== typeof defaults[name])
49
+ continue;
50
+ merged[name] = value;
51
+ }
52
+ return merged;
53
+ }
54
+ catch {
55
+ return { ...defaults };
56
+ }
57
+ },
58
+ save(value) {
59
+ try {
60
+ localStorage.setItem(key, JSON.stringify(value));
61
+ }
62
+ catch {
63
+ // Full or unavailable storage: the add-on still works, it just
64
+ // will not remember. Not worth interrupting the player over.
65
+ }
66
+ },
67
+ clear() {
68
+ try {
69
+ localStorage.removeItem(key);
70
+ }
71
+ catch { /* nothing to do */ }
72
+ },
73
+ };
74
+ };
package/package.json CHANGED
@@ -1,6 +1,85 @@
1
1
  {
2
2
  "name": "@oss-idle/addon-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.5.0",
4
+ "type": "module",
5
+ "license": "AGPL-3.0-only",
6
+ "description": "oss!idle Addon SDK - build oss!idle add-ons in TypeScript. An Addon base class with automatic teardown, shadow-root React overlays, bridges to the game's database and beatmap store, and a bundler that emits the single-file ES module the add-on runtime loads.",
7
+ "keywords": [
8
+ "osu",
9
+ "oss-idle",
10
+ "addon",
11
+ "sdk",
12
+ "plugin",
13
+ "typescript",
14
+ "esbuild"
15
+ ],
16
+ "homepage": "https://github.com/oss-idle/addon-sdk#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/oss-idle/addon-sdk.git",
20
+ "directory": "packages/addon-sdk"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/oss-idle/addon-sdk/issues"
24
+ },
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./react": {
36
+ "types": "./dist/react/index.d.ts",
37
+ "default": "./dist/react/index.js"
38
+ },
39
+ "./build": {
40
+ "types": "./dist/build/index.d.ts",
41
+ "default": "./dist/build/index.js"
42
+ },
43
+ "./package.json": "./package.json",
44
+ "./scaffold": "./bin/scaffold.mjs"
45
+ },
46
+ "bin": {
47
+ "oss-idle-addon": "bin/cli.mjs"
48
+ },
49
+ "files": [
50
+ "dist",
51
+ "bin",
52
+ "templates",
53
+ "README.md",
54
+ "LICENSE"
55
+ ],
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "scripts": {
60
+ "build": "tsc -p tsconfig.json",
61
+ "dev": "tsc -p tsconfig.json --watch",
62
+ "prepublishOnly": "npm run build"
63
+ },
64
+ "dependencies": {
65
+ "esbuild": "^0.24.0"
66
+ },
67
+ "peerDependencies": {
68
+ "react": "^18.3.1",
69
+ "react-dom": "^18.3.1"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "react": {
73
+ "optional": true
74
+ },
75
+ "react-dom": {
76
+ "optional": true
77
+ }
78
+ },
79
+ "devDependencies": {
80
+ "@types/node": "^26.4.1",
81
+ "@types/react": "^18.3.12",
82
+ "@types/react-dom": "^18.3.1",
83
+ "typescript": "^5.6.3"
84
+ }
85
+ }
@@ -0,0 +1,31 @@
1
+ # __ADDON_NAME__
2
+
3
+ An add-on for [oss!idle](https://github.com/oss-idle/oss-idle), built with the
4
+ [oss!idle Addon SDK](https://github.com/oss-idle/addon-sdk).
5
+
6
+ ## Build
7
+
8
+ ```sh
9
+ npm install
10
+ npm run build
11
+ ```
12
+
13
+ That emits `__OUTFILE__` - a single self-contained ES module. Paste its contents
14
+ into the in-game add-on editor (Options → Add-ons → Manage) and enable it.
15
+
16
+ `npm run dev` rebuilds on save; re-paste to see changes.
17
+
18
+ ## How an add-on runs
19
+
20
+ The game imports your built file as an ES module from a Blob URL and calls its
21
+ `mount` / `unmount` exports. Nothing is injected and nothing resolves at load
22
+ time, so every dependency is bundled into that one file.
23
+
24
+ `unmount` has to genuinely undo everything, because add-ons are enabled,
25
+ disabled, updated and removed without a page reload. The SDK's `Addon` base
26
+ class records teardown for every listener, timer and overlay it hands you and
27
+ reverses it for you.
28
+
29
+ ## Licence
30
+
31
+ __LICENSE__
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "__ADDON_NAME__",
3
+ "license": "__LICENSE__",
4
+ "entry": "src/index.ts",
5
+ "outfile": "dist/__SLUG__.js"
6
+ }
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ dist/
3
+ *.tsbuildinfo
@@ -0,0 +1,17 @@
1
+ /** Stylesheets are inlined as strings and injected into the overlay's shadow root. */
2
+ declare module '*.css' {
3
+ const css: string;
4
+ export default css;
5
+ }
6
+
7
+ /** Workers are inlined into the single-file bundle and spawned from a Blob URL. */
8
+ declare module 'worker:*' {
9
+ const createWorker: () => Worker;
10
+ export default createWorker;
11
+ }
12
+
13
+ /** Binary assets inlined as base64 (e.g. `import wasm from './x.wasm?base64'`). */
14
+ declare module '*?base64' {
15
+ const base64: string;
16
+ export default base64;
17
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker"],
5
+ "module": "ES2022",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "skipLibCheck": true,
11
+ "noEmit": true
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * __ADDON_NAME__
3
+ *
4
+ * oss!idle loads an add-on by importing its built source as an ES module and
5
+ * calling `mount` / `unmount`. That pair is the whole contract - `defineAddon`
6
+ * produces it from the class below.
7
+ */
8
+
9
+ import { Addon, defineAddon, gameDb } from '@oss-idle/addon-sdk';
10
+
11
+ class __CLASS__ extends Addon {
12
+
13
+ constructor() {
14
+ super({ name: '__ADDON_NAME__' });
15
+ }
16
+
17
+ protected async onMount(): Promise<void> {
18
+ // The client opens its database lazily, so wait before querying it.
19
+ await gameDb.ready();
20
+
21
+ const played = await gameDb.value<number>('SELECT COUNT(*) FROM score');
22
+ this.log.info(`hello! you have ${played ?? 0} scores stored locally`);
23
+
24
+ // Anything registered through `this.on` / `this.interval` / `this.own`
25
+ // is torn down automatically when the add-on is disabled or updated.
26
+ this.on(window, 'keydown', event => {
27
+ if (event.key === 'F9') this.log.info('F9');
28
+ });
29
+ }
30
+ }
31
+
32
+ export const { mount, unmount } = defineAddon(() => new __CLASS__());
@@ -0,0 +1,44 @@
1
+ /**
2
+ * __ADDON_NAME__
3
+ *
4
+ * The panel is mounted for the add-on's whole life and only *hidden* when
5
+ * closed. Returning `null` instead would unmount the React tree, discarding its
6
+ * state and running cleanup - so anything the overlay had started would be
7
+ * thrown away the moment the player closed it.
8
+ */
9
+
10
+ import type { ReactNode } from 'react';
11
+ import { ReactAddon } from '@oss-idle/addon-sdk/react';
12
+ import Panel from './Panel.js';
13
+ import styles from './styles.css';
14
+
15
+ const TOGGLE = 'Ctrl+Shift+__KEY__';
16
+
17
+ export class __CLASS__ extends ReactAddon {
18
+
19
+ private open = false;
20
+
21
+ constructor() {
22
+ super({ name: '__ADDON_NAME__', overlayId: '__SLUG__', styles });
23
+ }
24
+
25
+ protected override async onReady(): Promise<void> {
26
+ // Registered in the capture phase, ahead of the game's own handlers, so
27
+ // the combo never reaches gameplay.
28
+ this.own(this.hotkeys.register(TOGGLE, () => this.toggle(), { allowInText: true }));
29
+ this.own(this.hotkeys.register('Escape', () => {
30
+ if (this.open) this.toggle();
31
+ }, { allowInText: true }));
32
+
33
+ this.log.info(`ready - press ${TOGGLE} to open`);
34
+ }
35
+
36
+ private toggle(): void {
37
+ this.open = !this.open;
38
+ this.refresh();
39
+ }
40
+
41
+ protected render(): ReactNode {
42
+ return <Panel open={this.open} version={this.meta.version ?? ''} onClose={() => this.toggle()} />;
43
+ }
44
+ }
@@ -0,0 +1,41 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { gameCharacter, type CharacterRow } from '@oss-idle/addon-sdk';
3
+
4
+ export default function Panel({
5
+ open,
6
+ version,
7
+ onClose,
8
+ }: {
9
+ open: boolean;
10
+ version: string;
11
+ onClose: () => void;
12
+ }) {
13
+ const [character, setCharacter] = useState<CharacterRow>();
14
+
15
+ // Re-read when opened: the client writes skill levels back to this row after
16
+ // every play, so a value read once at boot goes stale.
17
+ useEffect(() => {
18
+ if (open) void gameCharacter.live().then(setCharacter);
19
+ }, [open]);
20
+
21
+ return (
22
+ <div style={{ display: open ? 'contents' : 'none' }} aria-hidden={!open}>
23
+ <div className="backdrop" onClick={onClose} />
24
+ <div className="panel" role="dialog" aria-label="__ADDON_NAME__">
25
+ <header>
26
+ <h1>__ADDON_NAME__</h1>
27
+ <span className="spacer" />
28
+ <span className="hint">v{version}</span>
29
+ <button onClick={onClose}>Close</button>
30
+ </header>
31
+
32
+ <div className="body">
33
+ {character
34
+ ? <p>Playing as <strong>{character.name}</strong> (level {character.overallLevel}).</p>
35
+ : <p>Looking for your character...</p>}
36
+ <p>Edit <code>src/Panel.tsx</code> to build your add-on.</p>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * __ADDON_NAME__
3
+ *
4
+ * oss!idle loads an add-on by importing its built source as an ES module and
5
+ * calling `mount` / `unmount`. That pair is the whole contract - `defineAddon`
6
+ * produces it from the class in `Addon.tsx`.
7
+ */
8
+
9
+ import { defineAddon } from '@oss-idle/addon-sdk';
10
+ import { __CLASS__ } from './Addon.js';
11
+
12
+ export const { mount, unmount } = defineAddon(() => new __CLASS__());
@@ -0,0 +1,61 @@
1
+ /*
2
+ * Rendered inside a shadow root, so these selectors cannot reach the game and
3
+ * the game's stylesheet cannot reach them.
4
+ */
5
+
6
+ :host, * { box-sizing: border-box; }
7
+
8
+ .backdrop {
9
+ position: absolute;
10
+ inset: 0;
11
+ background: rgb(8 6 12 / 55%);
12
+ pointer-events: auto;
13
+ }
14
+
15
+ .panel {
16
+ position: absolute;
17
+ top: 50%;
18
+ left: 50%;
19
+ transform: translate(-50%, -50%);
20
+ width: min(720px, 92vw);
21
+ max-height: 80vh;
22
+ display: flex;
23
+ flex-direction: column;
24
+ pointer-events: auto;
25
+
26
+ font-family: ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
27
+ font-size: 13px;
28
+ color: #eceaf2;
29
+ background: #16141c;
30
+ border: 1px solid #322c40;
31
+ border-radius: 12px;
32
+ box-shadow: 0 24px 80px rgb(0 0 0 / 60%);
33
+ overflow: hidden;
34
+ }
35
+
36
+ header {
37
+ display: flex;
38
+ align-items: center;
39
+ gap: 12px;
40
+ padding: 14px 18px;
41
+ background: #1e1a28;
42
+ border-bottom: 1px solid #322c40;
43
+ }
44
+
45
+ header h1 { margin: 0; font-size: 15px; font-weight: 600; }
46
+ header .spacer { flex: 1; }
47
+ header .hint { color: #8b83a0; font-size: 11px; }
48
+
49
+ button {
50
+ font: inherit;
51
+ color: #eceaf2;
52
+ background: #2b2438;
53
+ border: 1px solid #3d3550;
54
+ border-radius: 6px;
55
+ padding: 6px 12px;
56
+ cursor: pointer;
57
+ }
58
+ button:hover { background: #362d47; }
59
+
60
+ .body { padding: 18px; overflow: auto; line-height: 1.7; }
61
+ code { background: #221d2e; border-radius: 4px; padding: 1px 5px; }