From cda29fe07dc8809fb2497bbd618691d4e92004e3 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Fri, 16 Dec 2022 08:14:41 +0000 Subject: [PATCH] feat: probably fix jubeat 1dp stuff --- _scripts/snap-remote-ugpt.sh | 14 ++++ .../_game/_playtype/SpecificSessionPage.tsx | 12 ++++ server/src/lib/migration/migrations.ts | 4 +- .../migrations/jubeat-musicrate-1dp.ts | 38 ++++++++++ .../framework/pb/create-pb-doc.ts | 65 ++++++++++------- .../framework/pb/game-specific-merge.ts | 72 +++++++++++-------- .../framework/sessions/sessions.ts | 2 +- .../common/batch-manual/converter.ts | 6 +- server/src/lib/score-mutation/update-score.ts | 60 ++++++++++++---- server/src/utils/misc.ts | 15 ++++ 10 files changed, 212 insertions(+), 76 deletions(-) create mode 100755 _scripts/snap-remote-ugpt.sh create mode 100644 server/src/lib/migration/migrations/jubeat-musicrate-1dp.ts diff --git a/_scripts/snap-remote-ugpt.sh b/_scripts/snap-remote-ugpt.sh new file mode 100755 index 000000000..3e7082338 --- /dev/null +++ b/_scripts/snap-remote-ugpt.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Copy a user from a remote Tachi instance and import it locally. + +if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then + echo "Usage 'snap-remote-ugpt.sh " + exit 1 +fi + +mongoexport mongodb://127.0.0.1:12345/ktchidb --jsonArray -c users -q "{ \"userID\": $1 }" | jq 'map(del(._id))' | mongoimport mongodb://127.0.0.1:27017/localdb --jsonArray -c users + +for collection in "game-stats" "scores" "sessions" "personal-bests"; do + mongoexport mongodb://127.0.0.1:12345/ktchidb --jsonArray -c "$collection" -q "{ \"userID\": $1, \"game\": \"$2\", \"playtype\": \"$3\" }" | jq 'map(del(._id))' | mongoimport mongodb://127.0.0.1:27017/localdb --jsonArray -c "$collection" +done diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage.tsx index df2a1df13..ff5705c7a 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage.tsx @@ -17,6 +17,9 @@ import { Redirect, useParams } from "react-router-dom"; import { GetGameConfig } from "tachi-common"; import { SessionReturns } from "types/api-returns"; import { UGPT } from "types/react"; +import DebugContent from "components/util/DebugContent"; +import { UserSettingsContext } from "context/UserSettingsContext"; +import Card from "components/layout/page/Card"; export default function SpecificSessionPage({ reqUser, game, playtype }: UGPT) { const { sessionID } = useParams<{ sessionID: string }>(); @@ -47,6 +50,7 @@ export default function SpecificSessionPage({ reqUser, game, playtype }: UGPT) { } function SessionPage({ data, game, playtype }: UGPT & { data: SessionReturns }) { + const { settings } = useContext(UserSettingsContext); const [sessionData, setSessionData] = useState(data); const { session, user, charts, scores, songs } = sessionData; @@ -290,6 +294,14 @@ function SessionPage({ data, game, playtype }: UGPT & { data: SessionReturns }) /> )} + {settings?.preferences.developerMode && ( + + + + + + + )} ); } diff --git a/server/src/lib/migration/migrations.ts b/server/src/lib/migration/migrations.ts index 3d1d979d0..e1e17a945 100644 --- a/server/src/lib/migration/migrations.ts +++ b/server/src/lib/migration/migrations.ts @@ -4,6 +4,7 @@ import UGPTRivalsMigration from "./migrations/add-rivals-to-ugpt"; import FixUndefinedBMSData from "./migrations/fix-undefined-bms-data"; import JoinINFCastHourCharts from "./migrations/join-inf-casthour-charts"; import JoinINFCastHourSongs from "./migrations/join-inf-casthour-songs"; +import JubeatMusicRate1DP from "./migrations/jubeat-musicrate-1dp"; import MarkFervidexAsWithoutIntent from "./migrations/mark-fervidex-as-without-intent"; import MarkKsHookAsWithoutIntent from "./migrations/mark-kshook-as-without-intent"; import NullLR2HookFailedBPs from "./migrations/null-lr2hook-failed-bps"; @@ -53,7 +54,8 @@ if (Environment.nodeEnv !== "test") { JoinINFCastHourCharts, JoinINFCastHourSongs, MarkFervidexAsWithoutIntent, - MarkKsHookAsWithoutIntent + MarkKsHookAsWithoutIntent, + JubeatMusicRate1DP ); } diff --git a/server/src/lib/migration/migrations/jubeat-musicrate-1dp.ts b/server/src/lib/migration/migrations/jubeat-musicrate-1dp.ts new file mode 100644 index 000000000..891e7f8a0 --- /dev/null +++ b/server/src/lib/migration/migrations/jubeat-musicrate-1dp.ts @@ -0,0 +1,38 @@ +import db from "external/mongo/db"; +import UpdateScore from "lib/score-mutation/update-score"; +import { EfficientDBIterate } from "utils/efficient-db-iterate"; +import { FloorToNDP } from "utils/misc"; +import type { Migration } from "utils/types"; + +const migration: Migration = { + id: "jubeat-musicrate-1dp", + up: async () => { + await EfficientDBIterate( + db.scores, + async (oldScore) => { + const newScore = { + ...oldScore, + scoreData: { + ...oldScore.scoreData, + percent: FloorToNDP(oldScore.scoreData.percent, 1), + }, + }; + + // migrate the score in the callback function. + await UpdateScore(oldScore, newScore); + }, + + // we don't need to save this back in the database, UpdateScore + // already handles that. + // eslint-disable-next-line @typescript-eslint/require-await + async () => void 0, + { game: "jubeat" }, + 1000 + ); + }, + down: () => { + throw new Error(`Cannot undo migration.`); + }, +}; + +export default migration; diff --git a/server/src/lib/score-import/framework/pb/create-pb-doc.ts b/server/src/lib/score-import/framework/pb/create-pb-doc.ts index 070941091..3c1320281 100644 --- a/server/src/lib/score-import/framework/pb/create-pb-doc.ts +++ b/server/src/lib/score-import/framework/pb/create-pb-doc.ts @@ -9,7 +9,7 @@ import { import db from "external/mongo/db"; import { GetEveryonesRivalIDs } from "lib/rivals/rivals"; import type { KtLogger } from "lib/logger/logger"; -import type { BulkWriteUpdateOneOperation } from "mongodb"; +import type { BulkWriteUpdateOneOperation, FilterQuery } from "mongodb"; import type { Game, IDStrings, @@ -24,21 +24,39 @@ export type PBScoreDocumentNoRank = Omit< "rankingData" >; -export async function CreatePBDoc(userID: integer, chartID: string, logger: KtLogger) { - const scorePB = await db.scores.findOne( - { - userID, - chartID, +/** + * Create a PB document for this user on this chart. Optionally, provide an "As Of" + * timestamp to constrain the generated PB to only one before the provided time. + */ +export async function CreatePBDoc( + userID: integer, + chartID: string, + logger: KtLogger, + asOfTimestamp?: number +) { + const query: FilterQuery = { + userID, + chartID, + }; + + if (asOfTimestamp !== undefined) { + query.timeAchieved = { $lt: asOfTimestamp }; + } + + const scorePB = await db.scores.findOne(query, { + sort: { + "scoreData.percent": -1, }, - { - sort: { - "scoreData.percent": -1, - }, - } - ); + }); if (!scorePB) { - logger.severe( + if (asOfTimestamp !== undefined) { + // if we were constraining the PB on a timestamp, this is likely to happen. + // ignore it. + return; + } + + logger.warn( `User ${userID} has no scores on chart, but a PB was attempted to be created?`, { chartID, @@ -48,23 +66,17 @@ export async function CreatePBDoc(userID: integer, chartID: string, logger: KtLo return; } - const lampPB = (await db.scores.findOne( - { - userID, - chartID, + const lampPB = (await db.scores.findOne(query, { + sort: { + "scoreData.lampIndex": -1, }, - { - sort: { - "scoreData.lampIndex": -1, - }, - } - )) as ScoreDocument; + })) as ScoreDocument; // ^ guaranteed to not be null, as this always resolves // to atleast one score (and we got ScorePB above, so we know there's // atleast one). - const pbDoc = await MergeScoreLampIntoPB(userID, scorePB, lampPB, logger); + const pbDoc = await MergeScoreLampIntoPB(userID, scorePB, lampPB, logger, asOfTimestamp); if (!pbDoc) { return; @@ -139,7 +151,8 @@ async function MergeScoreLampIntoPB( userID: integer, scorePB: ScoreDocument, lampPB: ScoreDocument, - logger: KtLogger + logger: KtLogger, + asOfTimestamp?: number ): Promise { // @hack // since time cannot be negative, this is a rough hack @@ -187,7 +200,7 @@ async function MergeScoreLampIntoPB( if (GameSpecificMergeFn) { // @ts-expect-error Yeah, this call sucks. It correctly warns us that scorePB and lampPB // might've diverged, but we know they haven't. - const success = await GameSpecificMergeFn(pbDoc, scorePB, lampPB, logger); + const success = await GameSpecificMergeFn(pbDoc, scorePB, lampPB, logger, asOfTimestamp); // If the mergeFn returns false, this means something has gone // rather wrong. We just return undefined here, which in turn diff --git a/server/src/lib/score-import/framework/pb/game-specific-merge.ts b/server/src/lib/score-import/framework/pb/game-specific-merge.ts index b3a090767..46594360f 100644 --- a/server/src/lib/score-import/framework/pb/game-specific-merge.ts +++ b/server/src/lib/score-import/framework/pb/game-specific-merge.ts @@ -5,13 +5,15 @@ import { Volforce } from "rg-stats"; import { DeleteUndefinedProps } from "utils/misc"; import { FindChartWithChartID } from "utils/queries/charts"; import type { KtLogger } from "lib/logger/logger"; +import type { FilterQuery } from "mongodb"; import type { PBScoreDocument, ScoreDocument } from "tachi-common"; export async function IIDXMergeFn( pbDoc: PBScoreDocument<"iidx:DP" | "iidx:SP">, scorePB: ScoreDocument<"iidx:DP" | "iidx:SP">, lampPB: ScoreDocument<"iidx:DP" | "iidx:SP">, - logger: KtLogger + logger: KtLogger, + asOfTimestamp?: number ): Promise { // lampRating needs to be updated. pbDoc.calculatedData.ktLampRating = lampPB.calculatedData.ktLampRating; @@ -25,7 +27,7 @@ export async function IIDXMergeFn( DeleteUndefinedProps(pbDoc.scoreData.hitMeta); - await MergeBPPB(pbDoc, scorePB, lampPB, logger); + await MergeBPPB(pbDoc, scorePB, lampPB, logger, asOfTimestamp); return true; } @@ -45,14 +47,15 @@ export async function BMSMergeFn( pbDoc: PBScoreDocument<"bms:7K" | "bms:14K">, scorePB: ScoreDocument<"bms:7K" | "bms:14K">, lampPB: ScoreDocument<"bms:7K" | "bms:14K">, - logger: KtLogger + logger: KtLogger, + asOfTimestamp?: number ) { pbDoc.calculatedData.sieglinde = lampPB.calculatedData.sieglinde; pbDoc.scoreData.hitMeta.gaugeHistory = lampPB.scoreData.hitMeta.gaugeHistory; pbDoc.scoreData.hitMeta.gauge = lampPB.scoreData.hitMeta.gauge; - await MergeBPPB(pbDoc, scorePB, lampPB, logger); + await MergeBPPB(pbDoc, scorePB, lampPB, logger, asOfTimestamp); return true; } @@ -61,11 +64,12 @@ export async function PMSMergeFn( pbDoc: PBScoreDocument<"pms:Controller" | "pms:Keyboard">, scorePB: ScoreDocument<"pms:Controller" | "pms:Keyboard">, lampPB: ScoreDocument<"pms:Controller" | "pms:Keyboard">, - logger: KtLogger + logger: KtLogger, + asOfTimestamp?: number ) { pbDoc.calculatedData.sieglinde = lampPB.calculatedData.sieglinde; - await MergeBPPB(pbDoc, scorePB, lampPB, logger); + await MergeBPPB(pbDoc, scorePB, lampPB, logger, asOfTimestamp); return true; } @@ -108,7 +112,8 @@ export async function SDVXMergeFn( pbDoc: PBScoreDocument<"sdvx:Single">, scorePB: ScoreDocument<"sdvx:Single">, lampPB: ScoreDocument<"sdvx:Single">, - logger: KtLogger + logger: KtLogger, + asOfTimestamp?: number ): Promise { // @optimisable // This is a re-fetch, but it's difficult to pass the chart all @@ -126,18 +131,21 @@ export async function SDVXMergeFn( chart.levelNum ); + const query: FilterQuery = { + chartID: pbDoc.chartID, + "scoreData.hitMeta.exScore": { $type: "number" }, + }; + + if (asOfTimestamp !== undefined) { + query.timeAchieved = { $lt: asOfTimestamp }; + } + // find the users score with the highest exScore - const bestExScore = (await db.scores.findOne( - { - chartID: pbDoc.chartID, - "scoreData.hitMeta.exScore": { $type: "number" }, + const bestExScore = (await db.scores.findOne(query, { + sort: { + "scoreData.hitMeta.exScore": -1, }, - { - sort: { - "scoreData.hitMeta.exScore": -1, - }, - } - )) as ScoreDocument<"sdvx:Single"> | null; + })) as ScoreDocument<"sdvx:Single"> | null; if (!bestExScore) { pbDoc.scoreData.hitMeta.exScore = undefined; @@ -168,22 +176,26 @@ async function MergeBPPB( pbDoc: PBScoreDocument, scorePB: ScoreDocument, lampPB: ScoreDocument, - logger: KtLogger + logger: KtLogger, + asOfTimestamp: number | undefined ) { // bad+poor PB document. This is a weird, third indepdenent metric that IIDX players sometimes care about. - const bpPB = (await db.scores.findOne( - { - userID: scorePB.userID, - chartID: scorePB.chartID, - "scoreData.hitMeta.bp": { $exists: true }, + const query: FilterQuery = { + userID: scorePB.userID, + chartID: scorePB.chartID, + "scoreData.hitMeta.bp": { $exists: true }, + }; + + if (asOfTimestamp !== undefined) { + query.timeAchieved = { $lt: asOfTimestamp }; + } + + const bpPB = (await db.scores.findOne(query, { + sort: { + // bp 0 is the best BP, bp 1 is worse, so on + "scoreData.hitMeta.bp": 1, }, - { - sort: { - // bp 0 is the best BP, bp 1 is worse, so on - "scoreData.hitMeta.bp": 1, - }, - } - )) as ScoreDocument<"iidx:DP" | "iidx:SP"> | null; + })) as ScoreDocument<"iidx:DP" | "iidx:SP"> | null; if (!bpPB) { logger.verbose( diff --git a/server/src/lib/score-import/framework/sessions/sessions.ts b/server/src/lib/score-import/framework/sessions/sessions.ts index aa6c4f3d8..cf6d66da8 100644 --- a/server/src/lib/score-import/framework/sessions/sessions.ts +++ b/server/src/lib/score-import/framework/sessions/sessions.ts @@ -51,7 +51,7 @@ export async function CreateSessions( * Compares a score and the previous PB the user had and returns the difference * as a SessionScoreInfo object. */ -function ProcessScoreIntoSessionScoreInfo( +export function ProcessScoreIntoSessionScoreInfo( score: ScoreDocument, previousPB: PBScoreDocument | undefined ): SessionScoreInfo { diff --git a/server/src/lib/score-import/import-types/common/batch-manual/converter.ts b/server/src/lib/score-import/import-types/common/batch-manual/converter.ts index d0298a198..516a0b684 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/converter.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/converter.ts @@ -10,7 +10,7 @@ import { } from "../../../framework/common/string-asserts"; import db from "external/mongo/db"; import { GetGamePTConfig } from "tachi-common"; -import { RoundToNDecimalPlaces } from "utils/misc"; +import { FloorToNDP, RoundToNDecimalPlaces } from "utils/misc"; import { FindBMSChartOnHash, FindChartWithPTDF, @@ -89,8 +89,8 @@ export const ConverterBatchManual: ConverterFunction e.scoreID) }, + }); + + // update calculated data too. + const newCalcData = CreateSessionCalcData(session.game, session.playtype, scores); + await db.sessions.update( { sessionID: session.sessionID, }, { - $set: { scoreInfo: session.scoreInfo }, + $set: { scoreInfo: session.scoreInfo, calculatedData: newCalcData }, } ); } diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index da18ad3e8..77a6d6bb6 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -165,6 +165,21 @@ export function RoundToNDecimalPlaces(value: number, n: integer) { return Number(value.toFixed(n)); } +/** + * Floor a number to N decimal places. + * + * @example `FloorToNDP(1.594, 1) -> 1.5` + * @example `FloorToNDP(1.599, 2) -> 1.59` + * + * @param number - The number to floor. + * @param dp - The amount of decimal places to floor to. + */ +export function FloorToNDP(number: number, dp: integer) { + const mul = 10 ** dp; + + return Math.floor(number * mul) / mul; +} + export function OmitUndefinedKeys(obj: Partial): Partial { const omittedObj: Partial = {};