mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-28 01:48:11 +03:00
feat: probably fix jubeat 1dp stuff
This commit is contained in:
Executable
+14
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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> = {};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user