feat: probably fix jubeat 1dp stuff

This commit is contained in:
zkldi
2022-12-16 08:14:41 +00:00
parent 2117cb7cfd
commit cda29fe07d
10 changed files with 212 additions and 76 deletions
+14
View File
@@ -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 <userID> <game> <playtype>"
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
@@ -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 })
/>
</Col>
)}
{settings?.preferences.developerMode && (
<Col xs={12}>
<Divider />
<Card header="Debug Content">
<DebugContent data={sessionData} />
</Card>
</Col>
)}
</Row>
);
}
+3 -1
View File
@@ -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
);
}
@@ -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;
@@ -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<I extends IDStrings = IDStrings> = 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<ScoreDocument> = {
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<PBScoreDocumentNoRank | undefined> {
// @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
@@ -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<boolean> {
// 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<boolean> {
// @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<ScoreDocument> = {
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<IDStringsWithBP>,
scorePB: ScoreDocument<IDStringsWithBP>,
lampPB: ScoreDocument<IDStringsWithBP>,
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<ScoreDocument> = {
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(
@@ -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 {
@@ -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<BatchManualScore, BatchManu
);
}
// jubeat music rate is forcibly round to 1dp. Funny.
percent = RoundToNDecimalPlaces(data.percent, 1);
// jubeat music rate is forcibly floored to 1dp. Funny.
percent = FloorToNDP(data.percent, 1);
grade = JubeatGetGrade(data.score);
} else {
+45 -15
View File
@@ -2,8 +2,10 @@
import db from "external/mongo/db";
import { rootLogger } from "lib/logger/logger";
import { CreateCalculatedData } from "lib/score-import/framework/calculated-data/calculated-data";
import { UpdateChartRanking } from "lib/score-import/framework/pb/create-pb-doc";
import { CreatePBDoc, UpdateChartRanking } from "lib/score-import/framework/pb/create-pb-doc";
import { CreateScoreID } from "lib/score-import/framework/score-importing/score-id";
import { CreateSessionCalcData } from "lib/score-import/framework/sessions/calculated-data";
import { ProcessScoreIntoSessionScoreInfo } from "lib/score-import/framework/sessions/sessions";
import { UpdateAllPBs } from "utils/calculations/recalc-scores";
import { FormatUserDoc, GetUserWithID } from "utils/user";
import type { KtLogger } from "lib/logger/logger";
@@ -108,37 +110,65 @@ export default async function UpdateScore(
"scoreInfo.scoreID": oldScoreID,
});
// another session already has the new score? (i.e. migrating to an already
// existing score?)
const existsElsewhere = await db.sessions.findOne({
"scoreInfo.scoreID": newScoreID,
});
logger.verbose(`Updating ${sessions.length} sessions.`);
// For every session that interacts with this score ID (there should only ever be one)
for (const session of sessions) {
const newScoreInfoArr = [];
// Go over all the scoreInfo and alter the ones that involve this scoreID.
for (const scoreInfo of session.scoreInfo) {
let newScoreInfo = scoreInfo;
// If this scoreInfo needs to be changed
if (scoreInfo.scoreID === oldScoreID) {
scoreInfo.scoreID = newScoreID;
// If there's any cached grade/lamp diffs, we need to update them.
if (!scoreInfo.isNewScore) {
const gradeDiff = oldScore.scoreData.gradeIndex - newScore.scoreData.gradeIndex;
const lampDiff = oldScore.scoreData.lampIndex - newScore.scoreData.lampIndex;
const percentDiff = oldScore.scoreData.percent - newScore.scoreData.percent;
const scoreDiff = oldScore.scoreData.score - newScore.scoreData.score;
scoreInfo.gradeDelta = scoreInfo.gradeDelta - gradeDiff;
scoreInfo.lampDelta = scoreInfo.lampDelta - lampDiff;
scoreInfo.scoreDelta = scoreInfo.scoreDelta - scoreDiff;
scoreInfo.percentDelta = scoreInfo.percentDelta - percentDiff;
if (existsElsewhere) {
// skip this, as this score already belongs to another session.
continue;
}
if (newScore.timeAchieved === null) {
// this shouldn't be in a session anymore.
continue;
}
newScoreInfo.scoreID = newScoreID;
// update scoreInfo
const pbAsOfThen = await CreatePBDoc(
newScore.userID,
newScore.chartID,
logger,
newScore.timeAchieved
);
// @ts-expect-error we don't need the ranking data props. ignore this
// err, it's nonsense.
newScoreInfo = ProcessScoreIntoSessionScoreInfo(newScore, pbAsOfThen ?? null);
}
newScoreInfoArr.push(newScoreInfo);
}
const scores = await db.scores.find({
scoreID: { $in: newScoreInfoArr.map((e) => 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 },
}
);
}
+15
View File
@@ -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<T>(obj: Partial<T>): Partial<T> {
const omittedObj: Partial<T> = {};