rhythia-api 242.0.0 → 243.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.
- package/api/createBeatmap.ts +57 -39
- package/api/editProfile.ts +4 -67
- package/api/executeAdminOperation.ts +1030 -681
- package/api/getAvatarUploadUrl.ts +90 -85
- package/api/getBeatmapPage.ts +2 -0
- package/api/getBeatmapPageById.ts +2 -0
- package/api/getBeatmaps.ts +110 -197
- package/api/getCollection.ts +44 -31
- package/api/getMapUploadUrl.ts +90 -93
- package/api/getScore.ts +2 -0
- package/api/getVideoUploadUrl.ts +90 -85
- package/api/submitScoreInternal.ts +506 -461
- package/api/updateBeatmapPage.ts +6 -0
- package/beatmap-file-urls.json +29398 -0
- package/handleApi.ts +24 -21
- package/index.ts +121 -112
- package/package.json +4 -2
- package/queries/admin_delete_user.sql +42 -39
- package/queries/admin_remove_all_scores.sql +6 -3
- package/queries/admin_remove_score.sql +107 -0
- package/queries/admin_update_profile.sql +22 -0
- package/queries/get_beatmaps_v2.sql +48 -0
- package/queries/get_top_scores_for_beatmap3.sql +47 -38
- package/queries/profile_update_guards.sql +66 -0
- package/types/database.ts +1525 -1450
- package/utils/beatmapFiles.ts +102 -0
- package/utils/beatmapHash.ts +336 -0
- package/utils/beatmapTopScores.ts +68 -84
- package/utils/getUserBySession.ts +3 -1
- package/utils/profileUpdateValidation.ts +51 -0
- package/utils/redis.ts +24 -0
- package/utils/rhrReplay.ts +122 -0
- package/utils/star-calc/sspmParser.ts +294 -160
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Buffer } from "buffer";
|
|
2
|
+
|
|
1
3
|
type DataTypeID =
|
|
2
4
|
| 0x00
|
|
3
5
|
| 0x01
|
|
@@ -13,6 +15,22 @@ type DataTypeID =
|
|
|
13
15
|
| 0x0b
|
|
14
16
|
| 0x0c;
|
|
15
17
|
|
|
18
|
+
const enum DataType {
|
|
19
|
+
End = 0x00,
|
|
20
|
+
Byte = 0x01,
|
|
21
|
+
UInt16 = 0x02,
|
|
22
|
+
UInt32 = 0x03,
|
|
23
|
+
UInt64 = 0x04,
|
|
24
|
+
Float = 0x05,
|
|
25
|
+
Double = 0x06,
|
|
26
|
+
Position = 0x07,
|
|
27
|
+
Buffer = 0x08,
|
|
28
|
+
String = 0x09,
|
|
29
|
+
LongBuffer = 0x0a,
|
|
30
|
+
LongString = 0x0b,
|
|
31
|
+
Array = 0x0c,
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
interface Header {
|
|
17
35
|
signature: Buffer;
|
|
18
36
|
version: number;
|
|
@@ -70,33 +88,66 @@ interface MarkerDefinition {
|
|
|
70
88
|
interface Marker {
|
|
71
89
|
position: number;
|
|
72
90
|
type: number;
|
|
73
|
-
data:
|
|
91
|
+
data: any;
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
export class SSPMParser {
|
|
95
|
+
private static readonly SIGNATURE = 0x6d2b5353;
|
|
77
96
|
private buffer: Buffer;
|
|
78
|
-
private offset
|
|
97
|
+
private offset = 0;
|
|
79
98
|
|
|
80
99
|
constructor(buffer: Buffer) {
|
|
81
100
|
this.buffer = buffer;
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
private checkBounds(length: number): void {
|
|
85
|
-
if (this.offset + length > this.buffer.length) {
|
|
104
|
+
if (length < 0 || this.offset + length > this.buffer.length) {
|
|
86
105
|
throw new RangeError(
|
|
87
|
-
`Attempt to read beyond buffer length:
|
|
106
|
+
`Attempt to read beyond buffer length: offset=${this.offset}, length=${length}, bufferLength=${this.buffer.length}`
|
|
88
107
|
);
|
|
89
108
|
}
|
|
90
109
|
}
|
|
91
110
|
|
|
92
|
-
private
|
|
93
|
-
|
|
111
|
+
private seek(position: number): void {
|
|
112
|
+
if (
|
|
113
|
+
!Number.isInteger(position) ||
|
|
114
|
+
position < 0 ||
|
|
115
|
+
position > this.buffer.length
|
|
116
|
+
) {
|
|
117
|
+
throw new RangeError(`Invalid seek position: ${position}`);
|
|
118
|
+
}
|
|
119
|
+
this.offset = position;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private skip(length: number): void {
|
|
123
|
+
this.checkBounds(length);
|
|
124
|
+
this.offset += length;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private log(_message: string): void {}
|
|
128
|
+
|
|
129
|
+
private readUInt8(): number {
|
|
130
|
+
this.checkBounds(1);
|
|
131
|
+
return this.buffer.readUInt8(this.offset++);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private readInt32(): number {
|
|
135
|
+
this.checkBounds(4);
|
|
136
|
+
const value = this.buffer.readInt32LE(this.offset);
|
|
137
|
+
this.offset += 4;
|
|
138
|
+
return value;
|
|
94
139
|
}
|
|
95
140
|
|
|
96
141
|
private readUInt16(): number {
|
|
97
142
|
this.checkBounds(2);
|
|
98
143
|
const value = this.buffer.readUInt16LE(this.offset);
|
|
99
|
-
this.
|
|
144
|
+
this.offset += 2;
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private readInt16(): number {
|
|
149
|
+
this.checkBounds(2);
|
|
150
|
+
const value = this.buffer.readInt16LE(this.offset);
|
|
100
151
|
this.offset += 2;
|
|
101
152
|
return value;
|
|
102
153
|
}
|
|
@@ -104,39 +155,55 @@ export class SSPMParser {
|
|
|
104
155
|
private readUInt32(): number {
|
|
105
156
|
this.checkBounds(4);
|
|
106
157
|
const value = this.buffer.readUInt32LE(this.offset);
|
|
107
|
-
this.log(`Read UInt32: ${value}`);
|
|
108
158
|
this.offset += 4;
|
|
109
159
|
return value;
|
|
110
160
|
}
|
|
111
161
|
|
|
162
|
+
private readUInt64BigInt(): bigint {
|
|
163
|
+
this.checkBounds(8);
|
|
164
|
+
const value = this.buffer.readBigUInt64LE(this.offset);
|
|
165
|
+
this.offset += 8;
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
|
|
112
169
|
private readUInt64(): number {
|
|
170
|
+
const value = this.readUInt64BigInt();
|
|
171
|
+
const asNumber = Number(value);
|
|
172
|
+
|
|
173
|
+
if (!Number.isSafeInteger(asNumber)) {
|
|
174
|
+
throw new RangeError(`OutOfRange: ${value.toString()}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return asNumber;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private readFloat32(): number {
|
|
181
|
+
this.checkBounds(4);
|
|
182
|
+
const value = this.buffer.readFloatLE(this.offset);
|
|
183
|
+
this.offset += 4;
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private readFloat64(): number {
|
|
113
188
|
this.checkBounds(8);
|
|
114
|
-
const
|
|
115
|
-
const high = this.buffer.readUInt32LE(this.offset + 4);
|
|
189
|
+
const value = this.buffer.readDoubleLE(this.offset);
|
|
116
190
|
this.offset += 8;
|
|
117
|
-
const value = low + high * 2 ** 32;
|
|
118
|
-
this.log(`Read UInt64: ${value} (Low: ${low}, High: ${high})`);
|
|
119
191
|
return value;
|
|
120
192
|
}
|
|
121
193
|
|
|
122
194
|
private readBytes(length: number): Buffer {
|
|
123
195
|
this.checkBounds(length);
|
|
124
|
-
const value = this.buffer.
|
|
125
|
-
this.log(`Read ${length} bytes`);
|
|
196
|
+
const value = this.buffer.subarray(this.offset, this.offset + length);
|
|
126
197
|
this.offset += length;
|
|
127
198
|
return value;
|
|
128
199
|
}
|
|
129
200
|
|
|
130
|
-
private readString(): string {
|
|
131
|
-
const length = this.readUInt16();
|
|
132
|
-
this.
|
|
133
|
-
const value = this.readBytes(length).toString("utf-8");
|
|
134
|
-
this.log(`Read String of length ${length}: ${value}`);
|
|
135
|
-
return value;
|
|
201
|
+
private readString(longString = false): string {
|
|
202
|
+
const length = longString ? this.readUInt32() : this.readUInt16();
|
|
203
|
+
return this.readBytes(length).toString("utf8");
|
|
136
204
|
}
|
|
137
205
|
|
|
138
206
|
private readStringList(count: number): string[] {
|
|
139
|
-
console.log("[DANGER] Reading String List");
|
|
140
207
|
const list: string[] = [];
|
|
141
208
|
for (let i = 0; i < count; i++) {
|
|
142
209
|
list.push(this.readString());
|
|
@@ -144,77 +211,129 @@ export class SSPMParser {
|
|
|
144
211
|
return list;
|
|
145
212
|
}
|
|
146
213
|
|
|
147
|
-
private
|
|
214
|
+
private readPosition(): { x: number; y: number; type: "int" | "quantum" } {
|
|
215
|
+
const type = this.readUInt8();
|
|
216
|
+
|
|
217
|
+
if (type === 0x00) {
|
|
218
|
+
return {
|
|
219
|
+
x: this.readUInt8(),
|
|
220
|
+
y: this.readUInt8(),
|
|
221
|
+
type: "int",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (type === 0x01) {
|
|
226
|
+
return {
|
|
227
|
+
x: this.readFloat32(),
|
|
228
|
+
y: this.readFloat32(),
|
|
229
|
+
type: "quantum",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
throw new Error(`Invalid position type: ${type}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private readTypedValue(typeID: DataTypeID): any {
|
|
148
237
|
switch (typeID) {
|
|
149
|
-
case
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.
|
|
153
|
-
|
|
154
|
-
case 0x02: // 2 byte uint
|
|
238
|
+
case DataType.End:
|
|
239
|
+
return null;
|
|
240
|
+
case DataType.Byte:
|
|
241
|
+
return this.readUInt8();
|
|
242
|
+
case DataType.UInt16:
|
|
155
243
|
return this.readUInt16();
|
|
156
|
-
case
|
|
244
|
+
case DataType.UInt32:
|
|
157
245
|
return this.readUInt32();
|
|
158
|
-
case
|
|
246
|
+
case DataType.UInt64:
|
|
159
247
|
return this.readUInt64();
|
|
160
|
-
case
|
|
161
|
-
this.
|
|
162
|
-
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
return
|
|
166
|
-
case
|
|
167
|
-
this.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return
|
|
172
|
-
case
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
248
|
+
case DataType.Float:
|
|
249
|
+
return this.readFloat32();
|
|
250
|
+
case DataType.Double:
|
|
251
|
+
return this.readFloat64();
|
|
252
|
+
case DataType.Position:
|
|
253
|
+
return this.readPosition();
|
|
254
|
+
case DataType.Buffer: {
|
|
255
|
+
const length = this.readUInt16();
|
|
256
|
+
return this.readBytes(length);
|
|
257
|
+
}
|
|
258
|
+
case DataType.String:
|
|
259
|
+
return this.readString(false);
|
|
260
|
+
case DataType.LongBuffer: {
|
|
261
|
+
const length = this.readUInt32();
|
|
262
|
+
return this.readBytes(length);
|
|
263
|
+
}
|
|
264
|
+
case DataType.LongString:
|
|
265
|
+
return this.readString(true);
|
|
266
|
+
case DataType.Array:
|
|
267
|
+
throw new Error("Array type must be handled separately.");
|
|
268
|
+
default:
|
|
269
|
+
throw new Error(`Unknown data type: ${typeID}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private skipTypedValue(typeID: DataTypeID): void {
|
|
274
|
+
switch (typeID) {
|
|
275
|
+
case DataType.End:
|
|
276
|
+
return;
|
|
277
|
+
case DataType.Byte:
|
|
278
|
+
this.skip(1);
|
|
279
|
+
return;
|
|
280
|
+
case DataType.UInt16:
|
|
281
|
+
this.skip(2);
|
|
282
|
+
return;
|
|
283
|
+
case DataType.UInt32:
|
|
284
|
+
this.skip(4);
|
|
285
|
+
return;
|
|
286
|
+
case DataType.UInt64:
|
|
287
|
+
this.skip(8);
|
|
288
|
+
return;
|
|
289
|
+
case DataType.Float:
|
|
290
|
+
this.skip(4);
|
|
291
|
+
return;
|
|
292
|
+
case DataType.Double:
|
|
293
|
+
this.skip(8);
|
|
294
|
+
return;
|
|
295
|
+
case DataType.Position: {
|
|
296
|
+
const posType = this.readUInt8();
|
|
297
|
+
if (posType === 0x00) {
|
|
298
|
+
this.skip(2);
|
|
299
|
+
return;
|
|
189
300
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
case 0x00: // end type, should not appear here
|
|
301
|
+
if (posType === 0x01) {
|
|
302
|
+
this.skip(8);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
throw new Error(`Invalid position type while skipping: ${posType}`);
|
|
306
|
+
}
|
|
307
|
+
case DataType.Buffer:
|
|
308
|
+
case DataType.String:
|
|
309
|
+
this.skip(this.readUInt16());
|
|
310
|
+
return;
|
|
311
|
+
case DataType.LongBuffer:
|
|
312
|
+
case DataType.LongString:
|
|
313
|
+
this.skip(this.readUInt32());
|
|
314
|
+
return;
|
|
315
|
+
case DataType.Array:
|
|
316
|
+
throw new Error("Array is not allowed in markers.");
|
|
207
317
|
default:
|
|
208
|
-
throw new Error(
|
|
318
|
+
throw new Error(`Unknown data type while skipping: ${typeID}`);
|
|
209
319
|
}
|
|
210
320
|
}
|
|
211
321
|
|
|
212
|
-
private
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
322
|
+
private readCustomValue(type: DataTypeID, arrayType?: DataTypeID): any {
|
|
323
|
+
if (type === DataType.Array) {
|
|
324
|
+
if (arrayType == null) {
|
|
325
|
+
throw new Error("Array custom field missing array element type.");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const count = this.readUInt32();
|
|
329
|
+
const values: any[] = [];
|
|
330
|
+
for (let i = 0; i < count; i++) {
|
|
331
|
+
values.push(this.readTypedValue(arrayType));
|
|
332
|
+
}
|
|
333
|
+
return values;
|
|
216
334
|
}
|
|
217
|
-
|
|
335
|
+
|
|
336
|
+
return this.readTypedValue(type);
|
|
218
337
|
}
|
|
219
338
|
|
|
220
339
|
parse(): {
|
|
@@ -228,30 +347,33 @@ export class SSPMParser {
|
|
|
228
347
|
markerDefinitions: MarkerDefinition[];
|
|
229
348
|
markers: Marker[];
|
|
230
349
|
} {
|
|
231
|
-
// Header
|
|
232
350
|
const header: Header = {
|
|
233
351
|
signature: this.readBytes(4),
|
|
234
|
-
version: this.
|
|
352
|
+
version: this.readInt16(),
|
|
235
353
|
reserved: this.readBytes(4),
|
|
236
354
|
};
|
|
237
|
-
|
|
238
|
-
|
|
355
|
+
|
|
356
|
+
const signatureValue = header.signature.readInt32LE(0);
|
|
357
|
+
if (signatureValue !== SSPMParser.SIGNATURE) {
|
|
358
|
+
throw new Error("Invalid SSPM signature.");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (header.version !== 2) {
|
|
362
|
+
throw new Error(`Invalid SSPM version: ${header.version}`);
|
|
239
363
|
}
|
|
240
364
|
|
|
241
|
-
// Static Metadata
|
|
242
365
|
const metadata: StaticMetadata = {
|
|
243
366
|
sha1: this.readBytes(20),
|
|
244
367
|
lastMarkerPos: this.readUInt32(),
|
|
245
368
|
noteCount: this.readUInt32(),
|
|
246
369
|
markerCount: this.readUInt32(),
|
|
247
|
-
difficulty: this.
|
|
370
|
+
difficulty: this.readUInt8(),
|
|
248
371
|
rating: this.readUInt16(),
|
|
249
|
-
hasAudio: this.
|
|
250
|
-
hasCover: this.
|
|
251
|
-
requiresMod: this.
|
|
372
|
+
hasAudio: this.readUInt8() === 0x01,
|
|
373
|
+
hasCover: this.readUInt8() === 0x01,
|
|
374
|
+
requiresMod: this.readUInt8() === 0x01,
|
|
252
375
|
};
|
|
253
376
|
|
|
254
|
-
// Pointers
|
|
255
377
|
const pointers: Pointers = {
|
|
256
378
|
customDataOffset: this.readUInt64(),
|
|
257
379
|
customDataLength: this.readUInt64(),
|
|
@@ -265,19 +387,6 @@ export class SSPMParser {
|
|
|
265
387
|
markerLength: this.readUInt64(),
|
|
266
388
|
};
|
|
267
389
|
|
|
268
|
-
// Log derived pointer values
|
|
269
|
-
this.log(`customDataOffset: ${pointers.customDataOffset}`);
|
|
270
|
-
this.log(`customDataLength: ${pointers.customDataLength}`);
|
|
271
|
-
this.log(`audioOffset: ${pointers.audioOffset}`);
|
|
272
|
-
this.log(`audioLength: ${pointers.audioLength}`);
|
|
273
|
-
this.log(`coverOffset: ${pointers.coverOffset}`);
|
|
274
|
-
this.log(`coverLength: ${pointers.coverLength}`);
|
|
275
|
-
this.log(`markerDefinitionsOffset: ${pointers.markerDefinitionsOffset}`);
|
|
276
|
-
this.log(`markerDefinitionsLength: ${pointers.markerDefinitionsLength}`);
|
|
277
|
-
this.log(`markerOffset: ${pointers.markerOffset}`);
|
|
278
|
-
this.log(`markerLength: ${pointers.markerLength}`);
|
|
279
|
-
|
|
280
|
-
// Strings
|
|
281
390
|
const strings: Strings = {
|
|
282
391
|
mapID: this.readString(),
|
|
283
392
|
mapName: this.readString(),
|
|
@@ -285,89 +394,114 @@ export class SSPMParser {
|
|
|
285
394
|
mappers: this.readStringList(this.readUInt16()),
|
|
286
395
|
};
|
|
287
396
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
this.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (type === 0x0c) {
|
|
302
|
-
arrayType = this.buffer.readUInt8(this.offset++) as DataTypeID;
|
|
303
|
-
}
|
|
304
|
-
const length = this.readUInt32();
|
|
305
|
-
// this.checkBounds(length);
|
|
306
|
-
const value = this.readBytes(length);
|
|
307
|
-
customData.fields.push({ id, type, arrayType, value });
|
|
397
|
+
const customData: CustomData = { fields: [] };
|
|
398
|
+
|
|
399
|
+
if (pointers.customDataOffset !== 0) {
|
|
400
|
+
this.seek(pointers.customDataOffset);
|
|
401
|
+
|
|
402
|
+
const fieldCount = this.readUInt16();
|
|
403
|
+
for (let i = 0; i < fieldCount; i++) {
|
|
404
|
+
const id = this.readString();
|
|
405
|
+
const type = this.readUInt8() as DataTypeID;
|
|
406
|
+
|
|
407
|
+
let arrayType: DataTypeID | undefined;
|
|
408
|
+
if (type === DataType.Array) {
|
|
409
|
+
arrayType = this.readUInt8() as DataTypeID;
|
|
308
410
|
}
|
|
411
|
+
|
|
412
|
+
const value = this.readCustomValue(type, arrayType);
|
|
413
|
+
customData.fields.push({ id, type, arrayType, value });
|
|
309
414
|
}
|
|
310
|
-
}
|
|
415
|
+
}
|
|
311
416
|
|
|
312
417
|
let audio: Buffer | undefined;
|
|
313
418
|
if (
|
|
314
419
|
metadata.hasAudio &&
|
|
315
|
-
pointers.audioOffset
|
|
316
|
-
pointers.audioLength
|
|
420
|
+
pointers.audioOffset !== 0 &&
|
|
421
|
+
pointers.audioLength !== 0
|
|
317
422
|
) {
|
|
318
|
-
this.
|
|
319
|
-
|
|
320
|
-
);
|
|
321
|
-
this.offset = Number(pointers.audioOffset);
|
|
322
|
-
this.checkBounds(Number(pointers.audioLength));
|
|
323
|
-
audio = this.readBytes(Number(pointers.audioLength));
|
|
423
|
+
this.seek(pointers.audioOffset);
|
|
424
|
+
audio = this.readBytes(pointers.audioLength);
|
|
324
425
|
}
|
|
325
426
|
|
|
326
427
|
let cover: Buffer | undefined;
|
|
327
428
|
if (
|
|
328
429
|
metadata.hasCover &&
|
|
329
|
-
pointers.coverOffset
|
|
330
|
-
pointers.coverLength
|
|
430
|
+
pointers.coverOffset !== 0 &&
|
|
431
|
+
pointers.coverLength !== 0
|
|
331
432
|
) {
|
|
332
|
-
this.
|
|
333
|
-
|
|
334
|
-
);
|
|
335
|
-
this.offset = Number(pointers.coverOffset);
|
|
336
|
-
this.checkBounds(Number(pointers.coverLength));
|
|
337
|
-
cover = this.readBytes(Number(pointers.coverLength));
|
|
433
|
+
this.seek(pointers.coverOffset);
|
|
434
|
+
cover = this.readBytes(pointers.coverLength);
|
|
338
435
|
}
|
|
339
436
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
this.
|
|
345
|
-
|
|
437
|
+
if (pointers.markerDefinitionsOffset === 0) {
|
|
438
|
+
throw new Error("Missing marker definitions offset.");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this.seek(pointers.markerDefinitionsOffset);
|
|
442
|
+
|
|
443
|
+
const markerDefCount = this.readUInt8();
|
|
346
444
|
const markerDefinitions: MarkerDefinition[] = [];
|
|
445
|
+
|
|
347
446
|
for (let i = 0; i < markerDefCount; i++) {
|
|
348
447
|
const id = this.readString();
|
|
349
|
-
const valueCount = this.
|
|
448
|
+
const valueCount = this.readUInt8();
|
|
449
|
+
|
|
350
450
|
const values: DataTypeID[] = [];
|
|
351
451
|
for (let j = 0; j < valueCount; j++) {
|
|
352
|
-
values.push(this.
|
|
452
|
+
values.push(this.readUInt8() as DataTypeID);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const terminator = this.readUInt8();
|
|
456
|
+
if (terminator !== 0x00) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
`Invalid marker definition terminator for "${id}": expected 0x00, got 0x${terminator.toString(16)}`
|
|
459
|
+
);
|
|
353
460
|
}
|
|
461
|
+
|
|
354
462
|
markerDefinitions.push({ id, values });
|
|
355
463
|
}
|
|
356
464
|
|
|
357
|
-
|
|
358
|
-
this.log(
|
|
359
|
-
`Reading Markers, Offset: ${pointers.markerOffset}, Length: ${pointers.markerLength}`
|
|
360
|
-
);
|
|
361
|
-
this.offset = Number(pointers.markerOffset);
|
|
362
|
-
const endOffset = this.offset + Number(pointers.markerLength);
|
|
465
|
+
this.seek(pointers.markerOffset);
|
|
363
466
|
|
|
364
467
|
const markers: Marker[] = [];
|
|
468
|
+
const noteDefIndex = markerDefinitions.findIndex(
|
|
469
|
+
(d) => d.id === "ssp_note"
|
|
470
|
+
);
|
|
471
|
+
const effectiveNoteDefIndex = noteDefIndex >= 0 ? noteDefIndex : 0;
|
|
365
472
|
|
|
366
|
-
|
|
473
|
+
for (let i = 0; i < metadata.markerCount; i++) {
|
|
367
474
|
const position = this.readUInt32();
|
|
368
|
-
const type = this.
|
|
369
|
-
|
|
370
|
-
|
|
475
|
+
const type = this.readUInt8();
|
|
476
|
+
|
|
477
|
+
if (type >= markerDefinitions.length) {
|
|
478
|
+
throw new Error(`Invalid marker type index: ${type}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const def = markerDefinitions[type];
|
|
482
|
+
const isNote = type === effectiveNoteDefIndex;
|
|
483
|
+
|
|
484
|
+
const data: Record<string, any> = {};
|
|
485
|
+
let firstNotePositionCaptured = false;
|
|
486
|
+
|
|
487
|
+
for (let fieldIndex = 0; fieldIndex < def.values.length; fieldIndex++) {
|
|
488
|
+
const valueType = def.values[fieldIndex];
|
|
489
|
+
|
|
490
|
+
if (
|
|
491
|
+
isNote &&
|
|
492
|
+
valueType === DataType.Position &&
|
|
493
|
+
!firstNotePositionCaptured
|
|
494
|
+
) {
|
|
495
|
+
const pos = this.readPosition();
|
|
496
|
+
data[`field${fieldIndex}`] = pos;
|
|
497
|
+
data.position = pos;
|
|
498
|
+
firstNotePositionCaptured = true;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
data[`field${fieldIndex}`] = this.readTypedValue(valueType);
|
|
503
|
+
}
|
|
504
|
+
|
|
371
505
|
markers.push({ position, type, data });
|
|
372
506
|
}
|
|
373
507
|
|