@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.
Files changed (70) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +262 -2
  3. package/bin/cli.mjs +137 -0
  4. package/bin/scaffold.mjs +157 -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 +73 -0
  21. package/dist/build/index.d.ts.map +1 -0
  22. package/dist/build/index.js +126 -0
  23. package/dist/build/plugins.d.ts +95 -0
  24. package/dist/build/plugins.d.ts.map +1 -0
  25. package/dist/build/plugins.js +240 -0
  26. package/dist/index.d.ts +12 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +11 -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 +94 -0
  42. package/dist/runtime/addon.d.ts.map +1 -0
  43. package/dist/runtime/addon.js +129 -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 +26 -0
  48. package/dist/runtime/define.d.ts.map +1 -0
  49. package/dist/runtime/define.js +40 -0
  50. package/dist/runtime/host.d.ts +113 -0
  51. package/dist/runtime/host.d.ts.map +1 -0
  52. package/dist/runtime/host.js +8 -0
  53. package/dist/runtime/input.d.ts +58 -0
  54. package/dist/runtime/input.d.ts.map +1 -0
  55. package/dist/runtime/input.js +119 -0
  56. package/dist/runtime/settings.d.ts +36 -0
  57. package/dist/runtime/settings.d.ts.map +1 -0
  58. package/dist/runtime/settings.js +74 -0
  59. package/package.json +83 -4
  60. package/templates/common/README.md +50 -0
  61. package/templates/common/addon.config.json +7 -0
  62. package/templates/common/eslint.config.js +54 -0
  63. package/templates/common/gitignore +3 -0
  64. package/templates/common/src/env.d.ts +17 -0
  65. package/templates/common/tsconfig.json +14 -0
  66. package/templates/plain/src/index.ts +32 -0
  67. package/templates/react/src/Addon.tsx +44 -0
  68. package/templates/react/src/Panel.tsx +41 -0
  69. package/templates/react/src/index.ts +12 -0
  70. package/templates/react/src/styles.css +61 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Bridge to the player's characters.
