rhythia-api 108.0.0 → 110.0.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.
@@ -1,15 +1,12 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import z from "zod";
3
3
  import { protectedApi, validUser } from "../utils/requestUtils";
4
- import { supabase } from "../utils/supabase";
5
- import {
6
- SSPMParsedMap,
7
- SSPMParser,
8
- } from "rhythia-star-calculator/src/sspmParser";
4
+ import { SSPMParser } from "../utils/star-calc/sspmParser";
9
5
 
10
6
  export const Schema = {
11
7
  input: z.strictObject({
12
8
  url: z.string(),
9
+ session: z.string(),
13
10
  }),
14
11
  output: z.strictObject({
15
12
  hash: z.string().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "108.0.0",
3
+ "version": "110.0.0",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "update": "bun ./scripts/update.ts",
@@ -30,7 +30,8 @@
30
30
  "isomorphic-git": "^1.27.1",
31
31
  "lodash": "^4.17.21",
32
32
  "next": "^14.2.5",
33
- "rhythia-star-calculator": "^1.0.4",
33
+ "osu-parsers": "^4.1.7",
34
+ "osu-standard-stable": "^5.0.0",
34
35
  "simple-git": "^3.25.0",
35
36
  "supabase": "^1.192.5",
36
37
  "tsx": "^4.17.0",
@@ -0,0 +1,43 @@
1
+ import { BeatmapDecoder } from "osu-parsers";
2
+ import { StandardRuleset } from "osu-standard-stable";
3
+ import { sampleMap } from "./osuUtils";
4
+ import { SSPMParsedMap } from "./sspmParser";
5
+
6
+ function easeInExpoDeq(x: number) {
7
+ return x === 0 ? 0 : Math.pow(2, 35 * x - 35);
8
+ }
9
+
10
+ export function calculatePerformancePoints(
11
+ starRating: number,
12
+ accuracy: number
13
+ ) {
14
+ return Math.round(
15
+ Math.pow((starRating * easeInExpoDeq(accuracy) * 100) / 2, 2) / 1000
16
+ );
17
+ }
18
+
19
+ export function rateMap(map: SSPMParsedMap) {
20
+ const decoder = new BeatmapDecoder();
21
+ const beatmap1 = decoder.decodeFromString(sampleMap);
22
+
23
+ const notes = map.markers
24
+ .filter((marker) => marker.type === 0)
25
+ .map((marker) => ({
26
+ time: marker.position,
27
+ x: marker.data["field0"].x,
28
+ y: marker.data["field0"].y,
29
+ }));
30
+
31
+ for (const note of notes) {
32
+ const hittable = beatmap1.hitObjects[0].clone();
33
+ hittable.startX = Math.round((note.x / 2) * 100);
34
+ hittable.startY = Math.round((note.y / 2) * 100);
35
+ hittable.startTime = note.time;
36
+ beatmap1.hitObjects.push(hittable);
37
+ }
38
+ const ruleset = new StandardRuleset();
39
+ const mods = ruleset.createModCombination("RX");
40
+ const difficultyCalculator = ruleset.createDifficultyCalculator(beatmap1);
41
+ const difficultyAttributes = difficultyCalculator.calculateWithMods(mods);
42
+ return difficultyAttributes.starRating;
43
+ }
@@ -0,0 +1,53 @@
1
+ export const sampleMap = `osu file format v14
2
+
3
+ [General]
4
+ AudioFilename: audio.mp3
5
+ AudioLeadIn: 0
6
+ PreviewTime: 99664
7
+ Countdown: 0
8
+ SampleSet: Normal
9
+ StackLeniency: 0
10
+ Mode: 0
11
+ LetterboxInBreaks: 0
12
+ UseSkinSprites: 1
13
+ SkinPreference:Default
14
+ WidescreenStoryboard: 1
15
+ SamplesMatchPlaybackRate: 1
16
+
17
+ [Editor]
18
+
19
+
20
+ [Metadata]
21
+ Title:new beginnings
22
+ TitleUnicode:new beginnings
23
+ Artist:nekodex
24
+ ArtistUnicode:nekodex
25
+ Creator:pishifat
26
+ Version:tutorial
27
+ Source:
28
+ Tags:
29
+ BeatmapID:2116202
30
+ BeatmapSetID:1011011
31
+
32
+ [Difficulty]
33
+ HPDrainRate:5
34
+ CircleSize:2
35
+ OverallDifficulty:8
36
+ ApproachRate:8
37
+ SliderMultiplier:1
38
+ SliderTickRate:1
39
+
40
+ [Events]
41
+ //Background and Video events
42
+ 0,0,"new-beginnings.jpg",0,0
43
+
44
+ [TimingPoints]
45
+ -28,461.538461538462,4,1,0,100,1,0
46
+
47
+
48
+ [Colours]
49
+
50
+
51
+ [HitObjects]
52
+ 256,192,24202,1,0,0:0:0:0:
53
+ `;
@@ -0,0 +1,394 @@
1
+ type DataTypeID =
2
+ | 0x00
3
+ | 0x01
4
+ | 0x02
5
+ | 0x03
6
+ | 0x04
7
+ | 0x05
8
+ | 0x06
9
+ | 0x07
10
+ | 0x08
11
+ | 0x09
12
+ | 0x0a
13
+ | 0x0b
14
+ | 0x0c;
15
+
16
+ interface Header {
17
+ signature: Buffer;
18
+ version: number;
19
+ reserved: Buffer;
20
+ }
21
+
22
+ interface StaticMetadata {
23
+ sha1: Buffer;
24
+ lastMarkerPos: number;
25
+ noteCount: number;
26
+ markerCount: number;
27
+ difficulty: number;
28
+ rating: number;
29
+ hasAudio: boolean;
30
+ hasCover: boolean;
31
+ requiresMod: boolean;
32
+ }
33
+
34
+ interface Pointers {
35
+ customDataOffset: number;
36
+ customDataLength: number;
37
+ audioOffset: number;
38
+ audioLength: number;
39
+ coverOffset: number;
40
+ coverLength: number;
41
+ markerDefinitionsOffset: number;
42
+ markerDefinitionsLength: number;
43
+ markerOffset: number;
44
+ markerLength: number;
45
+ }
46
+
47
+ interface Strings {
48
+ mapID: string;
49
+ mapName: string;
50
+ songName: string;
51
+ mappers: string[];
52
+ }
53
+
54
+ interface CustomField {
55
+ id: string;
56
+ type: DataTypeID;
57
+ arrayType?: DataTypeID;
58
+ value: any;
59
+ }
60
+
61
+ interface CustomData {
62
+ fields: CustomField[];
63
+ }
64
+
65
+ interface MarkerDefinition {
66
+ id: string;
67
+ values: DataTypeID[];
68
+ }
69
+
70
+ interface Marker {
71
+ position: number;
72
+ type: number;
73
+ data: Buffer;
74
+ }
75
+
76
+ export class SSPMParser {
77
+ private buffer: Buffer;
78
+ private offset: number = 0;
79
+
80
+ constructor(buffer: Buffer) {
81
+ this.buffer = buffer;
82
+ }
83
+
84
+ private checkBounds(length: number): void {
85
+ if (this.offset + length > this.buffer.length) {
86
+ throw new RangeError(
87
+ `Attempt to read beyond buffer length: Offset=${this.offset}, Length=${length}, Buffer Length=${this.buffer.length}`
88
+ );
89
+ }
90
+ }
91
+
92
+ private log(message: string): void {
93
+ // console.log(`[Offset: ${this.offset}] ${message}`);
94
+ }
95
+
96
+ private readUInt16(): number {
97
+ this.checkBounds(2);
98
+ const value = this.buffer.readUInt16LE(this.offset);
99
+ this.log(`Read UInt16: ${value}`);
100
+ this.offset += 2;
101
+ return value;
102
+ }
103
+
104
+ private readUInt32(): number {
105
+ this.checkBounds(4);
106
+ const value = this.buffer.readUInt32LE(this.offset);
107
+ this.log(`Read UInt32: ${value}`);
108
+ this.offset += 4;
109
+ return value;
110
+ }
111
+
112
+ private readUInt64(): number {
113
+ this.checkBounds(8);
114
+ const low = this.buffer.readUInt32LE(this.offset);
115
+ const high = this.buffer.readUInt32LE(this.offset + 4);
116
+ this.offset += 8;
117
+ const value = low + high * 2 ** 32;
118
+ this.log(`Read UInt64: ${value} (Low: ${low}, High: ${high})`);
119
+ return value;
120
+ }
121
+
122
+ private readBytes(length: number): Buffer {
123
+ this.checkBounds(length);
124
+ const value = this.buffer.slice(this.offset, this.offset + length);
125
+ this.log(`Read ${length} bytes`);
126
+ this.offset += length;
127
+ return value;
128
+ }
129
+
130
+ private readString(): string {
131
+ const length = this.readUInt16();
132
+ this.checkBounds(length);
133
+ const value = this.readBytes(length).toString("utf-8");
134
+ this.log(`Read String of length ${length}: ${value}`);
135
+ return value;
136
+ }
137
+
138
+ private readStringList(count: number): string[] {
139
+ const list: string[] = [];
140
+ for (let i = 0; i < count; i++) {
141
+ list.push(this.readString());
142
+ }
143
+ return list;
144
+ }
145
+
146
+ private readMarkerField(typeID: DataTypeID): any {
147
+ switch (typeID) {
148
+ case 0x01: // 1 byte integer
149
+ this.checkBounds(1);
150
+ const int8 = this.buffer.readInt8(this.offset++);
151
+ this.log(`Read Int8: ${int8}`);
152
+ return int8;
153
+ case 0x02: // 2 byte uint
154
+ return this.readUInt16();
155
+ case 0x03: // 4 byte uint
156
+ return this.readUInt32();
157
+ case 0x04: // 8 byte uint
158
+ return this.readUInt64();
159
+ case 0x05: // 4 byte float
160
+ this.checkBounds(4);
161
+ const floatVal32 = this.buffer.readFloatLE(this.offset);
162
+ this.log(`Read Float32: ${floatVal32}`);
163
+ this.offset += 4;
164
+ return floatVal32;
165
+ case 0x06: // 8 byte float
166
+ this.checkBounds(8);
167
+ const floatVal64 = this.buffer.readDoubleLE(this.offset);
168
+ this.log(`Read Float64: ${floatVal64}`);
169
+ this.offset += 8;
170
+ return floatVal64;
171
+ case 0x07: // position type
172
+ const isQuantum = this.buffer.readUInt8(this.offset++);
173
+ let posData;
174
+ if (isQuantum === 0x00) {
175
+ this.checkBounds(2);
176
+ const posX = this.buffer.readUInt8(this.offset++);
177
+ const posY = this.buffer.readUInt8(this.offset++);
178
+ this.log(`Read Position Int: x=${posX}, y=${posY}`);
179
+ posData = { x: posX, y: posY, type: "int" };
180
+ } else {
181
+ this.checkBounds(8);
182
+ const posX = this.buffer.readFloatLE(this.offset);
183
+ this.offset += 4;
184
+ const posY = this.buffer.readFloatLE(this.offset);
185
+ this.offset += 4;
186
+ this.log(`Read Position Quantum: x=${posX}, y=${posY}`);
187
+ posData = { x: posX, y: posY, type: "quantum" };
188
+ }
189
+ return posData;
190
+ case 0x08: // buffer
191
+ case 0x09: // string
192
+ const length16 = this.readUInt16();
193
+ this.checkBounds(length16);
194
+ const value = this.readBytes(length16).toString("utf-8");
195
+ this.log(`Read Buffer/String of length ${length16}`);
196
+ return value;
197
+ case 0x0a: // long buffer
198
+ case 0x0b: // long string
199
+ const length32 = this.readUInt32();
200
+ this.log(`Reading Buffer/String of length ${length32}`);
201
+ this.checkBounds(length32);
202
+ const longValue = this.readBytes(length32).toString("utf-8");
203
+ this.log(`Read Long Buffer/String of length ${length32}`);
204
+ return longValue;
205
+ case 0x00: // end type, should not appear here
206
+ default:
207
+ throw new Error("Unexpected DataTypeID in marker definition.");
208
+ }
209
+ }
210
+
211
+ private readMarkerData(typeIDs: DataTypeID[]): any {
212
+ const dataObject: any = {};
213
+ for (const [index, typeID] of typeIDs.entries()) {
214
+ dataObject[`field${index}`] = this.readMarkerField(typeID);
215
+ }
216
+ return dataObject;
217
+ }
218
+
219
+ parse(): {
220
+ header: Header;
221
+ metadata: StaticMetadata;
222
+ pointers: Pointers;
223
+ strings: Strings;
224
+ customData: CustomData;
225
+ audio?: Buffer;
226
+ cover?: Buffer;
227
+ markerDefinitions: MarkerDefinition[];
228
+ markers: Marker[];
229
+ } {
230
+ // Header
231
+ const header: Header = {
232
+ signature: this.readBytes(4),
233
+ version: this.readUInt16(),
234
+ reserved: this.readBytes(4),
235
+ };
236
+
237
+ // Static Metadata
238
+ const metadata: StaticMetadata = {
239
+ sha1: this.readBytes(20),
240
+ lastMarkerPos: this.readUInt32(),
241
+ noteCount: this.readUInt32(),
242
+ markerCount: this.readUInt32(),
243
+ difficulty: this.buffer.readUInt8(this.offset++),
244
+ rating: this.readUInt16(),
245
+ hasAudio: this.buffer.readUInt8(this.offset++) === 1,
246
+ hasCover: this.buffer.readUInt8(this.offset++) === 1,
247
+ requiresMod: this.buffer.readUInt8(this.offset++) === 1,
248
+ };
249
+
250
+ // Pointers
251
+ const pointers: Pointers = {
252
+ customDataOffset: this.readUInt64(),
253
+ customDataLength: this.readUInt64(),
254
+ audioOffset: this.readUInt64(),
255
+ audioLength: this.readUInt64(),
256
+ coverOffset: this.readUInt64(),
257
+ coverLength: this.readUInt64(),
258
+ markerDefinitionsOffset: this.readUInt64(),
259
+ markerDefinitionsLength: this.readUInt64(),
260
+ markerOffset: this.readUInt64(),
261
+ markerLength: this.readUInt64(),
262
+ };
263
+
264
+ // Log derived pointer values
265
+ this.log(`customDataOffset: ${pointers.customDataOffset}`);
266
+ this.log(`customDataLength: ${pointers.customDataLength}`);
267
+ this.log(`audioOffset: ${pointers.audioOffset}`);
268
+ this.log(`audioLength: ${pointers.audioLength}`);
269
+ this.log(`coverOffset: ${pointers.coverOffset}`);
270
+ this.log(`coverLength: ${pointers.coverLength}`);
271
+ this.log(`markerDefinitionsOffset: ${pointers.markerDefinitionsOffset}`);
272
+ this.log(`markerDefinitionsLength: ${pointers.markerDefinitionsLength}`);
273
+ this.log(`markerOffset: ${pointers.markerOffset}`);
274
+ this.log(`markerLength: ${pointers.markerLength}`);
275
+
276
+ // Strings
277
+ const strings: Strings = {
278
+ mapID: this.readString(),
279
+ mapName: this.readString(),
280
+ songName: this.readString(),
281
+ mappers: this.readStringList(this.readUInt16()),
282
+ };
283
+
284
+ let customData: CustomData = { fields: [] };
285
+ try {
286
+ if (pointers.customDataOffset && pointers.customDataLength) {
287
+ this.log(
288
+ `Reading Custom Data, Offset: ${pointers.customDataOffset}, Length: ${pointers.customDataLength}`
289
+ );
290
+ this.offset = Number(pointers.customDataOffset);
291
+ const fieldCount = this.readUInt16();
292
+ this.log(`Fields: ${fieldCount.toString()}`);
293
+ for (let i = 0; i < fieldCount; i++) {
294
+ const id = this.readString();
295
+ const type = this.buffer.readUInt8(this.offset++) as DataTypeID;
296
+ let arrayType: DataTypeID | undefined;
297
+ if (type === 0x0c) {
298
+ arrayType = this.buffer.readUInt8(this.offset++) as DataTypeID;
299
+ }
300
+ const length = this.readUInt32();
301
+ // this.checkBounds(length);
302
+ const value = this.readBytes(length);
303
+ customData.fields.push({ id, type, arrayType, value });
304
+ }
305
+ }
306
+ } catch (error) {}
307
+
308
+ let audio: Buffer | undefined;
309
+ if (
310
+ metadata.hasAudio &&
311
+ pointers.audioOffset != 0 &&
312
+ pointers.audioLength != 0
313
+ ) {
314
+ this.log(
315
+ `Reading Audio Data, Offset: ${pointers.audioOffset}, Length: ${pointers.audioLength}`
316
+ );
317
+ this.offset = Number(pointers.audioOffset);
318
+ this.checkBounds(Number(pointers.audioLength));
319
+ audio = this.readBytes(Number(pointers.audioLength));
320
+ }
321
+
322
+ let cover: Buffer | undefined;
323
+ if (
324
+ metadata.hasCover &&
325
+ pointers.coverOffset != 0 &&
326
+ pointers.coverLength != 0
327
+ ) {
328
+ this.log(
329
+ `Reading Cover Data, Offset: ${pointers.coverOffset}, Length: ${pointers.coverLength}`
330
+ );
331
+ this.offset = Number(pointers.coverOffset);
332
+ this.checkBounds(Number(pointers.coverLength));
333
+ cover = this.readBytes(Number(pointers.coverLength));
334
+ }
335
+
336
+ // Marker Definitions
337
+ this.log(
338
+ `Reading Marker Definitions, Offset: ${pointers.markerDefinitionsOffset}, Length: ${pointers.markerDefinitionsLength}`
339
+ );
340
+ this.offset = Number(pointers.markerDefinitionsOffset);
341
+ const markerDefCount = this.buffer.readUInt8(this.offset++);
342
+ const markerDefinitions: MarkerDefinition[] = [];
343
+ for (let i = 0; i < markerDefCount; i++) {
344
+ const id = this.readString();
345
+ const valueCount = this.buffer.readUInt8(this.offset++);
346
+ const values: DataTypeID[] = [];
347
+ for (let j = 0; j < valueCount; j++) {
348
+ values.push(this.buffer.readUInt8(this.offset++) as DataTypeID);
349
+ }
350
+ markerDefinitions.push({ id, values });
351
+ }
352
+
353
+ // Markers
354
+ this.log(
355
+ `Reading Markers, Offset: ${pointers.markerOffset}, Length: ${pointers.markerLength}`
356
+ );
357
+ this.offset = Number(pointers.markerOffset);
358
+ const endOffset = this.offset + Number(pointers.markerLength);
359
+
360
+ const markers: Marker[] = [];
361
+
362
+ while (this.offset < endOffset) {
363
+ const position = this.readUInt32();
364
+ const type = this.buffer.readUInt8(this.offset++);
365
+ const def = markerDefinitions.find((d) => d.id === "ssp_note"); // Adjust as needed for other marker types
366
+ const data = def ? this.readMarkerData(def.values) : {};
367
+ markers.push({ position, type, data });
368
+ }
369
+
370
+ return {
371
+ header,
372
+ metadata,
373
+ pointers,
374
+ strings,
375
+ customData,
376
+ audio,
377
+ cover,
378
+ markerDefinitions,
379
+ markers,
380
+ };
381
+ }
382
+ }
383
+
384
+ export type SSPMParsedMap = {
385
+ header: Header;
386
+ metadata: StaticMetadata;
387
+ pointers: Pointers;
388
+ strings: Strings;
389
+ customData: CustomData;
390
+ audio?: Buffer;
391
+ cover?: Buffer;
392
+ markerDefinitions: MarkerDefinition[];
393
+ markers: Marker[];
394
+ };