rhythia-api 243.0.0 → 244.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,336 +1,239 @@
1
- import { createHash } from "node:crypto";
2
- import { SSPMParser, type SSPMParsedMap } from "./star-calc/sspmParser.ts";
3
- import { V1SSPMParser, type SSPMMap } from "./star-calc/sspmv1Parser.ts";
4
-
5
- const IntrosortSizeThreshold = 16;
6
- const GeneralScientificLowerExponentDotNet = -5;
7
- const DecimalTieTolerance = Number.EPSILON * 4;
8
-
9
- type Marker = SSPMParsedMap["markers"][number];
10
-
11
- function formatSingle(value: number) {
12
- value = Math.fround(value);
13
-
14
- for (let precision = 1; precision <= 9; precision++) {
15
- const rounded = Number(value.toPrecision(precision));
16
- const formatted = formatGeneralDotNet(
17
- breakDecimalTieDotNet(value, rounded, precision)
18
- );
19
-
20
- if (Math.fround(Number(formatted)) === value) {
21
- return formatted;
22
- }
23
- }
24
-
25
- return formatGeneralDotNet(value);
26
- }
27
-
28
- function formatGeneralDotNet(value: number) {
29
- if (value === 0) {
30
- return "0";
31
- }
32
-
33
- const formatted = value.toString();
34
- const exponent = getDecimalExponent(value);
35
-
36
- if (
37
- formatted.includes("e") ||
38
- exponent <= GeneralScientificLowerExponentDotNet
39
- ) {
40
- return formatExponential(value, getSignificantDigitCount(formatted));
41
- }
42
-
43
- return formatted;
44
- }
45
-
46
- function getDecimalExponent(value: number) {
47
- return Math.floor(Math.log10(Math.abs(value)));
48
- }
49
-
50
- function getSignificantDigitCount(value: string) {
51
- const [mantissa] = value.toLowerCase().split("e");
52
- const digits = mantissa!.replace("-", "").replace(".", "").replace(/^0+/, "");
53
- return Math.max(digits.length, 1);
54
- }
55
-
56
- function formatExponential(value: number, significantDigitCount: number) {
57
- const [mantissa, exponent] = value
58
- .toExponential(significantDigitCount - 1)
59
- .split("e");
60
- const exponentValue = Number(exponent);
61
- const exponentSign = exponentValue < 0 ? "-" : "+";
62
- const exponentDigits = Math.abs(exponentValue).toString().padStart(2, "0");
63
- const normalizedMantissa = mantissa!
64
- .replace(/(\.\d*?)0+$/, "$1")
65
- .replace(/\.$/, "");
66
-
67
- return `${normalizedMantissa}E${exponentSign}${exponentDigits}`;
68
- }
69
-
70
- function breakDecimalTieDotNet(
71
- value: number,
72
- rounded: number,
73
- precision: number
74
- ) {
75
- if (rounded === value || Math.fround(rounded) !== value) {
76
- return rounded;
77
- }
78
-
79
- const exponent = getDecimalExponent(rounded);
80
- const step = 10 ** (exponent - precision + 1);
81
- const alternate = Number(
82
- (rounded > value ? rounded - step : rounded + step).toPrecision(precision)
83
- );
84
-
85
- if (alternate === rounded || Math.fround(alternate) !== value) {
86
- return rounded;
87
- }
88
-
89
- const roundedDistance = Math.abs(value - rounded);
90
- const alternateDistance = Math.abs(value - alternate);
91
- const tolerance =
92
- DecimalTieTolerance *
93
- Math.max(1, Math.abs(value), Math.abs(rounded), Math.abs(alternate));
94
-
95
- if (alternateDistance + tolerance < roundedDistance) {
96
- return alternate;
97
- }
98
-
99
- if (roundedDistance + tolerance < alternateDistance) {
100
- return rounded;
101
- }
102
-
103
- return hasEvenLastSignificantDigit(alternate, precision)
104
- ? alternate
105
- : rounded;
106
- }
107
-
108
- function hasEvenLastSignificantDigit(value: number, precision: number) {
109
- const [mantissa] = Math.abs(value).toPrecision(precision).split("e");
110
- const digits = mantissa!.replace(".", "").replace(/^0+/, "");
111
- const lastDigit = Number(digits.at(-1));
112
-
113
- return lastDigit % 2 === 0;
114
- }
115
-
116
- function compareMarkerPosition(a: Marker, b: Marker) {
117
- return a.position - b.position;
118
- }
119
-
120
- function swap(items: Marker[], left: number, right: number) {
121
- const temp = items[left]!;
122
- items[left] = items[right]!;
123
- items[right] = temp;
124
- }
125
-
126
- function swapIfGreater(items: Marker[], left: number, right: number) {
127
- if (
128
- left !== right &&
129
- compareMarkerPosition(items[left]!, items[right]!) > 0
130
- ) {
131
- swap(items, left, right);
132
- }
133
- }
134
-
135
- function insertionSort(items: Marker[], start: number, length: number) {
136
- for (let i = start; i < start + length - 1; i++) {
137
- const value = items[i + 1]!;
138
- let j = i;
139
-
140
- while (j >= start && compareMarkerPosition(value, items[j]!) < 0) {
141
- items[j + 1] = items[j]!;
142
- j--;
143
- }
144
-
145
- items[j + 1] = value;
146
- }
147
- }
148
-
149
- function downHeap(
150
- items: Marker[],
151
- start: number,
152
- index: number,
153
- heapSize: number
154
- ) {
155
- const value = items[start + index - 1]!;
156
- let child: number;
157
-
158
- while ((child = 2 * index) <= heapSize) {
159
- if (
160
- child < heapSize &&
161
- compareMarkerPosition(items[start + child - 1]!, items[start + child]!) <
162
- 0
163
- ) {
164
- child++;
165
- }
166
-
167
- if (compareMarkerPosition(value, items[start + child - 1]!) >= 0) {
168
- break;
169
- }
170
-
171
- items[start + index - 1] = items[start + child - 1]!;
172
- index = child;
173
- }
174
-
175
- items[start + index - 1] = value;
176
- }
177
-
178
- function heapSort(items: Marker[], start: number, length: number) {
179
- for (let i = Math.floor(length / 2); i >= 1; i--) {
180
- downHeap(items, start, i, length);
181
- }
182
-
183
- for (let i = length; i > 1; i--) {
184
- swap(items, start, start + i - 1);
185
- downHeap(items, start, 1, i - 1);
186
- }
187
- }
188
-
189
- function pickPivotAndPartition(items: Marker[], start: number, length: number) {
190
- const high = start + length - 1;
191
- const middle = start + ((high - start) >> 1);
192
-
193
- swapIfGreater(items, start, middle);
194
- swapIfGreater(items, start, high);
195
- swapIfGreater(items, middle, high);
196
-
197
- const pivot = items[middle]!;
198
- swap(items, middle, high - 1);
199
-
200
- let left = start;
201
- let right = high - 1;
202
-
203
- while (left < right) {
204
- while (compareMarkerPosition(items[++left]!, pivot) < 0) {}
205
- while (compareMarkerPosition(pivot, items[--right]!) < 0) {}
206
-
207
- if (left >= right) {
208
- break;
209
- }
210
-
211
- swap(items, left, right);
212
- }
213
-
214
- if (left !== high - 1) {
215
- swap(items, left, high - 1);
216
- }
217
-
218
- return left;
219
- }
220
-
221
- function introSort(
222
- items: Marker[],
223
- start: number,
224
- length: number,
225
- depthLimit: number
226
- ) {
227
- let partitionSize = length;
228
-
229
- while (partitionSize > 1) {
230
- if (partitionSize <= IntrosortSizeThreshold) {
231
- if (partitionSize === 2) {
232
- swapIfGreater(items, start, start + 1);
233
- return;
234
- }
235
-
236
- if (partitionSize === 3) {
237
- swapIfGreater(items, start, start + 1);
238
- swapIfGreater(items, start, start + 2);
239
- swapIfGreater(items, start + 1, start + 2);
240
- return;
241
- }
242
-
243
- insertionSort(items, start, partitionSize);
244
- return;
245
- }
246
-
247
- if (depthLimit === 0) {
248
- heapSort(items, start, partitionSize);
249
- return;
250
- }
251
-
252
- depthLimit--;
253
- const pivot = pickPivotAndPartition(items, start, partitionSize);
254
- introSort(
255
- items,
256
- pivot + 1,
257
- start + partitionSize - (pivot + 1),
258
- depthLimit
259
- );
260
- partitionSize = pivot - start;
261
- }
262
- }
263
-
264
- function sortByPositionDotNet(markers: Marker[]) {
265
- const result = [...markers];
266
-
267
- if (result.length > 1) {
268
- const depthLimit = 2 * (Math.floor(Math.log2(result.length)) + 1);
269
- introSort(result, 0, result.length, depthLimit);
270
- }
271
-
272
- return result;
273
- }
274
-
275
- export function computeBeatmapHash(map: SSPMParsedMap) {
276
- const noteType = Math.max(
277
- map.markerDefinitions.findIndex((x) => x.id === "ssp_note"),
278
- 0
279
- );
280
- const customDifficultyName =
281
- map.customData.fields.find((x) => x.id === "difficulty_name")?.value ?? "";
282
-
283
- const hashString =
284
- map.strings.mapName +
285
- map.strings.mapName +
286
- map.strings.mappers.join(",") +
287
- map.metadata.lastMarkerPos +
288
- sortByPositionDotNet(map.markers.filter((x) => x.type === noteType))
289
- .map(
290
- (x) =>
291
- x.position +
292
- formatSingle(x.data.position.x) +
293
- formatSingle(x.data.position.y)
294
- )
295
- .join(",") +
296
- map.metadata.difficulty +
297
- customDifficultyName;
298
-
299
- return createHash("sha256").update(hashString, "utf16le").digest("hex");
300
- }
301
-
302
- export function computeV1BeatmapHash(map: SSPMMap) {
303
- const hashString =
304
- map.name +
305
- map.name +
306
- map.creator
307
- .split(/[&,]/)
308
- .map((x) => x.trim())
309
- .join(",") +
310
- map.lastNotePosition +
311
- map.notes
312
- .map(
313
- (x) =>
314
- x.position +
315
- formatSingle(x.x) +
316
- formatSingle(x.y)
317
- )
318
- .join(",") +
319
- map.difficulty;
320
-
321
- return createHash("sha256").update(hashString, "utf16le").digest("hex");
322
- }
323
-
324
- export function computeBeatmapHashFromBytes(bytes: Buffer) {
325
- const version = bytes.readUInt16LE(4);
326
-
327
- if (version === 1) {
328
- return computeV1BeatmapHash(new V1SSPMParser(bytes).parse());
329
- }
330
-
331
- if (version === 2) {
332
- return computeBeatmapHash(new SSPMParser(bytes).parse());
333
- }
334
-
335
- throw new Error(`Invalid SSPM version: ${version}`);
336
- }
1
+ import { createHash } from "node:crypto";
2
+ import { SSPMParser, type SSPMParsedMap } from "./star-calc/sspmParser.ts";
3
+ import { V1SSPMParser, type SSPMMap } from "./star-calc/sspmv1Parser.ts";
4
+ import { formatSingle } from "./star-calc/formatSingle.ts";
5
+ import {
6
+ computeRhythiaMapHash,
7
+ isRHM,
8
+ parseRHM,
9
+ } from "./star-calc/rhmParser.ts";
10
+
11
+ const IntrosortSizeThreshold = 16;
12
+
13
+ type Marker = SSPMParsedMap["markers"][number];
14
+
15
+ function compareMarkerPosition(a: Marker, b: Marker) {
16
+ return a.position - b.position;
17
+ }
18
+
19
+ function swap(items: Marker[], left: number, right: number) {
20
+ const temp = items[left]!;
21
+ items[left] = items[right]!;
22
+ items[right] = temp;
23
+ }
24
+
25
+ function swapIfGreater(items: Marker[], left: number, right: number) {
26
+ if (
27
+ left !== right &&
28
+ compareMarkerPosition(items[left]!, items[right]!) > 0
29
+ ) {
30
+ swap(items, left, right);
31
+ }
32
+ }
33
+
34
+ function insertionSort(items: Marker[], start: number, length: number) {
35
+ for (let i = start; i < start + length - 1; i++) {
36
+ const value = items[i + 1]!;
37
+ let j = i;
38
+
39
+ while (j >= start && compareMarkerPosition(value, items[j]!) < 0) {
40
+ items[j + 1] = items[j]!;
41
+ j--;
42
+ }
43
+
44
+ items[j + 1] = value;
45
+ }
46
+ }
47
+
48
+ function downHeap(
49
+ items: Marker[],
50
+ start: number,
51
+ index: number,
52
+ heapSize: number
53
+ ) {
54
+ const value = items[start + index - 1]!;
55
+ let child: number;
56
+
57
+ while ((child = 2 * index) <= heapSize) {
58
+ if (
59
+ child < heapSize &&
60
+ compareMarkerPosition(items[start + child - 1]!, items[start + child]!) <
61
+ 0
62
+ ) {
63
+ child++;
64
+ }
65
+
66
+ if (compareMarkerPosition(value, items[start + child - 1]!) >= 0) {
67
+ break;
68
+ }
69
+
70
+ items[start + index - 1] = items[start + child - 1]!;
71
+ index = child;
72
+ }
73
+
74
+ items[start + index - 1] = value;
75
+ }
76
+
77
+ function heapSort(items: Marker[], start: number, length: number) {
78
+ for (let i = Math.floor(length / 2); i >= 1; i--) {
79
+ downHeap(items, start, i, length);
80
+ }
81
+
82
+ for (let i = length; i > 1; i--) {
83
+ swap(items, start, start + i - 1);
84
+ downHeap(items, start, 1, i - 1);
85
+ }
86
+ }
87
+
88
+ function pickPivotAndPartition(items: Marker[], start: number, length: number) {
89
+ const high = start + length - 1;
90
+ const middle = start + ((high - start) >> 1);
91
+
92
+ swapIfGreater(items, start, middle);
93
+ swapIfGreater(items, start, high);
94
+ swapIfGreater(items, middle, high);
95
+
96
+ const pivot = items[middle]!;
97
+ swap(items, middle, high - 1);
98
+
99
+ let left = start;
100
+ let right = high - 1;
101
+
102
+ while (left < right) {
103
+ while (compareMarkerPosition(items[++left]!, pivot) < 0) {}
104
+ while (compareMarkerPosition(pivot, items[--right]!) < 0) {}
105
+
106
+ if (left >= right) {
107
+ break;
108
+ }
109
+
110
+ swap(items, left, right);
111
+ }
112
+
113
+ if (left !== high - 1) {
114
+ swap(items, left, high - 1);
115
+ }
116
+
117
+ return left;
118
+ }
119
+
120
+ function introSort(
121
+ items: Marker[],
122
+ start: number,
123
+ length: number,
124
+ depthLimit: number
125
+ ) {
126
+ let partitionSize = length;
127
+
128
+ while (partitionSize > 1) {
129
+ if (partitionSize <= IntrosortSizeThreshold) {
130
+ if (partitionSize === 2) {
131
+ swapIfGreater(items, start, start + 1);
132
+ return;
133
+ }
134
+
135
+ if (partitionSize === 3) {
136
+ swapIfGreater(items, start, start + 1);
137
+ swapIfGreater(items, start, start + 2);
138
+ swapIfGreater(items, start + 1, start + 2);
139
+ return;
140
+ }
141
+
142
+ insertionSort(items, start, partitionSize);
143
+ return;
144
+ }
145
+
146
+ if (depthLimit === 0) {
147
+ heapSort(items, start, partitionSize);
148
+ return;
149
+ }
150
+
151
+ depthLimit--;
152
+ const pivot = pickPivotAndPartition(items, start, partitionSize);
153
+ introSort(
154
+ items,
155
+ pivot + 1,
156
+ start + partitionSize - (pivot + 1),
157
+ depthLimit
158
+ );
159
+ partitionSize = pivot - start;
160
+ }
161
+ }
162
+
163
+ function sortByPositionDotNet(markers: Marker[]) {
164
+ const result = [...markers];
165
+
166
+ if (result.length > 1) {
167
+ const depthLimit = 2 * (Math.floor(Math.log2(result.length)) + 1);
168
+ introSort(result, 0, result.length, depthLimit);
169
+ }
170
+
171
+ return result;
172
+ }
173
+
174
+ export function computeBeatmapHash(map: SSPMParsedMap) {
175
+ const noteType = Math.max(
176
+ map.markerDefinitions.findIndex((x) => x.id === "ssp_note"),
177
+ 0
178
+ );
179
+ const customDifficultyName =
180
+ map.customData.fields.find((x) => x.id === "difficulty_name")?.value ?? "";
181
+
182
+ const hashString =
183
+ map.strings.mapName +
184
+ map.strings.mapName +
185
+ map.strings.mappers.join(",") +
186
+ map.metadata.lastMarkerPos +
187
+ sortByPositionDotNet(map.markers.filter((x) => x.type === noteType))
188
+ .map(
189
+ (x) =>
190
+ x.position +
191
+ formatSingle(x.data.position.x) +
192
+ formatSingle(x.data.position.y)
193
+ )
194
+ .join(",") +
195
+ map.metadata.difficulty +
196
+ customDifficultyName;
197
+
198
+ return createHash("sha256").update(hashString, "utf16le").digest("hex");
199
+ }
200
+
201
+ export function computeV1BeatmapHash(map: SSPMMap) {
202
+ const hashString =
203
+ map.name +
204
+ map.name +
205
+ map.creator
206
+ .split(/[&,]/)
207
+ .map((x) => x.trim())
208
+ .join(",") +
209
+ map.lastNotePosition +
210
+ map.notes
211
+ .map(
212
+ (x) =>
213
+ x.position +
214
+ formatSingle(x.x) +
215
+ formatSingle(x.y)
216
+ )
217
+ .join(",") +
218
+ map.difficulty;
219
+
220
+ return createHash("sha256").update(hashString, "utf16le").digest("hex");
221
+ }
222
+
223
+ export function computeBeatmapHashFromBytes(bytes: Buffer) {
224
+ if (isRHM(bytes)) {
225
+ return computeRhythiaMapHash(parseRHM(bytes).map);
226
+ }
227
+
228
+ const version = bytes.readUInt16LE(4);
229
+
230
+ if (version === 1) {
231
+ return computeV1BeatmapHash(new V1SSPMParser(bytes).parse());
232
+ }
233
+
234
+ if (version === 2) {
235
+ return computeBeatmapHash(new SSPMParser(bytes).parse());
236
+ }
237
+
238
+ throw new Error(`Invalid SSPM version: ${version}`);
239
+ }
@@ -0,0 +1,101 @@
1
+ import type { Json } from "../types/database";
2
+ import { supabase } from "./supabase";
3
+
4
+ export type ViolationType = "excluded" | "silenced" | "restricted";
5
+
6
+ export type AddViolationInput = {
7
+ profileId: number;
8
+ type: ViolationType;
9
+ reason: string;
10
+ expiresAt?: string | null;
11
+ moderatedBy: number;
12
+ metadata?: Record<string, unknown>;
13
+ };
14
+
15
+ export type RevokeViolationInput = {
16
+ profileId: number;
17
+ type: ViolationType;
18
+ revokedBy: number;
19
+ reason?: string;
20
+ };
21
+
22
+ export type ModerationState = {
23
+ excluded: boolean;
24
+ silenced: boolean;
25
+ restricted: boolean;
26
+ activeViolations: Array<{
27
+ id: number;
28
+ type: ViolationType;
29
+ reason: string;
30
+ createdAt: string;
31
+ expiresAt: string | null;
32
+ moderatedBy: number | null;
33
+ }>;
34
+ };
35
+
36
+ type RpcActiveViolation = ModerationState["activeViolations"][number] & {
37
+ violation_type?: ViolationType;
38
+ created_at?: string;
39
+ expires_at?: string | null;
40
+ moderated_by?: number | null;
41
+ };
42
+
43
+ export async function addViolation(input: AddViolationInput) {
44
+ return supabase.rpc("add_user_violation", {
45
+ _profile_id: input.profileId,
46
+ _violation_type: input.type,
47
+ _reason: input.reason,
48
+ _expires_at: input.expiresAt ?? undefined,
49
+ _moderated_by: input.moderatedBy,
50
+ _metadata: (input.metadata ?? {}) as Json,
51
+ });
52
+ }
53
+
54
+ export async function revokeViolation(input: RevokeViolationInput) {
55
+ return supabase.rpc("revoke_user_violation", {
56
+ _profile_id: input.profileId,
57
+ _violation_type: input.type,
58
+ _revoked_by: input.revokedBy,
59
+ _revoke_reason: input.reason,
60
+ });
61
+ }
62
+
63
+ export async function getUserViolationState(profileId: number) {
64
+ return supabase.rpc("get_user_violation_state", {
65
+ _profile_id: profileId,
66
+ });
67
+ }
68
+
69
+ export async function getModerationState(profileId: number) {
70
+ const { data, error } = await getUserViolationState(profileId);
71
+
72
+ if (error) {
73
+ return { data: null, error };
74
+ }
75
+
76
+ const state = data?.[0];
77
+
78
+ if (!state) {
79
+ return { data: null, error: { message: "Moderation state not found" } };
80
+ }
81
+
82
+ const activeViolations = (state.active_violations ??
83
+ []) as RpcActiveViolation[];
84
+
85
+ return {
86
+ data: {
87
+ excluded: state.excluded,
88
+ silenced: state.silenced,
89
+ restricted: state.restricted,
90
+ activeViolations: activeViolations.map((violation) => ({
91
+ id: violation.id,
92
+ type: violation.type ?? violation.violation_type!,
93
+ reason: violation.reason,
94
+ createdAt: violation.createdAt ?? violation.created_at!,
95
+ expiresAt: violation.expiresAt ?? violation.expires_at ?? null,
96
+ moderatedBy: violation.moderatedBy ?? violation.moderated_by ?? null,
97
+ })),
98
+ } satisfies ModerationState,
99
+ error: null,
100
+ };
101
+ }