osu-stable-db 0.1.2 → 0.2.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/README.md CHANGED
@@ -10,7 +10,7 @@ This library currently supports the latest database structure from version 20250
10
10
  - collection.db
11
11
  - scores.db
12
12
 
13
- The core binary and database logic is browser-compatible. A separate Node-only subpath is provided for direct file reads and writes.
13
+ The core binary and database logic is browser-compatible. A separate Node-only subpath is provided for direct file reads, writes, and osu! folder helpers.
14
14
 
15
15
  ## Scope
16
16
 
@@ -64,30 +64,32 @@ Main entry exports:
64
64
 
65
65
  ## Node Usage
66
66
 
67
- Use the Node subpath when you want direct file reads and writes through node:fs/promises.
67
+ Use the Node subpath when you want to work with a full osu! folder or with direct file reads and writes through node:fs/promises.
68
68
 
69
69
  ```ts
70
70
  import {
71
- readOsuDatabaseFile,
72
- writeCollectionDatabaseFile,
71
+ OsuFolder,
73
72
  } from 'osu-stable-db/node'
74
73
 
75
- const osuDatabase = await readOsuDatabaseFile('path/to/osu!.db')
74
+ const osuFolder = new OsuFolder('C:/Games/osu!')
75
+ const osuDatabase = await osuFolder.readOsuDatabase()
76
+ const scoresDatabase = await osuFolder.readScoresDatabase()
76
77
 
77
- await writeCollectionDatabaseFile(
78
- 'path/to/collection.db',
79
- {
80
- version: osuDatabase.version,
81
- collections: [],
82
- },
83
- )
84
- ```
78
+ const newestBeatmap = osuDatabase.beatmaps.at(-1)
79
+ const newestScore = scoresDatabase.beatmaps.at(-1)?.scores.at(-1)
80
+
81
+ if (newestBeatmap !== undefined) {
82
+ const osuFilePath = osuFolder.getOsuFilePath(newestBeatmap)
83
+ console.log(osuFilePath)
84
+ }
85
85
 
86
- Node subpath exports:
86
+ if (newestScore !== undefined) {
87
+ const osrFilePath = osuFolder.getOsrFilePath(newestScore)
88
+ console.log(osrFilePath)
89
+ }
90
+ ```
87
91
 
88
- - readOsuDatabaseFile and writeOsuDatabaseFile
89
- - readCollectionDatabaseFile and writeCollectionDatabaseFile
90
- - readScoresDatabaseFile and writeScoresDatabaseFile
92
+ If you only need path-based file IO, the same subpath also exports helpers such as readOsuDatabaseFile, writeCollectionDatabaseFile, and writeScoresDatabaseFile.
91
93
 
92
94
  ## Types And Time Values
93
95
 
@@ -107,12 +109,21 @@ Committed minimal fixtures live in [tests/files](tests/files).
107
109
  Large real-world local fixtures can be placed in:
108
110
 
109
111
  - [tests/files/local](tests/files/local)
112
+ - [tests/files/local/osu!](tests/files/local/osu!) via a directory link created by the package script below
110
113
 
111
114
  That directory is git-ignored. When those files are present, the test suite will:
112
115
 
113
116
  - parse them
114
117
  - verify byte-for-byte round-trip for osu!.db, collection.db, and scores.db
115
118
 
119
+ To link your real local osu! folder into the workspace on Windows, run:
120
+
121
+ ```bash
122
+ pnpm run local:link -- "C:\\path\\to\\osu!"
123
+ ```
124
+
125
+ This creates [tests/files/local/osu!](tests/files/local/osu!) as a directory junction. Tests and local scripts now prefer databases from that linked folder, and fall back to [tests/files/local](tests/files/local) for the previous workflow.
126
+
116
127
  You can also generate a local inspection report for a specific beatmap identifier with:
117
128
 
118
129
  ```bash
@@ -134,7 +145,7 @@ pnpm run build
134
145
 
135
146
  ## Notes On Validation
136
147
 
137
- Local validation also passes against private real-world database files in [tests/files/local](tests/files/local):
148
+ Local validation also passes against private real-world database files in [tests/files/local/osu!](tests/files/local/osu!):
138
149
 
139
150
  - osu!.db: 58,295,932 bytes, 72,038 beatmaps