3
+ *
4
+ * The client stores skills as flat columns on the `character` table - one group
5
+ * per skill (`speed`, `speedXP`, `speedUpgrades`, `speedOverdrive`,
6
+ * `speedPrestige`, ...) - and reconstitutes them into `Skill` objects when it
7
+ * builds its DAO. An add-on reads the row here and reconstitutes them the same
8
+ * way, using the skill factory bundled from the game's own sim, so a simulated
9
+ * play sees exactly the character the player is on.
10
+ *
11
+ * **Which row is "the" character is not obvious, and guessing wrong is quiet.**
12
+ * The table holds two populations:
13
+ *
14
+ * - the **local lineage** (`local = 1`, negative ids) - the Guest and its past
15
+ * generations. Exactly one carries `current = 1`, set by `makeCurrent()`,
16
+ * which updates `WHERE local = 1` and so never touches an account character.
17
+ * - **cached account characters** (`local = 0`, the server's own positive ids),
18
+ * written whenever a signed-in session resolves.
19
+ *
20
+ * So `current = 1` means "the live *guest*", not "the character being played" -
21
+ * reading it while signed in silently returns a much weaker character, and the
22
+ * only symptom is that simulated results come out too low.
23
+ *
24
+ * The client resolves the real answer from session state held in module memory
25
+ * (`Entities.character`), which an add-on cannot read. But the answer is still
26
+ * recoverable from data: every finished play is stored locally with the live
27
+ * character's id, so **the most recent score names the character being played**
28
+ * - across sign-in, sign-out and account switches alike. {@link live} resolves
29
+ * through that, and {@link list} exists so an add-on can still show its choice
30
+ * and let the player correct it.
31
+ */
32
+ import { type SqlValue } from './db.js';
33
+ export type CharacterRow = Record<string, SqlValue> & {
34
+ id: number;
35
+ name: string;
36
+ generation: number;
37
+ overallLevel: number;
38
+ overallTotalXp: number;
39
+ memoryResetAt: number;
40
+ /** 1 for the local guest lineage, 0 for a cached account character. */
41
+ local: number;
42
+ /** Marks the live *guest*; meaningless for account characters. */
43
+ current: number;
44
+ };
45
+ export declare const gameCharacter: {
46
+ /**
47
+ * Every character on this client, account characters first, then the guest
48
+ * lineage - each list strongest first, which is also the order of "most
49
+ * likely to be the one being played".
50
+ */
51
+ list(): Promise<CharacterRow[]>;
52
+ /**
53
+ * The character being played.
54
+ *
55
+ * Resolved in order of how directly each signal answers the question:
56
+ *
57
+ * 1. **The most recent stored score's `characterId`.** Plays are saved with
58
+ * the live character's id, so this is the game's own record of who was
59
+ * playing, and it follows account switches for free.
60
+ * 2. **A cached account character** (`local = 0`), for a signed-in player who
61
+ * has no stored plays yet - a fresh install, or a cleared local database.
62
+ * 3. **The guest**, for a player who has never signed in.
63
+ *
64
+ * Only step 1 is a fact; the rest are inference, so an add-on that acts on
65
+ * this should still show which character it used.
66
+ */
67
+ live(): Promise<CharacterRow | undefined>;
68
+ /** One character by id. */
69
+ byId(id: number): Promise<CharacterRow | undefined>;
70
+ /**
71
+ * The live *guest* - the local lineage's current character.
72
+ *
73
+ * This is what `current = 1` actually marks. Use it only when you
74
+ * specifically want the offline character; for "who is playing", use
75
+ * {@link live}.
76
+ */
77
+ guest(): Promise<CharacterRow | undefined>;
78
+ /**
79
+ * Apply a character row onto skill objects built by the game's factory.
80
+ *
81
+ * Mirrors the client's `Character` constructor. `skills` is whatever
82
+ * `makeOrderedSkills()` returned - typed loosely so this module stays
83
+ * independent of the bundled sim's own types.
84
+ */
85
+ applySkills<T extends {
86
+ name: string;
87
+ } & Record<string, {
88
+ set(value: never): unknown;
89
+ }>>(skills: readonly T[], row: CharacterRow): readonly T[];
90
+ };
91
+ //# sourceMappingURL=character.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"character.d.ts","sourceRoot":"","sources":["../../src/bridge/character.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEhD,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,eAAO,MAAM,aAAa;IAEzB;;;;OAIG;YACW,OAAO,CAAC,YAAY,EAAE,CAAC;IAOrC;;;;;;;;;;;;;;OAcG;YACW,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAgB/C,2BAA2B;aACZ,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAIzD;;;;;;OAMG;aACY,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAMhD;;;;;;OAMG;gBACS,CAAC,SAAS;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE;QAAE,GAAG,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAA;KAAE,CAAC,UAC9E,SAAS,CAAC,EAAE,OACf,YAAY,GACf,SAAS,CAAC,EAAE;CAgBf,CAAC"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Bridge to the player's characters.
3
+ *
4
+ * The client stores skills as flat columns on the `character` table - one group
5
+ * per skill (`speed`, `speedXP`, `speedUpgrades`, `speedOverdrive`,
6
+ * `speedPrestige`, ...) - and reconstitutes them into `Skill` objects when it
7
+ * builds its DAO. An add-on reads the row here and reconstitutes them the same
8
+ * way, using the skill factory bundled from the game's own sim, so a simulated
9
+ * play sees exactly the character the player is on.
10
+ *
11
+ * **Which row is "the" character is not obvious, and guessing wrong is quiet.**
12
+ * The table holds two populations:
13
+ *
14
+ * - the **local lineage** (`local = 1`, negative ids) - the Guest and its past
15
+ * generations. Exactly one carries `current = 1`, set by `makeCurrent()`,
16
+ * which updates `WHERE local = 1` and so never touches an account character.
17
+ * - **cached account characters** (`local = 0`, the server's own positive ids),
18
+ * written whenever a signed-in session resolves.
19
+ *
20
+ * So `current = 1` means "the live *guest*", not "the character being played" -
21
+ * reading it while signed in silently returns a much weaker character, and the
22
+ * only symptom is that simulated results come out too low.
23
+ *
24
+ * The client resolves the real answer from session state held in module memory
25
+ * (`Entities.character`), which an add-on cannot read. But the answer is still
26
+ * recoverable from data: every finished play is stored locally with the live
27
+ * character's id, so **the most recent score names the character being played**
28
+ * - across sign-in, sign-out and account switches alike. {@link live} resolves
29
+ * through that, and {@link list} exists so an add-on can still show its choice
30
+ * and let the player correct it.
31
+ */
32
+ import { gameDb } from './db.js';
33
+ export const gameCharacter = {
34
+ /**
35
+ * Every character on this client, account characters first, then the guest
36
+ * lineage - each list strongest first, which is also the order of "most
37
+ * likely to be the one being played".
38
+ */
39
+ async list() {
40
+ return gameDb.select(`
41
+ SELECT * FROM character
42
+ ORDER BY local ASC, current DESC, overallTotalXp DESC, generation DESC
43
+ `);
44
+ },
45
+ /**
46
+ * The character being played.
47
+ *
48
+ * Resolved in order of how directly each signal answers the question:
49
+ *
50
+ * 1. **The most recent stored score's `characterId`.** Plays are saved with
51
+ * the live character's id, so this is the game's own record of who was
52
+ * playing, and it follows account switches for free.
53
+ * 2. **A cached account character** (`local = 0`), for a signed-in player who
54
+ * has no stored plays yet - a fresh install, or a cleared local database.
55
+ * 3. **The guest**, for a player who has never signed in.
56
+ *
57
+ * Only step 1 is a fact; the rest are inference, so an add-on that acts on
58
+ * this should still show which character it used.
59
+ */
60
+ async live() {
61
+ const lastPlayed = await gameDb.value('SELECT characterId FROM score ORDER BY playedAt DESC LIMIT 1').catch(() => undefined);
62
+ if (typeof lastPlayed === 'number') {
63
+ const played = await this.byId(lastPlayed);
64
+ if (played)
65
+ return played;
66
+ }
67
+ const account = await gameDb.first('SELECT * FROM character WHERE local = 0 ORDER BY overallTotalXp DESC LIMIT 1');
68
+ return account ?? await this.guest();
69
+ },
70
+ /** One character by id. */
71
+ async byId(id) {
72
+ return gameDb.first('SELECT * FROM character WHERE id = ? LIMIT 1', [id]);
73
+ },
74
+ /**
75
+ * The live *guest* - the local lineage's current character.
76
+ *
77
+ * This is what `current = 1` actually marks. Use it only when you
78
+ * specifically want the offline character; for "who is playing", use
79
+ * {@link live}.
80
+ */
81
+ async guest() {
82
+ return gameDb.first('SELECT * FROM character WHERE local = 1 ORDER BY current DESC, generation DESC LIMIT 1');
83
+ },
84
+ /**
85
+ * Apply a character row onto skill objects built by the game's factory.
86
+ *
87
+ * Mirrors the client's `Character` constructor. `skills` is whatever
88
+ * `makeOrderedSkills()` returned - typed loosely so this module stays
89
+ * independent of the bundled sim's own types.
90
+ */
91
+ applySkills(skills, row) {
92
+ for (const skill of skills) {
93
+ const set = (field, column) => {
94
+ const value = row[column];
95
+ if (typeof value === 'number') {
96
+ skill[field].set(value);
97
+ }
98
+ };
99
+ set('level', skill.name);
100
+ set('xp', `${skill.name}XP`);
101
+ set('upgrades', `${skill.name}Upgrades`);
102
+ set('overdrive', `${skill.name}Overdrive`);
103
+ set('prestige', `${skill.name}Prestige`);
104
+ }
105
+ return skills;
106
+ },
107
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Bridge to the game's local database.
3
+ *
4
+ * The client keeps its state in an in-page sql.js database persisted to
5
+ * IndexedDB, and publishes the managed handle as `window.db` (see the client's
6
+ * `db/dao.ts`). An add-on cannot import the client's typed DAOs - it is a
7
+ * separate module graph - so it talks to that handle in raw SQL instead.
8
+ *
9
+ * Reads go through `db.read`; writes go through `db.write`, which is what
10
+ * persists the database back to IndexedDB. Writing with `read` would mutate the
11
+ * in-memory copy only, and the change would vanish on reload.
12
+ */
13
+ export type SqlValue = string | number | Uint8Array | null;
14
+ /**
15
+ * The game's database, addressed in SQL. `T` is the caller's assertion about
16
+ * the row shape - the game's schema is not visible to this module graph, so
17
+ * nothing can check it for you; keep queries close to the tables you know.
18
+ */
19
+ export declare const gameDb: {
20
+ /** Every row a query returns. */
21
+ select<T = Record<string, SqlValue>>(sql: string, params?: SqlValue[]): Promise<T[]>;
22
+ /** The first row, or `undefined`. */
23
+ first<T = Record<string, SqlValue>>(sql: string, params?: SqlValue[]): Promise<T | undefined>;
24
+ /** A single scalar (first column of the first row), or `undefined`. */
25
+ value<T extends SqlValue>(sql: string, params?: SqlValue[]): Promise<T | undefined>;
26
+ /** One write statement, persisted to IndexedDB. */
27
+ run(sql: string, params?: SqlValue[]): Promise<void>;
28
+ /**
29
+ * Several statements persisted as one save - cheaper than a `run` each, and
30
+ * it never leaves the database half-updated on disk.
31
+ */
32
+ batch(statements: readonly {
33
+ sql: string;
34
+ params?: SqlValue[];
35
+ }[]): Promise<void>;
36
+ /** Whether the game has published its database yet. */
37
+ available(): boolean;
38
+ /**
39
+ * Resolve once the database is published. The client opens it lazily, so an
40
+ * add-on mounted at boot can easily beat it there.
41
+ */
42
+ ready(timeoutMs?: number): Promise<void>;
43
+ };
44
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/bridge/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC;AAsC3D;;;;GAIG;AACH,eAAO,MAAM,MAAM;IAElB,iCAAiC;WACpB,CAAC,kCAAkC,MAAM,WAAU,QAAQ,EAAE,GAAQ,OAAO,CAAC,CAAC,EAAE,CAAC;IAI9F,qCAAqC;UACzB,CAAC,kCAAkC,MAAM,WAAU,QAAQ,EAAE,GAAQ,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAIvG,uEAAuE;UAC3D,CAAC,SAAS,QAAQ,OAAO,MAAM,WAAU,QAAQ,EAAE,GAAQ,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAK7F,mDAAmD;aACpC,MAAM,WAAU,QAAQ,EAAE,GAAQ,OAAO,CAAC,IAAI,CAAC;IAI9D;;;OAGG;sBACqB,SAAS;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAA;KAAE,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAOvF,uDAAuD;iBAC1C,OAAO;IAIpB;;;OAGG;+BAC8B,OAAO,CAAC,IAAI,CAAC;CAO9C,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Bridge to the game's local database.
3
+ *
4
+ * The client keeps its state in an in-page sql.js database persisted to
5
+ * IndexedDB, and publishes the managed handle as `window.db` (see the client's
6
+ * `db/dao.ts`). An add-on cannot import the client's typed DAOs - it is a
7
+ * separate module graph - so it talks to that handle in raw SQL instead.
8
+ *
9
+ * Reads go through `db.read`; writes go through `db.write`, which is what
10
+ * persists the database back to IndexedDB. Writing with `read` would mutate the
11
+ * in-memory copy only, and the change would vanish on reload.
12
+ */
13
+ const handle = () => {
14
+ const db = window.db;
15
+ if (!db?.read) {
16
+ throw new Error('oss!idle database not found on window - the game has not opened it yet, ' +
17
+ 'or this add-on is running outside the client.');
18
+ }
19
+ return db;
20
+ };
21
+ /** Reshape one sql.js result set into plain row objects. */
22
+ const toRows = (result) => {
23
+ const first = result[0];
24
+ if (!first)
25
+ return [];
26
+ return first.values.map(row => {
27
+ const out = {};
28
+ first.columns.forEach((column, i) => { out[column] = row[i] ?? null; });
29
+ return out;
30
+ });
31
+ };
32
+ /**
33
+ * The game's database, addressed in SQL. `T` is the caller's assertion about
34
+ * the row shape - the game's schema is not visible to this module graph, so
35
+ * nothing can check it for you; keep queries close to the tables you know.
36
+ */
37
+ export const gameDb = {
38
+ /** Every row a query returns. */
39
+ async select(sql, params = []) {
40
+ return toRows(await handle().read(db => db.exec(sql, params)));
41
+ },
42
+ /** The first row, or `undefined`. */
43
+ async first(sql, params = []) {
44
+ return (await this.select(sql, params))[0];
45
+ },
46
+ /** A single scalar (first column of the first row), or `undefined`. */
47
+ async value(sql, params = []) {
48
+ const result = await handle().read(db => db.exec(sql, params));
49
+ return result[0]?.values[0]?.[0];
50
+ },
51
+ /** One write statement, persisted to IndexedDB. */
52
+ async run(sql, params = []) {
53
+ await handle().write(db => { db.exec(sql, params); });
54
+ },
55
+ /**
56
+ * Several statements persisted as one save - cheaper than a `run` each, and
57
+ * it never leaves the database half-updated on disk.
58
+ */
59
+ async batch(statements) {
60
+ if (!statements.length)
61
+ return;
62
+ await handle().write(db => {
63
+ for (const { sql, params } of statements)
64
+ db.exec(sql, params ?? []);
65
+ });
66
+ },
67
+ /** Whether the game has published its database yet. */
68
+ available() {
69
+ return !!window.db?.read;
70
+ },
71
+ /**
72
+ * Resolve once the database is published. The client opens it lazily, so an
73
+ * add-on mounted at boot can easily beat it there.
74
+ */
75
+ async ready(timeoutMs = 30_000) {
76
+ const deadline = Date.now() + timeoutMs;
77
+ while (!this.available()) {
78
+ if (Date.now() > deadline)
79
+ throw new Error('oss!idle database did not open in time');
80
+ await new Promise(resolve => setTimeout(resolve, 100));
81
+ }
82
+ },
83
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Bridge to the player's playlists.
3
+ *
4
+ * A playlist is local to the client's sql.js database: one `playlist` row and
5
+ * one `playlist_entry` per difficulty, keyed on `(playlistId, beatmapId)`.
6
+ *
7
+ * **Writes must go through {@link gameDb}'s write path.** It persists the
8
+ * database back to IndexedDB; a mutation issued on the read side applies to the
9
+ * in-memory copy and is gone at the next reload, with nothing to show for it in
10
+ * between.
11
+ *
12
+ * **A write here does not refresh the client's own views.** They re-query on a
13
+ * version counter the client bumps itself, which lives in module state an
14
+ * add-on cannot reach - so song select keeps showing the list it last read
15
+ * until something else makes it re-read. Tell the player to reopen song select
16
+ * rather than leaving them to wonder why nothing changed.
17
+ */
18
+ export type PlaylistRow = {
19
+ id: number;
20
+ name: string;
21
+ createdAt: number;
22
+ };
23
+ export declare const playlists: {
24
+ /** Every playlist, with how many difficulties each holds. */
25
+ list(): Promise<(PlaylistRow & {
26
+ count: number;
27
+ })[]>;
28
+ /**
29
+ * The playlist with this name, oldest first.
30
+ *
31
+ * The game does not enforce unique playlist names - `Playlist.create` inserts
32
+ * whatever it is given - so a player can hold two called the same thing.
33
+ * Ordering by id makes the choice deterministic: regenerating a playlist
34
+ * keeps overwriting the same one instead of picking arbitrarily.
35
+ */
36
+ byName(name: string): Promise<PlaylistRow | undefined>;
37
+ /** The difficulty ids in a playlist. */
38
+ entries(playlistId: number): Promise<number[]>;
39
+ /**
40
+ * Create a playlist holding exactly `beatmapIds`, replacing one of the same
41
+ * name. A generated playlist is regenerated often, so replace is the useful
42
+ * default - and clearing and refilling in one batch means it is never
43
+ * persisted in a half-empty state.
44
+ *
45
+ * Replacing keeps the existing playlist's **id**, so anything already
46
+ * pointing at it keeps pointing at it.
47
+ */
48
+ replace(name: string, beatmapIds: readonly number[]): Promise<{
49
+ id: number;
50
+ created: boolean;
51
+ }>;
52
+ /** Delete a playlist and its entries. */
53
+ remove(playlistId: number): Promise<void>;
54
+ };
55
+ //# sourceMappingURL=playlists.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playlists.d.ts","sourceRoot":"","sources":["../../src/bridge/playlists.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1E,eAAO,MAAM,SAAS;IAErB,6DAA6D;YAC/C,OAAO,CAAC,CAAC,WAAW,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAC;IAS1D;;;;;;;OAOG;iBACgB,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IAO5D,wCAAwC;wBACd,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAQpD;;;;;;;;OAQG;kBAEI,MAAM,cACA,SAAS,MAAM,EAAE,GAC3B,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IA8B5C,yCAAyC;uBAChB,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAM/C,CAAC"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Bridge to the player's playlists.
3
+ *
4
+ * A playlist is local to the client's sql.js database: one `playlist` row and
5
+ * one `playlist_entry` per difficulty, keyed on `(playlistId, beatmapId)`.
6
+ *
7
+ * **Writes must go through {@link gameDb}'s write path.** It persists the
8
+ * database back to IndexedDB; a mutation issued on the read side applies to the
9
+ * in-memory copy and is gone at the next reload, with nothing to show for it in
10
+ * between.
11
+ *
12
+ * **A write here does not refresh the client's own views.** They re-query on a
13
+ * version counter the client bumps itself, which lives in module state an
14
+ * add-on cannot reach - so song select keeps showing the list it last read
15
+ * until something else makes it re-read. Tell the player to reopen song select
16
+ * rather than leaving them to wonder why nothing changed.
17
+ */
18
+ import { gameDb } from './db.js';
19
+ export const playlists = {
20
+ /** Every playlist, with how many difficulties each holds. */
21
+ async list() {
22
+ return gameDb.select(`
23
+ SELECT p.id, p.name, p.createdAt,
24
+ (SELECT COUNT(*) FROM playlist_entry e WHERE e.playlistId = p.id) AS count
25
+ FROM playlist p
26
+ ORDER BY p.createdAt DESC
27
+ `);
28
+ },
29
+ /**
30
+ * The playlist with this name, oldest first.
31
+ *
32
+ * The game does not enforce unique playlist names - `Playlist.create` inserts
33
+ * whatever it is given - so a player can hold two called the same thing.
34
+ * Ordering by id makes the choice deterministic: regenerating a playlist
35
+ * keeps overwriting the same one instead of picking arbitrarily.
36
+ */
37
+ async byName(name) {
38
+ return gameDb.first('SELECT * FROM playlist WHERE name = ? ORDER BY id ASC LIMIT 1', [name]);
39
+ },
40
+ /** The difficulty ids in a playlist. */
41
+ async entries(playlistId) {
42
+ const rows = await gameDb.select('SELECT beatmapId FROM playlist_entry WHERE playlistId = ?', [playlistId]);
43
+ return rows.map(row => row.beatmapId);
44
+ },
45
+ /**
46
+ * Create a playlist holding exactly `beatmapIds`, replacing one of the same
47
+ * name. A generated playlist is regenerated often, so replace is the useful
48
+ * default - and clearing and refilling in one batch means it is never
49
+ * persisted in a half-empty state.
50
+ *
51
+ * Replacing keeps the existing playlist's **id**, so anything already
52
+ * pointing at it keeps pointing at it.
53
+ */
54
+ async replace(name, beatmapIds) {
55
+ const existing = await this.byName(name);
56
+ if (existing) {
57
+ await gameDb.batch([
58
+ { sql: 'DELETE FROM playlist_entry WHERE playlistId = ?', params: [existing.id] },
59
+ ...beatmapIds.map(id => ({
60
+ sql: 'INSERT OR REPLACE INTO playlist_entry (playlistId, beatmapId) VALUES (?, ?)',
61
+ params: [existing.id, id],
62
+ })),
63
+ ]);
64
+ return { id: existing.id, created: false };
65
+ }
66
+ await gameDb.run('INSERT INTO playlist (name, createdAt) VALUES (?, ?)', [name, Date.now()]);
67
+ const created = await this.byName(name);
68
+ if (!created)
69
+ throw new Error(`Failed to create playlist "${name}"`);
70
+ if (beatmapIds.length) {
71
+ await gameDb.batch(beatmapIds.map(id => ({
72
+ sql: 'INSERT OR REPLACE INTO playlist_entry (playlistId, beatmapId) VALUES (?, ?)',
73
+ params: [created.id, id],
74
+ })));
75
+ }
76
+ return { id: created.id, created: true };
77
+ },
78
+ /** Delete a playlist and its entries. */
79
+ async remove(playlistId) {
80
+ await gameDb.batch([
81
+ { sql: 'DELETE FROM playlist_entry WHERE playlistId = ?', params: [playlistId] },
82
+ { sql: 'DELETE FROM playlist WHERE id = ?', params: [playlistId] },
83
+ ]);
84
+ },
85
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Bridge to the player's play history.
3
+ *
4
+ * Every finished play is stored locally in `score`, stamped with the character
5
+ * that played it and when. That makes this table the only record of what a
6
+ * player has actually been doing - the client's own session state is module
7
+ * memory an add-on cannot read.
8
+ *
9
+ * Both helpers here count plays across the whole library in one query rather
10
+ * than asking per map, because the callers that want counts want all of them.
11
+ */
12
+ /**
13
+ * Plays of each map by this character since `since`, by beatmap id.
14
+ *
15
+ * Mirrors the client's `Score.countPlays`, which is called per map with the
16
+ * character's `memoryResetAt`: plays from before memory was last prestiged do
17
+ * not count towards it, because that training is what the prestige spent.
18
+ */
19
+ export declare const playCountsSince: (characterId: number, since: number) => Promise<Record<number, number>>;
20
+ /**
21
+ * Plays of each map in the last 24 hours, by beatmap id.
22
+ *
23
+ * Mirrors the server's `recentMapPlays`: the last 50 plays inside a 24h window,
24
+ * counted per map. That is the window the server scores repetition over, so an
25
+ * add-on predicting what a play is worth has to read it the same way.
26
+ */
27
+ export declare const recentPlayCounts: (characterId: number) => Promise<Map<number, number>>;
28
+ //# sourceMappingURL=scores.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scores.d.ts","sourceRoot":"","sources":["../../src/bridge/scores.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC3B,aAAa,MAAM,EACnB,OAAO,MAAM,KACX,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUhC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAU,aAAa,MAAM,KAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CASvF,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Bridge to the player's play history.
3
+ *
4
+ * Every finished play is stored locally in `score`, stamped with the character
5
+ * that played it and when. That makes this table the only record of what a
6
+ * player has actually been doing - the client's own session state is module
7
+ * memory an add-on cannot read.
8
+ *
9
+ * Both helpers here count plays across the whole library in one query rather
10
+ * than asking per map, because the callers that want counts want all of them.
11
+ */
12
+ import { gameDb } from './db.js';
13
+ /**
14
+ * Plays of each map by this character since `since`, by beatmap id.
15
+ *
16
+ * Mirrors the client's `Score.countPlays`, which is called per map with the
17
+ * character's `memoryResetAt`: plays from before memory was last prestiged do
18
+ * not count towards it, because that training is what the prestige spent.
19
+ */
20
+ export const playCountsSince = async (characterId, since) => {
21
+ const rows = await gameDb.select(`SELECT beatmapId, COUNT(*) AS n FROM score
22
+ WHERE characterId = ? AND playedAt >= ?
23
+ GROUP BY beatmapId`, [characterId, since]);
24
+ const counts = {};
25
+ for (const row of rows)
26
+ counts[row.beatmapId] = row.n;
27
+ return counts;
28
+ };
29
+ /**
30
+ * Plays of each map in the last 24 hours, by beatmap id.
31
+ *
32
+ * Mirrors the server's `recentMapPlays`: the last 50 plays inside a 24h window,
33
+ * counted per map. That is the window the server scores repetition over, so an
34
+ * add-on predicting what a play is worth has to read it the same way.
35
+ */
36
+ export const recentPlayCounts = async (characterId) => {
37
+ const since = Date.now() - 24 * 60 * 60 * 1000;
38
+ const rows = await gameDb.select('SELECT beatmapId FROM score WHERE characterId = ? AND playedAt >= ? ORDER BY playedAt DESC LIMIT 50', [characterId, since]);
39
+ const counts = new Map();
40
+ for (const row of rows)
41
+ counts.set(row.beatmapId, (counts.get(row.beatmapId) ?? 0) + 1);
42
+ return counts;
43
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The add-on bundler.
3
+ *
4
+ * oss!idle stores an add-on as one `source` string and loads it with
5
+ * `import(URL.createObjectURL(new Blob([source])))`. A Blob module has no base
6
+ * URL and no import map, so **nothing** resolves at load time: React, the
7
+ * game's simulation, workers and CSS must all already be inside that one file,
8
+ * and it must be an ES module exporting `mount` and `unmount`.
9
+ *
10
+ * That is what this produces.
11
+ *
12
+ * Output is **not minified by default**. Published add-ons are read by a human
13
+ * moderator before they go live, and a single minified line is a poor thing to
14
+ * ask anyone to review.
15
+ */
16
+ import { type Plugin } from 'esbuild';
17
+ /**
18
+ * The individual esbuild plugins, for a build that needs to assemble its own
19
+ * pipeline rather than call {@link bundleAddon} - compiling a slice of an add-on
20
+ * for a test, say, or bundling a worker on its own.
21
+ */
22
+ export { assetQueryPlugin, cssTextPlugin, dedupePlugin, hostReactPlugin, inlineWorkerPlugin, sharedSourcePlugin, stubModulePlugin, tsExtensionPlugin, } from './plugins.js';
23
+ export type BundleOptions = {
24
+ /** Entry module, exporting `mount` / `unmount` (usually via `defineAddon`). */
25
+ entry: string;
26
+ /** Where to write the single-file add-on source. */
27
+ outfile: string;
28
+ /** Root of an oss!idle checkout, to resolve `@oss-idle/shared` against. */
29
+ gameRoot?: string;
30
+ /** Banner metadata, so a reader can tell what the file is. */
31
+ meta?: {
32
+ name?: string;
33
+ version?: string;
34
+ gameVersion?: string;
35
+ license?: string;
36
+ };
37
+ minify?: boolean;
38
+ sourcemap?: boolean;
39
+ /**
40
+ * Modules to replace with throwing stubs, by path suffix - for trimming a
41
+ * heavy dependency the add-on never calls. See `stubModulePlugin`.
42
+ */
43
+ stubModules?: Record<string, readonly string[]>;
44
+ /**
45
+ * Packages that must resolve to a single copy even though the add-on's
46
+ * sources and the game's sources sit in different checkouts. See
47
+ * `dedupePlugin` - without this, shared libraries are bundled twice and
48
+ * prototype patches and `instanceof` stop working across the seam.
49
+ */
50
+ dedupe?: readonly string[];
51
+ /** Directory the deduped packages resolve from. Defaults to the entry's directory. */
52
+ dedupeFrom?: string;
53
+ /**
54
+ * Extra esbuild plugins, for the add-on and every inlined worker. They run
55
+ * before the SDK's own, so an `onResolve` here can redirect an import -
56
+ * one of the game's own modules, say - before the SDK resolves it.
57
+ */
58
+ plugins?: readonly Plugin[];
59
+ /**
60
+ * Use the game's React (`globalThis.ossIdle.react`) instead of bundling one:
61
+ * a much smaller add-on, and one React on the page. Needs an oss!idle that
62
+ * hands its React to add-ons; an older one refuses the add-on with a message.
63
+ */
64
+ sharedReact?: boolean;
65
+ };
66
+ export type BundleResult = {
67
+ outfile: string;
68
+ /** Byte length of the emitted source - what the workshop stores. */
69
+ bytes: number;
70
+ };
71
+ /** Bundle an add-on into the single ES module the runtime loads. */
72
+ export declare const bundleAddon: (options: BundleOptions) => Promise<BundleResult>;
73
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/build/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAuC,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAe3E;;;;GAIG;AACH,OAAO,EACN,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,GACjB,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,aAAa,GAAG;IAC3B,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAChD;;;;;OAKG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AA8DF,MAAM,MAAM,YAAY,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;CACd,CAAC;AASF,oEAAoE;AACpE,eAAO,MAAM,WAAW,GAAU,SAAS,aAAa,KAAG,OAAO,CAAC,YAAY,CAiD9E,CAAC"}