mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-13 09:48:27 +03:00
feat: initial pb-union code
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
import DebugContent from "components/util/DebugContent";
|
||||
import useApiQuery from "components/util/query/useApiQuery";
|
||||
import useLUGPTSettings from "components/util/useLUGPTSettings";
|
||||
import React from "react";
|
||||
import { PublicUserDocument } from "tachi-common";
|
||||
import { GamePT } from "types/react";
|
||||
|
||||
export default function RivalCompareTop100Page({
|
||||
reqUser,
|
||||
game,
|
||||
playtype,
|
||||
}: GamePT & {
|
||||
reqUser: PublicUserDocument;
|
||||
}) {
|
||||
const { settings } = useLUGPTSettings();
|
||||
|
||||
if (!settings) {
|
||||
return <div>You have no settings. How did you get here?</div>;
|
||||
}
|
||||
|
||||
const { data, error } = useApiQuery(
|
||||
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=nythil`
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DebugContent data={data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+4
-1
@@ -9,6 +9,7 @@ import React from "react";
|
||||
import { Col, Row } from "react-bootstrap";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { FormatGame, Game, GetGameConfig, Playtype, PublicUserDocument } from "tachi-common";
|
||||
import RivalCompareTop100Page from "./RivalCompareTop100Page";
|
||||
|
||||
export default function RivalsComparePage({
|
||||
reqUser,
|
||||
@@ -73,7 +74,9 @@ export default function RivalsComparePage({
|
||||
<Route
|
||||
exact
|
||||
path="/dashboard/users/:userID/games/:game/:playtype/rivals/compare"
|
||||
></Route>
|
||||
>
|
||||
<RivalCompareTop100Page reqUser={reqUser} game={game} playtype={playtype} />
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
exact
|
||||
|
||||
@@ -2,14 +2,15 @@ import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { GetRivalUsers } from "lib/rivals/rivals";
|
||||
import { SearchSpecificGameSongsAndCharts } from "lib/search/search";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
import { AggressiveRateLimitMiddleware } from "server/middleware/rate-limiter";
|
||||
import { GetGamePTConfig } from "tachi-common";
|
||||
import { FormatGame, GetGamePTConfig } from "tachi-common";
|
||||
import { GetRelevantSongsAndCharts } from "utils/db";
|
||||
import { IsValidScoreAlg } from "utils/misc";
|
||||
import { GetAdjacentAbove, GetAdjacentBelow } from "utils/queries/pbs";
|
||||
import { GetAdjacentAbove, GetAdjacentBelow, GetPBsWithUserRankings } from "utils/queries/pbs";
|
||||
import { GetUGPT } from "utils/req-tachi-data";
|
||||
import { FilterChartsAndSongs, GetPBOnChart, GetScoreIDsFromComposed } from "utils/scores";
|
||||
import { GetUsersWithIDs } from "utils/user";
|
||||
import { GetUsersWithIDs, ResolveUser } from "utils/user";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
@@ -97,7 +98,7 @@ router.get("/all", AggressiveRateLimitMiddleware, async (req, res) => {
|
||||
*
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/pbs/best
|
||||
*/
|
||||
router.get("/best", async (req, res) => {
|
||||
router.get("/best", prValidate({ alg: "*string" }), async (req, res) => {
|
||||
const { user, game, playtype } = GetUGPT(req);
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
@@ -141,6 +142,121 @@ router.get("/best", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Return this user's best 100 personal-bests, unioned with the best 100 of another
|
||||
* player.
|
||||
*
|
||||
* This is used for comparing best-100s with a player, so for example, if player B
|
||||
* has charts in their top 100 that player A doesn't, it will still fetch the PB
|
||||
* of player A for comparison reasons.
|
||||
*
|
||||
* @param alg - Specifies an override for the default algorithm
|
||||
* to sort on.
|
||||
* @param withUser - The userID to union personal bests with.
|
||||
*/
|
||||
router.get("/best-union", prValidate({ alg: "*string", withUser: "string" }), async (req, res) => {
|
||||
const { user, game, playtype } = GetUGPT(req);
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const query = req.query as {
|
||||
alg?: string;
|
||||
withUser: string;
|
||||
};
|
||||
|
||||
const otherUser = await ResolveUser(query.withUser);
|
||||
|
||||
if (!otherUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `The user '${query.withUser}' does not exist.`,
|
||||
});
|
||||
}
|
||||
|
||||
const hasPlayed = await db["game-stats"].findOne({
|
||||
game,
|
||||
playtype,
|
||||
userID: otherUser.id,
|
||||
});
|
||||
|
||||
if (!hasPlayed) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `The user '${otherUser.username}' has not played ${FormatGame(
|
||||
game,
|
||||
playtype
|
||||
)}.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (req.query.alg !== undefined && !IsValidScoreAlg(gptConfig, req.query.alg)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid score algorithm. Expected any of ${gptConfig.scoreRatingAlgs.join(
|
||||
", "
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
|
||||
const alg = req.query.alg ?? gptConfig.defaultScoreRatingAlg;
|
||||
|
||||
const options = {
|
||||
projection: {
|
||||
chartID: 1,
|
||||
},
|
||||
limit: 100,
|
||||
sort: {
|
||||
[`calculatedData.${alg}`]: -1,
|
||||
},
|
||||
};
|
||||
|
||||
// lets fetch the set of chartIDs in each users best 100s
|
||||
const baseUserBestChartIDs = await db["personal-bests"].find(
|
||||
{
|
||||
userID: user.id,
|
||||
game,
|
||||
playtype,
|
||||
isPrimary: true,
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
const withUserBestChartIDs = await db["personal-bests"].find(
|
||||
{
|
||||
userID: otherUser.id,
|
||||
game,
|
||||
playtype,
|
||||
isPrimary: true,
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
const chartIDs = [
|
||||
...baseUserBestChartIDs.map((e) => e.chartID),
|
||||
...withUserBestChartIDs.map((e) => e.chartID),
|
||||
];
|
||||
|
||||
// then fetch both of their scores on each.
|
||||
const baseUserPBs = await GetPBsWithUserRankings(user.id, chartIDs, alg);
|
||||
const withUserPBs = await GetPBsWithUserRankings(user.id, chartIDs, alg);
|
||||
|
||||
const { songs, charts } = await GetRelevantSongsAndCharts(
|
||||
[...baseUserPBs, ...withUserPBs],
|
||||
game
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Retrieved the union best 100 of ${user.username} and ${otherUser.username}.`,
|
||||
body: {
|
||||
baseUserPBs,
|
||||
withUserPBs,
|
||||
songs,
|
||||
charts,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a user's PB on the given chart. If the user has not played this chart, 404 is
|
||||
* returned.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import db from "external/mongo/db";
|
||||
import type { PBScoreDocument } from "tachi-common";
|
||||
import type { IDStrings, integer, PBScoreDocument, ScoreCalculatedDataLookup } from "tachi-common";
|
||||
|
||||
export async function GetAdjacentAbove(userPB: PBScoreDocument, size = 5) {
|
||||
const adjAbove = (await db["personal-bests"].find(
|
||||
@@ -30,3 +30,39 @@ export async function GetAdjacentBelow(userPB: PBScoreDocument, size = 5) {
|
||||
|
||||
return adjAbove;
|
||||
}
|
||||
|
||||
export type PBsWithPersonalRanking = Array<PBScoreDocument & { __personalRanking: integer }>;
|
||||
|
||||
/**
|
||||
* Given a user, an algorithm, and an array of chartIDs, return the user's PBs
|
||||
* on those charts with where they rank in their personal PBs.
|
||||
*/
|
||||
export async function GetPBsWithUserRankings(
|
||||
userID: integer,
|
||||
chartIDs: Array<string>,
|
||||
alg: ScoreCalculatedDataLookup[IDStrings]
|
||||
) {
|
||||
// i was going to do this with a fancy aggregate pipeline
|
||||
// but it's slower.
|
||||
// ah well
|
||||
const pbs = (await db["personal-bests"].find({
|
||||
userID,
|
||||
chartID: { $in: chartIDs },
|
||||
isPrimary: true,
|
||||
})) as Array<PBScoreDocument & { __personalRanking?: integer }>;
|
||||
|
||||
await Promise.all(
|
||||
pbs.map(async (pb) => {
|
||||
const personalRanking = await db["personal-bests"].count({
|
||||
userID,
|
||||
[`calculatedData.${alg}`]: { $gt: pb.calculatedData[alg] },
|
||||
});
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
pb.__personalRanking = personalRanking + 1;
|
||||
})
|
||||
);
|
||||
|
||||
// __personalRanking guaranteed to be assigned
|
||||
return pbs as PBsWithPersonalRanking;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user