140
151
  - collection.db: 195,402 bytes, 11 collections, 5,743 stored beatmap references
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as readCollectionDatabase, c as Grades, d as RankedStatuses, f as UserPermissions, i as writeOsuDatabase, l as MINIMUM_SUPPORTED_VERSION, n as writeScoresDatabase, o as writeCollectionDatabase, r as readOsuDatabase, s as GameplayModes, t as readScoresDatabase, u as Mods } from "./scores-D_f7sIP0.mjs";
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
2
  export { GameplayModes, Grades, MINIMUM_SUPPORTED_VERSION, Mods, RankedStatuses, UserPermissions, 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 } from "./types-DiZMN3fQ.mjs";
1
+ import { b as ScoresDatabase, m as OsuDatabase, n as CollectionDatabase, t as BeatmapEntry, v as ScoreEntry } from "./types-DiZMN3fQ.mjs";
2
2
 
3
3
  //#region src/node.d.ts
4
4
  type DatabaseFilePath = string | URL;
@@ -8,5 +8,20 @@ declare function readCollectionDatabaseFile(path: DatabaseFilePath): Promise<Col
8
8
  declare function writeCollectionDatabaseFile(path: DatabaseFilePath, database: CollectionDatabase): Promise<void>;
9
9
  declare function readScoresDatabaseFile(path: DatabaseFilePath): Promise<ScoresDatabase>;
10
10
  declare function writeScoresDatabaseFile(path: DatabaseFilePath, database: ScoresDatabase): Promise<void>;
11
+ declare class OsuFolder {
12
+ folderPath: string;
13
+ constructor(folderPath: string);
14
+ getOsuDatabasePath(): string;
15
+ getCollectionDatabasePath(): string;
16
+ getScoresDatabasePath(): string;
17
+ readOsuDatabase(): Promise<OsuDatabase>;
18
+ writeOsuDatabase(database: OsuDatabase): Promise<void>;
19
+ readCollectionDatabase(): Promise<CollectionDatabase>;
20
+ writeCollectionDatabase(database: CollectionDatabase): Promise<void>;
21
+ readScoresDatabase(): Promise<ScoresDatabase>;
22
+ writeScoresDatabase(database: ScoresDatabase): Promise<void>;
23
+ getOsuFilePath(beatmap: BeatmapEntry): string;
24
+ getOsrFilePath(score: ScoreEntry): string;
25
+ }
11
26
  //#endregion
12
- export { DatabaseFilePath, readCollectionDatabaseFile, readOsuDatabaseFile, readScoresDatabaseFile, writeCollectionDatabaseFile, writeOsuDatabaseFile, writeScoresDatabaseFile };
27
+ export { DatabaseFilePath, OsuFolder, readCollectionDatabaseFile, readOsuDatabaseFile, readScoresDatabaseFile, writeCollectionDatabaseFile, writeOsuDatabaseFile, writeScoresDatabaseFile };
package/dist/node.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { a as readCollectionDatabase, i as writeOsuDatabase, n as writeScoresDatabase, o as writeCollectionDatabase, r as readOsuDatabase, t as readScoresDatabase } from "./scores-D_f7sIP0.mjs";
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";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
3
4
  //#region src/node.ts
