rhythia-api 183.0.0 → 184.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/.prettierrc.json +6 -6
- package/api/addCollectionMap.ts +82 -82
- package/api/approveMap.ts +78 -78
- package/api/chartPublicStats.ts +32 -32
- package/api/createBeatmap.ts +168 -168
- package/api/createBeatmapPage.ts +64 -64
- package/api/createClan.ts +81 -81
- package/api/createCollection.ts +58 -58
- package/api/deleteBeatmapPage.ts +77 -77
- package/api/deleteCollection.ts +59 -59
- package/api/deleteCollectionMap.ts +71 -71
- package/api/editAboutMe.ts +91 -91
- package/api/editClan.ts +90 -90
- package/api/editCollection.ts +77 -77
- package/api/editProfile.ts +123 -123
- package/api/getAvatarUploadUrl.ts +85 -85
- package/api/getBadgedUsers.ts +56 -56
- package/api/getBeatmapComments.ts +57 -57
- package/api/getBeatmapPage.ts +106 -106
- package/api/getBeatmapPageById.ts +99 -99
- package/api/getBeatmapStarRating.ts +53 -53
- package/api/getBeatmaps.ts +159 -159
- package/api/getClan.ts +77 -77
- package/api/getCollection.ts +130 -130
- package/api/getCollections.ts +126 -128
- package/api/getLeaderboard.ts +136 -136
- package/api/getMapUploadUrl.ts +93 -93
- package/api/getPassToken.ts +55 -55
- package/api/getProfile.ts +146 -146
- package/api/getPublicStats.ts +180 -180
- package/api/getRawStarRating.ts +57 -57
- package/api/getScore.ts +85 -85
- package/api/getTimestamp.ts +23 -23
- package/api/getUserScores.ts +175 -175
- package/api/nominateMap.ts +82 -82
- package/api/postBeatmapComment.ts +59 -59
- package/api/rankMapsArchive.ts +64 -64
- package/api/searchUsers.ts +56 -56
- package/api/setPasskey.ts +59 -59
- package/api/submitScore.ts +433 -433
- package/api/updateBeatmapPage.ts +229 -229
- package/handleApi.ts +22 -20
- package/index.html +2 -2
- package/index.ts +864 -865
- package/package-lock.json +8913 -0
- package/package.json +2 -2
- package/types/database.ts +0 -39
- package/utils/getUserBySession.ts +48 -48
- package/utils/requestUtils.ts +87 -87
- package/utils/security.ts +20 -20
- package/utils/star-calc/index.ts +72 -72
- package/utils/star-calc/osuUtils.ts +53 -53
- package/utils/star-calc/sspmParser.ts +398 -398
- package/utils/star-calc/sspmv1Parser.ts +165 -165
- package/utils/supabase.ts +13 -13
- package/utils/test +4 -4
- package/utils/validateToken.ts +7 -7
- package/vercel.json +12 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rhythia-api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "184.0.0",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"author": "online-contributors",
|
|
6
6
|
"scripts": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"osu-standard-stable": "^5.0.0",
|
|
42
42
|
"sharp": "^0.33.5",
|
|
43
43
|
"simple-git": "^3.25.0",
|
|
44
|
-
"supabase": "^2.
|
|
44
|
+
"supabase": "^2.1.1",
|
|
45
45
|
"tsx": "^4.17.0",
|
|
46
46
|
"utf-8-validate": "^6.0.4",
|
|
47
47
|
"validator": "^13.12.0",
|
package/types/database.ts
CHANGED
|
@@ -256,21 +256,18 @@ export type Database = {
|
|
|
256
256
|
collection: number
|
|
257
257
|
created_at: string
|
|
258
258
|
id: number
|
|
259
|
-
sort: number
|
|
260
259
|
}
|
|
261
260
|
Insert: {
|
|
262
261
|
beatmapPage?: number | null
|
|
263
262
|
collection: number
|
|
264
263
|
created_at?: string
|
|
265
264
|
id?: number
|
|
266
|
-
sort?: number
|
|
267
265
|
}
|
|
268
266
|
Update: {
|
|
269
267
|
beatmapPage?: number | null
|
|
270
268
|
collection?: number
|
|
271
269
|
created_at?: string
|
|
272
270
|
id?: number
|
|
273
|
-
sort?: number
|
|
274
271
|
}
|
|
275
272
|
Relationships: [
|
|
276
273
|
{
|
|
@@ -579,42 +576,6 @@ export type Database = {
|
|
|
579
576
|
total_pages: number
|
|
580
577
|
}[]
|
|
581
578
|
}
|
|
582
|
-
get_collections_v3: {
|
|
583
|
-
Args: {
|
|
584
|
-
page_number?: number
|
|
585
|
-
items_per_page?: number
|
|
586
|
-
owner_filter?: number
|
|
587
|
-
}
|
|
588
|
-
Returns: {
|
|
589
|
-
id: number
|
|
590
|
-
title: string
|
|
591
|
-
description: string
|
|
592
|
-
created_at: string
|
|
593
|
-
owner: number
|
|
594
|
-
owner_username: string
|
|
595
|
-
owner_avatar_url: string
|
|
596
|
-
beatmap_count: number
|
|
597
|
-
star1: number
|
|
598
|
-
star2: number
|
|
599
|
-
star3: number
|
|
600
|
-
star4: number
|
|
601
|
-
star5: number
|
|
602
|
-
star6: number
|
|
603
|
-
star7: number
|
|
604
|
-
star8: number
|
|
605
|
-
star9: number
|
|
606
|
-
star10: number
|
|
607
|
-
star11: number
|
|
608
|
-
star12: number
|
|
609
|
-
star13: number
|
|
610
|
-
star14: number
|
|
611
|
-
star15: number
|
|
612
|
-
star16: number
|
|
613
|
-
star17: number
|
|
614
|
-
star18: number
|
|
615
|
-
total_pages: number
|
|
616
|
-
}[]
|
|
617
|
-
}
|
|
618
579
|
}
|
|
619
580
|
Enums: {
|
|
620
581
|
banTypes: "cool" | "silenced" | "restricted" | "excluded"
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import { User } from "@supabase/supabase-js";
|
|
2
|
-
import { decryptString } from "./security";
|
|
3
|
-
import { supabase } from "./supabase";
|
|
4
|
-
|
|
5
|
-
export async function getUserBySession(session: string): Promise<User | null> {
|
|
6
|
-
const user = (await supabase.auth.getUser(session)).data.user;
|
|
7
|
-
|
|
8
|
-
if (user) {
|
|
9
|
-
return user;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
console.log("trying legacy token");
|
|
14
|
-
const decryptedToken = JSON.parse(decryptString(session)) as {
|
|
15
|
-
userId: number;
|
|
16
|
-
email: string;
|
|
17
|
-
passKey: string;
|
|
18
|
-
computerName: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
for (const key of Object.keys(decryptedToken)) {
|
|
22
|
-
if (decryptedToken[key] === undefined || decryptedToken[key] === null) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
console.log(decryptedToken);
|
|
28
|
-
|
|
29
|
-
let { data: queryPasskey, error } = await supabase
|
|
30
|
-
.from("passkeys")
|
|
31
|
-
.select("*,profiles(uid)")
|
|
32
|
-
.eq("id", decryptedToken.userId)
|
|
33
|
-
.eq("email", decryptedToken.email)
|
|
34
|
-
.eq("passkey", decryptedToken.passKey)
|
|
35
|
-
.single();
|
|
36
|
-
|
|
37
|
-
console.log(queryPasskey);
|
|
38
|
-
|
|
39
|
-
if (!queryPasskey) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return (await supabase.auth.admin.getUserById(queryPasskey.profiles?.uid!))
|
|
44
|
-
.data.user;
|
|
45
|
-
} catch (error) {}
|
|
46
|
-
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
1
|
+
import { User } from "@supabase/supabase-js";
|
|
2
|
+
import { decryptString } from "./security";
|
|
3
|
+
import { supabase } from "./supabase";
|
|
4
|
+
|
|
5
|
+
export async function getUserBySession(session: string): Promise<User | null> {
|
|
6
|
+
const user = (await supabase.auth.getUser(session)).data.user;
|
|
7
|
+
|
|
8
|
+
if (user) {
|
|
9
|
+
return user;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
console.log("trying legacy token");
|
|
14
|
+
const decryptedToken = JSON.parse(decryptString(session)) as {
|
|
15
|
+
userId: number;
|
|
16
|
+
email: string;
|
|
17
|
+
passKey: string;
|
|
18
|
+
computerName: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (const key of Object.keys(decryptedToken)) {
|
|
22
|
+
if (decryptedToken[key] === undefined || decryptedToken[key] === null) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(decryptedToken);
|
|
28
|
+
|
|
29
|
+
let { data: queryPasskey, error } = await supabase
|
|
30
|
+
.from("passkeys")
|
|
31
|
+
.select("*,profiles(uid)")
|
|
32
|
+
.eq("id", decryptedToken.userId)
|
|
33
|
+
.eq("email", decryptedToken.email)
|
|
34
|
+
.eq("passkey", decryptedToken.passKey)
|
|
35
|
+
.single();
|
|
36
|
+
|
|
37
|
+
console.log(queryPasskey);
|
|
38
|
+
|
|
39
|
+
if (!queryPasskey) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (await supabase.auth.admin.getUserById(queryPasskey.profiles?.uid!))
|
|
44
|
+
.data.user;
|
|
45
|
+
} catch (error) {}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
package/utils/requestUtils.ts
CHANGED
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import { set, ZodObject } from "zod";
|
|
3
|
-
import { getUserBySession } from "./getUserBySession";
|
|
4
|
-
import { supabase } from "./supabase";
|
|
5
|
-
|
|
6
|
-
interface Props<
|
|
7
|
-
K = (...args: any[]) => Promise<NextResponse<any>>,
|
|
8
|
-
T = ZodObject<any>
|
|
9
|
-
> {
|
|
10
|
-
request: Request;
|
|
11
|
-
schema: { input: T; output: T };
|
|
12
|
-
authorization?: Function;
|
|
13
|
-
activity: K;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function protectedApi({
|
|
17
|
-
request,
|
|
18
|
-
schema,
|
|
19
|
-
authorization,
|
|
20
|
-
activity,
|
|
21
|
-
}: Props) {
|
|
22
|
-
try {
|
|
23
|
-
const toParse = await request.json();
|
|
24
|
-
const data = schema.input.parse(toParse);
|
|
25
|
-
|
|
26
|
-
const dataClone = structuredClone(data);
|
|
27
|
-
if (dataClone) {
|
|
28
|
-
if (dataClone["token"]) {
|
|
29
|
-
dataClone["token"] = "********";
|
|
30
|
-
}
|
|
31
|
-
Object.keys(dataClone).forEach((key) => {
|
|
32
|
-
console.log("KEY: ", key, dataClone[key]);
|
|
33
|
-
if (key == "data") {
|
|
34
|
-
try {
|
|
35
|
-
Object.keys(dataClone[key]).forEach((key2) => {
|
|
36
|
-
console.log("KEY2: ", key2, dataClone[key][key2]);
|
|
37
|
-
});
|
|
38
|
-
} catch (error) {}
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
setActivity(data);
|
|
44
|
-
if (authorization) {
|
|
45
|
-
const authorizationResponse = await authorization(data);
|
|
46
|
-
if (authorizationResponse) {
|
|
47
|
-
return authorizationResponse;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return await activity(data, request);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
return NextResponse.json({ error: error.toString() }, { status: 400 });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function setActivity(data: Record<string, any>) {
|
|
57
|
-
if (data.session) {
|
|
58
|
-
const user = (await supabase.auth.getUser(data.session)).data.user;
|
|
59
|
-
if (user) {
|
|
60
|
-
await supabase.from("profileActivities").upsert({
|
|
61
|
-
uid: user.id,
|
|
62
|
-
last_activity: Date.now(),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function validUser(data) {
|
|
69
|
-
if (!data.session) {
|
|
70
|
-
return NextResponse.json(
|
|
71
|
-
{
|
|
72
|
-
error: "Session is missing",
|
|
73
|
-
},
|
|
74
|
-
{ status: 501 }
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const user = await getUserBySession(data.session);
|
|
79
|
-
if (!user) {
|
|
80
|
-
return NextResponse.json(
|
|
81
|
-
{
|
|
82
|
-
error: "Invalid user session",
|
|
83
|
-
},
|
|
84
|
-
{ status: 400 }
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { set, ZodObject } from "zod";
|
|
3
|
+
import { getUserBySession } from "./getUserBySession";
|
|
4
|
+
import { supabase } from "./supabase";
|
|
5
|
+
|
|
6
|
+
interface Props<
|
|
7
|
+
K = (...args: any[]) => Promise<NextResponse<any>>,
|
|
8
|
+
T = ZodObject<any>
|
|
9
|
+
> {
|
|
10
|
+
request: Request;
|
|
11
|
+
schema: { input: T; output: T };
|
|
12
|
+
authorization?: Function;
|
|
13
|
+
activity: K;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function protectedApi({
|
|
17
|
+
request,
|
|
18
|
+
schema,
|
|
19
|
+
authorization,
|
|
20
|
+
activity,
|
|
21
|
+
}: Props) {
|
|
22
|
+
try {
|
|
23
|
+
const toParse = await request.json();
|
|
24
|
+
const data = schema.input.parse(toParse);
|
|
25
|
+
|
|
26
|
+
const dataClone = structuredClone(data);
|
|
27
|
+
if (dataClone) {
|
|
28
|
+
if (dataClone["token"]) {
|
|
29
|
+
dataClone["token"] = "********";
|
|
30
|
+
}
|
|
31
|
+
Object.keys(dataClone).forEach((key) => {
|
|
32
|
+
console.log("KEY: ", key, dataClone[key]);
|
|
33
|
+
if (key == "data") {
|
|
34
|
+
try {
|
|
35
|
+
Object.keys(dataClone[key]).forEach((key2) => {
|
|
36
|
+
console.log("KEY2: ", key2, dataClone[key][key2]);
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setActivity(data);
|
|
44
|
+
if (authorization) {
|
|
45
|
+
const authorizationResponse = await authorization(data);
|
|
46
|
+
if (authorizationResponse) {
|
|
47
|
+
return authorizationResponse;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return await activity(data, request);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return NextResponse.json({ error: error.toString() }, { status: 400 });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function setActivity(data: Record<string, any>) {
|
|
57
|
+
if (data.session) {
|
|
58
|
+
const user = (await supabase.auth.getUser(data.session)).data.user;
|
|
59
|
+
if (user) {
|
|
60
|
+
await supabase.from("profileActivities").upsert({
|
|
61
|
+
uid: user.id,
|
|
62
|
+
last_activity: Date.now(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function validUser(data) {
|
|
69
|
+
if (!data.session) {
|
|
70
|
+
return NextResponse.json(
|
|
71
|
+
{
|
|
72
|
+
error: "Session is missing",
|
|
73
|
+
},
|
|
74
|
+
{ status: 501 }
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const user = await getUserBySession(data.session);
|
|
79
|
+
if (!user) {
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{
|
|
82
|
+
error: "Invalid user session",
|
|
83
|
+
},
|
|
84
|
+
{ status: 400 }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
package/utils/security.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
2
|
-
import { managedNonce } from "@noble/ciphers/webcrypto";
|
|
3
|
-
import {
|
|
4
|
-
bytesToHex,
|
|
5
|
-
bytesToUtf8,
|
|
6
|
-
hexToBytes,
|
|
7
|
-
utf8ToBytes,
|
|
8
|
-
} from "@noble/ciphers/utils";
|
|
9
|
-
const key = hexToBytes(process.env.TOKEN_SECRET || "");
|
|
10
|
-
const chacha = managedNonce(xchacha20poly1305)(key); // manages nonces for you
|
|
11
|
-
|
|
12
|
-
export function encryptString(str: string) {
|
|
13
|
-
const data = utf8ToBytes(str);
|
|
14
|
-
return bytesToHex(chacha.encrypt(data));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function decryptString(str: string) {
|
|
18
|
-
const data = hexToBytes(str);
|
|
19
|
-
return bytesToUtf8(chacha.decrypt(data));
|
|
20
|
-
}
|
|
1
|
+
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
2
|
+
import { managedNonce } from "@noble/ciphers/webcrypto";
|
|
3
|
+
import {
|
|
4
|
+
bytesToHex,
|
|
5
|
+
bytesToUtf8,
|
|
6
|
+
hexToBytes,
|
|
7
|
+
utf8ToBytes,
|
|
8
|
+
} from "@noble/ciphers/utils";
|
|
9
|
+
const key = hexToBytes(process.env.TOKEN_SECRET || "");
|
|
10
|
+
const chacha = managedNonce(xchacha20poly1305)(key); // manages nonces for you
|
|
11
|
+
|
|
12
|
+
export function encryptString(str: string) {
|
|
13
|
+
const data = utf8ToBytes(str);
|
|
14
|
+
return bytesToHex(chacha.encrypt(data));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function decryptString(str: string) {
|
|
18
|
+
const data = hexToBytes(str);
|
|
19
|
+
return bytesToUtf8(chacha.decrypt(data));
|
|
20
|
+
}
|
package/utils/star-calc/index.ts
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
import { BeatmapDecoder, BeatmapEncoder } from "osu-parsers";
|
|
2
|
-
import { StandardRuleset } from "osu-standard-stable";
|
|
3
|
-
import { sampleMap } from "./osuUtils";
|
|
4
|
-
import { SSPMParsedMap } from "./sspmParser";
|
|
5
|
-
import { SSPMMap, V1SSPMParser } from "./sspmv1Parser";
|
|
6
|
-
|
|
7
|
-
export function rateMap(map: SSPMParsedMap) {
|
|
8
|
-
let notes = map.markers
|
|
9
|
-
.filter((marker) => marker.type === 0)
|
|
10
|
-
.map((marker) => ({
|
|
11
|
-
time: marker.position,
|
|
12
|
-
x: marker.data["field0"].x,
|
|
13
|
-
y: marker.data["field0"].y,
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
return rate(notes);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function rateMapNotes(map: [number, number, number][]) {
|
|
20
|
-
let notes = map.map((marker) => ({
|
|
21
|
-
time: marker[2],
|
|
22
|
-
x: marker[0],
|
|
23
|
-
y: marker[1],
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
return rate(notes);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function rateMapOld(map: SSPMMap) {
|
|
30
|
-
let notes = map.notes.map((marker) => ({
|
|
31
|
-
time: marker.position,
|
|
32
|
-
x: marker.x,
|
|
33
|
-
y: marker.y,
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
|
-
return rate(notes);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function rate(
|
|
40
|
-
notes: {
|
|
41
|
-
time: number;
|
|
42
|
-
x: number;
|
|
43
|
-
y: number;
|
|
44
|
-
}[]
|
|
45
|
-
) {
|
|
46
|
-
notes = notes.sort((a, b) => a.time - b.time);
|
|
47
|
-
const decoder = new BeatmapDecoder();
|
|
48
|
-
const beatmap1 = decoder.decodeFromString(sampleMap);
|
|
49
|
-
let i = 0;
|
|
50
|
-
while (i < notes.length - 1) {
|
|
51
|
-
const note = notes[i];
|
|
52
|
-
const nextNote = notes[i + 1];
|
|
53
|
-
if (distance(note.x, note.y, nextNote.x, nextNote.y) < 1.25) {
|
|
54
|
-
notes.splice(i + 1, 1);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const hittable = beatmap1.hitObjects[0].clone();
|
|
59
|
-
hittable.startX = Math.round((note.x / 2) * 100);
|
|
60
|
-
hittable.startY = Math.round((note.y / 2) * 100);
|
|
61
|
-
hittable.startTime = note.time;
|
|
62
|
-
beatmap1.hitObjects.push(hittable);
|
|
63
|
-
i++;
|
|
64
|
-
}
|
|
65
|
-
const ruleset = new StandardRuleset();
|
|
66
|
-
const mods = ruleset.createModCombination("RX");
|
|
67
|
-
const difficultyCalculator = ruleset.createDifficultyCalculator(beatmap1);
|
|
68
|
-
const difficultyAttributes = difficultyCalculator.calculateWithMods(mods);
|
|
69
|
-
return difficultyAttributes.starRating;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const distance = (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1);
|
|
1
|
+
import { BeatmapDecoder, BeatmapEncoder } from "osu-parsers";
|
|
2
|
+
import { StandardRuleset } from "osu-standard-stable";
|
|
3
|
+
import { sampleMap } from "./osuUtils";
|
|
4
|
+
import { SSPMParsedMap } from "./sspmParser";
|
|
5
|
+
import { SSPMMap, V1SSPMParser } from "./sspmv1Parser";
|
|
6
|
+
|
|
7
|
+
export function rateMap(map: SSPMParsedMap) {
|
|
8
|
+
let notes = map.markers
|
|
9
|
+
.filter((marker) => marker.type === 0)
|
|
10
|
+
.map((marker) => ({
|
|
11
|
+
time: marker.position,
|
|
12
|
+
x: marker.data["field0"].x,
|
|
13
|
+
y: marker.data["field0"].y,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
return rate(notes);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function rateMapNotes(map: [number, number, number][]) {
|
|
20
|
+
let notes = map.map((marker) => ({
|
|
21
|
+
time: marker[2],
|
|
22
|
+
x: marker[0],
|
|
23
|
+
y: marker[1],
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
return rate(notes);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function rateMapOld(map: SSPMMap) {
|
|
30
|
+
let notes = map.notes.map((marker) => ({
|
|
31
|
+
time: marker.position,
|
|
32
|
+
x: marker.x,
|
|
33
|
+
y: marker.y,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
return rate(notes);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function rate(
|
|
40
|
+
notes: {
|
|
41
|
+
time: number;
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
}[]
|
|
45
|
+
) {
|
|
46
|
+
notes = notes.sort((a, b) => a.time - b.time);
|
|
47
|
+
const decoder = new BeatmapDecoder();
|
|
48
|
+
const beatmap1 = decoder.decodeFromString(sampleMap);
|
|
49
|
+
let i = 0;
|
|
50
|
+
while (i < notes.length - 1) {
|
|
51
|
+
const note = notes[i];
|
|
52
|
+
const nextNote = notes[i + 1];
|
|
53
|
+
if (distance(note.x, note.y, nextNote.x, nextNote.y) < 1.25) {
|
|
54
|
+
notes.splice(i + 1, 1);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hittable = beatmap1.hitObjects[0].clone();
|
|
59
|
+
hittable.startX = Math.round((note.x / 2) * 100);
|
|
60
|
+
hittable.startY = Math.round((note.y / 2) * 100);
|
|
61
|
+
hittable.startTime = note.time;
|
|
62
|
+
beatmap1.hitObjects.push(hittable);
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
const ruleset = new StandardRuleset();
|
|
66
|
+
const mods = ruleset.createModCombination("RX");
|
|
67
|
+
const difficultyCalculator = ruleset.createDifficultyCalculator(beatmap1);
|
|
68
|
+
const difficultyAttributes = difficultyCalculator.calculateWithMods(mods);
|
|
69
|
+
return difficultyAttributes.starRating;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const distance = (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1);
|
|
@@ -1,53 +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
|
-
`;
|
|
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
|
+
`;
|