Skip to content

Commit

Permalink
implement WACCA import from MYT (#1039)
Browse files Browse the repository at this point in the history
* add inGameID to WACCA charts

* add WACCA import support from MYT

* add client support for api/myt-wacca

* add bot support for api/myt-wacca

* remove artistJP and titleJP from songs-wacca

Turns out these aren't actually in the game data or anything, and we won't be
able to get their values for omni songs or future songs added in plus.

* add some missing omni WACCA songs

* add test

* fix tabs in test.conf.json5

* revamp grpc call handling a bit and add more error handling

* feat: drag the protofiles in rather than alter build

---------

Co-authored-by: zkldi <[email protected]>
  • Loading branch information
cg505 and zkrising committed May 8, 2024
1 parent 8167fb3 commit c64623c
Show file tree
Hide file tree
Showing 55 changed files with 7,182 additions and 1,192 deletions.
1 change: 1 addition & 0 deletions bot/src/slashCommands/commands/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const choices: Array<[string, string]> = (
["CG SDVX", "api/cg-prod-sdvx"],
["CG MUSECA", "api/cg-prod-museca"],
["CG Pop'n", "api/cg-prod-popn"],
["MYT WACCA", "api/myt-wacca"],
] as Array<[string, string]>
)

Expand Down
11 changes: 11 additions & 0 deletions client/src/app/pages/dashboard/import/ImportPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ function ImportInfoDisplayer({ game }: { game: Game }) {
);
} else if (game === "wacca") {
Content.unshift(
<ImportTypeInfoCard key="api/myt-wacca" importType="api/myt-wacca" />,
<ImportInfoCard
name="WaccaMyPageScraper"
href="wacca-mypage-scraper"
Expand Down Expand Up @@ -525,6 +526,16 @@ function ImportTypeInfoCard({
key="cg-nag-museca"
/>
);
case "api/myt-wacca":
return (
<ImportInfoCard
name="MYT Integration"
href="myt-wacca"
desc="Pull your WACCA scores from the MYT Network."
moreInfo="Note: All networks are reduced to their first three letters for anonymity reasons."
key="myt-wacca"
/>
);
case "file/eamusement-iidx-csv":
return (
<ImportInfoCard
Expand Down
49 changes: 49 additions & 0 deletions client/src/app/pages/dashboard/users/UserIntegrationsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import {
UserDocument,
TachiAPIClientDocument,
CGCardInfo,
MytCardInfo,
} from "tachi-common";
import { SetState } from "types/react";
import { CGNeedsIntegrate } from "components/imports/CGIntegrationPage";
import { MytNeedsIntegrate } from "components/imports/MYTIntegrationPage";
import FervidexIntegrationPage from "./FervidexIntegrationPage";
import KsHookSV6CIntegrationPage from "./KsHookSV6CIntegrationPage";

Expand Down Expand Up @@ -641,6 +643,7 @@ function ServicesPage({ reqUser }: { reqUser: UserDocument }) {
<SelectLinkButton to={`${baseUrl}/fervidex`}>Fervidex</SelectLinkButton>
<SelectLinkButton to={`${baseUrl}/cg`}>CG</SelectLinkButton>
<SelectLinkButton to={`${baseUrl}/cg-dev`}>CG Dev</SelectLinkButton>
<SelectLinkButton to={`${baseUrl}/myt`}>MYT</SelectLinkButton>
<SelectLinkButton to={`${baseUrl}/kshook`}>KsHook</SelectLinkButton>
<SelectLinkButton to={`${baseUrl}/flo`}>FLO</SelectLinkButton>
<SelectLinkButton to={`${baseUrl}/eag`}>EAG</SelectLinkButton>
Expand All @@ -661,6 +664,9 @@ function ServicesPage({ reqUser }: { reqUser: UserDocument }) {
<Route exact path={`${baseUrl}/cg-dev`}>
<CGIntegrationInfo key="cg" userID={reqUser.id} cgType="dev" />
</Route>
<Route exact path={`${baseUrl}/myt`}>
<MytIntegrationInfo userID={reqUser.id} />
</Route>
<Route exact path={`${baseUrl}/kshook`}>
<KsHookSV6CIntegrationPage reqUser={reqUser} />
</Route>
Expand Down Expand Up @@ -986,3 +992,46 @@ function CGIntegrationInfo({ cgType, userID }: { cgType: "dev" | "gan" | "nag";
/>
);
}

function MytIntegrationInfo({ userID }: { userID: integer }) {
const [reload, shouldReloadCardInfo] = useReducer((x) => x + 1, 0);

const { data, error } = useApiQuery<MytCardInfo | null>(
`/users/${userID}/integrations/myt`,
undefined,
[reload]
);

if (error) {
return <ApiError error={error} />;
}

// null is a valid response for this call, so be explicit with going to loading
if (data === undefined) {
return <Loading />;
}

return (
<MytNeedsIntegrate
onSubmit={async (cardAccessCode) => {
const res = await APIFetchV1(
`/users/${userID}/integrations/myt`,
{
method: "PUT",
body: JSON.stringify({ cardAccessCode }),
headers: {
"Content-Type": "application/json",
},
},
true,
true
);

if (res.success) {
shouldReloadCardInfo();
}
}}
initialCardAccessCode={data?.cardAccessCode ?? undefined}
/>
);
}
5 changes: 5 additions & 0 deletions client/src/app/routes/ImportRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import SaekawaPage from "app/pages/dashboard/import/SaekawaPage";
import AquaArtemisExport from "app/pages/dashboard/import/AquaArtemisExportPage";
import ChunithmMYTExport from "app/pages/dashboard/import/ChunithmMYTExportPage";
import OngekiArtemisExport from "app/pages/dashboard/import/OngekiArtemisExportPage";
import MytIntegrationPage from "components/imports/MYTIntegrationPage";

export default function ImportRoutes() {
const { user } = useContext(UserContext);
Expand Down Expand Up @@ -218,6 +219,10 @@ export default function ImportRoutes() {
<CGIntegrationPage cgType="gan" game="museca" />
</Route>

<Route exact path="/import/myt-wacca">
<MytIntegrationPage game="wacca" />
</Route>

<Route exact path="/import/wacca-mypage-scraper">
<WACCAMyPageScraperPage />
</Route>
Expand Down
208 changes: 208 additions & 0 deletions client/src/components/imports/MYTIntegrationPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { APIFetchV1 } from "util/api";
import { ErrorPage } from "app/pages/ErrorPage";
import useSetSubheader from "components/layout/header/useSetSubheader";
import ApiError from "components/util/ApiError";
import Divider from "components/util/Divider";
import FormInput from "components/util/FormInput";
import Loading from "components/util/Loading";
import useImport from "components/util/import/useImport";
import useApiQuery from "components/util/query/useApiQuery";
import { UserContext } from "context/UserContext";
import React, { useContext, useMemo, useReducer, useState } from "react";
import { Button, Form } from "react-bootstrap";
import { APIImportTypes, GetGameConfig, MytCardInfo } from "tachi-common";
import { SetState } from "types/react";
import Icon from "components/util/Icon";
import ImportStateRenderer from "./ImportStateRenderer";

interface Props {
// Other games will be added in the future.
game: "wacca";
}

export default function MytIntegrationPage({ game }: Props) {
const gameConfig = GetGameConfig(game);

const [reload, shouldReloadCardInfo] = useReducer((x) => x + 1, 0);
const [showEdit, setShowEdit] = useState(false);

useSetSubheader(["Import Scores", `${gameConfig.name} Sync (MYT)`]);

const { user } = useContext(UserContext);

if (!user) {
return <ErrorPage statusCode={401} />;
}

const { data, error } = useApiQuery<MytCardInfo | null>(
`/users/${user.id}/integrations/myt`,
undefined,
[reload]
);

if (error) {
return <ApiError error={error} />;
}

// null is a valid response for this call, so be explicit with going to loading
if (data === undefined) {
return <Loading />;
}

return (
<div>
{(showEdit || !data) && (
<>
<MytNeedsIntegrate
onSubmit={async (cardAccessCode) => {
const res = await APIFetchV1(
`/users/${user.id}/integrations/myt`,
{
method: "PUT",
body: JSON.stringify({ cardAccessCode }),
headers: {
"Content-Type": "application/json",
},
},
true,
true
);

if (res.success) {
shouldReloadCardInfo();
}
}}
initialCardAccessCode={data?.cardAccessCode ?? undefined}
/>
<Divider />
</>
)}
{data && (
<MytImporter
game={game}
cardAccessCode={data.cardAccessCode}
setShowEdit={setShowEdit}
showEdit={showEdit}
/>
)}
</div>
);
}

function MytImporter({
game,
cardAccessCode,
showEdit,
setShowEdit,
}: Pick<Props, "game"> & {
cardAccessCode: string;
showEdit: boolean;
setShowEdit: SetState<boolean>;
}) {
const importType: APIImportTypes = `api/myt-${game}`;

const { importState, runImport } = useImport("/import/from-api", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
importType,
}),
});

return (
<div>
<h2 className="text-center mb-4">
Importing scores from MYT card{" "}
<code>{cardAccessCode.match(/.{1,4}/gu)?.join(" ")}</code>{" "}
<Icon
onClick={() => setShowEdit(!showEdit)}
type={showEdit ? "times" : "pencil-alt"}
noPad
/>
.
</h2>
<Divider />
<div className="d-flex w-100 justify-content-center">
<Button
className="mx-auto"
variant="primary"
onClick={() => runImport()}
disabled={
importState.state === "waiting_init" ||
importState.state === "waiting_processing"
}
>
{importState.state === "waiting_init" ||
importState.state === "waiting_processing"
? "Syncing..."
: "Click to Sync!"}
</Button>
</div>
<Divider />
<div>
Play on MYT a lot? You can synchronise your scores straight from the discord by
typing <code>/sync</code>!
</div>
<Divider />
<ImportStateRenderer state={importState} />
</div>
);
}

export function MytNeedsIntegrate({
initialCardAccessCode,
onSubmit,
}: {
initialCardAccessCode?: string;
onSubmit: (cardAccessCode: string) => Promise<void>;
}) {
const [cardAccessCode, setCardAccessCode] = useState(initialCardAccessCode ?? "");

// strip any whitespace the user feels like entering
const realCardAccessCode = useMemo(() => cardAccessCode.replace(/\s+/gu, ""), [cardAccessCode]);

const validCardAccessCode = useMemo(
// Note: valid Myt cards must start with 0 or 3 as 0008, sega, or aime cards.
// See https://sega.bsnk.me/misc/card_convert/#lookup-a-card / https://sega.bsnk.me/allnet/access_codes/
// We only implement this in client-side because it's just meant to be a
// user hint, in case someone tries to enter an AIC code or something.
() => /^[03][0-9]{19}$/u.exec(realCardAccessCode),
[realCardAccessCode]
);

return (
<div>
<h3 className="text-center mb-4">Set your MYT card.</h3>

<FormInput
fieldName="Card Access Code"
setValue={setCardAccessCode}
value={cardAccessCode}
/>
<Form.Label>
This is the card access code that's displayed in game. It should be 20 digits.
<br />
{cardAccessCode.length > 0 && !validCardAccessCode ? (
<span className="text-danger">
Invalid card access code. This should be exactly 20 digits as displayed in
game. It may not be the same as the code on the back of your card.
</span>
) : (
cardAccessCode.length > 0 && <span className="text-success">Looking good!</span>
)}
</Form.Label>

<Divider />
<div className="w-100 d-flex justify-content-center">
<Button
disabled={!validCardAccessCode}
onClick={() => onSubmit(realCardAccessCode)}
>
Submit Card Access Code
</Button>
</div>
</div>
);
}
5 changes: 2 additions & 3 deletions common/src/config/game-support/wacca.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FAST_SLOW_MAXCOMBO } from "./_common";
import { FmtNum } from "../../utils/util";
import { ClassValue } from "../config-utils";
import { ClassValue, zodNonNegativeInt } from "../config-utils";
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
Expand All @@ -9,8 +9,6 @@ export const WACCA_CONF = {
name: "WACCA",
playtypes: ["Single"],
songData: z.strictObject({
titleJP: z.string(),
artistJP: z.string(),
genre: z.string(),
displayVersion: z.nullable(z.string()),
}),
Expand Down Expand Up @@ -144,6 +142,7 @@ export const WACCA_SINGLE_CONF = {

chartData: z.strictObject({
isHot: z.boolean(),
inGameID: zodNonNegativeInt,
}),

preferences: z.strictObject({}),
Expand Down
1 change: 1 addition & 0 deletions common/src/constants/import-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const API_IMPORT_TYPES: Record<APIImportTypes, true> = {
"api/flo-iidx": true,
"api/flo-sdvx": true,
"api/min-sdvx": true,
"api/myt-wacca": true,
"api/cg-dev-museca": true,
"api/cg-dev-popn": true,
"api/cg-dev-sdvx": true,
Expand Down
5 changes: 5 additions & 0 deletions common/src/types/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,11 @@ export interface CGCardInfo {
pin: string;
}

export interface MytCardInfo {
userID: integer;
cardAccessCode: string; // matches /^[0-9]{20}$/
}

interface BMSCourseInner<GPT extends GPTStrings["bms"], Set extends keyof ExtractedClasses[GPT]> {
set: Set;
playtype: GPTStringToPlaytype[GPT];
Expand Down
1 change: 1 addition & 0 deletions common/src/types/import-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type APIImportTypes =
| "api/flo-iidx"
| "api/flo-sdvx"
| "api/min-sdvx"
| "api/myt-wacca"

// cg has dev and prod supported
// with four games.
Expand Down
Loading

0 comments on commit c64623c

Please sign in to comment.