+ }
+ >
+ {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.
+
+
+
+