4
5
  async function readOsuDatabaseFile(path) {
5
6
  return readOsuDatabase(new Uint8Array(await readFile(path)));
@@ -19,5 +20,46 @@ async function readScoresDatabaseFile(path) {
19
20
  async function writeScoresDatabaseFile(path, database) {
20
21
  await writeFile(path, writeScoresDatabase(database));
21
22
  }
23
+ var OsuFolder = class {
24
+ folderPath;
25
+ constructor(folderPath) {
26
+ this.folderPath = folderPath;
27
+ }
28
+ getOsuDatabasePath() {
29
+ return join(this.folderPath, "osu!.db");
30
+ }
31
+ getCollectionDatabasePath() {
32
+ return join(this.folderPath, "collection.db");
33
+ }
34
+ getScoresDatabasePath() {
35
+ return join(this.folderPath, "scores.db");
36
+ }
37
+ async readOsuDatabase() {
38
+ return readOsuDatabaseFile(this.getOsuDatabasePath());
39
+ }
40
+ async writeOsuDatabase(database) {
41
+ await writeOsuDatabaseFile(this.getOsuDatabasePath(), database);
42
+ }
43
+ async readCollectionDatabase() {
44
+ return readCollectionDatabaseFile(this.getCollectionDatabasePath());
45
+ }
46
+ async writeCollectionDatabase(database) {
47
+ await writeCollectionDatabaseFile(this.getCollectionDatabasePath(), database);
48
+ }
49
+ async readScoresDatabase() {
50
+ return readScoresDatabaseFile(this.getScoresDatabasePath());
51
+ }
52
+ async writeScoresDatabase(database) {
53
+ await writeScoresDatabaseFile(this.getScoresDatabasePath(), database);
54
+ }
55
+ getOsuFilePath(beatmap) {
56
+ if (beatmap.beatmapFolderName === null || beatmap.osuFileName === null) throw new Error("Beatmap entry is missing beatmapFolderName or osuFileName");
57
+ return join(this.folderPath, "Songs", beatmap.beatmapFolderName, beatmap.osuFileName);
58
+ }
59
+ getOsrFilePath(score) {
60
+ if (score.beatmapMd5Hash === null) throw new Error("Score entry is missing beatmapMd5Hash");
61
+ return join(this.folderPath, "Data", "r", `${score.beatmapMd5Hash}-${dateTimeTicksToWindowsFileTimeTicks(score.replayTimestamp)}.osr`);
62
+ }
63
+ };
22
64
  //#endregion
23
- export { readCollectionDatabaseFile, readOsuDatabaseFile, readScoresDatabaseFile, writeCollectionDatabaseFile, writeOsuDatabaseFile, writeScoresDatabaseFile };
65
+ export { OsuFolder, readCollectionDatabaseFile, readOsuDatabaseFile, readScoresDatabaseFile, writeCollectionDatabaseFile, writeOsuDatabaseFile, writeScoresDatabaseFile };
@@ -352,8 +352,20 @@ function writeOsuDatabase(database) {
352
352
  writer.writeInt32(database.userPermissions);
353
353
  return writer.toUint8Array();
354
354
  }
355
+ //#endregion
356
+ //#region src/core/utils.ts
357
+ const WINDOWS_FILE_TIME_EPOCH_DATE_TIME_TICKS = 504911232000000000n;
355
358
  Object.values(Mods).filter((value) => value !== Mods.None);
356
359
  /**
360
+ * Converts .NET DateTime ticks to Windows FILETIME ticks.
361
+ *
362
+ * scores.db stores replay timestamps as .NET DateTime ticks, while replay
363
+ * filenames under Data/r use FILETIME ticks starting at 1601-01-01.
364
+ */
365
+ function dateTimeTicksToWindowsFileTimeTicks(ticks) {
366
+ return ticks - WINDOWS_FILE_TIME_EPOCH_DATE_TIME_TICKS;
367
+ }
368
+ /**
357
369
  * Tests whether all bits from a mod mask are present in the flags value.
358
370
  */
359
371
  function hasMod(flags, mod) {
@@ -466,4 +478,4 @@ function writeScoresDatabase(database) {
466
478
  return writer.toUint8Array();
467
479
  }
468
480
  //#endregion
469
- export { readCollectionDatabase as a, Grades as c, RankedStatuses as d, UserPermissions as f, writeOsuDatabase as i, MINIMUM_SUPPORTED_VERSION as l, writeScoresDatabase as n, writeCollectionDatabase as o, readOsuDatabase as r, GameplayModes as s, readScoresDatabase as t, Mods as u };
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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "osu-stable-db",
3
3
  "type": "module",
4
- "version": "0.1.2",
4
+ "version": "0.2.0",
5
5
  "description": "TypeScript reader and writer for osu!stable database files.",
6
6
  "author": "zzzzv",
7
7
  "license": "MIT",
@@ -29,6 +29,7 @@
29
29
  "scripts": {
30
30
  "build": "tsdown",
31
31
  "dev": "tsdown --watch",
32
+ "local:link": "node scripts/local-link.mjs",
32
33
  "local:inspect": "pnpm run build && node scripts/local-inspect.mjs",
33
34
  "local:roundtrip": "pnpm run build && node scripts/local-roundtrip.mjs",
34
35
  "test": "vitest",