feat: rival ranking on server

This commit is contained in:
zkldi
2022-10-14 23:53:35 +01:00
parent 5379dd491e
commit 2c3af78b7c
13 changed files with 129 additions and 23 deletions
+6
View File
@@ -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;
+30
View File
@@ -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];
}
+16 -4
View File
@@ -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,
+1
View File
@@ -95,6 +95,7 @@ export const TestingIIDXSPScorePB: PBScoreDocument<"iidx:SP"> = {
rankingData: {
rank: 1,
outOf: 2,
rivalRank: null,
},
songID: 1,
game: "iidx",
+22 -5
View File
@@ -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!`);