mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-12 01:08:29 +03:00
checkpoint
This commit is contained in:
@@ -2,7 +2,12 @@ import { type BMSCourseWithRelated, type CellsRenderFN } from "#types/seeds";
|
||||
import { StrSOV } from "#util/sorts";
|
||||
import { type SearchFunctions } from "#util/ztable/search";
|
||||
import React from "react";
|
||||
import { FormatChart, GetBMSCourseIndex } from "tachi-common";
|
||||
import {
|
||||
type ChartDocument,
|
||||
type ChartDocumentData,
|
||||
FormatChart,
|
||||
GetBMSCourseIndex,
|
||||
} from "tachi-common";
|
||||
|
||||
import { type Header } from "../components/TachiTable";
|
||||
|
||||
@@ -40,7 +45,7 @@ export const SeedsBMSCourseLookupSearchFns: SearchFunctions<BMSCourseWithRelated
|
||||
return `UNKNOWN CHART (${e})`;
|
||||
}
|
||||
|
||||
return FormatChart(e.chart);
|
||||
return FormatChart({ ...e.chart, song: e.song } as unknown as ChartDocument);
|
||||
})
|
||||
.join("\n"),
|
||||
};
|
||||
@@ -59,12 +64,17 @@ export const SeedsBMSCourseLookupCells: CellsRenderFN<BMSCourseWithRelated> = ({
|
||||
{typeof e === "string" ? (
|
||||
<span className="text-danger">UNKNOWN CHART</span>
|
||||
) : (
|
||||
<span>{FormatChart(e.chart)} </span>
|
||||
<span>{FormatChart({ ...e.chart, song: e.song } as unknown as ChartDocument)} </span>
|
||||
)}
|
||||
|
||||
{!compress && (
|
||||
<div className="ms-auto" key={i}>
|
||||
<code>{typeof e === "string" ? e : e.chart.data.hashMD5}</code>
|
||||
<code>
|
||||
{typeof e === "string"
|
||||
? e
|
||||
: (e.chart.data as ChartDocumentData["bms-7k"] | ChartDocumentData["bms-14k"])
|
||||
.hashMD5}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FlattenValue, StringifyKeyChain } from "#util/misc";
|
||||
import { StrSOV } from "#util/sorts";
|
||||
import { type SearchFunctions } from "#util/ztable/search";
|
||||
import React from "react";
|
||||
import { COLOUR_SET, type V3Game } from "tachi-common";
|
||||
import { type ChartDocument, COLOUR_SET, type V3Game } from "tachi-common";
|
||||
|
||||
import DifficultyCell from "../cells/DifficultyCell";
|
||||
import ObjCell from "../cells/ObjCell";
|
||||
@@ -21,7 +21,7 @@ export function MakeSeedsChartsControls(game: V3Game): {
|
||||
} {
|
||||
return {
|
||||
headers: [
|
||||
ChartHeader(game, (k) => k),
|
||||
ChartHeader(game, (k) => k as unknown as ChartDocument),
|
||||
["Title", "Title", StrSOV((x) => x.__related.song?.title ?? "")],
|
||||
["Data", "Data"],
|
||||
],
|
||||
@@ -31,7 +31,7 @@ export function MakeSeedsChartsControls(game: V3Game): {
|
||||
songID: (x) => x.__related.song?.id ?? null,
|
||||
searchTerms: (x) => x.__related.song?.searchTerms.join(", ") ?? null,
|
||||
altTitles: (x) => x.__related.song?.altTitles.join(", ") ?? null,
|
||||
difficulty: (x) => x.difficulty,
|
||||
difficulty: (x) => String(x.difficulty),
|
||||
level: (x) => x.levelNum,
|
||||
data: (x) =>
|
||||
FlattenValue(x.data)
|
||||
@@ -40,14 +40,14 @@ export function MakeSeedsChartsControls(game: V3Game): {
|
||||
},
|
||||
Cells: ({ data }) => (
|
||||
<>
|
||||
<DifficultyCell chart={data} game={game} />
|
||||
<DifficultyCell chart={data as unknown as ChartDocument} game={game} />
|
||||
|
||||
{/* if we have a song, render the title cell. Otherwise, this chart has no parent song, and is invalid. */}
|
||||
{data.__related.song ? (
|
||||
<TitleCell chart={data} game={game} song={data.__related.song} />
|
||||
<TitleCell chart={data as unknown as ChartDocument} game={game} song={data.__related.song} />
|
||||
) : (
|
||||
<td style={{ backgroundColor: ChangeOpacity(COLOUR_SET.red, 0.3) }}>
|
||||
INVALID SONG ({data.song.id})
|
||||
INVALID SONG ({data.songID})
|
||||
<QuickTooltip tooltipContent="This chart points to a songID that does not exist at the time of this commit. This is a fatal error, and this commit doesn't pass tests.">
|
||||
<div>
|
||||
<Icon type="info" />
|
||||
|
||||
@@ -4,7 +4,12 @@ import { type CellsRenderFN, type QuestWithRelated } from "#types/seeds";
|
||||
import { StrSOV } from "#util/sorts";
|
||||
import { type SearchFunctions } from "#util/ztable/search";
|
||||
import React from "react";
|
||||
import { FormatGame } from "tachi-common";
|
||||
import {
|
||||
type GameGroup,
|
||||
type GoalDocument,
|
||||
LEGACY_FormatGameGroupPT,
|
||||
type LEGACY_Playtypes,
|
||||
} from "tachi-common";
|
||||
|
||||
import { type Header } from "../components/TachiTable";
|
||||
|
||||
@@ -19,7 +24,11 @@ export const SeedsQuestSearchFns: SearchFunctions<QuestWithRelated> = {
|
||||
name: (x) => x.name,
|
||||
questID: (x) => x.questID,
|
||||
game: (x) => x.game,
|
||||
gpt: (x) => FormatGame(x.game),
|
||||
gpt: (x) =>
|
||||
LEGACY_FormatGameGroupPT(
|
||||
x.game as GameGroup,
|
||||
x.playtype as LEGACY_Playtypes[typeof x.game],
|
||||
),
|
||||
goals: (x) =>
|
||||
Object.values(x.__related.goals)
|
||||
.map((e) => e!.name)
|
||||
@@ -38,7 +47,12 @@ export const SeedsQuestCells: CellsRenderFN<QuestWithRelated> = ({
|
||||
<td>
|
||||
<strong>{data.name}</strong>
|
||||
</td>
|
||||
<td>{FormatGame(data.game)}</td>
|
||||
<td>
|
||||
{LEGACY_FormatGameGroupPT(
|
||||
data.game as GameGroup,
|
||||
data.playtype as LEGACY_Playtypes[typeof data.game],
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div style={{ maxHeight: "200px", overflowY: "auto" }}>
|
||||
{data.questData.map((section) => (
|
||||
@@ -51,7 +65,7 @@ export const SeedsQuestCells: CellsRenderFN<QuestWithRelated> = ({
|
||||
if (goal) {
|
||||
return (
|
||||
<div className="text-start" key={ref.goalID}>
|
||||
<InnerQuestSectionGoal goal={goal} />
|
||||
<InnerQuestSectionGoal goal={goal as GoalDocument} />
|
||||
{ref.note && <Muted>{ref.note}</Muted>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,11 @@ import { type CellsRenderFN, type QuestlineWithRelated } from "#types/seeds";
|
||||
import { StrSOV } from "#util/sorts";
|
||||
import { type SearchFunctions } from "#util/ztable/search";
|
||||
import React from "react";
|
||||
import { FormatGame } from "tachi-common";
|
||||
import {
|
||||
type GameGroup,
|
||||
LEGACY_FormatGameGroupPT,
|
||||
type LEGACY_Playtypes,
|
||||
} from "tachi-common";
|
||||
|
||||
import { type Header } from "../components/TachiTable";
|
||||
|
||||
@@ -19,7 +23,11 @@ export const SeedsQuestlineSearchFns: SearchFunctions<QuestlineWithRelated> = {
|
||||
questlineID: (x) => x.questlineID,
|
||||
desc: (x) => x.desc,
|
||||
game: (x) => x.game,
|
||||
gpt: (x) => FormatGame(x.game),
|
||||
gpt: (x) =>
|
||||
LEGACY_FormatGameGroupPT(
|
||||
x.game as GameGroup,
|
||||
x.playtype as LEGACY_Playtypes[typeof x.game],
|
||||
),
|
||||
quests: (x) =>
|
||||
Object.values(x.__related.quests)
|
||||
.filter((e) => e !== undefined)
|
||||
@@ -37,7 +45,12 @@ export const SeedsQuestlineCells: CellsRenderFN<QuestlineWithRelated> = ({ data
|
||||
<br />
|
||||
<Muted>{data.desc}</Muted>
|
||||
</td>
|
||||
<td>{FormatGame(data.game)}</td>
|
||||
<td>
|
||||
{LEGACY_FormatGameGroupPT(
|
||||
data.game as GameGroup,
|
||||
data.playtype as LEGACY_Playtypes[typeof data.game],
|
||||
)}
|
||||
</td>
|
||||
<td className="text-start">
|
||||
<div style={{ maxHeight: "200px", overflowY: "auto" }}>
|
||||
{data.quests.map((e) => {
|
||||
@@ -47,7 +60,12 @@ export const SeedsQuestlineCells: CellsRenderFN<QuestlineWithRelated> = ({ data
|
||||
<div key={e}>
|
||||
{quest ? (
|
||||
<span>
|
||||
{quest.name} ({FormatGame(quest.game)})
|
||||
{quest.name} (
|
||||
{LEGACY_FormatGameGroupPT(
|
||||
quest.game as GameGroup,
|
||||
quest.playtype as LEGACY_Playtypes[typeof quest.game],
|
||||
)}
|
||||
)
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-danger">UNKNOWN QUEST {e}</span>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { FormatGame } from "tachi-common";
|
||||
import { type Header } from "../components/TachiTable";
|
||||
|
||||
export const SeedsTableHeaders: Header<TableWithRelated>[] = [
|
||||
["ID", "ID", StrSOV((x) => x.tableID)],
|
||||
["ID", "ID", StrSOV((x) => x.id)],
|
||||
["Name", "Name", StrSOV((x) => x.title)],
|
||||
["GPT", "GPT", StrSOV((x) => x.game)],
|
||||
["Folders", "Folders", StrSOV((x) => x.title)],
|
||||
@@ -17,7 +17,7 @@ export const SeedsTableHeaders: Header<TableWithRelated>[] = [
|
||||
|
||||
export const SeedsTableSearchFns: SearchFunctions<TableWithRelated> = {
|
||||
title: (x) => x.title,
|
||||
tableID: (x) => x.tableID,
|
||||
tableID: (x) => x.id,
|
||||
inactive: (x) => x.inactive,
|
||||
default: (x) => x.default,
|
||||
description: (x) => x.description,
|
||||
@@ -33,7 +33,7 @@ export const SeedsTableSearchFns: SearchFunctions<TableWithRelated> = {
|
||||
export const SeedsTableCells: CellsRenderFN<TableWithRelated> = ({ data }) => (
|
||||
<>
|
||||
<td>
|
||||
<code>{data.tableID}</code>
|
||||
<code>{data.id}</code>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{data.title}</strong>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { type JSONAttributeDiff } from "#util/misc";
|
||||
import {
|
||||
type BMSCourseDocument,
|
||||
type ChartDocument,
|
||||
type FolderDocument,
|
||||
type GameGroup,
|
||||
type GoalDocument,
|
||||
type QuestDocument,
|
||||
type QuestlineDocument,
|
||||
type SEEDS_BMSCourseDocument,
|
||||
type SEEDS_ChartDocument,
|
||||
type SEEDS_FolderDocument,
|
||||
type SEEDS_GoalDocument,
|
||||
type SEEDS_QuestDocument,
|
||||
type SEEDS_QuestlineDocument,
|
||||
type SEEDS_SongDocument,
|
||||
type SEEDS_TableDocument,
|
||||
type SongDocument,
|
||||
type TableDocument,
|
||||
type V3Game,
|
||||
} from "tachi-common";
|
||||
|
||||
@@ -23,46 +24,46 @@ export type BMSCourseWithRelated = {
|
||||
entries: Array<
|
||||
| string
|
||||
| {
|
||||
chart: ChartDocument<"bms-7k" | "bms-14k">;
|
||||
chart: SEEDS_ChartDocument<"bms-7k" | "bms-14k">;
|
||||
song: SongDocument<"bms">;
|
||||
}
|
||||
>;
|
||||
};
|
||||
} & BMSCourseDocument;
|
||||
} & SEEDS_BMSCourseDocument;
|
||||
|
||||
export type TableWithRelated = {
|
||||
__related: {
|
||||
/** Keys match `tables.json` folder slug strings (per game). */
|
||||
folders: {
|
||||
[folderSlug: string]: FolderDocument | undefined;
|
||||
[folderSlug: string]: SEEDS_FolderDocument | undefined;
|
||||
};
|
||||
};
|
||||
} & TableDocument;
|
||||
} & SEEDS_TableDocument;
|
||||
|
||||
export type QuestlineWithRelated = {
|
||||
__related: {
|
||||
quests: {
|
||||
[questID: string]: QuestDocument | undefined;
|
||||
[questID: string]: SEEDS_QuestDocument | undefined;
|
||||
};
|
||||
};
|
||||
} & QuestlineDocument;
|
||||
} & SEEDS_QuestlineDocument;
|
||||
|
||||
export type QuestWithRelated = {
|
||||
__related: {
|
||||
goals: {
|
||||
[goalID: string]: GoalDocument | undefined;
|
||||
[goalID: string]: SEEDS_GoalDocument | undefined;
|
||||
};
|
||||
};
|
||||
} & QuestDocument;
|
||||
} & SEEDS_QuestDocument;
|
||||
|
||||
export type ChartWithRelated<T extends V3Game = V3Game> = {
|
||||
__related: {
|
||||
song: SongDocument | undefined;
|
||||
};
|
||||
} & ChartDocument<T>;
|
||||
} & SEEDS_ChartDocument<T>;
|
||||
|
||||
type SongSeedsWithRelated = {
|
||||
[G in GameGroup as `songs-${G}.json`]: Array<SongDocument<G>>;
|
||||
[G in GameGroup as `songs-${G}.json`]: Array<SEEDS_SongDocument<G>>;
|
||||
};
|
||||
|
||||
type ChartSeedsWithRelated = {
|
||||
@@ -78,9 +79,9 @@ export type DatabaseSeedsWithRelated = {
|
||||
"bms-course-lookup.json": Array<BMSCourseWithRelated>;
|
||||
|
||||
// intentional: folders don't need to be joined with anything.
|
||||
"folders.json": Array<FolderDocument>;
|
||||
"folders.json": Array<SEEDS_FolderDocument>;
|
||||
|
||||
"goals.json": Array<GoalDocument>;
|
||||
"goals.json": Array<SEEDS_GoalDocument>;
|
||||
"questlines.json": Array<QuestlineWithRelated>;
|
||||
"quests.json": Array<QuestWithRelated>;
|
||||
"tables.json": Array<TableWithRelated>;
|
||||
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
type GameGroup,
|
||||
type GoalDocument,
|
||||
type integer,
|
||||
type QuestDocument,
|
||||
type ScoreDocument,
|
||||
type SessionScoreInfo,
|
||||
type SEEDS_SongDocument,
|
||||
type SongDocument,
|
||||
type UserDocument,
|
||||
type V3Game,
|
||||
@@ -28,11 +28,13 @@ export function GetPBs(scoreInfo: SessionScoreInfo[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export function CreateSongMap<G extends GameGroup = GameGroup>(songs: SongDocument<G>[]) {
|
||||
export function CreateSongMap<G extends GameGroup = GameGroup>(
|
||||
songs: Array<SongDocument<G> | SEEDS_SongDocument<G>>,
|
||||
) {
|
||||
const songMap = new Map<string, SongDocument<G>>();
|
||||
|
||||
for (const song of songs) {
|
||||
songMap.set(song.id, song);
|
||||
songMap.set(song.id, song as SongDocument<G>);
|
||||
}
|
||||
|
||||
return songMap;
|
||||
@@ -48,8 +50,8 @@ export function CreateUserMap(users: UserDocument[]) {
|
||||
return userMap;
|
||||
}
|
||||
|
||||
export function CreateGoalMap(goals: GoalDocument[]) {
|
||||
const goalMap = new Map<string, GoalDocument>();
|
||||
export function CreateGoalMap<G extends { goalID: string }>(goals: Array<G>) {
|
||||
const goalMap = new Map<string, G>();
|
||||
|
||||
for (const goal of goals) {
|
||||
goalMap.set(goal.goalID, goal);
|
||||
@@ -93,7 +95,9 @@ export function CreateChartLink(chart: ChartDocument) {
|
||||
}
|
||||
|
||||
// stolen from server
|
||||
export function GetGoalIDsFromQuest(quest: QuestDocument) {
|
||||
export function GetGoalIDsFromQuest(quest: {
|
||||
questData: Array<{ goals: Array<{ goalID: string }> }>;
|
||||
}) {
|
||||
// this sucks - maybe a nicer way to do this, because nested
|
||||
// maps are just ugly
|
||||
return quest.questData.map((e) => e.goals.map((e) => e.goalID)).flat(1);
|
||||
|
||||
@@ -544,8 +544,8 @@ export function clamp(a: number, low: number, up: number) {
|
||||
return a;
|
||||
}
|
||||
|
||||
export function CreateQuestMap(quests: Array<QuestDocument>) {
|
||||
const map = new Map<string, QuestDocument>();
|
||||
export function CreateQuestMap<Q extends { questID: string }>(quests: Array<Q>) {
|
||||
const map = new Map<string, Q>();
|
||||
|
||||
for (const q of quests) {
|
||||
map.set(q.questID, q);
|
||||
|
||||
@@ -12,19 +12,20 @@ import {
|
||||
} from "#types/seeds";
|
||||
import {
|
||||
type AllDatabaseSeeds,
|
||||
type BMSCourseDocument,
|
||||
type ChartDocument,
|
||||
type ChartDocumentData,
|
||||
computeFolderSlug,
|
||||
CreateSongMap,
|
||||
DatabaseSeedNames,
|
||||
type FolderDocument,
|
||||
type GameGroup,
|
||||
type GoalDocument,
|
||||
type QuestDocument,
|
||||
type QuestlineDocument,
|
||||
type SEEDS_BMSCourseDocument,
|
||||
type SEEDS_ChartDocument,
|
||||
type SEEDS_FolderDocument,
|
||||
type SEEDS_GoalDocument,
|
||||
type SEEDS_QuestDocument,
|
||||
type SEEDS_QuestlineDocument,
|
||||
type SEEDS_TableDocument,
|
||||
type SeedFolderRow,
|
||||
type SongDocument,
|
||||
type TableDocument,
|
||||
} from "tachi-common";
|
||||
|
||||
import { APIFetchV1 } from "./api";
|
||||
@@ -211,10 +212,11 @@ function RelateBMSCourses(data: Partial<AllDatabaseSeeds>): BMSCourseWithRelated
|
||||
}
|
||||
|
||||
const songMap = CreateSongMap(bmsSongs);
|
||||
const chartMap = new Map<string, ChartDocument<"bms-7k" | "bms-14k">>();
|
||||
const chartMap = new Map<string, SEEDS_ChartDocument<"bms-7k" | "bms-14k">>();
|
||||
|
||||
for (const chart of bmsCharts) {
|
||||
chartMap.set(chart.data.hashMD5, chart);
|
||||
const d = chart.data as ChartDocumentData["bms-7k"] | ChartDocumentData["bms-14k"];
|
||||
chartMap.set(d.hashMD5, chart);
|
||||
}
|
||||
|
||||
return base.map((course) => {
|
||||
@@ -236,7 +238,7 @@ function RelateBMSCourses(data: Partial<AllDatabaseSeeds>): BMSCourseWithRelated
|
||||
|
||||
return {
|
||||
chart: chartMap.get(e)!,
|
||||
song: songMap.get(chartMap.get(e)!.song.id)!,
|
||||
song: songMap.get(chartMap.get(e)!.songID)! as SongDocument<"bms">,
|
||||
};
|
||||
}),
|
||||
},
|
||||
@@ -253,17 +255,12 @@ function RelateTables(data: Partial<AllDatabaseSeeds>): TableWithRelated[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
const folderById = new Map<string, FolderDocument>();
|
||||
const folderByGameSlug = new Map<string, FolderDocument>();
|
||||
const folderById = new Map<string, SEEDS_FolderDocument>();
|
||||
const folderByGameSlug = new Map<string, SEEDS_FolderDocument>();
|
||||
|
||||
for (const folder of data["folders.json"] ?? []) {
|
||||
const raw = folder as {
|
||||
id?: string;
|
||||
slug?: string;
|
||||
versionFilter?: Array<string>;
|
||||
where?: string;
|
||||
} & FolderDocument;
|
||||
const id = raw.folderID ?? raw.id;
|
||||
const raw = folder;
|
||||
const id = raw.id;
|
||||
|
||||
if (id !== undefined) {
|
||||
folderById.set(id, folder);
|
||||
@@ -340,10 +337,12 @@ function RelateQuests(data: Partial<AllDatabaseSeeds>): QuestWithRelated[] {
|
||||
function RelateCharts(data: Partial<AllDatabaseSeeds>, file: string): ChartWithRelated[] {
|
||||
const [_, game] = /charts-(.*?)\.json/u.exec(file)!;
|
||||
|
||||
// @ts-expect-error too lazy to fix this properly
|
||||
const songs: SongDocument[] = data[`songs-${game}.json`];
|
||||
// @ts-expect-error too lazy to fix this properly
|
||||
const charts: ChartDocument[] = data[`charts-${game}.json`];
|
||||
const songs = data[`songs-${game}.json` as keyof AllDatabaseSeeds] as
|
||||
| Array<SongDocument>
|
||||
| undefined;
|
||||
const charts = data[`charts-${game}.json` as keyof AllDatabaseSeeds] as
|
||||
| Array<SEEDS_ChartDocument>
|
||||
| undefined;
|
||||
|
||||
if (!charts) {
|
||||
return [];
|
||||
@@ -358,7 +357,7 @@ function RelateCharts(data: Partial<AllDatabaseSeeds>, file: string): ChartWithR
|
||||
return charts.map((e) => ({
|
||||
...e,
|
||||
__related: {
|
||||
song: songMap.get(e.song.id),
|
||||
song: songMap.get(e.songID),
|
||||
},
|
||||
}));
|
||||
}
|
||||
@@ -542,34 +541,34 @@ function GetUniqID<K extends keyof AllDatabaseSeeds>(collection: K, value: AllDa
|
||||
if (collection.startsWith("songs-")) {
|
||||
return (value as SongDocument).id.toString();
|
||||
} else if (collection.startsWith("charts-")) {
|
||||
return (value as ChartDocument).chartID;
|
||||
return (value as SEEDS_ChartDocument).id;
|
||||
}
|
||||
|
||||
const c = collection as NotSongsChartsSeeds;
|
||||
|
||||
switch (c) {
|
||||
case "bms-course-lookup.json": {
|
||||
const v = value as BMSCourseDocument;
|
||||
const v = value as SEEDS_BMSCourseDocument;
|
||||
return `${v.set}-${v.game}-${v.value}`;
|
||||
}
|
||||
case "folders.json": {
|
||||
const v = value as { id?: string } & FolderDocument;
|
||||
return v.folderID ?? v.id ?? "";
|
||||
const v = value as SEEDS_FolderDocument;
|
||||
return v.id;
|
||||
}
|
||||
case "tables.json": {
|
||||
const v = value as TableDocument;
|
||||
return v.tableID;
|
||||
const v = value as SEEDS_TableDocument;
|
||||
return v.id;
|
||||
}
|
||||
case "goals.json": {
|
||||
const v = value as GoalDocument;
|
||||
const v = value as SEEDS_GoalDocument;
|
||||
return v.goalID;
|
||||
}
|
||||
case "quests.json": {
|
||||
const v = value as QuestDocument;
|
||||
const v = value as SEEDS_QuestDocument;
|
||||
return v.questID;
|
||||
}
|
||||
case "questlines.json": {
|
||||
const v = value as QuestlineDocument;
|
||||
const v = value as SEEDS_QuestlineDocument;
|
||||
return v.questlineID;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ export const GAME_MAIMAI_DX_CONF = {
|
||||
defaultProfileRatingAlg: "naiveRate",
|
||||
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
type: "CHUGEKIMAI_STYLE",
|
||||
order: [
|
||||
"Basic",
|
||||
"Advanced",
|
||||
|
||||
@@ -124,12 +124,8 @@ export const GAME_MAIMAI_CONF = {
|
||||
defaultSessionRatingAlg: "rate",
|
||||
defaultProfileRatingAlg: "naiveRate",
|
||||
|
||||
// Similar to CHUNITHM, this game has a dynamic set of difficulties,
|
||||
// with a song able to have as many UTAGE difficulties as it likes.
|
||||
// This will also not be implemented for the same reasons as CHUNITHM.
|
||||
// Sorry!
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
type: "CHUGEKIMAI_STYLE",
|
||||
order: ["Easy", "Basic", "Advanced", "Expert", "Master", "Re:Master"],
|
||||
format: {
|
||||
Easy: "ESY",
|
||||
|
||||
@@ -182,7 +182,7 @@ export const GAME_ONGEKI_CONF = {
|
||||
defaultProfileRatingAlg: "naiveRating",
|
||||
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
type: "CHUGEKIMAI_STYLE",
|
||||
order: ["BASIC", "ADVANCED", "EXPERT", "MASTER", "LUNATIC"],
|
||||
format: {
|
||||
BASIC: "BAS",
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { GameGroup, V3Game } from "../types";
|
||||
import type {
|
||||
BMSCourseDocument,
|
||||
ChartDocument,
|
||||
FolderDocument,
|
||||
GameGroup,
|
||||
GoalDocument,
|
||||
QuestDocument,
|
||||
QuestlineDocument,
|
||||
SongDocument,
|
||||
TableDocument,
|
||||
V3Game,
|
||||
} from "../types";
|
||||
SEEDS_BMSCourseDocument,
|
||||
SEEDS_ChartDocument,
|
||||
SEEDS_FolderDocument,
|
||||
SEEDS_GoalDocument,
|
||||
SEEDS_QuestDocument,
|
||||
SEEDS_QuestlineDocument,
|
||||
SEEDS_SongDocument,
|
||||
SEEDS_TableDocument,
|
||||
} from "../types/seeds-documents";
|
||||
|
||||
import { allSupportedGameGroups } from "../config/config";
|
||||
|
||||
@@ -17,20 +16,20 @@ import { allSupportedGameGroups } from "../config/config";
|
||||
// note that TS won't let you do this multiple times within an object
|
||||
// so, we have to join them ourselves. Ah well, not that bad.
|
||||
type ChartDBSeeds = {
|
||||
[TGame in V3Game as `charts-${TGame}.json`]: Array<ChartDocument<TGame>>;
|
||||
[TGame in V3Game as `charts-${TGame}.json`]: Array<SEEDS_ChartDocument<TGame>>;
|
||||
};
|
||||
|
||||
type SongDBSeeds = {
|
||||
[G in GameGroup as `songs-${G}.json`]: Array<SongDocument<G>>;
|
||||
[G in GameGroup as `songs-${G}.json`]: Array<SEEDS_SongDocument<G>>;
|
||||
};
|
||||
|
||||
interface OtherDBSeeds {
|
||||
"bms-course-lookup.json": Array<BMSCourseDocument>;
|
||||
"folders.json": Array<FolderDocument>;
|
||||
"goals.json": Array<GoalDocument>;
|
||||
"questlines.json": Array<QuestlineDocument>;
|
||||
"quests.json": Array<QuestDocument>;
|
||||
"tables.json": Array<TableDocument>;
|
||||
"bms-course-lookup.json": Array<SEEDS_BMSCourseDocument>;
|
||||
"folders.json": Array<SEEDS_FolderDocument>;
|
||||
"goals.json": Array<SEEDS_GoalDocument>;
|
||||
"questlines.json": Array<SEEDS_QuestlineDocument>;
|
||||
"quests.json": Array<SEEDS_QuestDocument>;
|
||||
"tables.json": Array<SEEDS_TableDocument>;
|
||||
}
|
||||
|
||||
export type AllDatabaseSeeds = ChartDBSeeds & OtherDBSeeds & SongDBSeeds;
|
||||
|
||||
@@ -31,3 +31,4 @@ export * from "./types/documents";
|
||||
export * from "./types/game-config";
|
||||
export * from "./types/import-types";
|
||||
export * from "./types/notifications";
|
||||
export * from "./types/seeds-documents";
|
||||
|
||||
@@ -123,10 +123,36 @@ export interface FixedDifficulties<Difficulty extends string> {
|
||||
order: ReadonlyArray<Difficulty>;
|
||||
|
||||
/**
|
||||
* For mobile view (and certain pages where things are generally smaller)
|
||||
* how should we shorten these difficulty names?
|
||||
* How should we format these difficulty names?
|
||||
*/
|
||||
format: Partial<Record<Difficulty, string>>;
|
||||
|
||||
default: Difficulty;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is for chugekimai; where they have a "fixed" set of difficulties, and then a dynamic amount
|
||||
* of difficulties for LUNATIC/WORLD'S END/UTAGE charts. This way, we can have the fixed set and
|
||||
* still support the dynamic set of additional ones.
|
||||
*
|
||||
* This is a bit of a bodge. We don't have a nice way of handling this - but we also don't have a
|
||||
* nice way of disambiguating score imports as usual - we can't let people import "i got xxx on
|
||||
* the worlds end for yyy", because such a uniqueness relation *does not exist*. the difficulty name
|
||||
* is not the unique bit - it is meaningless flavour like the dynamic diffnames.
|
||||
*
|
||||
* Originally, I called this "SEMI_FIXED", which is the sort of ZK quirkiness you've came to know at
|
||||
* this point, but now there's no point, this is for chugekimai, and this makes it clear.
|
||||
*/
|
||||
export interface ChuGekiMaiDifficulties<Difficulty extends string> {
|
||||
type: "CHUGEKIMAI_STYLE";
|
||||
|
||||
order: ReadonlyArray<Difficulty>;
|
||||
|
||||
/**
|
||||
* How should we format these difficulty names?
|
||||
*
|
||||
* For example, in IIDX "ANOTHER" is generally shortened to "A".
|
||||
* Dynamic ones (i.e. ones not in the Order set) are not formatted - they are printed
|
||||
* exactly as-is.
|
||||
*/
|
||||
format: Partial<Record<Difficulty, string>>;
|
||||
|
||||
@@ -134,5 +160,6 @@ export interface FixedDifficulties<Difficulty extends string> {
|
||||
}
|
||||
|
||||
export type DifficultyConfig<D extends string = string> =
|
||||
| ChuGekiMaiDifficulties<D>
|
||||
| DynamicDifficulties
|
||||
| FixedDifficulties<D>;
|
||||
|
||||
@@ -241,6 +241,7 @@ export type LEGACY_GameToPlaytype = {
|
||||
export type Difficulties = {
|
||||
// If this game has fixed difficulties, infer what they are
|
||||
// otherwise, difficulties are an arbitrary string
|
||||
// n.b. still true even with the new chugekimai stuff
|
||||
[TGame in V3Game]: (typeof GAME_CONFIGS)[TGame]["difficulties"] extends FixedDifficulties<
|
||||
infer D
|
||||
>
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import z from "zod";
|
||||
|
||||
import type {
|
||||
ChartDocumentData,
|
||||
Difficulties,
|
||||
GameGroup,
|
||||
SongDocumentData,
|
||||
V3Game,
|
||||
Versions,
|
||||
} from "./game-config";
|
||||
import type { DifficultyConfig } from "./game-config-utils";
|
||||
|
||||
import {
|
||||
ALL_GAMES,
|
||||
allSupportedGameGroups,
|
||||
GAME_GROUP_CONFIGS,
|
||||
GetSpecificGameConfig,
|
||||
} from "../config/config";
|
||||
|
||||
/** Tachi seed row id: one-letter prefix + 19 lowercase hex digits. */
|
||||
export function SEEDS_TachiIdSchema(prefix: string) {
|
||||
return z.string().regex(new RegExp(`^${prefix}[0-9a-f]{19}$`, "u"));
|
||||
}
|
||||
|
||||
export const SEEDS_V3_GAME_SCHEMA = z.enum(ALL_GAMES as [V3Game, ...Array<V3Game>]);
|
||||
|
||||
export const SEEDS_V3_GAME_GROUP_SCHEMA = z.enum(
|
||||
allSupportedGameGroups as [GameGroup, ...Array<GameGroup>],
|
||||
);
|
||||
|
||||
function difficultyZodSchema<G extends V3Game>(game: G): z.ZodType<Difficulties[G]> {
|
||||
const d = GetSpecificGameConfig(game).difficulties as DifficultyConfig;
|
||||
if (d.type === "DYNAMIC") {
|
||||
return z.string() as unknown as z.ZodType<Difficulties[G]>;
|
||||
}
|
||||
const order = d.order as ReadonlyArray<string>;
|
||||
return z.enum(order as [string, ...Array<string>]) as unknown as z.ZodType<Difficulties[G]>;
|
||||
}
|
||||
|
||||
function versionsZodSchema<G extends V3Game>(game: G): z.ZodType<Array<Versions[G]>> {
|
||||
const versionKeys = Object.keys(GetSpecificGameConfig(game).versions);
|
||||
|
||||
return z.array(z.string()).superRefine((val, ctx) => {
|
||||
if (versionKeys.length === 0) {
|
||||
if (val.length > 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "This game defines no chart versions; expected [].",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const v of val) {
|
||||
if (!versionKeys.includes(v)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Invalid version ${v}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (new Set(val).size !== val.length) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Versions array shouldn't contain the same version twice",
|
||||
});
|
||||
}
|
||||
}) as unknown as z.ZodType<Array<Versions[G]>>;
|
||||
}
|
||||
|
||||
function seedsChartDocumentSchemaForGame<G extends V3Game>(game: G) {
|
||||
const chartData = GetSpecificGameConfig(game).chartData as unknown as z.ZodType<ChartDocumentData[G]>;
|
||||
|
||||
return z.strictObject({
|
||||
data: chartData,
|
||||
difficulty: difficultyZodSchema(game),
|
||||
id: SEEDS_TachiIdSchema("C"),
|
||||
isPrimary: z.boolean(),
|
||||
legacyChartID: z.string(),
|
||||
level: z.string(),
|
||||
levelNum: z.number(),
|
||||
songID: SEEDS_TachiIdSchema("S"),
|
||||
versions: versionsZodSchema(game),
|
||||
}) as unknown as z.ZodType<{
|
||||
data: ChartDocumentData[G];
|
||||
difficulty: Difficulties[G];
|
||||
id: string;
|
||||
isPrimary: boolean;
|
||||
legacyChartID: string;
|
||||
level: string;
|
||||
levelNum: number;
|
||||
songID: string;
|
||||
versions: Array<Versions[G]>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** One schema per `charts-${game}.json` collection. */
|
||||
export const SEEDS_CHART_DOCUMENT_SCHEMAS = Object.fromEntries(
|
||||
ALL_GAMES.map((game) => [game, seedsChartDocumentSchemaForGame(game)]),
|
||||
) as {
|
||||
readonly [G in V3Game]: ReturnType<typeof seedsChartDocumentSchemaForGame>;
|
||||
};
|
||||
|
||||
export type SEEDS_ChartDocument<TGame extends V3Game = V3Game> = z.infer<
|
||||
(typeof SEEDS_CHART_DOCUMENT_SCHEMAS)[TGame]
|
||||
>;
|
||||
|
||||
function seedsSongDocumentSchemaForGroup<G extends GameGroup>(group: G) {
|
||||
const songData = GAME_GROUP_CONFIGS[group].songData as unknown as z.ZodType<SongDocumentData[G]>;
|
||||
|
||||
return z.strictObject({
|
||||
altTitles: z.array(z.string()),
|
||||
artist: z.string(),
|
||||
data: songData,
|
||||
id: SEEDS_TachiIdSchema("S"),
|
||||
legacySongID: z.number(),
|
||||
searchTerms: z.array(z.string()),
|
||||
title: z.string(),
|
||||
}) as unknown as z.ZodType<{
|
||||
altTitles: Array<string>;
|
||||
artist: string;
|
||||
data: SongDocumentData[G];
|
||||
id: string;
|
||||
legacySongID: number;
|
||||
searchTerms: Array<string>;
|
||||
title: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** One schema per `songs-${gameGroup}.json` collection. */
|
||||
export const SEEDS_SONG_DOCUMENT_SCHEMAS = Object.fromEntries(
|
||||
allSupportedGameGroups.map((group) => [group, seedsSongDocumentSchemaForGroup(group)]),
|
||||
) as {
|
||||
readonly [G in GameGroup]: ReturnType<typeof seedsSongDocumentSchemaForGroup>;
|
||||
};
|
||||
|
||||
export type SEEDS_SongDocument<G extends GameGroup = GameGroup> = z.infer<
|
||||
(typeof SEEDS_SONG_DOCUMENT_SCHEMAS)[G]
|
||||
>;
|
||||
|
||||
export const SEEDS_BMS_COURSE_DOCUMENT_SCHEMA = z.strictObject({
|
||||
game: z.enum(["bms-14k", "bms-7k", "pms-controller", "pms-keyboard"]),
|
||||
md5sums: z.string(),
|
||||
set: z.string(),
|
||||
title: z.string(),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type SEEDS_BMSCourseDocument = z.infer<typeof SEEDS_BMS_COURSE_DOCUMENT_SCHEMA>;
|
||||
|
||||
export const SEEDS_TABLE_DOCUMENT_SCHEMA = z.strictObject({
|
||||
default: z.boolean(),
|
||||
description: z.string(),
|
||||
folders: z.array(z.string()),
|
||||
game: SEEDS_V3_GAME_SCHEMA,
|
||||
id: SEEDS_TachiIdSchema("T"),
|
||||
inactive: z.boolean(),
|
||||
legacyTableID: z.string(),
|
||||
slug: z.string().optional(),
|
||||
title: z.string(),
|
||||
});
|
||||
|
||||
export type SEEDS_TableDocument = z.infer<typeof SEEDS_TABLE_DOCUMENT_SCHEMA>;
|
||||
|
||||
const seedsQuestGoalRefSchema = z.strictObject({
|
||||
goalID: z.string(),
|
||||
note: z.string().optional(),
|
||||
});
|
||||
|
||||
const seedsQuestSectionSchema = z.strictObject({
|
||||
desc: z.string().optional(),
|
||||
goals: z.array(seedsQuestGoalRefSchema),
|
||||
title: z.string(),
|
||||
});
|
||||
|
||||
export const SEEDS_QUEST_DOCUMENT_SCHEMA = z.strictObject({
|
||||
desc: z.string(),
|
||||
game: SEEDS_V3_GAME_GROUP_SCHEMA,
|
||||
name: z.string(),
|
||||
playtype: z.string(),
|
||||
questData: z.array(seedsQuestSectionSchema),
|
||||
questID: z.string(),
|
||||
});
|
||||
|
||||
export type SEEDS_QuestDocument = z.infer<typeof SEEDS_QUEST_DOCUMENT_SCHEMA>;
|
||||
|
||||
export const SEEDS_QUESTLINE_DOCUMENT_SCHEMA = z.strictObject({
|
||||
desc: z.string(),
|
||||
game: SEEDS_V3_GAME_GROUP_SCHEMA,
|
||||
name: z.string(),
|
||||
playtype: z.string(),
|
||||
questlineID: z.string(),
|
||||
quests: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type SEEDS_QuestlineDocument = z.infer<typeof SEEDS_QUESTLINE_DOCUMENT_SCHEMA>;
|
||||
|
||||
/** Row shape in `folders.json` seed collections. */
|
||||
export const SEEDS_FOLDER_DOCUMENT_SCHEMA = z.strictObject({
|
||||
game: SEEDS_V3_GAME_SCHEMA,
|
||||
id: SEEDS_TachiIdSchema("F"),
|
||||
inactive: z.boolean(),
|
||||
legacyFolderID: z.string(),
|
||||
searchTerms: z.array(z.string()),
|
||||
slug: z.string(),
|
||||
title: z.string(),
|
||||
versionFilter: z.array(z.string()).optional(),
|
||||
where: z.string(),
|
||||
});
|
||||
|
||||
export type SEEDS_FolderDocument = z.infer<typeof SEEDS_FOLDER_DOCUMENT_SCHEMA>;
|
||||
|
||||
const SEEDS_GOAL_CRITERIA_SCHEMA = z.union([
|
||||
z.strictObject({
|
||||
countNum: z.number(),
|
||||
key: z.string(),
|
||||
mode: z.union([z.literal("absolute"), z.literal("proportion")]),
|
||||
value: z.number(),
|
||||
}),
|
||||
z.strictObject({
|
||||
key: z.string(),
|
||||
mode: z.literal("single"),
|
||||
value: z.number(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const SEEDS_GOAL_CHARTS_SCHEMA = z.discriminatedUnion("type", [
|
||||
z.strictObject({
|
||||
data: z.string(),
|
||||
folderSlug: z.string().optional(),
|
||||
type: z.literal("folder"),
|
||||
}),
|
||||
z.strictObject({
|
||||
data: z.array(z.string()),
|
||||
type: z.literal("multi"),
|
||||
}),
|
||||
z.strictObject({
|
||||
data: z.string(),
|
||||
type: z.literal("single"),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const SEEDS_GOAL_DOCUMENT_SCHEMA = z.strictObject({
|
||||
charts: SEEDS_GOAL_CHARTS_SCHEMA,
|
||||
criteria: SEEDS_GOAL_CRITERIA_SCHEMA,
|
||||
game: SEEDS_V3_GAME_GROUP_SCHEMA,
|
||||
goalID: z.string(),
|
||||
name: z.string(),
|
||||
playtype: z.string(),
|
||||
});
|
||||
|
||||
export type SEEDS_GoalDocument = z.infer<typeof SEEDS_GOAL_DOCUMENT_SCHEMA>;
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./seeds-documents-zod";
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
GamesForGroup,
|
||||
integer,
|
||||
LEGACY_Playtypes,
|
||||
SEEDS_BMSCourseDocument,
|
||||
SEEDS_SongDocument,
|
||||
SongDocument,
|
||||
V3Game,
|
||||
} from "../types";
|
||||
@@ -357,11 +359,13 @@ export function GetCloserGradeDelta<G extends string>(
|
||||
return lower;
|
||||
}
|
||||
|
||||
export function CreateSongMap<G extends GameGroup = GameGroup>(songs: Array<SongDocument<G>>) {
|
||||
export function CreateSongMap<G extends GameGroup = GameGroup>(
|
||||
songs: Array<SEEDS_SongDocument<G> | SongDocument<G>>,
|
||||
) {
|
||||
const songMap = new Map<string, SongDocument<G>>();
|
||||
|
||||
for (const song of songs) {
|
||||
songMap.set(song.id, song);
|
||||
songMap.set(song.id, song as SongDocument<G>);
|
||||
}
|
||||
|
||||
return songMap;
|
||||
@@ -391,7 +395,7 @@ export function FormatPrError(err: PrudenceError, foreword = "Error"): string {
|
||||
return `${foreword}: ${err.keychain} | ${err.message}${receivedText}.`;
|
||||
}
|
||||
|
||||
export function GetBMSCourseIndex(course: BMSCourseDocument) {
|
||||
export function GetBMSCourseIndex(course: BMSCourseDocument | SEEDS_BMSCourseDocument) {
|
||||
const gameConfig = GetGameConfig(course.game);
|
||||
|
||||
const cls = gameConfig.classes[course.set as keyof typeof gameConfig.classes];
|
||||
|
||||
@@ -71,8 +71,6 @@ for (const collection of collections) {
|
||||
} else {
|
||||
success++;
|
||||
}
|
||||
|
||||
success++;
|
||||
}
|
||||
|
||||
const report = `GOOD: ${success}, BAD: ${fails}(${Math.min(
|
||||
|
||||
@@ -54,7 +54,7 @@ const refChecks: Array<ReferenceCheck> = [
|
||||
base: "tables",
|
||||
baseKey: (table) => table.folders.map((slug: string) => `${table.game}:${slug}`),
|
||||
parent: "folders",
|
||||
parentKey: (folder) => `${folder.game}:${folderSeedSlug(folder)}`,
|
||||
parentKey: (folder) => [`${folder.game}:${folderSeedSlug(folder)}`],
|
||||
gameGroup: null,
|
||||
game: null,
|
||||
},
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import {
|
||||
ALL_GAMES,
|
||||
allSupportedGameGroups,
|
||||
GAME_CONFIGS,
|
||||
GAME_GROUP_CONFIGS,
|
||||
SEEDS_BMS_COURSE_DOCUMENT_SCHEMA,
|
||||
SEEDS_CHART_DOCUMENT_SCHEMAS,
|
||||
SEEDS_FOLDER_DOCUMENT_SCHEMA,
|
||||
SEEDS_GOAL_DOCUMENT_SCHEMA,
|
||||
SEEDS_QUEST_DOCUMENT_SCHEMA,
|
||||
SEEDS_QUESTLINE_DOCUMENT_SCHEMA,
|
||||
SEEDS_SONG_DOCUMENT_SCHEMAS,
|
||||
SEEDS_TABLE_DOCUMENT_SCHEMA,
|
||||
type GameGroup,
|
||||
type V3Game,
|
||||
} from "tachi-common";
|
||||
import { z, type ZodType } from "zod";
|
||||
import { type ZodType } from "zod";
|
||||
|
||||
export type AllCollections =
|
||||
| "bms-course-lookup.json"
|
||||
@@ -18,94 +24,23 @@ export type AllCollections =
|
||||
| `charts-${V3Game}.json`
|
||||
| `songs-${GameGroup}.json`;
|
||||
|
||||
export const V3_GAME_SCHEMA = z.enum(ALL_GAMES as [V3Game, ...Array<V3Game>]);
|
||||
export const V3_GAME_GROUP_SCHEMA = z.enum(
|
||||
allSupportedGameGroups as [GameGroup, ...Array<GameGroup>],
|
||||
);
|
||||
/** @deprecated Use {@link SEEDS_CHART_DOCUMENT_SCHEMAS} from tachi-common. */
|
||||
export const V3_CHART_SCHEMAS = Object.fromEntries(
|
||||
ALL_GAMES.map((game) => [`charts-${game}.json` as const, SEEDS_CHART_DOCUMENT_SCHEMAS[game]]),
|
||||
) as Record<`charts-${V3Game}.json`, ZodType>;
|
||||
|
||||
export const V3_TACHI_ID = (prefix: string) =>
|
||||
z.string().regex(new RegExp(`^${prefix}[0-9a-f]{19}$`, "u"));
|
||||
|
||||
export const V3_FOLDER_SCHEMA = z.strictObject({
|
||||
title: z.string(),
|
||||
game: V3_GAME_SCHEMA,
|
||||
id: V3_TACHI_ID("F"),
|
||||
legacyFolderID: z.string(),
|
||||
inactive: z.boolean(),
|
||||
searchTerms: z.array(z.string()),
|
||||
versionFilter: z.array(z.string()).optional(),
|
||||
where: z.string(),
|
||||
slug: z.string().optional(),
|
||||
});
|
||||
|
||||
export const V3_TABLE_SCHEMA = z.strictObject({
|
||||
game: V3_GAME_SCHEMA,
|
||||
id: V3_TACHI_ID("T"),
|
||||
legacyTableID: z.string(),
|
||||
inactive: z.boolean(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
slug: z.string().optional(),
|
||||
default: z.boolean(),
|
||||
folders: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const V3_BMS_COURSE_LOOKUP_SCHEMA = z.strictObject({
|
||||
md5sums: z.string(),
|
||||
title: z.string(),
|
||||
set: z.string(),
|
||||
game: z.enum(["bms-7k", "bms-14k"]),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
// TODO(zk): impl
|
||||
export const V3_QUEST_SCHEMA = z.any();
|
||||
export const V3_QUESTLINE_SCHEMA = z.any();
|
||||
export const V3_GOAL_SCHEMA = z.any();
|
||||
|
||||
export const V3_SONG_SCHEMAS: Record<`songs-${GameGroup}.json`, ZodType> = Object.fromEntries(
|
||||
allSupportedGameGroups.map((gameGroup) => [
|
||||
`songs-${gameGroup}.json` as const,
|
||||
z.strictObject({
|
||||
id: V3_TACHI_ID("S"),
|
||||
legacySongID: z.number(),
|
||||
title: z.string(),
|
||||
artist: z.string(),
|
||||
altTitles: z.array(z.string()),
|
||||
searchTerms: z.array(z.string()),
|
||||
data: GAME_GROUP_CONFIGS[gameGroup].songData,
|
||||
}),
|
||||
]),
|
||||
) as unknown as Record<`songs-${GameGroup}.json`, ZodType>;
|
||||
// ^ object.fromEntries is hardcoded to [k: string]
|
||||
// even when k is a subset of string
|
||||
|
||||
export const V3_CHART_SCHEMAS: Record<`charts-${V3Game}.json`, ZodType> = Object.fromEntries(
|
||||
ALL_GAMES.map((game) => [
|
||||
`charts-${game}.json` as const,
|
||||
z.strictObject({
|
||||
id: V3_TACHI_ID("C"),
|
||||
// TODO(zk): Game specific?
|
||||
difficulty: z.string(),
|
||||
isPrimary: z.boolean(),
|
||||
legacyChartID: z.string(),
|
||||
level: z.string(),
|
||||
levelNum: z.number(),
|
||||
songID: V3_TACHI_ID("S"),
|
||||
versions: z.array(z.string()),
|
||||
data: GAME_CONFIGS[game].chartData,
|
||||
}),
|
||||
]),
|
||||
) as unknown as Record<`charts-${V3Game}.json`, ZodType>;
|
||||
/** @deprecated Use {@link SEEDS_SONG_DOCUMENT_SCHEMAS} from tachi-common. */
|
||||
export const V3_SONG_SCHEMAS = Object.fromEntries(
|
||||
allSupportedGameGroups.map((g) => [`songs-${g}.json` as const, SEEDS_SONG_DOCUMENT_SCHEMAS[g]]),
|
||||
) as Record<`songs-${GameGroup}.json`, ZodType>;
|
||||
|
||||
export const V3_SCHEMAS: Record<AllCollections, ZodType> = {
|
||||
"bms-course-lookup.json": V3_BMS_COURSE_LOOKUP_SCHEMA,
|
||||
"folders.json": V3_FOLDER_SCHEMA,
|
||||
"tables.json": V3_TABLE_SCHEMA,
|
||||
"quests.json": V3_QUEST_SCHEMA,
|
||||
"questlines.json": V3_QUESTLINE_SCHEMA,
|
||||
"goals.json": V3_GOAL_SCHEMA,
|
||||
|
||||
"bms-course-lookup.json": SEEDS_BMS_COURSE_DOCUMENT_SCHEMA,
|
||||
"folders.json": SEEDS_FOLDER_DOCUMENT_SCHEMA,
|
||||
"goals.json": SEEDS_GOAL_DOCUMENT_SCHEMA,
|
||||
"questlines.json": SEEDS_QUESTLINE_DOCUMENT_SCHEMA,
|
||||
"quests.json": SEEDS_QUEST_DOCUMENT_SCHEMA,
|
||||
"tables.json": SEEDS_TABLE_DOCUMENT_SCHEMA,
|
||||
...V3_SONG_SCHEMAS,
|
||||
...V3_CHART_SCHEMAS,
|
||||
};
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { z } from "zod";
|
||||
|
||||
import chalk from "chalk";
|
||||
import { ALL_GAMES, FormatGame } from "tachi-common";
|
||||
import { ALL_GAMES, FormatGame, type SEEDS_TABLE_DOCUMENT_SCHEMA } from "tachi-common";
|
||||
|
||||
import { ReadCollection } from "../util";
|
||||
import { type V3_TABLE_SCHEMA } from "./schemas";
|
||||
|
||||
const defaultTableMap = {};
|
||||
|
||||
const tables: Array<z.infer<typeof V3_TABLE_SCHEMA>> = ReadCollection("tables.json");
|
||||
const tables: Array<z.infer<typeof SEEDS_TABLE_DOCUMENT_SCHEMA>> = ReadCollection("tables.json");
|
||||
|
||||
let errs = 0;
|
||||
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"#*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", "rerunners/**"]
|
||||
}
|
||||
|
||||
@@ -11,10 +11,6 @@ export function AssertStrAsDifficulty(strVal: string, game: V3Game): Difficultie
|
||||
}
|
||||
|
||||
if (!diffConf.order.includes(strVal)) {
|
||||
if (game === "chunithm" && strVal === "WORLD'S END") {
|
||||
throw new InvalidScoreFailure("WORLD'S END is not supported. Sorry.");
|
||||
}
|
||||
|
||||
throw new InvalidScoreFailure(
|
||||
`Invalid Difficulty for ${FormatGame(game)} - Expected any of ${diffConf.order.join(
|
||||
", ",
|
||||
|
||||
@@ -482,6 +482,23 @@ export async function ResolveChartFromSong(
|
||||
|
||||
const difficulty = AssertStrAsDifficulty(resolver.difficulty, game);
|
||||
|
||||
const gameConfig = GetGameConfig(resolver.game);
|
||||
switch (gameConfig.difficulties.type) {
|
||||
case "DYNAMIC":
|
||||
throw new InvalidScoreFailure(
|
||||
"You cannot use a songTitle+difficulty lookup for a game with 'DYNAMIC' difficulties",
|
||||
);
|
||||
case "CHUGEKIMAI_STYLE":
|
||||
if (!gameConfig.difficulties.order.includes(difficulty)) {
|
||||
throw new InvalidScoreFailure(
|
||||
`This difficulty "${difficulty}" is not supported for songTitle+difficulty lookups. If you are trying to import for WORLD'S END/LUNATIC/UTAGE scores, you must use inGameID, or a different lookup method.`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "FIXED":
|
||||
break;
|
||||
}
|
||||
|
||||
let chart;
|
||||
|
||||
if (resolver.version) {
|
||||
|
||||
Reference in New Issue
Block a user