diff --git a/bot/src/slashCommands/commands/sync.ts b/bot/src/slashCommands/commands/sync.ts
index a62df0c26..fad32efb7 100644
--- a/bot/src/slashCommands/commands/sync.ts
+++ b/bot/src/slashCommands/commands/sync.ts
@@ -18,6 +18,7 @@ const choices: Array<[string, string]> = (
["CG MUSECA", "api/cg-prod-museca"],
["CG Pop'n", "api/cg-prod-popn"],
["MYT CHUNITHM", "api/myt-chunithm"],
+ ["MYT MAIMAI DX", "api/myt-maimaidx"],
["MYT ONGEKI", "api/myt-ongeki"],
["MYT WACCA", "api/myt-wacca"],
] as Array<[string, string]>
diff --git a/client/src/app/pages/dashboard/import/ImportPage.tsx b/client/src/app/pages/dashboard/import/ImportPage.tsx
index fb64cd2c6..cbef105d5 100644
--- a/client/src/app/pages/dashboard/import/ImportPage.tsx
+++ b/client/src/app/pages/dashboard/import/ImportPage.tsx
@@ -319,7 +319,8 @@ function ImportInfoDisplayer({ game }: { game: Game }) {
desc="Use your data from maimai DX NET."
moreInfo="If you are playing on an official maimai DX server, you can import play data from it here."
key="maimai DX NET Importer"
- />
+ />,
+
);
} else if (game === "museca") {
Content.unshift(
@@ -621,6 +622,16 @@ function ImportTypeInfoCard({
key="myt-chunithm"
/>
);
+ case "api/myt-maimaidx":
+ return (
+
+ );
case "api/myt-ongeki":
return (
+
+
+
diff --git a/client/src/components/imports/MYTIntegrationPage.tsx b/client/src/components/imports/MYTIntegrationPage.tsx
index 3d93f0b2a..39c3ae9a1 100644
--- a/client/src/components/imports/MYTIntegrationPage.tsx
+++ b/client/src/components/imports/MYTIntegrationPage.tsx
@@ -16,8 +16,7 @@ import Icon from "components/util/Icon";
import ImportStateRenderer from "./ImportStateRenderer";
interface Props {
- // Other games will be added in the future.
- game: "chunithm" | "ongeki" | "wacca";
+ game: "chunithm" | "maimaidx" | "ongeki" | "wacca";
}
export default function MytIntegrationPage({ game }: Props) {
diff --git a/common/src/constants/import-types.ts b/common/src/constants/import-types.ts
index 1286c7af2..f50a535a1 100644
--- a/common/src/constants/import-types.ts
+++ b/common/src/constants/import-types.ts
@@ -35,6 +35,7 @@ const API_IMPORT_TYPES: Record = {
"api/flo-sdvx": true,
"api/min-sdvx": true,
"api/myt-chunithm": true,
+ "api/myt-maimaidx": true,
"api/myt-ongeki": true,
"api/myt-wacca": true,
"api/cg-dev-museca": true,
diff --git a/common/src/types/import-types.ts b/common/src/types/import-types.ts
index 9cad282db..aab7ee546 100644
--- a/common/src/types/import-types.ts
+++ b/common/src/types/import-types.ts
@@ -14,6 +14,7 @@ export type APIImportTypes =
| "api/flo-sdvx"
| "api/min-sdvx"
| "api/myt-chunithm"
+ | "api/myt-maimaidx"
| "api/myt-ongeki"
| "api/myt-wacca"
diff --git a/server/example/conf.json5 b/server/example/conf.json5
index ae3d21abb..572d12e36 100644
--- a/server/example/conf.json5
+++ b/server/example/conf.json5
@@ -80,6 +80,8 @@
"api/cg-gan-sdvx",
"api/cg-gan-popn",
"api/cg-gan-museca",
+ "api/myt-chunithm",
+ "api/myt-maimaidx",
"api/myt-ongeki",
"api/myt-wacca",
],
diff --git a/server/src/lib/score-import/import-types/api/myt-chunithm/converter.test.ts b/server/src/lib/score-import/import-types/api/myt-chunithm/converter.test.ts
index b9d0e80ae..effe12955 100644
--- a/server/src/lib/score-import/import-types/api/myt-chunithm/converter.test.ts
+++ b/server/src/lib/score-import/import-types/api/myt-chunithm/converter.test.ts
@@ -40,7 +40,7 @@ const parsedScore: MytChunithmScore = {
},
};
-t.test("#ConvertAPIMytOngeki", (t) => {
+t.test("#ConvertAPIMytChunithm", (t) => {
t.beforeEach(ResetDBState);
function convert(modifier: any = {}) {
diff --git a/server/src/lib/score-import/import-types/api/myt-maimaidx/converter.test.ts b/server/src/lib/score-import/import-types/api/myt-maimaidx/converter.test.ts
new file mode 100644
index 000000000..a0b5157e0
--- /dev/null
+++ b/server/src/lib/score-import/import-types/api/myt-maimaidx/converter.test.ts
@@ -0,0 +1,213 @@
+import ConvertAPIMytMaimaiDx from "./converter";
+import CreateLogCtx from "lib/logger/logger";
+import { ParseDateFromString } from "lib/score-import/framework/common/score-utils";
+import {
+ MaimaiComboStatus,
+ MaimaiLevel,
+ MaimaiScoreRank,
+ MaimaiSyncStatus,
+} from "proto/generated/maimai/common_pb";
+import t from "tap";
+import { dmf } from "test-utils/misc";
+import ResetDBState from "test-utils/resets";
+import { TestingMaimaiDXChartConverter, TestingMaimaiDXSongConverter } from "test-utils/test-data";
+import type { MytMaimaiDxScore } from "./types";
+
+const logger = CreateLogCtx(__filename);
+
+const parsedScore: MytMaimaiDxScore = {
+ playlogApiId: "6071c489-6ab9-4674-a443-f88b603fa596",
+ info: {
+ musicId: 11294,
+ level: MaimaiLevel.MAIMAI_LEVEL_EXPERT,
+ achievement: 990562,
+ deluxscore: 1825,
+ scoreRank: MaimaiScoreRank.MAIMAI_SCORE_RANK_SS,
+ comboStatus: MaimaiComboStatus.MAIMAI_COMBO_STATUS_NONE,
+ syncStatus: MaimaiSyncStatus.MAIMAI_SYNC_STATUS_NONE,
+ isClear: true,
+ isAchieveNewRecord: true,
+ isDeluxscoreNewRecord: true,
+ track: 1,
+ userPlayDate: "2022-11-03T04:21:05.000+09:00",
+ },
+ judge: {
+ judgeCriticalPerfect: 10,
+ judgePerfect: 656,
+ judgeGreat: 19,
+ judgeGood: 1,
+ judgeMiss: 8,
+ maxCombo: 279,
+ fastCount: 5,
+ lateCount: 8,
+ },
+};
+
+t.test("#ConvertAPIMytMaimaiDx", (t) => {
+ t.beforeEach(ResetDBState);
+
+ function convert(modifier: any = {}) {
+ return ConvertAPIMytMaimaiDx(dmf(parsedScore, modifier), {}, "api/myt-maimaidx", logger);
+ }
+
+ t.test("Should return a dryScore on valid input.", async (t) => {
+ const res = await convert();
+
+ t.strictSame(res, {
+ song: TestingMaimaiDXSongConverter,
+ chart: TestingMaimaiDXChartConverter,
+ dryScore: {
+ service: "MYT",
+ game: "maimaidx",
+ scoreMeta: {},
+ timeAchieved: ParseDateFromString("2022-11-03T04:21:05.000+09:00"),
+ comment: null,
+ importType: "api/myt-maimaidx",
+ scoreData: {
+ percent: 99.0562,
+ lamp: "CLEAR",
+ judgements: {
+ pcrit: 10,
+ perfect: 656,
+ great: 19,
+ good: 1,
+ miss: 8,
+ },
+ optional: {
+ fast: 5,
+ slow: 8,
+ maxCombo: 279,
+ },
+ },
+ },
+ });
+ t.end();
+ });
+
+ t.test("Should reject Utage charts", (t) => {
+ t.rejects(
+ () =>
+ convert({
+ info: {
+ musicId: 8032,
+ level: MaimaiLevel.MAIMAI_LEVEL_UTAGE,
+ },
+ }),
+ {
+ message: /Utage charts are not supported/u,
+ }
+ );
+ t.end();
+ });
+
+ t.test("Should reject unspecified difficulty", (t) => {
+ t.rejects(
+ () =>
+ convert({
+ info: {
+ level: MaimaiLevel.MAIMAI_LEVEL_UNSPECIFIED,
+ },
+ }),
+ {
+ message: /Can't process a score with unspecified difficulty/u,
+ }
+ );
+ t.end();
+ });
+
+ t.test("lamp", async (t) => {
+ t.hasStrict(
+ await convert({
+ info: {
+ comboStatus: MaimaiComboStatus.MAIMAI_COMBO_STATUS_ALL_PERFECT_PLUS,
+ isClear: true,
+ },
+ }),
+ {
+ dryScore: { scoreData: { lamp: "ALL PERFECT+" } },
+ }
+ );
+ t.hasStrict(
+ await convert({
+ info: {
+ comboStatus: MaimaiComboStatus.MAIMAI_COMBO_STATUS_ALL_PERFECT,
+ isClear: true,
+ },
+ }),
+ {
+ dryScore: { scoreData: { lamp: "ALL PERFECT" } },
+ }
+ );
+ t.hasStrict(
+ await convert({
+ info: {
+ comboStatus: MaimaiComboStatus.MAIMAI_COMBO_STATUS_FULL_COMBO_PLUS,
+ isClear: true,
+ },
+ }),
+ {
+ dryScore: { scoreData: { lamp: "FULL COMBO+" } },
+ }
+ );
+ t.hasStrict(
+ await convert({
+ info: {
+ comboStatus: MaimaiComboStatus.MAIMAI_COMBO_STATUS_FULL_COMBO,
+ isClear: true,
+ },
+ }),
+ {
+ dryScore: { scoreData: { lamp: "FULL COMBO" } },
+ }
+ );
+ t.hasStrict(
+ await convert({
+ info: {
+ comboStatus: MaimaiComboStatus.MAIMAI_COMBO_STATUS_NONE,
+ isClear: true,
+ },
+ }),
+ {
+ dryScore: { scoreData: { lamp: "CLEAR" } },
+ }
+ );
+ t.hasStrict(
+ await convert({
+ info: {
+ comboStatus: MaimaiComboStatus.MAIMAI_COMBO_STATUS_NONE,
+ isClear: false,
+ },
+ }),
+ {
+ dryScore: { scoreData: { lamp: "FAILED" } },
+ }
+ );
+ t.hasStrict(
+ await convert({
+ info: {
+ comboStatus: MaimaiComboStatus.MAIMAI_COMBO_STATUS_FULL_COMBO,
+ isClear: false,
+ },
+ }),
+ {
+ dryScore: { scoreData: { lamp: "FAILED" } },
+ }
+ );
+ t.end();
+ });
+
+ t.test("Should throw on missing chart", (t) => {
+ t.rejects(
+ () =>
+ convert({
+ info: { musicId: 999999, level: MaimaiLevel.MAIMAI_LEVEL_MASTER },
+ }),
+ {
+ message: /Can't find chart with id 999999 and difficulty DX Master/u,
+ }
+ );
+ t.end();
+ });
+
+ t.end();
+});
diff --git a/server/src/lib/score-import/import-types/api/myt-maimaidx/converter.ts b/server/src/lib/score-import/import-types/api/myt-maimaidx/converter.ts
new file mode 100644
index 000000000..8568ddf1e
--- /dev/null
+++ b/server/src/lib/score-import/import-types/api/myt-maimaidx/converter.ts
@@ -0,0 +1,141 @@
+import {
+ InternalFailure,
+ InvalidScoreFailure,
+ SkipScoreFailure,
+ SongOrChartNotFoundFailure,
+} from "lib/score-import/framework/common/converter-failures";
+import { ParseDateFromString } from "lib/score-import/framework/common/score-utils";
+import { MaimaiComboStatus, MaimaiLevel } from "proto/generated/maimai/common_pb";
+import { FindChartOnInGameID } from "utils/queries/charts";
+import { FindSongOnID } from "utils/queries/songs";
+import type { ConverterFunction } from "../../common/types";
+import type { MytMaimaiDxScore } from "./types";
+import type { DryScore } from "lib/score-import/framework/common/types";
+import type { ScoreData } from "tachi-common";
+import type { EmptyObject } from "utils/types";
+
+const DIFFICULTIES = {
+ [MaimaiLevel.MAIMAI_LEVEL_UNSPECIFIED]: undefined,
+ [MaimaiLevel.MAIMAI_LEVEL_BASIC]: "Basic",
+ [MaimaiLevel.MAIMAI_LEVEL_ADVANCED]: "Advanced",
+ [MaimaiLevel.MAIMAI_LEVEL_EXPERT]: "Expert",
+ [MaimaiLevel.MAIMAI_LEVEL_MASTER]: "Master",
+ [MaimaiLevel.MAIMAI_LEVEL_REMASTER]: "Re:Master",
+ [MaimaiLevel.MAIMAI_LEVEL_UTAGE]: "Utage",
+};
+
+function getLamp(
+ comboStatus: number,
+ isClear: boolean
+): ScoreData<"maimaidx:Single">["lamp"] | undefined {
+ if (comboStatus === MaimaiComboStatus.MAIMAI_COMBO_STATUS_UNSPECIFIED) {
+ return undefined;
+ }
+
+ if (!isClear) {
+ return "FAILED";
+ }
+
+ if (comboStatus === MaimaiComboStatus.MAIMAI_COMBO_STATUS_NONE) {
+ return "CLEAR";
+ }
+
+ if (comboStatus === MaimaiComboStatus.MAIMAI_COMBO_STATUS_FULL_COMBO) {
+ return "FULL COMBO";
+ }
+
+ if (comboStatus === MaimaiComboStatus.MAIMAI_COMBO_STATUS_FULL_COMBO_PLUS) {
+ return "FULL COMBO+";
+ }
+
+ if (comboStatus === MaimaiComboStatus.MAIMAI_COMBO_STATUS_ALL_PERFECT) {
+ return "ALL PERFECT";
+ }
+
+ if (comboStatus === MaimaiComboStatus.MAIMAI_COMBO_STATUS_ALL_PERFECT_PLUS) {
+ return "ALL PERFECT+";
+ }
+
+ return undefined;
+}
+
+const ConvertAPIMytMaimaiDx: ConverterFunction = async (
+ data,
+ _context,
+ importType,
+ logger
+) => {
+ if (data.info === undefined || data.judge === undefined) {
+ throw new InvalidScoreFailure("Failed to receive score data from MYT API");
+ }
+
+ const baseDifficulty = DIFFICULTIES[data.info.level];
+
+ if (baseDifficulty === undefined) {
+ throw new InvalidScoreFailure(
+ `Can't process a score with unspecified difficulty (musicId ${data.info.musicId})`
+ );
+ }
+
+ if (baseDifficulty === "Utage") {
+ throw new SkipScoreFailure("Utage charts are not supported");
+ }
+
+ // Songs with an ID higher than 10000 are considered DX charts
+ const difficulty = data.info.musicId >= 10000 ? `DX ${baseDifficulty}` : baseDifficulty;
+
+ const lamp = getLamp(data.info.comboStatus, data.info.isClear);
+
+ if (lamp === undefined) {
+ throw new InvalidScoreFailure(
+ "Can't process a score with an invalid combo status and/or clear status"
+ );
+ }
+
+ const chart = await FindChartOnInGameID("maimaidx", data.info.musicId, "Single", difficulty);
+
+ if (chart === null) {
+ throw new SongOrChartNotFoundFailure(
+ `Can't find chart with id ${data.info.musicId} and difficulty ${difficulty}`,
+ importType,
+ data,
+ {}
+ );
+ }
+
+ const song = await FindSongOnID("maimaidx", chart.songID);
+
+ if (song === null) {
+ logger.severe(`Song/chart desync: ${chart.songID} for chart ${chart.chartID}`, { chart });
+ throw new InternalFailure(`Song/chart desync: ${chart.songID} for chart ${chart.chartID}`);
+ }
+
+ const dryScore: DryScore<"maimaidx:Single"> = {
+ service: "MYT",
+ game: "maimaidx",
+ scoreMeta: {},
+ timeAchieved: ParseDateFromString(data.info.userPlayDate),
+ comment: null,
+ importType,
+ scoreData: {
+ percent: data.info.achievement / 10000,
+ lamp,
+ judgements: {
+ pcrit: data.judge.judgeCriticalPerfect,
+ perfect: data.judge.judgePerfect,
+ great: data.judge.judgeGreat,
+ good: data.judge.judgeGood,
+ miss: data.judge.judgeMiss,
+ },
+ optional: {
+ fast: data.judge.fastCount,
+ slow: data.judge.lateCount,
+ maxCombo: data.judge.maxCombo,
+ },
+ },
+ };
+
+ return { chart, song, dryScore };
+};
+
+export default ConvertAPIMytMaimaiDx;
diff --git a/server/src/lib/score-import/import-types/api/myt-maimaidx/parser.ts b/server/src/lib/score-import/import-types/api/myt-maimaidx/parser.ts
new file mode 100644
index 000000000..3a388135b
--- /dev/null
+++ b/server/src/lib/score-import/import-types/api/myt-maimaidx/parser.ts
@@ -0,0 +1,56 @@
+import {
+ FetchMytTitleAPIID,
+ GetMytHostname,
+ StreamRPCAsAsync,
+} from "../../common/api-myt/traverse-api";
+import { credentials } from "@grpc/grpc-js";
+import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error";
+import { MaimaiUserClient } from "proto/generated/maimai/user_grpc_pb";
+import { GetPlaylogRequest } from "proto/generated/maimai/user_pb";
+import type { ParserFunctionReturns } from "../../common/types";
+import type { MytMaimaiDxScore } from "./types";
+import type { KtLogger } from "lib/logger/logger";
+import type { GetPlaylogStreamItem } from "proto/generated/maimai/user_pb";
+import type { integer } from "tachi-common";
+import type { EmptyObject } from "utils/types";
+
+async function* getObjectsFromGrpcIterable(
+ iterable: AsyncIterable
+): AsyncIterable {
+ for await (const item of iterable) {
+ yield item.toObject();
+ }
+}
+
+export default async function ParseMytMaimaiDx(
+ userID: integer,
+ logger: KtLogger
+): Promise> {
+ const profileApiId = await FetchMytTitleAPIID(userID, "maimaidx", logger);
+ const endpoint = GetMytHostname();
+ const client = new MaimaiUserClient(endpoint, credentials.createSsl());
+ const request = new GetPlaylogRequest();
+
+ request.setProfileApiId(profileApiId);
+
+ let iterable;
+
+ try {
+ const stream = StreamRPCAsAsync(client.getPlaylog.bind(client), request, logger);
+
+ iterable = getObjectsFromGrpcIterable(stream);
+ } catch (err) {
+ logger.error(
+ `Unexpected MYT error while streaming maimai DX playlog items for userID ${userID}: ${err}`
+ );
+
+ throw new ScoreImportFatalError(500, `Failed to get scores from MYT.`);
+ }
+
+ return {
+ iterable,
+ context: {},
+ classProvider: null,
+ game: "maimaidx",
+ };
+}
diff --git a/server/src/lib/score-import/import-types/api/myt-maimaidx/types.ts b/server/src/lib/score-import/import-types/api/myt-maimaidx/types.ts
new file mode 100644
index 000000000..e2bca355c
--- /dev/null
+++ b/server/src/lib/score-import/import-types/api/myt-maimaidx/types.ts
@@ -0,0 +1,3 @@
+import type { GetPlaylogStreamItem } from "proto/generated/maimai/user_pb";
+
+export type MytMaimaiDxScore = GetPlaylogStreamItem.AsObject;
diff --git a/server/src/lib/score-import/import-types/common/types.ts b/server/src/lib/score-import/import-types/common/types.ts
index 7ccbf3a22..cd8f466c1 100644
--- a/server/src/lib/score-import/import-types/common/types.ts
+++ b/server/src/lib/score-import/import-types/common/types.ts
@@ -1,6 +1,7 @@
import type { ConverterFailure } from "../../framework/common/converter-failures";
import type { DryScore } from "../../framework/common/types";
import type { MytChunithmScore } from "../api/myt-chunithm/types";
+import type { MytMaimaiDxScore } from "../api/myt-maimaidx/types";
import type { MytOngekiScore } from "../api/myt-ongeki/types";
import type { MytWaccaScore } from "../api/myt-wacca/types";
import type { SDVXEamusementCSVData } from "../file/eamusement-sdvx-csv/types";
@@ -59,6 +60,7 @@ export interface ImportTypeDataMap {
"api/eag-sdvx": unknown;
"api/myt-chunithm": MytChunithmScore;
+ "api/myt-maimaidx": MytMaimaiDxScore;
"api/myt-ongeki": MytOngekiScore;
"api/myt-wacca": MytWaccaScore;
@@ -104,6 +106,7 @@ export interface ImportTypeContextMap {
"api/eag-iidx": KaiContext;
"api/eag-sdvx": KaiContext;
"api/myt-chunithm": EmptyObject;
+ "api/myt-maimaidx": EmptyObject;
"api/myt-ongeki": EmptyObject;
"api/myt-wacca": EmptyObject;
diff --git a/server/src/lib/score-import/import-types/converters.ts b/server/src/lib/score-import/import-types/converters.ts
index e1fbdeacd..d1168e29c 100644
--- a/server/src/lib/score-import/import-types/converters.ts
+++ b/server/src/lib/score-import/import-types/converters.ts
@@ -1,4 +1,5 @@
import ConvertAPIMytChunithm from "./api/myt-chunithm/converter";
+import ConvertAPIMytMaimaiDx from "./api/myt-maimaidx/converter";
import ConvertAPIMytOngeki from "./api/myt-ongeki/converter";
import ConvertAPIMytWACCA from "./api/myt-wacca/converter";
import { ConverterAPICGMuseca } from "./common/api-cg/museca/converter";
@@ -46,6 +47,7 @@ export const Converters: ConverterMap = {
"api/flo-sdvx": ConvertAPIKaiSDVX,
"api/min-sdvx": ConvertAPIKaiSDVX,
"api/myt-chunithm": ConvertAPIMytChunithm,
+ "api/myt-maimaidx": ConvertAPIMytMaimaiDx,
"api/myt-ongeki": ConvertAPIMytOngeki,
"api/myt-wacca": ConvertAPIMytWACCA,
diff --git a/server/src/lib/score-import/import-types/parsers.ts b/server/src/lib/score-import/import-types/parsers.ts
index c75a358c6..18a056930 100644
--- a/server/src/lib/score-import/import-types/parsers.ts
+++ b/server/src/lib/score-import/import-types/parsers.ts
@@ -4,6 +4,7 @@ import { ParseFloIIDX } from "./api/flo-iidx/parser";
import { ParseFloSDVX } from "./api/flo-sdvx/parser";
import { ParseMinSDVX } from "./api/min-sdvx/parser";
import ParseMytChunithm from "./api/myt-chunithm/parser";
+import ParseMytMaimaiDx from "./api/myt-maimaidx/parser";
import ParseMytOngeki from "./api/myt-ongeki/parser";
import ParseMytWACCA from "./api/myt-wacca/parser";
import {
@@ -68,6 +69,7 @@ export const Parsers = {
"api/cg-gan-museca": ParseCGGanMuseca,
"api/myt-chunithm": ParseMytChunithm,
+ "api/myt-maimaidx": ParseMytMaimaiDx,
"api/myt-ongeki": ParseMytOngeki,
"api/myt-wacca": ParseMytWACCA,
diff --git a/server/src/proto/generated/maimai/common_grpc_pb.d.ts b/server/src/proto/generated/maimai/common_grpc_pb.d.ts
new file mode 100644
index 000000000..51b4d6959
--- /dev/null
+++ b/server/src/proto/generated/maimai/common_grpc_pb.d.ts
@@ -0,0 +1 @@
+// GENERATED CODE -- NO SERVICES IN PROTO
diff --git a/server/src/proto/generated/maimai/common_grpc_pb.js b/server/src/proto/generated/maimai/common_grpc_pb.js
new file mode 100644
index 000000000..97b3a2461
--- /dev/null
+++ b/server/src/proto/generated/maimai/common_grpc_pb.js
@@ -0,0 +1 @@
+// GENERATED CODE -- NO SERVICES IN PROTO
\ No newline at end of file
diff --git a/server/src/proto/generated/maimai/common_pb.d.ts b/server/src/proto/generated/maimai/common_pb.d.ts
new file mode 100644
index 000000000..88168cc88
--- /dev/null
+++ b/server/src/proto/generated/maimai/common_pb.d.ts
@@ -0,0 +1,67 @@
+// package: mythos.maimai.v0
+// file: maimai/common.proto
+
+import * as jspb from "google-protobuf";
+
+export interface MaimaiRankingTypeMap {
+ MAIMAI_RANKING_TYPE_UNSPECIFIED: 0;
+ MAIMAI_RANKING_TYPE_ACHIEVEMENT: 1;
+ MAIMAI_RANKING_TYPE_DX_SCORE: 2;
+}
+
+export const MaimaiRankingType: MaimaiRankingTypeMap;
+
+export interface MaimaiLevelMap {
+ MAIMAI_LEVEL_UNSPECIFIED: 0;
+ MAIMAI_LEVEL_BASIC: 1;
+ MAIMAI_LEVEL_ADVANCED: 2;
+ MAIMAI_LEVEL_EXPERT: 3;
+ MAIMAI_LEVEL_MASTER: 4;
+ MAIMAI_LEVEL_REMASTER: 5;
+ MAIMAI_LEVEL_UTAGE: 6;
+}
+
+export const MaimaiLevel: MaimaiLevelMap;
+
+export interface MaimaiComboStatusMap {
+ MAIMAI_COMBO_STATUS_UNSPECIFIED: 0;
+ MAIMAI_COMBO_STATUS_NONE: 1;
+ MAIMAI_COMBO_STATUS_FULL_COMBO: 2;
+ MAIMAI_COMBO_STATUS_FULL_COMBO_PLUS: 3;
+ MAIMAI_COMBO_STATUS_ALL_PERFECT: 4;
+ MAIMAI_COMBO_STATUS_ALL_PERFECT_PLUS: 5;
+}
+
+export const MaimaiComboStatus: MaimaiComboStatusMap;
+
+export interface MaimaiSyncStatusMap {
+ MAIMAI_SYNC_STATUS_UNSPECIFIED: 0;
+ MAIMAI_SYNC_STATUS_NONE: 1;
+ MAIMAI_SYNC_STATUS_FULL_SYNC: 2;
+ MAIMAI_SYNC_STATUS_FULL_SYNC_PLUS: 3;
+ MAIMAI_SYNC_STATUS_FULL_SYNC_DX: 4;
+ MAIMAI_SYNC_STATUS_FULL_SYNC_DX_PLUS: 5;
+}
+
+export const MaimaiSyncStatus: MaimaiSyncStatusMap;
+
+export interface MaimaiScoreRankMap {
+ MAIMAI_SCORE_RANK_UNSPECIFIED: 0;
+ MAIMAI_SCORE_RANK_D: 1;
+ MAIMAI_SCORE_RANK_C: 2;
+ MAIMAI_SCORE_RANK_B: 3;
+ MAIMAI_SCORE_RANK_BB: 4;
+ MAIMAI_SCORE_RANK_BBB: 5;
+ MAIMAI_SCORE_RANK_A: 6;
+ MAIMAI_SCORE_RANK_AA: 7;
+ MAIMAI_SCORE_RANK_AAA: 8;
+ MAIMAI_SCORE_RANK_S: 9;
+ MAIMAI_SCORE_RANK_S_PLUS: 10;
+ MAIMAI_SCORE_RANK_SS: 11;
+ MAIMAI_SCORE_RANK_SS_PLUS: 12;
+ MAIMAI_SCORE_RANK_SSS: 13;
+ MAIMAI_SCORE_RANK_SSS_PLUS: 14;
+}
+
+export const MaimaiScoreRank: MaimaiScoreRankMap;
+
diff --git a/server/src/proto/generated/maimai/common_pb.js b/server/src/proto/generated/maimai/common_pb.js
new file mode 100644
index 000000000..ebec3d669
--- /dev/null
+++ b/server/src/proto/generated/maimai/common_pb.js
@@ -0,0 +1,96 @@
+// source: maimai/common.proto
+/**
+ * @fileoverview
+ * @enhanceable
+ * @suppress {missingRequire} reports error on implicit type usages.
+ * @suppress {messageConventions} JS Compiler reports an error if a variable or
+ * field starts with 'MSG_' and isn't a translatable message.
+ * @public
+ */
+// GENERATED CODE -- DO NOT EDIT!
+/* eslint-disable */
+// @ts-nocheck
+
+var jspb = require('google-protobuf');
+var goog = jspb;
+var global = (function() {
+ if (this) { return this; }
+ if (typeof window !== 'undefined') { return window; }
+ if (typeof global !== 'undefined') { return global; }
+ if (typeof self !== 'undefined') { return self; }
+ return Function('return this')();
+}.call(null));
+
+goog.exportSymbol('proto.mythos.maimai.v0.MaimaiComboStatus', null, global);
+goog.exportSymbol('proto.mythos.maimai.v0.MaimaiLevel', null, global);
+goog.exportSymbol('proto.mythos.maimai.v0.MaimaiRankingType', null, global);
+goog.exportSymbol('proto.mythos.maimai.v0.MaimaiScoreRank', null, global);
+goog.exportSymbol('proto.mythos.maimai.v0.MaimaiSyncStatus', null, global);
+/**
+ * @enum {number}
+ */
+proto.mythos.maimai.v0.MaimaiRankingType = {
+ MAIMAI_RANKING_TYPE_UNSPECIFIED: 0,
+ MAIMAI_RANKING_TYPE_ACHIEVEMENT: 1,
+ MAIMAI_RANKING_TYPE_DX_SCORE: 2
+};
+
+/**
+ * @enum {number}
+ */
+proto.mythos.maimai.v0.MaimaiLevel = {
+ MAIMAI_LEVEL_UNSPECIFIED: 0,
+ MAIMAI_LEVEL_BASIC: 1,
+ MAIMAI_LEVEL_ADVANCED: 2,
+ MAIMAI_LEVEL_EXPERT: 3,
+ MAIMAI_LEVEL_MASTER: 4,
+ MAIMAI_LEVEL_REMASTER: 5,
+ MAIMAI_LEVEL_UTAGE: 6
+};
+
+/**
+ * @enum {number}
+ */
+proto.mythos.maimai.v0.MaimaiComboStatus = {
+ MAIMAI_COMBO_STATUS_UNSPECIFIED: 0,
+ MAIMAI_COMBO_STATUS_NONE: 1,
+ MAIMAI_COMBO_STATUS_FULL_COMBO: 2,
+ MAIMAI_COMBO_STATUS_FULL_COMBO_PLUS: 3,
+ MAIMAI_COMBO_STATUS_ALL_PERFECT: 4,
+ MAIMAI_COMBO_STATUS_ALL_PERFECT_PLUS: 5
+};
+
+/**
+ * @enum {number}
+ */
+proto.mythos.maimai.v0.MaimaiSyncStatus = {
+ MAIMAI_SYNC_STATUS_UNSPECIFIED: 0,
+ MAIMAI_SYNC_STATUS_NONE: 1,
+ MAIMAI_SYNC_STATUS_FULL_SYNC: 2,
+ MAIMAI_SYNC_STATUS_FULL_SYNC_PLUS: 3,
+ MAIMAI_SYNC_STATUS_FULL_SYNC_DX: 4,
+ MAIMAI_SYNC_STATUS_FULL_SYNC_DX_PLUS: 5
+};
+
+/**
+ * @enum {number}
+ */
+proto.mythos.maimai.v0.MaimaiScoreRank = {
+ MAIMAI_SCORE_RANK_UNSPECIFIED: 0,
+ MAIMAI_SCORE_RANK_D: 1,
+ MAIMAI_SCORE_RANK_C: 2,
+ MAIMAI_SCORE_RANK_B: 3,
+ MAIMAI_SCORE_RANK_BB: 4,
+ MAIMAI_SCORE_RANK_BBB: 5,
+ MAIMAI_SCORE_RANK_A: 6,
+ MAIMAI_SCORE_RANK_AA: 7,
+ MAIMAI_SCORE_RANK_AAA: 8,
+ MAIMAI_SCORE_RANK_S: 9,
+ MAIMAI_SCORE_RANK_S_PLUS: 10,
+ MAIMAI_SCORE_RANK_SS: 11,
+ MAIMAI_SCORE_RANK_SS_PLUS: 12,
+ MAIMAI_SCORE_RANK_SSS: 13,
+ MAIMAI_SCORE_RANK_SSS_PLUS: 14
+};
+
+goog.object.extend(exports, proto.mythos.maimai.v0);
diff --git a/server/src/proto/generated/maimai/playlog_grpc_pb.d.ts b/server/src/proto/generated/maimai/playlog_grpc_pb.d.ts
new file mode 100644
index 000000000..51b4d6959
--- /dev/null
+++ b/server/src/proto/generated/maimai/playlog_grpc_pb.d.ts
@@ -0,0 +1 @@
+// GENERATED CODE -- NO SERVICES IN PROTO
diff --git a/server/src/proto/generated/maimai/playlog_grpc_pb.js b/server/src/proto/generated/maimai/playlog_grpc_pb.js
new file mode 100644
index 000000000..97b3a2461
--- /dev/null
+++ b/server/src/proto/generated/maimai/playlog_grpc_pb.js
@@ -0,0 +1 @@
+// GENERATED CODE -- NO SERVICES IN PROTO
\ No newline at end of file
diff --git a/server/src/proto/generated/maimai/playlog_pb.d.ts b/server/src/proto/generated/maimai/playlog_pb.d.ts
new file mode 100644
index 000000000..e5578e5f4
--- /dev/null
+++ b/server/src/proto/generated/maimai/playlog_pb.d.ts
@@ -0,0 +1,118 @@
+// package: mythos.maimai.v0
+// file: maimai/playlog.proto
+
+import * as jspb from "google-protobuf";
+import * as maimai_common_pb from "../maimai/common_pb";
+
+export class PlaylogInfo extends jspb.Message {
+ getMusicId(): number;
+ setMusicId(value: number): void;
+
+ getLevel(): maimai_common_pb.MaimaiLevelMap[keyof maimai_common_pb.MaimaiLevelMap];
+ setLevel(value: maimai_common_pb.MaimaiLevelMap[keyof maimai_common_pb.MaimaiLevelMap]): void;
+
+ getAchievement(): number;
+ setAchievement(value: number): void;
+
+ getDeluxscore(): number;
+ setDeluxscore(value: number): void;
+
+ getScoreRank(): maimai_common_pb.MaimaiScoreRankMap[keyof maimai_common_pb.MaimaiScoreRankMap];
+ setScoreRank(value: maimai_common_pb.MaimaiScoreRankMap[keyof maimai_common_pb.MaimaiScoreRankMap]): void;
+
+ getComboStatus(): maimai_common_pb.MaimaiComboStatusMap[keyof maimai_common_pb.MaimaiComboStatusMap];
+ setComboStatus(value: maimai_common_pb.MaimaiComboStatusMap[keyof maimai_common_pb.MaimaiComboStatusMap]): void;
+
+ getSyncStatus(): maimai_common_pb.MaimaiSyncStatusMap[keyof maimai_common_pb.MaimaiSyncStatusMap];
+ setSyncStatus(value: maimai_common_pb.MaimaiSyncStatusMap[keyof maimai_common_pb.MaimaiSyncStatusMap]): void;
+
+ getIsClear(): boolean;
+ setIsClear(value: boolean): void;
+
+ getIsAchieveNewRecord(): boolean;
+ setIsAchieveNewRecord(value: boolean): void;
+
+ getIsDeluxscoreNewRecord(): boolean;
+ setIsDeluxscoreNewRecord(value: boolean): void;
+
+ getTrack(): number;
+ setTrack(value: number): void;
+
+ getUserPlayDate(): string;
+ setUserPlayDate(value: string): void;
+
+ serializeBinary(): Uint8Array;
+ toObject(includeInstance?: boolean): PlaylogInfo.AsObject;
+ static toObject(includeInstance: boolean, msg: PlaylogInfo): PlaylogInfo.AsObject;
+ static extensions: {[key: number]: jspb.ExtensionFieldInfo};
+ static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo};
+ static serializeBinaryToWriter(message: PlaylogInfo, writer: jspb.BinaryWriter): void;
+ static deserializeBinary(bytes: Uint8Array): PlaylogInfo;
+ static deserializeBinaryFromReader(message: PlaylogInfo, reader: jspb.BinaryReader): PlaylogInfo;
+}
+
+export namespace PlaylogInfo {
+ export type AsObject = {
+ musicId: number,
+ level: maimai_common_pb.MaimaiLevelMap[keyof maimai_common_pb.MaimaiLevelMap],
+ achievement: number,
+ deluxscore: number,
+ scoreRank: maimai_common_pb.MaimaiScoreRankMap[keyof maimai_common_pb.MaimaiScoreRankMap],
+ comboStatus: maimai_common_pb.MaimaiComboStatusMap[keyof maimai_common_pb.MaimaiComboStatusMap],
+ syncStatus: maimai_common_pb.MaimaiSyncStatusMap[keyof maimai_common_pb.MaimaiSyncStatusMap],
+ isClear: boolean,
+ isAchieveNewRecord: boolean,
+ isDeluxscoreNewRecord: boolean,
+ track: number,
+ userPlayDate: string,
+ }
+}
+
+export class PlaylogJudge extends jspb.Message {
+ getJudgeCriticalPerfect(): number;
+ setJudgeCriticalPerfect(value: number): void;
+
+ getJudgePerfect(): number;
+ setJudgePerfect(value: number): void;
+
+ getJudgeGreat(): number;
+ setJudgeGreat(value: number): void;
+
+ getJudgeGood(): number;
+ setJudgeGood(value: number): void;
+
+ getJudgeMiss(): number;
+ setJudgeMiss(value: number): void;
+
+ getMaxCombo(): number;
+ setMaxCombo(value: number): void;
+
+ getFastCount(): number;
+ setFastCount(value: number): void;
+
+ getLateCount(): number;
+ setLateCount(value: number): void;
+
+ serializeBinary(): Uint8Array;
+ toObject(includeInstance?: boolean): PlaylogJudge.AsObject;
+ static toObject(includeInstance: boolean, msg: PlaylogJudge): PlaylogJudge.AsObject;
+ static extensions: {[key: number]: jspb.ExtensionFieldInfo};
+ static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo};
+ static serializeBinaryToWriter(message: PlaylogJudge, writer: jspb.BinaryWriter): void;
+ static deserializeBinary(bytes: Uint8Array): PlaylogJudge;
+ static deserializeBinaryFromReader(message: PlaylogJudge, reader: jspb.BinaryReader): PlaylogJudge;
+}
+
+export namespace PlaylogJudge {
+ export type AsObject = {
+ judgeCriticalPerfect: number,
+ judgePerfect: number,
+ judgeGreat: number,
+ judgeGood: number,
+ judgeMiss: number,
+ maxCombo: number,
+ fastCount: number,
+ lateCount: number,
+ }
+}
+
diff --git a/server/src/proto/generated/maimai/playlog_pb.js b/server/src/proto/generated/maimai/playlog_pb.js
new file mode 100644
index 000000000..1ffd3135e
--- /dev/null
+++ b/server/src/proto/generated/maimai/playlog_pb.js
@@ -0,0 +1,870 @@
+// source: maimai/playlog.proto
+/**
+ * @fileoverview
+ * @enhanceable
+ * @suppress {missingRequire} reports error on implicit type usages.
+ * @suppress {messageConventions} JS Compiler reports an error if a variable or
+ * field starts with 'MSG_' and isn't a translatable message.
+ * @public
+ */
+// GENERATED CODE -- DO NOT EDIT!
+/* eslint-disable */
+// @ts-nocheck
+
+var jspb = require('google-protobuf');
+var goog = jspb;
+var global = (function() {
+ if (this) { return this; }
+ if (typeof window !== 'undefined') { return window; }
+ if (typeof global !== 'undefined') { return global; }
+ if (typeof self !== 'undefined') { return self; }
+ return Function('return this')();
+}.call(null));
+
+var maimai_common_pb = require('../maimai/common_pb.js');
+goog.object.extend(proto, maimai_common_pb);
+goog.exportSymbol('proto.mythos.maimai.v0.PlaylogInfo', null, global);
+goog.exportSymbol('proto.mythos.maimai.v0.PlaylogJudge', null, global);
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.mythos.maimai.v0.PlaylogInfo = function(opt_data) {
+ jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.mythos.maimai.v0.PlaylogInfo, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+ /**
+ * @public
+ * @override
+ */
+ proto.mythos.maimai.v0.PlaylogInfo.displayName = 'proto.mythos.maimai.v0.PlaylogInfo';
+}
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.mythos.maimai.v0.PlaylogJudge = function(opt_data) {
+ jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.mythos.maimai.v0.PlaylogJudge, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+ /**
+ * @public
+ * @override
+ */
+ proto.mythos.maimai.v0.PlaylogJudge.displayName = 'proto.mythos.maimai.v0.PlaylogJudge';
+}
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ * net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ * JSPB instance for transitional soy proto support:
+ * http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.toObject = function(opt_includeInstance) {
+ return proto.mythos.maimai.v0.PlaylogInfo.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ * the JSPB instance for transitional soy proto support:
+ * http://goto/soy-param-migration
+ * @param {!proto.mythos.maimai.v0.PlaylogInfo} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.mythos.maimai.v0.PlaylogInfo.toObject = function(includeInstance, msg) {
+ var f, obj = {
+ musicId: jspb.Message.getFieldWithDefault(msg, 1, 0),
+ level: jspb.Message.getFieldWithDefault(msg, 2, 0),
+ achievement: jspb.Message.getFieldWithDefault(msg, 3, 0),
+ deluxscore: jspb.Message.getFieldWithDefault(msg, 4, 0),
+ scoreRank: jspb.Message.getFieldWithDefault(msg, 5, 0),
+ comboStatus: jspb.Message.getFieldWithDefault(msg, 6, 0),
+ syncStatus: jspb.Message.getFieldWithDefault(msg, 7, 0),
+ isClear: jspb.Message.getBooleanFieldWithDefault(msg, 8, false),
+ isAchieveNewRecord: jspb.Message.getBooleanFieldWithDefault(msg, 9, false),
+ isDeluxscoreNewRecord: jspb.Message.getBooleanFieldWithDefault(msg, 10, false),
+ track: jspb.Message.getFieldWithDefault(msg, 11, 0),
+ userPlayDate: jspb.Message.getFieldWithDefault(msg, 12, "")
+ };
+
+ if (includeInstance) {
+ obj.$jspbMessageInstance = msg;
+ }
+ return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.deserializeBinary = function(bytes) {
+ var reader = new jspb.BinaryReader(bytes);
+ var msg = new proto.mythos.maimai.v0.PlaylogInfo;
+ return proto.mythos.maimai.v0.PlaylogInfo.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.mythos.maimai.v0.PlaylogInfo} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.deserializeBinaryFromReader = function(msg, reader) {
+ while (reader.nextField()) {
+ if (reader.isEndGroup()) {
+ break;
+ }
+ var field = reader.getFieldNumber();
+ switch (field) {
+ case 1:
+ var value = /** @type {number} */ (reader.readInt32());
+ msg.setMusicId(value);
+ break;
+ case 2:
+ var value = /** @type {!proto.mythos.maimai.v0.MaimaiLevel} */ (reader.readEnum());
+ msg.setLevel(value);
+ break;
+ case 3:
+ var value = /** @type {number} */ (reader.readInt32());
+ msg.setAchievement(value);
+ break;
+ case 4:
+ var value = /** @type {number} */ (reader.readInt32());
+ msg.setDeluxscore(value);
+ break;
+ case 5:
+ var value = /** @type {!proto.mythos.maimai.v0.MaimaiScoreRank} */ (reader.readEnum());
+ msg.setScoreRank(value);
+ break;
+ case 6:
+ var value = /** @type {!proto.mythos.maimai.v0.MaimaiComboStatus} */ (reader.readEnum());
+ msg.setComboStatus(value);
+ break;
+ case 7:
+ var value = /** @type {!proto.mythos.maimai.v0.MaimaiSyncStatus} */ (reader.readEnum());
+ msg.setSyncStatus(value);
+ break;
+ case 8:
+ var value = /** @type {boolean} */ (reader.readBool());
+ msg.setIsClear(value);
+ break;
+ case 9:
+ var value = /** @type {boolean} */ (reader.readBool());
+ msg.setIsAchieveNewRecord(value);
+ break;
+ case 10:
+ var value = /** @type {boolean} */ (reader.readBool());
+ msg.setIsDeluxscoreNewRecord(value);
+ break;
+ case 11:
+ var value = /** @type {number} */ (reader.readInt32());
+ msg.setTrack(value);
+ break;
+ case 12:
+ var value = /** @type {string} */ (reader.readString());
+ msg.setUserPlayDate(value);
+ break;
+ default:
+ reader.skipField();
+ break;
+ }
+ }
+ return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.serializeBinary = function() {
+ var writer = new jspb.BinaryWriter();
+ proto.mythos.maimai.v0.PlaylogInfo.serializeBinaryToWriter(this, writer);
+ return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.mythos.maimai.v0.PlaylogInfo} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.mythos.maimai.v0.PlaylogInfo.serializeBinaryToWriter = function(message, writer) {
+ var f = undefined;
+ f = message.getMusicId();
+ if (f !== 0) {
+ writer.writeInt32(
+ 1,
+ f
+ );
+ }
+ f = message.getLevel();
+ if (f !== 0.0) {
+ writer.writeEnum(
+ 2,
+ f
+ );
+ }
+ f = message.getAchievement();
+ if (f !== 0) {
+ writer.writeInt32(
+ 3,
+ f
+ );
+ }
+ f = message.getDeluxscore();
+ if (f !== 0) {
+ writer.writeInt32(
+ 4,
+ f
+ );
+ }
+ f = message.getScoreRank();
+ if (f !== 0.0) {
+ writer.writeEnum(
+ 5,
+ f
+ );
+ }
+ f = message.getComboStatus();
+ if (f !== 0.0) {
+ writer.writeEnum(
+ 6,
+ f
+ );
+ }
+ f = message.getSyncStatus();
+ if (f !== 0.0) {
+ writer.writeEnum(
+ 7,
+ f
+ );
+ }
+ f = message.getIsClear();
+ if (f) {
+ writer.writeBool(
+ 8,
+ f
+ );
+ }
+ f = message.getIsAchieveNewRecord();
+ if (f) {
+ writer.writeBool(
+ 9,
+ f
+ );
+ }
+ f = message.getIsDeluxscoreNewRecord();
+ if (f) {
+ writer.writeBool(
+ 10,
+ f
+ );
+ }
+ f = message.getTrack();
+ if (f !== 0) {
+ writer.writeInt32(
+ 11,
+ f
+ );
+ }
+ f = message.getUserPlayDate();
+ if (f.length > 0) {
+ writer.writeString(
+ 12,
+ f
+ );
+ }
+};
+
+
+/**
+ * optional int32 music_id = 1;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.getMusicId = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo} returns this
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.setMusicId = function(value) {
+ return jspb.Message.setProto3IntField(this, 1, value);
+};
+
+
+/**
+ * optional MaimaiLevel level = 2;
+ * @return {!proto.mythos.maimai.v0.MaimaiLevel}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.getLevel = function() {
+ return /** @type {!proto.mythos.maimai.v0.MaimaiLevel} */ (jspb.Message.getFieldWithDefault(this, 2, 0));
+};
+
+
+/**
+ * @param {!proto.mythos.maimai.v0.MaimaiLevel} value
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo} returns this
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.setLevel = function(value) {
+ return jspb.Message.setProto3EnumField(this, 2, value);
+};
+
+
+/**
+ * optional int32 achievement = 3;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.getAchievement = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo} returns this
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.setAchievement = function(value) {
+ return jspb.Message.setProto3IntField(this, 3, value);
+};
+
+
+/**
+ * optional int32 deluxscore = 4;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.getDeluxscore = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 4, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo} returns this
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.setDeluxscore = function(value) {
+ return jspb.Message.setProto3IntField(this, 4, value);
+};
+
+
+/**
+ * optional MaimaiScoreRank score_rank = 5;
+ * @return {!proto.mythos.maimai.v0.MaimaiScoreRank}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.getScoreRank = function() {
+ return /** @type {!proto.mythos.maimai.v0.MaimaiScoreRank} */ (jspb.Message.getFieldWithDefault(this, 5, 0));
+};
+
+
+/**
+ * @param {!proto.mythos.maimai.v0.MaimaiScoreRank} value
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo} returns this
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.setScoreRank = function(value) {
+ return jspb.Message.setProto3EnumField(this, 5, value);
+};
+
+
+/**
+ * optional MaimaiComboStatus combo_status = 6;
+ * @return {!proto.mythos.maimai.v0.MaimaiComboStatus}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.getComboStatus = function() {
+ return /** @type {!proto.mythos.maimai.v0.MaimaiComboStatus} */ (jspb.Message.getFieldWithDefault(this, 6, 0));
+};
+
+
+/**
+ * @param {!proto.mythos.maimai.v0.MaimaiComboStatus} value
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo} returns this
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.setComboStatus = function(value) {
+ return jspb.Message.setProto3EnumField(this, 6, value);
+};
+
+
+/**
+ * optional MaimaiSyncStatus sync_status = 7;
+ * @return {!proto.mythos.maimai.v0.MaimaiSyncStatus}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.getSyncStatus = function() {
+ return /** @type {!proto.mythos.maimai.v0.MaimaiSyncStatus} */ (jspb.Message.getFieldWithDefault(this, 7, 0));
+};
+
+
+/**
+ * @param {!proto.mythos.maimai.v0.MaimaiSyncStatus} value
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo} returns this
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.setSyncStatus = function(value) {
+ return jspb.Message.setProto3EnumField(this, 7, value);
+};
+
+
+/**
+ * optional bool is_clear = 8;
+ * @return {boolean}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.getIsClear = function() {
+ return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 8, false));
+};
+
+
+/**
+ * @param {boolean} value
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo} returns this
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.setIsClear = function(value) {
+ return jspb.Message.setProto3BooleanField(this, 8, value);
+};
+
+
+/**
+ * optional bool is_achieve_new_record = 9;
+ * @return {boolean}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.getIsAchieveNewRecord = function() {
+ return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 9, false));
+};
+
+
+/**
+ * @param {boolean} value
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo} returns this
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.setIsAchieveNewRecord = function(value) {
+ return jspb.Message.setProto3BooleanField(this, 9, value);
+};
+
+
+/**
+ * optional bool is_deluxscore_new_record = 10;
+ * @return {boolean}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.getIsDeluxscoreNewRecord = function() {
+ return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 10, false));
+};
+
+
+/**
+ * @param {boolean} value
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo} returns this
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.setIsDeluxscoreNewRecord = function(value) {
+ return jspb.Message.setProto3BooleanField(this, 10, value);
+};
+
+
+/**
+ * optional int32 track = 11;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.getTrack = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 11, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo} returns this
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.setTrack = function(value) {
+ return jspb.Message.setProto3IntField(this, 11, value);
+};
+
+
+/**
+ * optional string user_play_date = 12;
+ * @return {string}
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.getUserPlayDate = function() {
+ return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 12, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.mythos.maimai.v0.PlaylogInfo} returns this
+ */
+proto.mythos.maimai.v0.PlaylogInfo.prototype.setUserPlayDate = function(value) {
+ return jspb.Message.setProto3StringField(this, 12, value);
+};
+
+
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ * net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ * JSPB instance for transitional soy proto support:
+ * http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.toObject = function(opt_includeInstance) {
+ return proto.mythos.maimai.v0.PlaylogJudge.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ * the JSPB instance for transitional soy proto support:
+ * http://goto/soy-param-migration
+ * @param {!proto.mythos.maimai.v0.PlaylogJudge} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.mythos.maimai.v0.PlaylogJudge.toObject = function(includeInstance, msg) {
+ var f, obj = {
+ judgeCriticalPerfect: jspb.Message.getFieldWithDefault(msg, 1, 0),
+ judgePerfect: jspb.Message.getFieldWithDefault(msg, 2, 0),
+ judgeGreat: jspb.Message.getFieldWithDefault(msg, 3, 0),
+ judgeGood: jspb.Message.getFieldWithDefault(msg, 4, 0),
+ judgeMiss: jspb.Message.getFieldWithDefault(msg, 5, 0),
+ maxCombo: jspb.Message.getFieldWithDefault(msg, 6, 0),
+ fastCount: jspb.Message.getFieldWithDefault(msg, 7, 0),
+ lateCount: jspb.Message.getFieldWithDefault(msg, 8, 0)
+ };
+
+ if (includeInstance) {
+ obj.$jspbMessageInstance = msg;
+ }
+ return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.mythos.maimai.v0.PlaylogJudge}
+ */
+proto.mythos.maimai.v0.PlaylogJudge.deserializeBinary = function(bytes) {
+ var reader = new jspb.BinaryReader(bytes);
+ var msg = new proto.mythos.maimai.v0.PlaylogJudge;
+ return proto.mythos.maimai.v0.PlaylogJudge.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.mythos.maimai.v0.PlaylogJudge} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.mythos.maimai.v0.PlaylogJudge}
+ */
+proto.mythos.maimai.v0.PlaylogJudge.deserializeBinaryFromReader = function(msg, reader) {
+ while (reader.nextField()) {
+ if (reader.isEndGroup()) {
+ break;
+ }
+ var field = reader.getFieldNumber();
+ switch (field) {
+ case 1:
+ var value = /** @type {number} */ (reader.readInt32());
+ msg.setJudgeCriticalPerfect(value);
+ break;
+ case 2:
+ var value = /** @type {number} */ (reader.readInt32());
+ msg.setJudgePerfect(value);
+ break;
+ case 3:
+ var value = /** @type {number} */ (reader.readInt32());
+ msg.setJudgeGreat(value);
+ break;
+ case 4:
+ var value = /** @type {number} */ (reader.readInt32());
+ msg.setJudgeGood(value);
+ break;
+ case 5:
+ var value = /** @type {number} */ (reader.readInt32());
+ msg.setJudgeMiss(value);
+ break;
+ case 6:
+ var value = /** @type {number} */ (reader.readInt32());
+ msg.setMaxCombo(value);
+ break;
+ case 7:
+ var value = /** @type {number} */ (reader.readInt32());
+ msg.setFastCount(value);
+ break;
+ case 8:
+ var value = /** @type {number} */ (reader.readInt32());
+ msg.setLateCount(value);
+ break;
+ default:
+ reader.skipField();
+ break;
+ }
+ }
+ return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.serializeBinary = function() {
+ var writer = new jspb.BinaryWriter();
+ proto.mythos.maimai.v0.PlaylogJudge.serializeBinaryToWriter(this, writer);
+ return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.mythos.maimai.v0.PlaylogJudge} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.mythos.maimai.v0.PlaylogJudge.serializeBinaryToWriter = function(message, writer) {
+ var f = undefined;
+ f = message.getJudgeCriticalPerfect();
+ if (f !== 0) {
+ writer.writeInt32(
+ 1,
+ f
+ );
+ }
+ f = message.getJudgePerfect();
+ if (f !== 0) {
+ writer.writeInt32(
+ 2,
+ f
+ );
+ }
+ f = message.getJudgeGreat();
+ if (f !== 0) {
+ writer.writeInt32(
+ 3,
+ f
+ );
+ }
+ f = message.getJudgeGood();
+ if (f !== 0) {
+ writer.writeInt32(
+ 4,
+ f
+ );
+ }
+ f = message.getJudgeMiss();
+ if (f !== 0) {
+ writer.writeInt32(
+ 5,
+ f
+ );
+ }
+ f = message.getMaxCombo();
+ if (f !== 0) {
+ writer.writeInt32(
+ 6,
+ f
+ );
+ }
+ f = message.getFastCount();
+ if (f !== 0) {
+ writer.writeInt32(
+ 7,
+ f
+ );
+ }
+ f = message.getLateCount();
+ if (f !== 0) {
+ writer.writeInt32(
+ 8,
+ f
+ );
+ }
+};
+
+
+/**
+ * optional int32 judge_critical_perfect = 1;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.getJudgeCriticalPerfect = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.PlaylogJudge} returns this
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.setJudgeCriticalPerfect = function(value) {
+ return jspb.Message.setProto3IntField(this, 1, value);
+};
+
+
+/**
+ * optional int32 judge_perfect = 2;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.getJudgePerfect = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.PlaylogJudge} returns this
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.setJudgePerfect = function(value) {
+ return jspb.Message.setProto3IntField(this, 2, value);
+};
+
+
+/**
+ * optional int32 judge_great = 3;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.getJudgeGreat = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.PlaylogJudge} returns this
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.setJudgeGreat = function(value) {
+ return jspb.Message.setProto3IntField(this, 3, value);
+};
+
+
+/**
+ * optional int32 judge_good = 4;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.getJudgeGood = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 4, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.PlaylogJudge} returns this
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.setJudgeGood = function(value) {
+ return jspb.Message.setProto3IntField(this, 4, value);
+};
+
+
+/**
+ * optional int32 judge_miss = 5;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.getJudgeMiss = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 5, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.PlaylogJudge} returns this
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.setJudgeMiss = function(value) {
+ return jspb.Message.setProto3IntField(this, 5, value);
+};
+
+
+/**
+ * optional int32 max_combo = 6;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.getMaxCombo = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 6, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.PlaylogJudge} returns this
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.setMaxCombo = function(value) {
+ return jspb.Message.setProto3IntField(this, 6, value);
+};
+
+
+/**
+ * optional int32 fast_count = 7;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.getFastCount = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 7, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.PlaylogJudge} returns this
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.setFastCount = function(value) {
+ return jspb.Message.setProto3IntField(this, 7, value);
+};
+
+
+/**
+ * optional int32 late_count = 8;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.getLateCount = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 8, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.PlaylogJudge} returns this
+ */
+proto.mythos.maimai.v0.PlaylogJudge.prototype.setLateCount = function(value) {
+ return jspb.Message.setProto3IntField(this, 8, value);
+};
+
+
+goog.object.extend(exports, proto.mythos.maimai.v0);
diff --git a/server/src/proto/generated/maimai/user_grpc_pb.d.ts b/server/src/proto/generated/maimai/user_grpc_pb.d.ts
new file mode 100644
index 000000000..55b3fa25d
--- /dev/null
+++ b/server/src/proto/generated/maimai/user_grpc_pb.d.ts
@@ -0,0 +1,23 @@
+// GENERATED CODE -- DO NOT EDIT!
+
+// package: mythos.maimai.v0
+// file: maimai/user.proto
+
+import * as maimai_user_pb from "../maimai/user_pb";
+import * as grpc from "@grpc/grpc-js";
+
+interface IMaimaiUserService extends grpc.ServiceDefinition {
+ getPlaylog: grpc.MethodDefinition;
+}
+
+export const MaimaiUserService: IMaimaiUserService;
+
+export interface IMaimaiUserServer extends grpc.UntypedServiceImplementation {
+ getPlaylog: grpc.handleServerStreamingCall;
+}
+
+export class MaimaiUserClient extends grpc.Client {
+ constructor(address: string, credentials: grpc.ChannelCredentials, options?: object);
+ getPlaylog(argument: maimai_user_pb.GetPlaylogRequest, metadataOrOptions?: grpc.Metadata | grpc.CallOptions | null): grpc.ClientReadableStream;
+ getPlaylog(argument: maimai_user_pb.GetPlaylogRequest, metadata?: grpc.Metadata | null, options?: grpc.CallOptions | null): grpc.ClientReadableStream;
+}
diff --git a/server/src/proto/generated/maimai/user_grpc_pb.js b/server/src/proto/generated/maimai/user_grpc_pb.js
new file mode 100644
index 000000000..724196445
--- /dev/null
+++ b/server/src/proto/generated/maimai/user_grpc_pb.js
@@ -0,0 +1,45 @@
+// GENERATED CODE -- DO NOT EDIT!
+
+'use strict';
+var grpc = require('@grpc/grpc-js');
+var maimai_user_pb = require('../maimai/user_pb.js');
+var maimai_playlog_pb = require('../maimai/playlog_pb.js');
+
+function serialize_mythos_maimai_v0_GetPlaylogRequest(arg) {
+ if (!(arg instanceof maimai_user_pb.GetPlaylogRequest)) {
+ throw new Error('Expected argument of type mythos.maimai.v0.GetPlaylogRequest');
+ }
+ return Buffer.from(arg.serializeBinary());
+}
+
+function deserialize_mythos_maimai_v0_GetPlaylogRequest(buffer_arg) {
+ return maimai_user_pb.GetPlaylogRequest.deserializeBinary(new Uint8Array(buffer_arg));
+}
+
+function serialize_mythos_maimai_v0_GetPlaylogStreamItem(arg) {
+ if (!(arg instanceof maimai_user_pb.GetPlaylogStreamItem)) {
+ throw new Error('Expected argument of type mythos.maimai.v0.GetPlaylogStreamItem');
+ }
+ return Buffer.from(arg.serializeBinary());
+}
+
+function deserialize_mythos_maimai_v0_GetPlaylogStreamItem(buffer_arg) {
+ return maimai_user_pb.GetPlaylogStreamItem.deserializeBinary(new Uint8Array(buffer_arg));
+}
+
+
+var MaimaiUserService = exports.MaimaiUserService = {
+ getPlaylog: {
+ path: '/mythos.maimai.v0.MaimaiUser/GetPlaylog',
+ requestStream: false,
+ responseStream: true,
+ requestType: maimai_user_pb.GetPlaylogRequest,
+ responseType: maimai_user_pb.GetPlaylogStreamItem,
+ requestSerialize: serialize_mythos_maimai_v0_GetPlaylogRequest,
+ requestDeserialize: deserialize_mythos_maimai_v0_GetPlaylogRequest,
+ responseSerialize: serialize_mythos_maimai_v0_GetPlaylogStreamItem,
+ responseDeserialize: deserialize_mythos_maimai_v0_GetPlaylogStreamItem,
+ },
+};
+
+exports.MaimaiUserClient = grpc.makeGenericClientConstructor(MaimaiUserService);
diff --git a/server/src/proto/generated/maimai/user_pb.d.ts b/server/src/proto/generated/maimai/user_pb.d.ts
new file mode 100644
index 000000000..d7a971164
--- /dev/null
+++ b/server/src/proto/generated/maimai/user_pb.d.ts
@@ -0,0 +1,70 @@
+// package: mythos.maimai.v0
+// file: maimai/user.proto
+
+import * as jspb from "google-protobuf";
+import * as maimai_playlog_pb from "../maimai/playlog_pb";
+
+export class GetPlaylogRequest extends jspb.Message {
+ getProfileApiId(): string;
+ setProfileApiId(value: string): void;
+
+ hasLastUserPlayDate(): boolean;
+ clearLastUserPlayDate(): void;
+ getLastUserPlayDate(): string;
+ setLastUserPlayDate(value: string): void;
+
+ hasLimit(): boolean;
+ clearLimit(): void;
+ getLimit(): number;
+ setLimit(value: number): void;
+
+ serializeBinary(): Uint8Array;
+ toObject(includeInstance?: boolean): GetPlaylogRequest.AsObject;
+ static toObject(includeInstance: boolean, msg: GetPlaylogRequest): GetPlaylogRequest.AsObject;
+ static extensions: {[key: number]: jspb.ExtensionFieldInfo};
+ static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo};
+ static serializeBinaryToWriter(message: GetPlaylogRequest, writer: jspb.BinaryWriter): void;
+ static deserializeBinary(bytes: Uint8Array): GetPlaylogRequest;
+ static deserializeBinaryFromReader(message: GetPlaylogRequest, reader: jspb.BinaryReader): GetPlaylogRequest;
+}
+
+export namespace GetPlaylogRequest {
+ export type AsObject = {
+ profileApiId: string,
+ lastUserPlayDate: string,
+ limit: number,
+ }
+}
+
+export class GetPlaylogStreamItem extends jspb.Message {
+ getPlaylogApiId(): string;
+ setPlaylogApiId(value: string): void;
+
+ hasInfo(): boolean;
+ clearInfo(): void;
+ getInfo(): maimai_playlog_pb.PlaylogInfo | undefined;
+ setInfo(value?: maimai_playlog_pb.PlaylogInfo): void;
+
+ hasJudge(): boolean;
+ clearJudge(): void;
+ getJudge(): maimai_playlog_pb.PlaylogJudge | undefined;
+ setJudge(value?: maimai_playlog_pb.PlaylogJudge): void;
+
+ serializeBinary(): Uint8Array;
+ toObject(includeInstance?: boolean): GetPlaylogStreamItem.AsObject;
+ static toObject(includeInstance: boolean, msg: GetPlaylogStreamItem): GetPlaylogStreamItem.AsObject;
+ static extensions: {[key: number]: jspb.ExtensionFieldInfo};
+ static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo};
+ static serializeBinaryToWriter(message: GetPlaylogStreamItem, writer: jspb.BinaryWriter): void;
+ static deserializeBinary(bytes: Uint8Array): GetPlaylogStreamItem;
+ static deserializeBinaryFromReader(message: GetPlaylogStreamItem, reader: jspb.BinaryReader): GetPlaylogStreamItem;
+}
+
+export namespace GetPlaylogStreamItem {
+ export type AsObject = {
+ playlogApiId: string,
+ info?: maimai_playlog_pb.PlaylogInfo.AsObject,
+ judge?: maimai_playlog_pb.PlaylogJudge.AsObject,
+ }
+}
+
diff --git a/server/src/proto/generated/maimai/user_pb.js b/server/src/proto/generated/maimai/user_pb.js
new file mode 100644
index 000000000..7a5c047d4
--- /dev/null
+++ b/server/src/proto/generated/maimai/user_pb.js
@@ -0,0 +1,528 @@
+// source: maimai/user.proto
+/**
+ * @fileoverview
+ * @enhanceable
+ * @suppress {missingRequire} reports error on implicit type usages.
+ * @suppress {messageConventions} JS Compiler reports an error if a variable or
+ * field starts with 'MSG_' and isn't a translatable message.
+ * @public
+ */
+// GENERATED CODE -- DO NOT EDIT!
+/* eslint-disable */
+// @ts-nocheck
+
+var jspb = require('google-protobuf');
+var goog = jspb;
+var global = (function() {
+ if (this) { return this; }
+ if (typeof window !== 'undefined') { return window; }
+ if (typeof global !== 'undefined') { return global; }
+ if (typeof self !== 'undefined') { return self; }
+ return Function('return this')();
+}.call(null));
+
+var maimai_playlog_pb = require('../maimai/playlog_pb.js');
+goog.object.extend(proto, maimai_playlog_pb);
+goog.exportSymbol('proto.mythos.maimai.v0.GetPlaylogRequest', null, global);
+goog.exportSymbol('proto.mythos.maimai.v0.GetPlaylogStreamItem', null, global);
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest = function(opt_data) {
+ jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.mythos.maimai.v0.GetPlaylogRequest, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+ /**
+ * @public
+ * @override
+ */
+ proto.mythos.maimai.v0.GetPlaylogRequest.displayName = 'proto.mythos.maimai.v0.GetPlaylogRequest';
+}
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem = function(opt_data) {
+ jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.mythos.maimai.v0.GetPlaylogStreamItem, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+ /**
+ * @public
+ * @override
+ */
+ proto.mythos.maimai.v0.GetPlaylogStreamItem.displayName = 'proto.mythos.maimai.v0.GetPlaylogStreamItem';
+}
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ * net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ * JSPB instance for transitional soy proto support:
+ * http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.prototype.toObject = function(opt_includeInstance) {
+ return proto.mythos.maimai.v0.GetPlaylogRequest.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ * the JSPB instance for transitional soy proto support:
+ * http://goto/soy-param-migration
+ * @param {!proto.mythos.maimai.v0.GetPlaylogRequest} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.toObject = function(includeInstance, msg) {
+ var f, obj = {
+ profileApiId: jspb.Message.getFieldWithDefault(msg, 1, ""),
+ lastUserPlayDate: jspb.Message.getFieldWithDefault(msg, 2, ""),
+ limit: jspb.Message.getFieldWithDefault(msg, 3, 0)
+ };
+
+ if (includeInstance) {
+ obj.$jspbMessageInstance = msg;
+ }
+ return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.mythos.maimai.v0.GetPlaylogRequest}
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.deserializeBinary = function(bytes) {
+ var reader = new jspb.BinaryReader(bytes);
+ var msg = new proto.mythos.maimai.v0.GetPlaylogRequest;
+ return proto.mythos.maimai.v0.GetPlaylogRequest.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.mythos.maimai.v0.GetPlaylogRequest} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.mythos.maimai.v0.GetPlaylogRequest}
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.deserializeBinaryFromReader = function(msg, reader) {
+ while (reader.nextField()) {
+ if (reader.isEndGroup()) {
+ break;
+ }
+ var field = reader.getFieldNumber();
+ switch (field) {
+ case 1:
+ var value = /** @type {string} */ (reader.readString());
+ msg.setProfileApiId(value);
+ break;
+ case 2:
+ var value = /** @type {string} */ (reader.readString());
+ msg.setLastUserPlayDate(value);
+ break;
+ case 3:
+ var value = /** @type {number} */ (reader.readUint64());
+ msg.setLimit(value);
+ break;
+ default:
+ reader.skipField();
+ break;
+ }
+ }
+ return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.prototype.serializeBinary = function() {
+ var writer = new jspb.BinaryWriter();
+ proto.mythos.maimai.v0.GetPlaylogRequest.serializeBinaryToWriter(this, writer);
+ return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.mythos.maimai.v0.GetPlaylogRequest} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.serializeBinaryToWriter = function(message, writer) {
+ var f = undefined;
+ f = message.getProfileApiId();
+ if (f.length > 0) {
+ writer.writeString(
+ 1,
+ f
+ );
+ }
+ f = /** @type {string} */ (jspb.Message.getField(message, 2));
+ if (f != null) {
+ writer.writeString(
+ 2,
+ f
+ );
+ }
+ f = /** @type {number} */ (jspb.Message.getField(message, 3));
+ if (f != null) {
+ writer.writeUint64(
+ 3,
+ f
+ );
+ }
+};
+
+
+/**
+ * optional string profile_api_id = 1;
+ * @return {string}
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.prototype.getProfileApiId = function() {
+ return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.mythos.maimai.v0.GetPlaylogRequest} returns this
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.prototype.setProfileApiId = function(value) {
+ return jspb.Message.setProto3StringField(this, 1, value);
+};
+
+
+/**
+ * optional string last_user_play_date = 2;
+ * @return {string}
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.prototype.getLastUserPlayDate = function() {
+ return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.mythos.maimai.v0.GetPlaylogRequest} returns this
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.prototype.setLastUserPlayDate = function(value) {
+ return jspb.Message.setField(this, 2, value);
+};
+
+
+/**
+ * Clears the field making it undefined.
+ * @return {!proto.mythos.maimai.v0.GetPlaylogRequest} returns this
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.prototype.clearLastUserPlayDate = function() {
+ return jspb.Message.setField(this, 2, undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.prototype.hasLastUserPlayDate = function() {
+ return jspb.Message.getField(this, 2) != null;
+};
+
+
+/**
+ * optional uint64 limit = 3;
+ * @return {number}
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.prototype.getLimit = function() {
+ return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0));
+};
+
+
+/**
+ * @param {number} value
+ * @return {!proto.mythos.maimai.v0.GetPlaylogRequest} returns this
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.prototype.setLimit = function(value) {
+ return jspb.Message.setField(this, 3, value);
+};
+
+
+/**
+ * Clears the field making it undefined.
+ * @return {!proto.mythos.maimai.v0.GetPlaylogRequest} returns this
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.prototype.clearLimit = function() {
+ return jspb.Message.setField(this, 3, undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.mythos.maimai.v0.GetPlaylogRequest.prototype.hasLimit = function() {
+ return jspb.Message.getField(this, 3) != null;
+};
+
+
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ * net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ * JSPB instance for transitional soy proto support:
+ * http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.prototype.toObject = function(opt_includeInstance) {
+ return proto.mythos.maimai.v0.GetPlaylogStreamItem.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ * the JSPB instance for transitional soy proto support:
+ * http://goto/soy-param-migration
+ * @param {!proto.mythos.maimai.v0.GetPlaylogStreamItem} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.toObject = function(includeInstance, msg) {
+ var f, obj = {
+ playlogApiId: jspb.Message.getFieldWithDefault(msg, 1, ""),
+ info: (f = msg.getInfo()) && maimai_playlog_pb.PlaylogInfo.toObject(includeInstance, f),
+ judge: (f = msg.getJudge()) && maimai_playlog_pb.PlaylogJudge.toObject(includeInstance, f)
+ };
+
+ if (includeInstance) {
+ obj.$jspbMessageInstance = msg;
+ }
+ return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.mythos.maimai.v0.GetPlaylogStreamItem}
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.deserializeBinary = function(bytes) {
+ var reader = new jspb.BinaryReader(bytes);
+ var msg = new proto.mythos.maimai.v0.GetPlaylogStreamItem;
+ return proto.mythos.maimai.v0.GetPlaylogStreamItem.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.mythos.maimai.v0.GetPlaylogStreamItem} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.mythos.maimai.v0.GetPlaylogStreamItem}
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.deserializeBinaryFromReader = function(msg, reader) {
+ while (reader.nextField()) {
+ if (reader.isEndGroup()) {
+ break;
+ }
+ var field = reader.getFieldNumber();
+ switch (field) {
+ case 1:
+ var value = /** @type {string} */ (reader.readString());
+ msg.setPlaylogApiId(value);
+ break;
+ case 2:
+ var value = new maimai_playlog_pb.PlaylogInfo;
+ reader.readMessage(value,maimai_playlog_pb.PlaylogInfo.deserializeBinaryFromReader);
+ msg.setInfo(value);
+ break;
+ case 3:
+ var value = new maimai_playlog_pb.PlaylogJudge;
+ reader.readMessage(value,maimai_playlog_pb.PlaylogJudge.deserializeBinaryFromReader);
+ msg.setJudge(value);
+ break;
+ default:
+ reader.skipField();
+ break;
+ }
+ }
+ return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.prototype.serializeBinary = function() {
+ var writer = new jspb.BinaryWriter();
+ proto.mythos.maimai.v0.GetPlaylogStreamItem.serializeBinaryToWriter(this, writer);
+ return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.mythos.maimai.v0.GetPlaylogStreamItem} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.serializeBinaryToWriter = function(message, writer) {
+ var f = undefined;
+ f = message.getPlaylogApiId();
+ if (f.length > 0) {
+ writer.writeString(
+ 1,
+ f
+ );
+ }
+ f = message.getInfo();
+ if (f != null) {
+ writer.writeMessage(
+ 2,
+ f,
+ maimai_playlog_pb.PlaylogInfo.serializeBinaryToWriter
+ );
+ }
+ f = message.getJudge();
+ if (f != null) {
+ writer.writeMessage(
+ 3,
+ f,
+ maimai_playlog_pb.PlaylogJudge.serializeBinaryToWriter
+ );
+ }
+};
+
+
+/**
+ * optional string playlog_api_id = 1;
+ * @return {string}
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.prototype.getPlaylogApiId = function() {
+ return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.mythos.maimai.v0.GetPlaylogStreamItem} returns this
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.prototype.setPlaylogApiId = function(value) {
+ return jspb.Message.setProto3StringField(this, 1, value);
+};
+
+
+/**
+ * optional PlaylogInfo info = 2;
+ * @return {?proto.mythos.maimai.v0.PlaylogInfo}
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.prototype.getInfo = function() {
+ return /** @type{?proto.mythos.maimai.v0.PlaylogInfo} */ (
+ jspb.Message.getWrapperField(this, maimai_playlog_pb.PlaylogInfo, 2));
+};
+
+
+/**
+ * @param {?proto.mythos.maimai.v0.PlaylogInfo|undefined} value
+ * @return {!proto.mythos.maimai.v0.GetPlaylogStreamItem} returns this
+*/
+proto.mythos.maimai.v0.GetPlaylogStreamItem.prototype.setInfo = function(value) {
+ return jspb.Message.setWrapperField(this, 2, value);
+};
+
+
+/**
+ * Clears the message field making it undefined.
+ * @return {!proto.mythos.maimai.v0.GetPlaylogStreamItem} returns this
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.prototype.clearInfo = function() {
+ return this.setInfo(undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.prototype.hasInfo = function() {
+ return jspb.Message.getField(this, 2) != null;
+};
+
+
+/**
+ * optional PlaylogJudge judge = 3;
+ * @return {?proto.mythos.maimai.v0.PlaylogJudge}
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.prototype.getJudge = function() {
+ return /** @type{?proto.mythos.maimai.v0.PlaylogJudge} */ (
+ jspb.Message.getWrapperField(this, maimai_playlog_pb.PlaylogJudge, 3));
+};
+
+
+/**
+ * @param {?proto.mythos.maimai.v0.PlaylogJudge|undefined} value
+ * @return {!proto.mythos.maimai.v0.GetPlaylogStreamItem} returns this
+*/
+proto.mythos.maimai.v0.GetPlaylogStreamItem.prototype.setJudge = function(value) {
+ return jspb.Message.setWrapperField(this, 3, value);
+};
+
+
+/**
+ * Clears the message field making it undefined.
+ * @return {!proto.mythos.maimai.v0.GetPlaylogStreamItem} returns this
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.prototype.clearJudge = function() {
+ return this.setJudge(undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.mythos.maimai.v0.GetPlaylogStreamItem.prototype.hasJudge = function() {
+ return jspb.Message.getField(this, 3) != null;
+};
+
+
+goog.object.extend(exports, proto.mythos.maimai.v0);
diff --git a/server/src/proto/myt/maimai/common.proto b/server/src/proto/myt/maimai/common.proto
new file mode 100644
index 000000000..63ce6e003
--- /dev/null
+++ b/server/src/proto/myt/maimai/common.proto
@@ -0,0 +1,55 @@
+syntax = "proto3";
+
+package mythos.maimai.v0;
+
+enum MaimaiRankingType {
+ MAIMAI_RANKING_TYPE_UNSPECIFIED = 0;
+ MAIMAI_RANKING_TYPE_ACHIEVEMENT = 1;
+ MAIMAI_RANKING_TYPE_DX_SCORE = 2;
+}
+
+enum MaimaiLevel {
+ MAIMAI_LEVEL_UNSPECIFIED = 0;
+ MAIMAI_LEVEL_BASIC = 1;
+ MAIMAI_LEVEL_ADVANCED = 2;
+ MAIMAI_LEVEL_EXPERT = 3;
+ MAIMAI_LEVEL_MASTER = 4;
+ MAIMAI_LEVEL_REMASTER = 5;
+ MAIMAI_LEVEL_UTAGE = 6;
+}
+
+enum MaimaiComboStatus {
+ MAIMAI_COMBO_STATUS_UNSPECIFIED = 0;
+ MAIMAI_COMBO_STATUS_NONE = 1;
+ MAIMAI_COMBO_STATUS_FULL_COMBO = 2;
+ MAIMAI_COMBO_STATUS_FULL_COMBO_PLUS = 3;
+ MAIMAI_COMBO_STATUS_ALL_PERFECT = 4;
+ MAIMAI_COMBO_STATUS_ALL_PERFECT_PLUS = 5;
+}
+
+enum MaimaiSyncStatus {
+ MAIMAI_SYNC_STATUS_UNSPECIFIED = 0;
+ MAIMAI_SYNC_STATUS_NONE = 1;
+ MAIMAI_SYNC_STATUS_FULL_SYNC = 2;
+ MAIMAI_SYNC_STATUS_FULL_SYNC_PLUS = 3;
+ MAIMAI_SYNC_STATUS_FULL_SYNC_DX = 4;
+ MAIMAI_SYNC_STATUS_FULL_SYNC_DX_PLUS = 5;
+}
+
+enum MaimaiScoreRank {
+ MAIMAI_SCORE_RANK_UNSPECIFIED = 0;
+ MAIMAI_SCORE_RANK_D = 1;
+ MAIMAI_SCORE_RANK_C = 2;
+ MAIMAI_SCORE_RANK_B = 3;
+ MAIMAI_SCORE_RANK_BB = 4;
+ MAIMAI_SCORE_RANK_BBB = 5;
+ MAIMAI_SCORE_RANK_A = 6;
+ MAIMAI_SCORE_RANK_AA = 7;
+ MAIMAI_SCORE_RANK_AAA = 8;
+ MAIMAI_SCORE_RANK_S = 9;
+ MAIMAI_SCORE_RANK_S_PLUS = 10;
+ MAIMAI_SCORE_RANK_SS = 11;
+ MAIMAI_SCORE_RANK_SS_PLUS = 12;
+ MAIMAI_SCORE_RANK_SSS = 13;
+ MAIMAI_SCORE_RANK_SSS_PLUS = 14;
+}
diff --git a/server/src/proto/myt/maimai/playlog.proto b/server/src/proto/myt/maimai/playlog.proto
new file mode 100644
index 000000000..bd1897501
--- /dev/null
+++ b/server/src/proto/myt/maimai/playlog.proto
@@ -0,0 +1,31 @@
+syntax = "proto3";
+
+package mythos.maimai.v0;
+
+import "maimai/common.proto";
+
+message PlaylogInfo {
+ int32 music_id = 1;
+ MaimaiLevel level = 2;
+ int32 achievement = 3;
+ int32 deluxscore = 4;
+ MaimaiScoreRank score_rank = 5;
+ MaimaiComboStatus combo_status = 6;
+ MaimaiSyncStatus sync_status = 7;
+ bool is_clear = 8;
+ bool is_achieve_new_record = 9;
+ bool is_deluxscore_new_record = 10;
+ int32 track = 11;
+ string user_play_date = 12;
+}
+
+message PlaylogJudge {
+ int32 judge_critical_perfect = 1;
+ int32 judge_perfect = 2;
+ int32 judge_great = 3;
+ int32 judge_good = 4;
+ int32 judge_miss = 5;
+ int32 max_combo = 6;
+ int32 fast_count = 7;
+ int32 late_count = 8;
+}
diff --git a/server/src/proto/myt/maimai/user.proto b/server/src/proto/myt/maimai/user.proto
new file mode 100644
index 000000000..8b996b1a3
--- /dev/null
+++ b/server/src/proto/myt/maimai/user.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+package mythos.maimai.v0;
+
+import "maimai/playlog.proto";
+
+service MaimaiUser {
+ rpc GetPlaylog(GetPlaylogRequest) returns (stream GetPlaylogStreamItem);
+}
+
+message GetPlaylogRequest {
+ string profile_api_id = 1;
+ optional string last_user_play_date = 2;
+ optional uint64 limit = 3;
+}
+
+message GetPlaylogStreamItem {
+ string playlog_api_id = 1;
+ PlaylogInfo info = 2;
+ PlaylogJudge judge = 3;
+}
diff --git a/server/src/test-utils/mock-db/charts-maimaidx.json b/server/src/test-utils/mock-db/charts-maimaidx.json
new file mode 100644
index 000000000..42238a092
--- /dev/null
+++ b/server/src/test-utils/mock-db/charts-maimaidx.json
@@ -0,0 +1,21 @@
+[
+ {
+ "chartID": "fab3d632610b9b98ee1e4f68e9ecf0161f9cb8cd",
+ "data": {
+ "inGameID": 11294
+ },
+ "difficulty": "DX Expert",
+ "isPrimary": true,
+ "level": "12",
+ "levelNum": 12.2,
+ "playtype": "Single",
+ "songID": 844,
+ "versions": [
+ "universeplus",
+ "festival",
+ "festivalplus",
+ "buddies",
+ "buddiesplus"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/server/src/test-utils/mock-db/songs-maimaidx.json b/server/src/test-utils/mock-db/songs-maimaidx.json
new file mode 100644
index 000000000..6aa38fbd1
--- /dev/null
+++ b/server/src/test-utils/mock-db/songs-maimaidx.json
@@ -0,0 +1,17 @@
+[
+ {
+ "altTitles": [],
+ "artist": "suzu",
+ "data": {
+ "displayVersion": "UNiVERSE",
+ "genre": "オンゲキ&CHUNITHM"
+ },
+ "id": 844,
+ "searchTerms": [
+ "Shukusei",
+ "Shukusei Shinpan",
+ "Syukusei Shinpan"
+ ],
+ "title": "宿星審判"
+ }
+]
\ No newline at end of file
diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts
index 2d882da7a..32f8c2826 100644
--- a/server/src/test-utils/test-data.ts
+++ b/server/src/test-utils/test-data.ts
@@ -1335,3 +1335,29 @@ export const TestingChunithmSongConverter: SongDocument<"chunithm"> = {
searchTerms: [],
title: "killy killy JOKER",
};
+
+export const TestingMaimaiDXSongConverter: SongDocument<"maimaidx"> = {
+ altTitles: [],
+ artist: "suzu",
+ data: {
+ displayVersion: "UNiVERSE",
+ genre: "オンゲキ&CHUNITHM",
+ },
+ id: 844,
+ searchTerms: ["Shukusei", "Shukusei Shinpan", "Syukusei Shinpan"],
+ title: "宿星審判",
+};
+
+export const TestingMaimaiDXChartConverter: ChartDocument<"maimaidx:Single"> = {
+ chartID: "fab3d632610b9b98ee1e4f68e9ecf0161f9cb8cd",
+ data: {
+ inGameID: 11294,
+ },
+ difficulty: "DX Expert",
+ isPrimary: true,
+ level: "12",
+ levelNum: 12.2,
+ playtype: "Single",
+ songID: 844,
+ versions: ["universeplus", "festival", "festivalplus", "buddies", "buddiesplus"],
+};