From b652335940e0bd5467f8bf683584ba43bb958a03 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Mon, 19 Dec 2022 09:09:51 +0000 Subject: [PATCH 1/2] feat: tools, bms table selector --- .../games/_game/_playtype/OverviewPage.tsx | 2 + .../games/_game/_playtype/tools/ToolsPage.tsx | 80 +++++++++++++++++++ client/src/app/routes/UserRoutes.tsx | 12 ++- .../components/tools/BMSCustomTablesTool.tsx | 74 +++++++++++++++++ client/src/components/tools/Tools.tsx | 36 +++++++++ client/src/components/user/UGPTHeader.tsx | 20 ++++- client/src/types/ugpt.ts | 9 +++ common/src/config/config.ts | 5 ++ pnpm-lock.yaml | 36 +-------- .../lib/game-specific/custom-bms-tables.ts | 6 ++ .../games/@gameSpecificRoutes/bms/router.ts | 30 +++++++ .../_userID/games/_game/_playtype/router.ts | 30 +++++-- 12 files changed, 294 insertions(+), 46 deletions(-) create mode 100644 client/src/app/pages/dashboard/users/games/_game/_playtype/tools/ToolsPage.tsx create mode 100644 client/src/components/tools/BMSCustomTablesTool.tsx create mode 100644 client/src/components/tools/Tools.tsx create mode 100644 client/src/types/ugpt.ts diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx index d1580f416..d52c6f9bf 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx @@ -126,6 +126,8 @@ function UserHistory({
diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/tools/ToolsPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/tools/ToolsPage.tsx new file mode 100644 index 000000000..db0a0ab17 --- /dev/null +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/tools/ToolsPage.tsx @@ -0,0 +1,80 @@ +import useSetSubheader from "components/layout/header/useSetSubheader"; +import Card from "components/layout/page/Card"; +import { GetGPTTools } from "components/tools/Tools"; +import Divider from "components/util/Divider"; +import LinkButton from "components/util/LinkButton"; +import React from "react"; +import { Col, Row } from "react-bootstrap"; +import { Link, Route, Switch } from "react-router-dom"; +import { FormatGame, GetGameConfig } from "tachi-common"; +import { UGPT } from "types/react"; + +export default function ToolsPage({ reqUser, game, playtype }: UGPT) { + const gameConfig = GetGameConfig(game); + useSetSubheader( + ["Users", reqUser.username, "Games", gameConfig.name, playtype, "Tools"], + [reqUser, game, playtype], + `${reqUser.username}'s ${FormatGame(game, playtype)} Tools` + ); + + const tools = GetGPTTools(game, playtype); + + if (tools.length === 0) { + return ( +
+ {FormatGame(game, playtype)} has no tools. How'd you get here! +
+ ); + } + + return ( + + + {tools.map((tool) => ( + + + + < Back to all tools... + + } + > + {tool.description} + + + + {tool.component({ reqUser, game, playtype })} + + ))} + + + {tools.map((tool) => ( + + + + Use Tool + +
+ } + > + {tool.description} + + + ))} + + + + ); +} diff --git a/client/src/app/routes/UserRoutes.tsx b/client/src/app/routes/UserRoutes.tsx index 13744f40d..6db0c6657 100644 --- a/client/src/app/routes/UserRoutes.tsx +++ b/client/src/app/routes/UserRoutes.tsx @@ -31,6 +31,7 @@ import { FormatGame, Game, GetGameConfig, UserDocument, UserGameStats } from "ta import { UGPTStatsReturn } from "types/api-returns"; import UserImportsPage from "app/pages/dashboard/users/UserImportsPage"; import { TargetsContextProvider } from "context/TargetsContext"; +import ToolsPage from "app/pages/dashboard/users/games/_game/_playtype/tools/ToolsPage"; import ScoresPage from "../pages/dashboard/users/games/_game/_playtype/ScoresPage"; import UserPage from "../pages/dashboard/users/UserPage"; @@ -64,9 +65,9 @@ export default function UserRoutes() { return null; } - // redirect to the users actual name. - if (userID.match(/^[0-9]+$/u)) { - const split = history.location.pathname.match(/^(\/u)\/([0-9]+)(.*)$/u); + // redirect to the users actual name if using a user ID or "me" + if (userID.match(/^([0-9]+|me)$/u)) { + const split = history.location.pathname.match(/^(\/u)\/([0-9]+|me)(.*)$/u); if (!split) { return ( @@ -229,6 +230,8 @@ function UserGamePlaytypeRoutes({ reqUser, game }: { reqUser: UserDocument; game header={`${reqUser.username}'s ${FormatGame(game, playtype)} Profile`} footer={ @@ -261,6 +264,9 @@ function UserGamePlaytypeRoutes({ reqUser, game }: { reqUser: UserDocument; game + + + diff --git a/client/src/components/tools/BMSCustomTablesTool.tsx b/client/src/components/tools/BMSCustomTablesTool.tsx new file mode 100644 index 000000000..ed03c899e --- /dev/null +++ b/client/src/components/tools/BMSCustomTablesTool.tsx @@ -0,0 +1,74 @@ +import { ToAPIURL } from "util/api"; +import { CopyToClipboard } from "util/misc"; +import Card from "components/layout/page/Card"; +import ApiError from "components/util/ApiError"; +import Loading from "components/util/Loading"; +import useApiQuery from "components/util/query/useApiQuery"; +import { TachiConfig } from "lib/config"; +import React from "react"; +import { Col, Row } from "react-bootstrap"; +import { UGPT } from "types/react"; +import { GPTTool } from "types/ugpt"; +import Icon from "components/util/Icon"; +import Divider from "components/util/Divider"; + +function Component({ game, playtype, reqUser }: UGPT) { + const { data, error } = useApiQuery< + Array<{ + forSpecificUser?: boolean; + urlName: string; + tableName: string; + symbol: string; + description: string; + }> + >(`/games/${game}/${playtype}/custom-tables`); + + if (error) { + return ; + } + + if (!data) { + return ; + } + + return ( + + + Just paste these URLs into your BMS client of choice. For LR2oraja, this is on the + setup screen. + + + {data.map((tbl) => { + const TABLE_URL = ToAPIURL( + `${ + tbl.forSpecificUser ? `/users/${reqUser.username}` : "" + }/games/bms/${playtype}/custom-tables/${tbl.urlName}` + ); + + return ( + + + {tbl.description} +
+
+ URL: {TABLE_URL} + { + CopyToClipboard(TABLE_URL); + }} + /> +
+ + ); + })} +
+ ); +} + +export const BMSCustomTablesTool: GPTTool = { + name: `${TachiConfig.name} BMS Tables`, + urlPath: "custom-tables", + description: `${TachiConfig.name} has its own BMS tables that you can use in-game!`, + component: Component, +}; diff --git a/client/src/components/tools/Tools.tsx b/client/src/components/tools/Tools.tsx new file mode 100644 index 000000000..45a46ddad --- /dev/null +++ b/client/src/components/tools/Tools.tsx @@ -0,0 +1,36 @@ +import { Game, GetIDString, IDStrings, Playtype } from "tachi-common"; +import { UGPT } from "types/react"; +import { GPTTool } from "types/ugpt"; +import { BMSCustomTablesTool } from "./BMSCustomTablesTool"; + +// A tool is something that users can use, like exporting to a specific format +// or having things integrate with the game in a neat way. +// Tools are **NOT** displayed on someone elses profile. For that, you want +// Insights (Fancy score renderers, basically). + +// What tools does each game support? +const GPT_TOOLS: Record> = { + "bms:7K": [BMSCustomTablesTool], + "bms:14K": [BMSCustomTablesTool], + "chunithm:Single": [], + "gitadora:Dora": [], + "gitadora:Gita": [], + "iidx:DP": [], + "iidx:SP": [], + "itg:Stamina": [], + "jubeat:Single": [], + "museca:Single": [], + "pms:Controller": [], + "pms:Keyboard": [], + "popn:9B": [], + "sdvx:Single": [], + "usc:Controller": [], + "usc:Keyboard": [], + "wacca:Single": [], +}; + +export function GetGPTTools(game: Game, playtype: Playtype) { + const idString = GetIDString(game, playtype); + + return GPT_TOOLS[idString]; +} diff --git a/client/src/components/user/UGPTHeader.tsx b/client/src/components/user/UGPTHeader.tsx index e824b1f44..255d1509c 100644 --- a/client/src/components/user/UGPTHeader.tsx +++ b/client/src/components/user/UGPTHeader.tsx @@ -14,6 +14,7 @@ import { UserContext } from "context/UserContext"; import useLUGPTSettings from "components/util/useLUGPTSettings"; import { UserSettingsContext } from "context/UserSettingsContext"; import FollowUserButton from "components/util/FollowUserButton"; +import { GetGPTTools } from "components/tools/Tools"; import ProfileBadges from "./ProfileBadges"; import ProfilePicture from "./ProfilePicture"; import RankingData from "./UGPTRankingData"; @@ -179,9 +180,13 @@ export function UGPTHeaderBody({ export function UGPTBottomNav({ baseUrl, isRequestedUser, + game, + playtype, }: { baseUrl: string; isRequestedUser: boolean; + game: Game; + playtype: Playtype; }) { const navItems = [ @@ -208,7 +213,20 @@ export function UGPTBottomNav({ , Goals & Quests - , + + ); + + const tools = GetGPTTools(game, playtype); + + if (tools.length > 0) { + navItems.push( + + Tools + + ); + } + + navItems.push( Settings diff --git a/client/src/types/ugpt.ts b/client/src/types/ugpt.ts new file mode 100644 index 000000000..b41602d1e --- /dev/null +++ b/client/src/types/ugpt.ts @@ -0,0 +1,9 @@ +import React from "react"; +import { UGPT } from "./react"; + +export interface GPTTool { + urlPath: string; + name: string; + description: React.ReactChild; + component: (ugpt: UGPT) => JSX.Element; +} diff --git a/common/src/config/config.ts b/common/src/config/config.ts index 66ca0d2c5..a9951eda0 100644 --- a/common/src/config/config.ts +++ b/common/src/config/config.ts @@ -28,6 +28,7 @@ import type { IDStrings, JudgementLookup, Lamps, + Playtype, Playtypes, ScoreCalculatedDataLookup, SessionCalculatedDataLookup, @@ -2085,3 +2086,7 @@ export function GetGamePTConfig( return GAME_PT_CONFIGS[idString] as unknown as GamePTConfig; } + +export function GetIDString(game: Game, playtype: Playtype): IDStrings { + return `${game}:${playtype}` as IDStrings; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9b9f6e98..33ec68df1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -330,12 +330,9 @@ importers: github-bot: specifiers: '@octokit/app': ^13.1.0 - '@octokit/webhooks': 10.1.5 - '@types/body-parser': ^1.19.2 '@types/express': 4.17.13 '@types/node': 16.11.7 '@types/node-fetch': 2.5.12 - body-parser: ^1.20.0 dotenv: 16.0.0 express: 4.17.2 node-fetch: 2.6.7 @@ -343,9 +340,6 @@ importers: typescript: 4.6.3 dependencies: '@octokit/app': 13.1.0 - '@octokit/webhooks': 10.1.5 - '@types/body-parser': 1.19.2 - body-parser: 1.20.0 dotenv: 16.0.0 express: 4.17.2 node-fetch: 2.6.7 @@ -4913,26 +4907,6 @@ packages: - supports-color dev: true - /body-parser/1.20.0: - resolution: {integrity: sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dependencies: - bytes: 3.1.2 - content-type: 1.0.4 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.10.3 - raw-body: 2.5.1 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: false - /body-parser/1.20.1: resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -6346,7 +6320,6 @@ packages: dependencies: ms: 2.1.2 supports-color: 6.1.0 - dev: true /decamelize/1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} @@ -7929,7 +7902,7 @@ packages: debug: optional: true dependencies: - debug: 4.3.4 + debug: 4.3.4_supports-color@6.1.0 /for-in/1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} @@ -12336,13 +12309,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} dev: false - /qs/6.10.3: - resolution: {integrity: sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==} - engines: {node: '>=0.6'} - dependencies: - side-channel: 1.0.4 - dev: false - /qs/6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} diff --git a/server/src/lib/game-specific/custom-bms-tables.ts b/server/src/lib/game-specific/custom-bms-tables.ts index f9a1ea170..b40e25166 100644 --- a/server/src/lib/game-specific/custom-bms-tables.ts +++ b/server/src/lib/game-specific/custom-bms-tables.ts @@ -103,6 +103,7 @@ export type TachiBMSTable = { urlName: string; // what do we call this in the url? tableName: string; // what should it be called in-game? symbol: string; // what symbol should this table have? + description: string; } & ( | { forSpecificUser: true; // if this table is user-dependent @@ -265,6 +266,8 @@ export const CUSTOM_TACHI_BMS_TABLES: Array = [ playtype: "7K", symbol: "sgl-", tableName: "Sieglinde EC", + description: + "Folders for the 'Sieglinde' rating algorithm. These are rough estimates of how hard it is to EASY CLEAR a given chart.", async getBody() { const table = await GetTableForIDGuaranteed("bms-7K-sgl-EC"); @@ -281,6 +284,8 @@ export const CUSTOM_TACHI_BMS_TABLES: Array = [ playtype: "7K", symbol: "sgl-", tableName: "Sieglinde HC", + description: + "Folders for the 'Sieglinde' rating algorithm. These are rough estimates of how hard it is to HARD CLEAR a given chart.", async getBody() { const table = await GetTableForIDGuaranteed("bms-7K-sgl-HC"); @@ -299,6 +304,7 @@ export const CUSTOM_TACHI_BMS_TABLES: Array = [ symbol: "Rival", tableName: `${TachiConfig.NAME} Rival Stats`, forSpecificUser: true, + description: `Folders for your rivals on ${TachiConfig.NAME}. This includes things like their recent highlights and plays.`, async getBody(userID, playtype) { const rivals = await GetRivalUsers(userID, "bms", playtype); diff --git a/server/src/server/router/api/v1/games/@gameSpecificRoutes/bms/router.ts b/server/src/server/router/api/v1/games/@gameSpecificRoutes/bms/router.ts index 8e281857a..05b05b7f2 100644 --- a/server/src/server/router/api/v1/games/@gameSpecificRoutes/bms/router.ts +++ b/server/src/server/router/api/v1/games/@gameSpecificRoutes/bms/router.ts @@ -7,6 +7,7 @@ import { } from "lib/game-specific/custom-bms-tables"; import { AssignToReqTachiData, GetTachiData } from "utils/req-tachi-data"; import type { RequestHandler } from "express"; +import type { TachiBMSTable } from "lib/game-specific/custom-bms-tables"; const router: Router = Router({ mergeParams: true }); @@ -42,6 +43,35 @@ const FindCustomBMSTable: RequestHandler = (req, res, next) => { next(); }; +/** + * List all custom BMS tables this instance of Tachi is emitting. + * + * @name GET /api/v1/games/bms/:playtype/custom-tables + */ +router.get("/:playtype/custom-tables", (req, res) => { + const tables: Array< + Pick + > = []; + + for (const table of CUSTOM_TACHI_BMS_TABLES.filter( + (e) => e.playtype === req.params.playtype || e.playtype === null + )) { + tables.push({ + forSpecificUser: table.forSpecificUser, + urlName: table.urlName, + tableName: table.tableName, + symbol: table.symbol, + description: table.description, + }); + } + + return res.status(200).json({ + success: true, + description: `Found ${tables.length} custom table(s).`, + body: tables, + }); +}); + /** * Return some HTML for this custom table. * diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts index c75ba1d72..750a0ce47 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts @@ -12,7 +12,7 @@ import { RequireAuthedAsUser, RequireSelfRequestFromUser } from "../../../middle import { Router } from "express"; import db from "external/mongo/db"; import { CreateActivityRouteHandler } from "lib/activity/activity"; -import { ONE_MONTH, ONE_YEAR } from "lib/constants/time"; +import { ONE_MONTH, ONE_WEEK, ONE_YEAR } from "lib/constants/time"; import CreateLogCtx from "lib/logger/logger"; import p from "prudence"; import prValidate from "server/middleware/prudence-validate"; @@ -103,17 +103,33 @@ router.get("/", async (req, res) => { router.get( "/history", prValidate({ - duration: p.optional(p.isIn("3mo", "year")), + duration: p.optional(p.isIn("week", "month", "3mo", "year")), }), async (req, res) => { - const duration = req.query.duration as "3mo" | "year" | undefined; + const duration = req.query.duration as "3mo" | "month" | "week" | "year" | undefined; let time = Date.now(); - if (duration === "year") { - time = time - ONE_YEAR; - } else { - time = time - ONE_MONTH * 3; + switch (duration) { + case "year": { + time = time - ONE_YEAR; + break; + } + + // if the user doesn't define anything, default to 3month + case "3mo": + case undefined: { + time = time - ONE_MONTH * 3; + break; + } + + case "month": { + time = time - ONE_MONTH; + break; + } + + case "week": + time = time - ONE_WEEK; } const { game, playtype, user } = GetUGPT(req); From 64a53cad3eaa47b94cd003e6766490687170d3b3 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Mon, 19 Dec 2022 10:09:22 +0000 Subject: [PATCH 2/2] feat: eamusement csv export --- .../tools/IIDXEamusementExportTool.tsx | 68 +++++++ client/src/components/tools/Tools.tsx | 6 +- common/src/utils/util.ts | 10 + .../common/eamusement-iidx-csv/parser.ts | 69 +++---- .../games/@gameSpecificRoutes/iidx/router.ts | 188 ++++++++++++++++++ .../games/@gameSpecificRoutes/router.ts | 2 + 6 files changed, 306 insertions(+), 37 deletions(-) create mode 100644 client/src/components/tools/IIDXEamusementExportTool.tsx create mode 100644 server/src/server/router/api/v1/users/_userID/games/@gameSpecificRoutes/iidx/router.ts diff --git a/client/src/components/tools/IIDXEamusementExportTool.tsx b/client/src/components/tools/IIDXEamusementExportTool.tsx new file mode 100644 index 000000000..6f952d511 --- /dev/null +++ b/client/src/components/tools/IIDXEamusementExportTool.tsx @@ -0,0 +1,68 @@ +import { CopyToClipboard } from "util/misc"; +import ApiError from "components/util/ApiError"; +import DebugContent from "components/util/DebugContent"; +import Divider from "components/util/Divider"; +import Loading from "components/util/Loading"; +import useApiQuery from "components/util/query/useApiQuery"; +import { TachiConfig } from "lib/config"; +import React from "react"; +import { Alert, Button, Col, Row } from "react-bootstrap"; +import { UGPT } from "types/react"; +import { GPTTool } from "types/ugpt"; + +function Component({ game, playtype, reqUser }: UGPT) { + const { data, error } = useApiQuery( + `/users/${reqUser.id}/games/${game}/${playtype}/eamusement-csv` + ); + + if (error) { + return ; + } + + if (!data) { + return ; + } + + return ( + + + + Hey! Please be aware that this format is lossy. You should use + this to integrate with other tools that only support eamusement CSVs. +
+ Tachi offers more fully fledged score exporting if you're looking to actually + get your scores elsewhere. +
+ + + +