mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-04 12:58:11 +03:00
feat: rival ranking on server
This commit is contained in:
@@ -1256,6 +1256,12 @@ export interface PBScoreDocument<I extends IDStrings = IDStrings> extends MongoD
|
||||
rankingData: {
|
||||
rank: integer;
|
||||
outOf: integer;
|
||||
|
||||
// out of their rivals, what is their position on this chart?
|
||||
// note that we don't need to store rivalOutOf, as it's pretty much a constant
|
||||
// that can just be read from the UGPT settings.
|
||||
// null if the user has no rivals.
|
||||
rivalRank: integer | null;
|
||||
};
|
||||
userID: integer;
|
||||
chartID: string;
|
||||
|
||||
@@ -48,6 +48,36 @@ export async function GetRivalUsers(userID: integer, game: Game, playtype: Playt
|
||||
return rivals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve *all* rival IDs for people on this game. Used to recalculate rival movements on charts,
|
||||
* since that is stored and cached.
|
||||
*/
|
||||
export async function GetEveryonesRivalIDs(
|
||||
game: Game,
|
||||
playtype: Playtype
|
||||
): Promise<Record<number, Array<number>>> {
|
||||
const allGameSettings = await db["game-settings"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
userID: 1,
|
||||
rivals: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const lookupTable: Record<integer, Array<integer>> = {};
|
||||
|
||||
for (const d of allGameSettings) {
|
||||
lookupTable[d.userID] = d.rivals;
|
||||
}
|
||||
|
||||
return lookupTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an array of userIDs to be this user's rivals. Performs validation on all of the
|
||||
* rivals being players of the game, and not being duplicates. The maximum amount of rivals
|
||||
|
||||
@@ -12,3 +12,4 @@ export type DryScore<I extends IDStrings = IDStrings> = Pick<
|
||||
};
|
||||
|
||||
export type ScorePlaytypeMap = Partial<Record<Playtype, Array<ScoreDocument>>>;
|
||||
export type ChartIDPlaytypeMap = Partial<Record<Playtype, Set<string>>>;
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
USCMergeFn,
|
||||
} from "./game-specific-merge";
|
||||
import db from "external/mongo/db";
|
||||
import { GetEveryonesRivalIDs, GetRivalIDs, GetRivalUsers } from "lib/rivals/rivals";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { BulkWriteUpdateOneOperation } from "mongodb";
|
||||
import type { Game, integer, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import type { Game, integer, PBScoreDocument, Playtype, ScoreDocument } from "tachi-common";
|
||||
|
||||
export type PBScoreDocumentNoRank = Omit<PBScoreDocument, "rankingData">;
|
||||
|
||||
@@ -64,9 +65,9 @@ export async function CreatePBDoc(userID: integer, chartID: string, logger: KtLo
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates user's rankings on a given chart.
|
||||
* Updates rankings on a given chart.
|
||||
*/
|
||||
export async function UpdateChartRanking(chartID: string) {
|
||||
export async function UpdateChartRanking(game: Game, playtype: Playtype, chartID: string) {
|
||||
const scores = await db["personal-bests"].find(
|
||||
{ chartID },
|
||||
{
|
||||
@@ -77,13 +78,26 @@ export async function UpdateChartRanking(chartID: string) {
|
||||
}
|
||||
);
|
||||
|
||||
const allRivals = await GetEveryonesRivalIDs(game, playtype);
|
||||
|
||||
const bwrite: Array<BulkWriteUpdateOneOperation<PBScoreDocument>> = [];
|
||||
|
||||
let rank = 0;
|
||||
|
||||
// what users have we saw so far? used for rivalRanking calculations
|
||||
const seenUserIDs: Array<integer> = [];
|
||||
|
||||
for (const score of scores) {
|
||||
rank++;
|
||||
|
||||
const thisUsersRivals = allRivals[score.userID];
|
||||
|
||||
let rivalRank: integer | null = null;
|
||||
|
||||
if (thisUsersRivals) {
|
||||
rivalRank = thisUsersRivals.filter((e) => seenUserIDs.includes(e)).length + 1;
|
||||
}
|
||||
|
||||
bwrite.push({
|
||||
updateOne: {
|
||||
filter: { chartID: score.chartID, userID: score.userID },
|
||||
@@ -92,6 +106,7 @@ export async function UpdateChartRanking(chartID: string) {
|
||||
rankingData: {
|
||||
rank,
|
||||
outOf: scores.length,
|
||||
rivalRank,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ t.test("#ProcessPBs", (t) => {
|
||||
await db["personal-bests"].remove({});
|
||||
|
||||
// scores on 511 SPA are pre-loaded into the database
|
||||
await ProcessPBs(1, new Set([Testing511SPA.chartID]), logger);
|
||||
await ProcessPBs("iidx", "SP", 1, new Set([Testing511SPA.chartID]), logger);
|
||||
|
||||
const pbs = await db["personal-bests"].find({});
|
||||
|
||||
@@ -48,7 +48,13 @@ t.test("#ProcessPBs", (t) => {
|
||||
}),
|
||||
]);
|
||||
|
||||
await ProcessPBs(1, new Set([Testing511SPA.chartID, "test1", "test2", "test3"]), logger);
|
||||
await ProcessPBs(
|
||||
"iidx",
|
||||
"SP",
|
||||
1,
|
||||
new Set([Testing511SPA.chartID, "test1", "test2", "test3"]),
|
||||
logger
|
||||
);
|
||||
|
||||
const pbs = await db["personal-bests"].find({});
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@ import { CreatePBDoc, UpdateChartRanking } from "./create-pb-doc";
|
||||
import db from "external/mongo/db";
|
||||
import type { PBScoreDocumentNoRank } from "./create-pb-doc";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { integer } from "tachi-common";
|
||||
import type { Game, integer, Playtype } from "tachi-common";
|
||||
|
||||
/**
|
||||
* Process, recalculate and update a users PBs for this set of chartIDs.
|
||||
*/
|
||||
export async function ProcessPBs(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
chartIDs: Set<string>,
|
||||
logger: KtLogger
|
||||
@@ -58,7 +60,7 @@ export async function ProcessPBs(
|
||||
|
||||
// now that everything has been updated or inserted, we can refresh
|
||||
// the chart rankings.
|
||||
await Promise.all(pbDocs.map((e) => UpdateChartRanking(e.chartID)));
|
||||
await Promise.all(pbDocs.map((e) => UpdateChartRanking(game, playtype, e.chartID)));
|
||||
|
||||
// and we're done!
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { GetGameConfig } from "tachi-common";
|
||||
import { GetMillisecondsSince } from "utils/misc";
|
||||
import { GetUserWithID } from "utils/user";
|
||||
import type { ConverterFunction, ImportInputParser } from "../../import-types/common/types";
|
||||
import type { ScorePlaytypeMap } from "../common/types";
|
||||
import type { ChartIDPlaytypeMap, ScorePlaytypeMap } from "../common/types";
|
||||
import type { ClassHandler } from "../user-game-stats/types";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { ScoreImportJob } from "lib/score-import/worker/types";
|
||||
@@ -292,21 +292,33 @@ export async function HandlePostImportSteps(
|
||||
|
||||
void SetJobProgress(job, "Processing scores and updating PBs.");
|
||||
|
||||
const playtypes = Object.keys(scorePlaytypeMap) as Array<Playtype>;
|
||||
|
||||
// --- 5. PersonalBests ---
|
||||
// We want to keep an updated reference of a users best score on a given chart.
|
||||
// This function also handles conjoining different scores together (such as unioning best lamp and
|
||||
// best score).
|
||||
const pbTimeStart = process.hrtime.bigint();
|
||||
|
||||
await ProcessPBs(user.id, chartIDs, logger);
|
||||
// processing PBs is a playtype-specific action. As such, we need to split chartIDs
|
||||
// accordingly
|
||||
const chartIDsSeparatedByPlaytype: ChartIDPlaytypeMap = {};
|
||||
|
||||
for (const [playtype, scores] of Object.entries(scorePlaytypeMap)) {
|
||||
chartIDsSeparatedByPlaytype[playtype as Playtype] = new Set(scores.map((e) => e.chartID));
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(chartIDsSeparatedByPlaytype).map(([playtype, chartIDs]) =>
|
||||
ProcessPBs(game, playtype as Playtype, user.id, chartIDs, logger)
|
||||
)
|
||||
);
|
||||
|
||||
const pbTime = GetMillisecondsSince(pbTimeStart);
|
||||
const pbTimeRel = pbTime / chartIDs.size;
|
||||
|
||||
logger.debug(`PB Processing took ${pbTime} milliseconds (${pbTimeRel}ms/doc)`);
|
||||
|
||||
const playtypes = Object.keys(scorePlaytypeMap) as Array<Playtype>;
|
||||
|
||||
void SetJobProgress(job, "Updating profile statistics.");
|
||||
|
||||
// --- 6. Game Stats ---
|
||||
@@ -413,7 +425,8 @@ function ParseImportInfo(importInfo: Array<ImportProcessingInfo>) {
|
||||
chartIDs.add(info.content.score.chartID);
|
||||
|
||||
if (scorePlaytypeMap[info.content.score.playtype]) {
|
||||
scorePlaytypeMap[info.content.score.playtype]!.push(info.content.score);
|
||||
// @ts-expect-error obviously delusional typescript moment
|
||||
scorePlaytypeMap[info.content.score.playtype].push(info.content.score);
|
||||
} else {
|
||||
scorePlaytypeMap[info.content.score.playtype] = [info.content.score];
|
||||
}
|
||||
|
||||
@@ -84,14 +84,20 @@ export async function DeleteScore(
|
||||
});
|
||||
|
||||
if (userHasOtherScores && attemptPBReprocess) {
|
||||
await ProcessPBs(score.userID, new Set([score.chartID]), logger);
|
||||
await ProcessPBs(
|
||||
score.game,
|
||||
score.playtype,
|
||||
score.userID,
|
||||
new Set([score.chartID]),
|
||||
logger
|
||||
);
|
||||
} else {
|
||||
await db["personal-bests"].remove({
|
||||
userID: score.userID,
|
||||
chartID: score.chartID,
|
||||
});
|
||||
|
||||
await UpdateChartRanking(score.chartID);
|
||||
await UpdateChartRanking(score.game, score.playtype, score.chartID);
|
||||
}
|
||||
|
||||
await UpdateUsersGamePlaytypeStats(score.game, score.playtype, score.userID, null, logger);
|
||||
@@ -186,14 +192,20 @@ export async function DeleteMultipleScores(scores: Array<ScoreDocument>, blackli
|
||||
});
|
||||
|
||||
if (userHasOtherScores) {
|
||||
await ProcessPBs(score.userID, new Set([score.chartID]), logger);
|
||||
await ProcessPBs(
|
||||
score.game,
|
||||
score.playtype,
|
||||
score.userID,
|
||||
new Set([score.chartID]),
|
||||
logger
|
||||
);
|
||||
} else {
|
||||
await db["personal-bests"].remove({
|
||||
userID: score.userID,
|
||||
chartID: score.chartID,
|
||||
});
|
||||
|
||||
await UpdateChartRanking(score.chartID);
|
||||
await UpdateChartRanking(score.game, score.playtype, score.chartID);
|
||||
}
|
||||
|
||||
if (blacklist) {
|
||||
|
||||
@@ -64,6 +64,7 @@ export default async function UpdateScore(
|
||||
|
||||
logger.verbose("Received Update Score request.");
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
newScore.calculatedData = await CreateCalculatedData(
|
||||
newScore,
|
||||
chart,
|
||||
@@ -148,13 +149,13 @@ export default async function UpdateScore(
|
||||
chartID: newScore.chartID,
|
||||
});
|
||||
|
||||
await UpdateChartRanking(newScore.chartID);
|
||||
await UpdateChartRanking(newScore.game, newScore.playtype, newScore.chartID);
|
||||
|
||||
if (updateOldChart) {
|
||||
await UpdateAllPBs([userID], {
|
||||
chartID: oldScore.chartID,
|
||||
});
|
||||
await UpdateChartRanking(oldScore.chartID);
|
||||
await UpdateChartRanking(oldScore.game, oldScore.playtype, oldScore.chartID);
|
||||
}
|
||||
|
||||
const imports = await db.imports.find({
|
||||
|
||||
@@ -132,6 +132,7 @@ const USC_SCORE_PB: PBScoreDocument = {
|
||||
rankingData: {
|
||||
rank: 1,
|
||||
outOf: 2,
|
||||
rivalRank: null,
|
||||
},
|
||||
songID: 1,
|
||||
userID: 1,
|
||||
|
||||
@@ -26,6 +26,7 @@ const mockScorePB: PBScoreDocument<"usc:Controller" | "usc:Keyboard"> = {
|
||||
rankingData: {
|
||||
outOf: 2,
|
||||
rank: 1,
|
||||
rivalRank: null,
|
||||
},
|
||||
scoreData: {
|
||||
esd: null,
|
||||
|
||||
@@ -95,6 +95,7 @@ export const TestingIIDXSPScorePB: PBScoreDocument<"iidx:SP"> = {
|
||||
rankingData: {
|
||||
rank: 1,
|
||||
outOf: 2,
|
||||
rivalRank: null,
|
||||
},
|
||||
songID: 1,
|
||||
game: "iidx",
|
||||
|
||||
@@ -7,6 +7,8 @@ import { GetAndUpdateUsersGoals } from "lib/score-import/framework/goals/goals";
|
||||
import { UpdateUsersMilestones } from "lib/score-import/framework/milestones/milestones";
|
||||
import { ProcessPBs } from "lib/score-import/framework/pb/process-pbs";
|
||||
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/user-game-stats/update-ugs";
|
||||
import { TachiConfig } from "lib/setup/config";
|
||||
import { GetGameConfig } from "tachi-common";
|
||||
import { EfficientDBIterate } from "utils/efficient-db-iterate";
|
||||
import { FormatUserDoc } from "utils/user";
|
||||
import type { Game, integer, Playtype, PublicUserDocument } from "tachi-common";
|
||||
@@ -95,12 +97,27 @@ export async function UpdateAllPBs(userIDs?: Array<integer>, filter = {}) {
|
||||
for (const user of allUsers) {
|
||||
logger.verbose(`Finding ${FormatUserDoc(user)}'s scores.`);
|
||||
|
||||
const scores = await db.scores.find(deepmerge({ userID: user.id }, filter), {
|
||||
projection: { chartID: 1 },
|
||||
});
|
||||
for (const game of TachiConfig.GAMES) {
|
||||
const gameConfig = GetGameConfig(game);
|
||||
|
||||
logger.verbose(`PBing ${FormatUserDoc(user)}'s scores.`);
|
||||
await ProcessPBs(user.id, new Set(scores.map((e) => e.chartID)), logger);
|
||||
for (const playtype of gameConfig.validPlaytypes) {
|
||||
const scores = await db.scores.find(
|
||||
deepmerge({ userID: user.id, game, playtype }, filter),
|
||||
{
|
||||
projection: { chartID: 1 },
|
||||
}
|
||||
);
|
||||
|
||||
logger.verbose(`PBing ${FormatUserDoc(user)}'s scores.`);
|
||||
await ProcessPBs(
|
||||
game,
|
||||
playtype,
|
||||
user.id,
|
||||
new Set(scores.map((e) => e.chartID)),
|
||||
logger
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.verbose(`Done!`);
|
||||
|
||||
Reference in New Issue
Block a user