osu-stable-db 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,22 +69,46 @@ const osuFolder = new OsuFolder('C:/osu!')
69
69
  const osuDatabase = await osuFolder.readOsuDatabase()
70
70
  const scoresDatabase = await osuFolder.readScoresDatabase()
71
71
 
72
- const newestBeatmap = osuDatabase.beatmaps.at(-1)
73
- const newestScore = scoresDatabase.beatmaps.at(-1)?.scores.at(-1)
72
+ const query = osuFolder.createBeatmapScoreQuery(osuDatabase, scoresDatabase)
74
73
 
75
- if (newestBeatmap !== undefined) {
76
- const osuFilePath = osuFolder.getOsuFilePath(newestBeatmap)
77
- console.log(osuFilePath)
78
- }
79
-
80
- if (newestScore !== undefined) {
81
- const osrFilePath = osuFolder.getOsrFilePath(newestScore)
82
- console.log(osrFilePath)
74
+ for (const { beatmap, score } of query.iterateBeatmapScores()) {
75
+ console.log(beatmap.getOsuFilePath())
76
+ console.log(score.getOsrFilePath())
83
77
  }
84
78
  ```
85
79
 
86
80
  If you only need path-based file IO, the same subpath also exports helpers such as readOsuDatabaseFile, writeCollectionDatabaseFile, and writeScoresDatabaseFile.
87
81
 
82
+ ## Query Helpers
83
+
84
+ Use createBeatmapScoreQuery when you want to join osu!.db beatmaps with scores.db entries by beatmap MD5 hash.
85
+
86
+ It accepts either full database objects or the underlying beatmap and score-group arrays, and returns two generators:
87
+
88
+ - iterateBeatmapScoreGroups yields one beatmap with its matching ScoreEntry array
89
+ - iterateBeatmapScores yields one beatmap with one flattened ScoreEntry at a time
90
+
91
+ ```ts
92
+ import {
93
+ createBeatmapScoreQuery,
94
+ readOsuDatabase,
95
+ readScoresDatabase,
96
+ } from 'osu-stable-db'
97
+
98
+ const osuDatabase = readOsuDatabase(osuBytes)
99
+ const scoresDatabase = readScoresDatabase(scoresBytes)
100
+
101
+ const query = createBeatmapScoreQuery(osuDatabase, scoresDatabase)
102
+
103
+ for (const { beatmap, scores } of query.iterateBeatmapScoreGroups()) {
104
+ console.log(beatmap.beatmapId, scores.length)
105
+ }
106
+
107
+ for (const { beatmap, score } of query.iterateBeatmapScores()) {
108
+ console.log(beatmap.difficultyName, score.playerName, score.totalScore)
109
+ }
110
+ ```
111
+
88
112
  ## Types And Time Values
89
113
 
90
114
  - Date-like 64-bit values are exposed as DateTimeTicks, backed by bigint
@@ -100,7 +124,7 @@ This project is 100% AI-generated.
100
124
 
101
125
  Committed minimal fixtures live in [tests/files](tests/files).
102
126
 
103
- To run local node tests and the inspection script against your real osu! installation, set this in [.env](.env):
127
+ To run local node tests against your real osu! installation, set this in [.env](.env):
104
128
 
105
129
  ```dotenv
106
130
  OSU_STABLE_DIR=C:/osu!
@@ -108,16 +132,6 @@ OSU_STABLE_DIR=C:/osu!
108
132
 
109
133
  When OSU_STABLE_DIR is set, local node tests read your real database files and verify byte-for-byte round-trip for osu!.db, collection.db, and scores.db.
110
134
 
111
- You can also generate a local inspection report for a specific beatmap identifier with:
112
-
113
- ```bash
114
- pnpm run local:inspect -- 5288868
115
- ```
116
-
117
- The generated report is written to:
118
-
119
- - [tests/files/local/reports](tests/files/local/reports)
120
-
121
135
  ## Development
122
136
 
123
137
  ```bash
package/dist/index.d.mts CHANGED
@@ -8,8 +8,24 @@ declare function writeCollectionDatabase(database: CollectionDatabase): Uint8Arr
8
8
  declare function readOsuDatabase(input: ArrayBuffer | Uint8Array): OsuDatabase;
9
9
  declare function writeOsuDatabase(database: OsuDatabase): Uint8Array;
10
10
  //#endregion
11
+ //#region src/db/query.d.ts
12
+ interface BeatmapScoresGroupMatch {
13
+ beatmap: BeatmapEntry;
14
+ scores: ScoreEntry[];
15
+ }
16
+ interface BeatmapScoreMatch {
17
+ beatmap: BeatmapEntry;
18
+ score: ScoreEntry;
19
+ }
20
+ type BeatmapQuerySource = OsuDatabase | BeatmapEntry[];
21
+ type ScoreQuerySource = ScoresDatabase | ScoresBeatmapEntry[];
22
+ declare function createBeatmapScoreQuery(beatmapsSource: BeatmapQuerySource, scoresSource: ScoreQuerySource): {
23
+ iterateBeatmapScoreGroups: () => Generator<BeatmapScoresGroupMatch>;
24
+ iterateBeatmapScores: () => Generator<BeatmapScoreMatch>;
25
+ };
26
+ //#endregion
11
27
  //#region src/db/scores.d.ts
12
28
  declare function readScoresDatabase(input: ArrayBuffer | Uint8Array): ScoresDatabase;
13
29
  declare function writeScoresDatabase(database: ScoresDatabase): Uint8Array;
14
30
  //#endregion
15
- export { BeatmapEntry, CollectionDatabase, CollectionEntry, DateTimeTicks, GameplayMode, GameplayModes, Grade, Grades, IntFloatPair, MINIMUM_SUPPORTED_VERSION, Mod, ModFlags, Mods, OsuDatabase, RankedStatus, RankedStatuses, ScoreAdditionalModInfo, ScoreEntry, ScoresBeatmapEntry, ScoresDatabase, TimingPoint, UserPermission, UserPermissionFlags, UserPermissions, readCollectionDatabase, readOsuDatabase, readScoresDatabase, writeCollectionDatabase, writeOsuDatabase, writeScoresDatabase };
31
+ export { BeatmapEntry, BeatmapQuerySource, BeatmapScoreMatch, BeatmapScoresGroupMatch, CollectionDatabase, CollectionEntry, DateTimeTicks, GameplayMode, GameplayModes, Grade, Grades, IntFloatPair, MINIMUM_SUPPORTED_VERSION, Mod, ModFlags, Mods, OsuDatabase, RankedStatus, RankedStatuses, ScoreAdditionalModInfo, ScoreEntry, ScoreQuerySource, ScoresBeatmapEntry, ScoresDatabase, TimingPoint, UserPermission, UserPermissionFlags, UserPermissions, createBeatmapScoreQuery, readCollectionDatabase, readOsuDatabase, readScoresDatabase, writeCollectionDatabase, writeOsuDatabase, writeScoresDatabase };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as writeOsuDatabase, c as GameplayModes, d as Mods, f as RankedStatuses, i as readOsuDatabase, l as Grades, n as writeScoresDatabase, o as readCollectionDatabase, p as UserPermissions, s as writeCollectionDatabase, t as readScoresDatabase, u as MINIMUM_SUPPORTED_VERSION } from "./scores-C35i3lk0.mjs";
2
- export { GameplayModes, Grades, MINIMUM_SUPPORTED_VERSION, Mods, RankedStatuses, UserPermissions, readCollectionDatabase, readOsuDatabase, readScoresDatabase, writeCollectionDatabase, writeOsuDatabase, writeScoresDatabase };
1
+ import { a as readOsuDatabase, c as writeCollectionDatabase, d as MINIMUM_SUPPORTED_VERSION, f as Mods, i as createBeatmapScoreQuery, l as GameplayModes, m as UserPermissions, n as writeScoresDatabase, o as writeOsuDatabase, p as RankedStatuses, s as readCollectionDatabase, t as readScoresDatabase, u as Grades } from "./scores-CW2CUXZ-.mjs";
2
+ export { GameplayModes, Grades, MINIMUM_SUPPORTED_VERSION, Mods, RankedStatuses, UserPermissions, createBeatmapScoreQuery, readCollectionDatabase, readOsuDatabase, readScoresDatabase, writeCollectionDatabase, writeOsuDatabase, writeScoresDatabase };
package/dist/node.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { b as ScoresDatabase, m as OsuDatabase, n as CollectionDatabase, t as BeatmapEntry, v as ScoreEntry } from "./types-DiZMN3fQ.mjs";
1
+ import { _ as ScoreAdditionalModInfo, a as GameplayMode, b as ScoresDatabase, f as ModFlags, h as RankedStatus, i as DateTimeTicks, l as IntFloatPair, m as OsuDatabase, n as CollectionDatabase, s as Grade, t as BeatmapEntry, v as ScoreEntry, x as TimingPoint } from "./types-DiZMN3fQ.mjs";
2
2
 
3
3
  //#region src/node.d.ts
4
4
  type DatabaseFilePath = string | URL;
@@ -23,6 +23,173 @@ declare class OsuFolder {
23
23
  writeCollectionDatabase(database: CollectionDatabase): Promise<void>;
24
24
  readScoresDatabase(): Promise<ScoresDatabase>;
25
25
  writeScoresDatabase(database: ScoresDatabase): Promise<void>;
26
+ createBeatmapScoreQuery(beatmapsSource: OsuDatabase | BeatmapEntry[], scoresSource: ScoresDatabase | Array<{
27
+ beatmapMd5Hash: string | null;
28
+ scores: ScoreEntry[];
29
+ }>): {
30
+ iterateBeatmapScoreGroups: () => Generator<{
31
+ beatmap: {
32
+ artist: string | null;
33
+ artistUnicode: string | null;
34
+ title: string | null;
35
+ titleUnicode: string | null;
36
+ creator: string | null;
37
+ difficultyName: string | null;
38
+ audioFileName: string | null;
39
+ md5Hash: string | null;
40
+ osuFileName: string | null;
41
+ rankedStatus: RankedStatus;
42
+ hitCircleCount: number;
43
+ sliderCount: number;
44
+ spinnerCount: number;
45
+ lastModificationTime: DateTimeTicks;
46
+ approachRate: number;
47
+ circleSize: number;
48
+ hpDrain: number;
49
+ overallDifficulty: number;
50
+ sliderVelocity: number;
51
+ standardStarRatings: IntFloatPair[];
52
+ taikoStarRatings: IntFloatPair[];
53
+ catchStarRatings: IntFloatPair[];
54
+ maniaStarRatings: IntFloatPair[];
55
+ drainTimeSeconds: number;
56
+ totalTimeMs: number;
57
+ previewOffsetMs: number;
58
+ timingPoints: TimingPoint[];
59
+ difficultyId: number;
60
+ beatmapId: number;
61
+ threadId: number;
62
+ standardGrade: Grade;
63
+ taikoGrade: Grade;
64
+ catchGrade: Grade;
65
+ maniaGrade: Grade;
66
+ localOffset: number;
67
+ stackLeniency: number;
68
+ gameplayMode: GameplayMode;
69
+ source: string | null;
70
+ tags: string | null;
71
+ onlineOffset: number;
72
+ titleFont: string | null;
73
+ isUnplayed: boolean;
74
+ lastPlayedAt: DateTimeTicks;
75
+ isOsz2: boolean;
76
+ beatmapFolderName: string | null;
77
+ lastCheckedAgainstRepositoryAt: DateTimeTicks;
78
+ ignoreBeatmapSound: boolean;
79
+ ignoreBeatmapSkin: boolean;
80
+ disableStoryboard: boolean;
81
+ disableVideo: boolean;
82
+ visualOverride: boolean;
83
+ lastModificationTimeUnknown: number;
84
+ maniaScrollSpeed: number;
85
+ getOsuFilePath: () => string;
86
+ };
87
+ scores: {
88
+ gameplayMode: GameplayMode;
89
+ version: number;
90
+ beatmapMd5Hash: string | null;
91
+ playerName: string | null;
92
+ replayMd5Hash: string | null;
93
+ count300: number;
94
+ count100: number;
95
+ count50: number;
96
+ countGeki: number;
97
+ countKatu: number;
98
+ countMiss: number;
99
+ totalScore: number;
100
+ maxCombo: number;
101
+ perfectCombo: boolean;
102
+ mods: ModFlags;
103
+ reservedEmptyString: string | null;
104
+ replayTimestamp: DateTimeTicks;
105
+ reservedInt32: number;
106
+ onlineScoreId: bigint;
107
+ additionalModInfo?: ScoreAdditionalModInfo;
108
+ getOsrFilePath: () => string;
109
+ }[];
110
+ }, void, unknown>;
111
+ iterateBeatmapScores: () => Generator<{
112
+ beatmap: {
113
+ artist: string | null;
114
+ artistUnicode: string | null;
115
+ title: string | null;
116
+ titleUnicode: string | null;
117
+ creator: string | null;
118
+ difficultyName: string | null;
119
+ audioFileName: string | null;
120
+ md5Hash: string | null;
121
+ osuFileName: string | null;
122
+ rankedStatus: RankedStatus;
123
+ hitCircleCount: number;
124
+ sliderCount: number;
125
+ spinnerCount: number;
126
+ lastModificationTime: DateTimeTicks;
127
+ approachRate: number;
128
+ circleSize: number;
129
+ hpDrain: number;
130
+ overallDifficulty: number;
131
+ sliderVelocity: number;
132
+ standardStarRatings: IntFloatPair[];
133
+ taikoStarRatings: IntFloatPair[];
134
+ catchStarRatings: IntFloatPair[];
135
+ maniaStarRatings: IntFloatPair[];
136
+ drainTimeSeconds: number;
137
+ totalTimeMs: number;
138
+ previewOffsetMs: number;
139
+ timingPoints: TimingPoint[];
140
+ difficultyId: number;
141
+ beatmapId: number;
142
+ threadId: number;
143
+ standardGrade: Grade;
144
+ taikoGrade: Grade;
145
+ catchGrade: Grade;
146
+ maniaGrade: Grade;
147
+ localOffset: number;
148
+ stackLeniency: number;
149
+ gameplayMode: GameplayMode;
150
+ source: string | null;
151
+ tags: string | null;
152
+ onlineOffset: number;
153
+ titleFont: string | null;
154
+ isUnplayed: boolean;
155
+ lastPlayedAt: DateTimeTicks;
156
+ isOsz2: boolean;
157
+ beatmapFolderName: string | null;
158
+ lastCheckedAgainstRepositoryAt: DateTimeTicks;
159
+ ignoreBeatmapSound: boolean;
160
+ ignoreBeatmapSkin: boolean;
161
+ disableStoryboard: boolean;
162
+ disableVideo: boolean;
163
+ visualOverride: boolean;
164
+ lastModificationTimeUnknown: number;
165
+ maniaScrollSpeed: number;
166
+ getOsuFilePath: () => string;
167
+ };
168
+ score: {
169
+ gameplayMode: GameplayMode;
170
+ version: number;
171
+ beatmapMd5Hash: string | null;
172
+ playerName: string | null;
173
+ replayMd5Hash: string | null;
174
+ count300: number;
175
+ count100: number;
176
+ count50: number;
177
+ countGeki: number;
178
+ countKatu: number;
179
+ countMiss: number;
180
+ totalScore: number;
181
+ maxCombo: number;
182
+ perfectCombo: boolean;
183
+ mods: ModFlags;
184
+ reservedEmptyString: string | null;
185
+ replayTimestamp: DateTimeTicks;
186
+ reservedInt32: number;
187
+ onlineScoreId: bigint;
188
+ additionalModInfo?: ScoreAdditionalModInfo;
189
+ getOsrFilePath: () => string;
190
+ };
191
+ }, void, unknown>;
192
+ };
26
193
  getOsuFilePath(beatmap: BeatmapEntry): string;
27
194
  getOsrFilePath(score: ScoreEntry): string;
28
195
  }
package/dist/node.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as writeOsuDatabase, i as readOsuDatabase, n as writeScoresDatabase, o as readCollectionDatabase, r as dateTimeTicksToWindowsFileTimeTicks, s as writeCollectionDatabase, t as readScoresDatabase } from "./scores-C35i3lk0.mjs";
1
+ import { a as readOsuDatabase, c as writeCollectionDatabase, i as createBeatmapScoreQuery, n as writeScoresDatabase, o as writeOsuDatabase, r as dateTimeTicksToWindowsFileTimeTicks, s as readCollectionDatabase, t as readScoresDatabase } from "./scores-CW2CUXZ-.mjs";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  //#region src/node.ts
@@ -61,6 +61,35 @@ var OsuFolder = class {
61
61
  async writeScoresDatabase(database) {
62
62
  await writeScoresDatabaseFile(this.getScoresDatabasePath(), database);
63
63
  }
64
+ createBeatmapScoreQuery(beatmapsSource, scoresSource) {
65
+ const query = createBeatmapScoreQuery(beatmapsSource, scoresSource);
66
+ const wrapBeatmap = (beatmap) => ({
67
+ ...beatmap,
68
+ getOsuFilePath: () => this.getOsuFilePath(beatmap)
69
+ });
70
+ const wrapScore = (score) => ({
71
+ ...score,
72
+ getOsrFilePath: () => this.getOsrFilePath(score)
73
+ });
74
+ const wrapBeatmapScoresGroupMatch = ({ beatmap, scores }) => ({
75
+ beatmap: wrapBeatmap(beatmap),
76
+ scores: scores.map(wrapScore)
77
+ });
78
+ const wrapBeatmapScoreMatch = ({ beatmap, score }) => ({
79
+ beatmap: wrapBeatmap(beatmap),
80
+ score: wrapScore(score)
81
+ });
82
+ function* iterateBeatmapScoreGroups() {
83
+ for (const match of query.iterateBeatmapScoreGroups()) yield wrapBeatmapScoresGroupMatch(match);
84
+ }
85
+ function* iterateBeatmapScores() {
86
+ for (const match of query.iterateBeatmapScores()) yield wrapBeatmapScoreMatch(match);
87
+ }
88
+ return {
89
+ iterateBeatmapScoreGroups,
90
+ iterateBeatmapScores
91
+ };
92
+ }
64
93
  getOsuFilePath(beatmap) {
65
94
  if (beatmap.beatmapFolderName === null || beatmap.osuFileName === null) throw new Error("Beatmap entry is missing beatmapFolderName or osuFileName");
66
95
  return join(this.folderPath, "Songs", beatmap.beatmapFolderName, beatmap.osuFileName);
@@ -353,6 +353,40 @@ function writeOsuDatabase(database) {
353
353
  return writer.toUint8Array();
354
354
  }
355
355
  //#endregion
356
+ //#region src/db/query.ts
357
+ function getBeatmapEntries(source) {
358
+ return Array.isArray(source) ? source : source.beatmaps;
359
+ }
360
+ function getScoreEntries(source) {
361
+ return Array.isArray(source) ? source : source.beatmaps;
362
+ }
363
+ function createBeatmapScoreQuery(beatmapsSource, scoresSource) {
364
+ const beatmaps = getBeatmapEntries(beatmapsSource);
365
+ const scoreGroups = getScoreEntries(scoresSource);
366
+ const beatmapByMd5Hash = /* @__PURE__ */ new Map();
367
+ for (const beatmap of beatmaps) if (beatmap.md5Hash !== null) beatmapByMd5Hash.set(beatmap.md5Hash, beatmap);
368
+ function* iterateBeatmapScoreGroups() {
369
+ for (const scoreGroup of scoreGroups) {
370
+ if (scoreGroup.beatmapMd5Hash === null) continue;
371
+ const beatmap = beatmapByMd5Hash.get(scoreGroup.beatmapMd5Hash);
372
+ if (beatmap !== void 0) yield {
373
+ beatmap,
374
+ scores: scoreGroup.scores
375
+ };
376
+ }
377
+ }
378
+ function* iterateBeatmapScores() {
379
+ for (const { beatmap, scores } of iterateBeatmapScoreGroups()) for (const score of scores) yield {
380
+ beatmap,
381
+ score
382
+ };
383
+ }
384
+ return {
385
+ iterateBeatmapScoreGroups,
386
+ iterateBeatmapScores
387
+ };
388
+ }
389
+ //#endregion
356
390
  //#region src/core/utils.ts
357
391
  const WINDOWS_FILE_TIME_EPOCH_DATE_TIME_TICKS = 504911232000000000n;
358
392
  Object.values(Mods).filter((value) => value !== Mods.None);
@@ -478,4 +512,4 @@ function writeScoresDatabase(database) {
478
512
  return writer.toUint8Array();
479
513
  }
480
514
  //#endregion
481
- export { writeOsuDatabase as a, GameplayModes as c, Mods as d, RankedStatuses as f, readOsuDatabase as i, Grades as l, writeScoresDatabase as n, readCollectionDatabase as o, UserPermissions as p, dateTimeTicksToWindowsFileTimeTicks as r, writeCollectionDatabase as s, readScoresDatabase as t, MINIMUM_SUPPORTED_VERSION as u };
515
+ export { readOsuDatabase as a, writeCollectionDatabase as c, MINIMUM_SUPPORTED_VERSION as d, Mods as f, createBeatmapScoreQuery as i, GameplayModes as l, UserPermissions as m, writeScoresDatabase as n, writeOsuDatabase as o, RankedStatuses as p, dateTimeTicksToWindowsFileTimeTicks as r, readCollectionDatabase as s, readScoresDatabase as t, Grades as u };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "osu-stable-db",
3
3
  "type": "module",
4
- "version": "0.2.2",
4
+ "version": "0.2.3",
5
5
  "description": "TypeScript reader and writer for osu!stable database files.",
6
6
  "author": "zzzzv",
7
7
  "license": "MIT",