port a bunch of seeds scripts to v3 (#1744)

This commit is contained in:
nairobi
2026-09-16 20:44:21 +01:00
committed by GitHub
parent bbce596079
commit 0cd0c4979f
17 changed files with 250 additions and 234 deletions
@@ -1,65 +0,0 @@
import { FormatGame } from "../../common/src/index.ts";
import { CreateFolderID, MutateCollection } from "../util.js";
export default function AddLevelVersionFolders(name, game, playtypes, version, levels) {
const ptFolders = {};
MutateCollection("folders.json", (folders) => {
for (const level of levels) {
const folder = {
data: {
level,
versions: version,
},
game,
inactive: false,
searchTerms: [],
title: `Level ${level} (${name})`,
type: "charts",
};
for (const playtype of playtypes) {
const folderID = CreateFolderID(folder.data, folder.game, playtype);
const realFolder = Object.assign({ folderID, playtype }, folder);
if (ptFolders[playtype]) {
ptFolders[playtype].push(realFolder);
} else {
ptFolders[playtype] = [realFolder];
}
folders.push(realFolder);
}
}
return folders;
});
MutateCollection("tables.json", (tables) => {
for (const playtype of playtypes) {
tables.push({
default: false,
description: `Levels for ${FormatGame(game, playtype)} in ${name}.`,
folders: ptFolders[playtype].map((e) => e.folderID),
game,
inactive: false,
playtype,
tableID: `${game}-${playtype}-${version}-levels`,
title: `${FormatGame(game, playtype)} (${name})`,
});
}
return tables;
});
}
// usage:
// const levels = [];
// for (let i = 1; i <= 12; i++) {
// levels.push(i.toString())
// }
//
// AddLevelVersionFolders("CastHour", "iidx", ["SP", "DP"], "29", levels);
@@ -0,0 +1,64 @@
import {
CreateTableID,
FormatGame,
SEEDS_FolderDocument,
SEEDS_TableDocument,
V3Game,
} from "tachi-common";
import { CreateFolderID, CreateRandomLegacyFolderID, MutateCollection } from "../util.js";
export default function AddLevelVersionFolders(
name: string,
shortName: string,
game: V3Game,
version: string,
levels: string[],
) {
const slugs: string[] = [];
MutateCollection("folders.json", (foldersCol: SEEDS_FolderDocument[]) => {
for (const level of levels) {
const slug = `${level}-${shortName}`;
const folder: SEEDS_FolderDocument = {
game,
id: CreateFolderID(),
inactive: false,
legacyFolderID: CreateRandomLegacyFolderID(),
searchTerms: [],
slug,
title: `Level ${level} (${name})`,
versionFilter: [version],
where: `chart.level = '${level}'`,
};
slugs.push(slug);
foldersCol.push(folder);
}
return foldersCol;
});
MutateCollection("tables.json", (tables: SEEDS_TableDocument[]) => {
tables.push({
default: false,
description: `Levels for ${FormatGame(game)} in ${name}.`,
folders: slugs,
game,
inactive: false,
id: CreateTableID(),
legacyTableID: `${game}-${shortName}-levels`,
title: `${FormatGame(game)} (${name})`,
});
return tables;
});
}
// usage:
// const levels = [];
// for (let i = 1; i <= 12; i++) {
// levels.push(i.toString())
// }
//
// AddLevelVersionFolders("CastHour", "casthour", "iidx-sp", "29", levels);
@@ -1,85 +0,0 @@
import { CreateFolderID, MutateCollection } from "../util.js";
// Change these for whatever table you are adding.
const GAME = "ddr";
const PLAYTYPES = ["SP", "DP"];
const PREFIX = "Level ";
const TITLE = "DDR A3";
const SHORTTITLE = "a3"; // this is used in the tableID
const DESCRIPTION = "All songs in DDR A3";
const LEVELS = [
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
];
const ptFolders = {};
MutateCollection("folders.json", (foldersCol) => {
for (const level of LEVELS) {
const folder = {
data: {
"data¬tableFolders": {
"~elemMatch": {
level,
table: PREFIX,
},
},
},
game: GAME,
inactive: false,
searchTerms: [],
title: `${PREFIX}${level}`,
type: "charts",
};
for (const playtype of PLAYTYPES) {
const folderID = CreateFolderID(folder.data, folder.game, playtype);
const realFolder = Object.assign({ folderID, playtype }, folder);
if (ptFolders[playtype]) {
ptFolders[playtype].push(realFolder);
} else {
ptFolders[playtype] = [realFolder];
}
foldersCol.push(realFolder);
}
}
return foldersCol;
});
MutateCollection("tables.json", (tables) => {
for (const playtype of PLAYTYPES) {
tables.push({
default: false,
description: DESCRIPTION,
folders: ptFolders[playtype].map((e) => e.folderID),
game: GAME,
inactive: false,
playtype,
tableID: `${GAME}-${playtype}-${SHORTTITLE}`,
title: TITLE,
});
}
return tables;
});
@@ -0,0 +1,73 @@
import { CreateTableID, SEEDS_FolderDocument, SEEDS_TableDocument } from "tachi-common";
import { CreateFolderID, CreateRandomLegacyFolderID, MutateCollection } from "../util.js";
// The original intention was for you to
// change these for whatever table you are adding.
// But it would be better if you only used this as a reference
// and made a dedicated rerunner.
const GAME = "ddr-sp";
const PREFIX = "Level ";
const TITLE = "DDR A3";
const SHORTTITLE = "a3"; // this is used in the tableID
const VERSION = "a3";
const DESCRIPTION = "All songs in DDR A3";
const LEVELS = [
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
];
const slugs: string[] = [];
MutateCollection("folders.json", (foldersCol: SEEDS_FolderDocument[]) => {
for (const level of LEVELS) {
const slug = `${level}-${SHORTTITLE}`;
const folder: SEEDS_FolderDocument = {
game: GAME,
id: CreateFolderID(),
inactive: false,
legacyFolderID: CreateRandomLegacyFolderID(),
searchTerms: [],
slug,
title: `${PREFIX}${level}`,
versionFilter: [VERSION],
where: `chart.level = '${level}'`,
};
slugs.push(slug);
foldersCol.push(folder);
}
return foldersCol;
});
MutateCollection("tables.json", (tables: SEEDS_TableDocument[]) => {
tables.push({
default: false,
description: DESCRIPTION,
folders: slugs,
game: GAME,
inactive: false,
id: CreateTableID(),
legacyTableID: `${GAME}-${SHORTTITLE}`,
title: TITLE,
});
return tables;
});
@@ -52,7 +52,7 @@ to adjust the constants for the version you're adding.
3. Deactivate old tables using `seeds/scripts/rerunners/toggle-table-inactive.js`:
```
bun toggle-table-inactive.js -t chunithm-Single-<VERSION>-levels
bun toggle-table-inactive.ts -t chunithm-Single-<VERSION>-levels
```
Keep tables for the current and last (aka n-1) versions.
@@ -1,14 +1,14 @@
import crypto from "crypto";
import { log as logger } from "../log.ts";
import { CreateFolderID, MutateCollection } from "../util.js";
import { CreateFolderID, CreateRandomLegacyFolderID, MutateCollection } from "../util.js";
import { SEEDS_FolderDocument } from "tachi-common";
MutateCollection("folders.json", (folders) => {
MutateCollection("folders.json", (folders: SEEDS_FolderDocument[]) => {
logger.info("Updating Folders.");
for (const folder of folders) {
if (folder.id === undefined || !folder.id.startsWith("F")) {
folder.id = CreateFolderID();
folder.legacyFolderID = `F${crypto.randomBytes(32).toString("hex")}`;
folder.legacyFolderID = CreateRandomLegacyFolderID();
logger.info(`Updating ${folder.slug}`);
}
}
@@ -1,15 +1,16 @@
import { SEEDS_GoalDocument, SEEDS_QuestDocument } from "tachi-common";
import { log as logger } from "../log.ts";
import { CreateGoalID, MutateCollection, ReadCollection, WriteCollection } from "../util.js";
const translateMap = new Map();
const origGoals = ReadCollection("goals.json", true);
const origGoals = ReadCollection<SEEDS_GoalDocument>("goals.json", true);
MutateCollection("goals.json", (goals) => {
MutateCollection("goals.json", (goals: SEEDS_GoalDocument[]) => {
logger.info("Updating goals.");
for (const goal of goals) {
const expectedGoalID = CreateGoalID(goal.charts, goal.criteria, goal.game, goal.playtype);
const expectedGoalID = CreateGoalID(goal.charts, goal.criteria, goal.game);
if (expectedGoalID !== goal.goalID) {
translateMap.set(goal.goalID, expectedGoalID);
@@ -21,7 +22,7 @@ MutateCollection("goals.json", (goals) => {
});
try {
MutateCollection("quests.json", (quests) => {
MutateCollection("quests.json", (quests: SEEDS_QuestDocument[]) => {
logger.info("Updating Quests.");
for (const quest of quests) {
@@ -52,6 +53,6 @@ try {
return quests;
});
} catch (err) {
logger.error("Failed to update tables.json, reverting all auto-folder fixes.", { err });
logger.error("Failed to update tables.json, reverting all auto-folder fixes. %s", { err });
WriteCollection("goals.json", origGoals);
}
@@ -4,7 +4,7 @@
* Run: bun typescript/seeds-scripts/rerunners/fix-goals.ts
*/
import { type V3Game } from "tachi-common";
import { SEEDS_GoalDocument, SEEDS_QuestDocument, type V3Game } from "tachi-common";
import {
buildGoalTitleContext,
@@ -14,10 +14,10 @@ import { log as logger } from "../log.ts";
import { CreateGoalID, MutateCollection, ReadCollection, WriteCollection } from "../util.js";
const translateMap = new Map<string, string>();
const origGoals = ReadCollection("goals.json", true);
const origGoals = ReadCollection<SEEDS_GoalDocument>("goals.json", true);
const ctx = buildGoalTitleContext();
MutateCollection("goals.json", (goals) => {
MutateCollection("goals.json", (goals: SEEDS_GoalDocument[]) => {
logger.info("Re-deriving goalID and name for all goals.");
let idUpdates = 0;
@@ -56,7 +56,7 @@ MutateCollection("goals.json", (goals) => {
if (translateMap.size > 0) {
try {
MutateCollection("quests.json", (quests) => {
MutateCollection("quests.json", (quests: SEEDS_QuestDocument[]) => {
logger.info("Updating quest goalID references.");
let patched = 0;
@@ -76,8 +76,8 @@ if (translateMap.size > 0) {
return quests;
});
} catch (err) {
logger.error("Failed to update quests.json, reverting goals.json.", { err });
} catch (err: unknown) {
logger.error("Failed to update quests.json, reverting goals.json. %s", { err });
WriteCollection("goals.json", origGoals);
throw err;
}
@@ -1,11 +1,11 @@
import { Command } from "commander";
import fs from "fs";
import { type GoalDocument, type QuestDocument } from "tachi-common";
import { SEEDS_GoalDocument, type GoalDocument, type QuestDocument } from "tachi-common";
import { CreateGoalID, CreateQuestID, MutateCollection } from "../util";
const program = new Command();
program.option("-f, --file <quests.json>");
program.requiredOption("-f, --file <quests.json>");
program.parse(process.argv);
const options = program.opts();
@@ -78,7 +78,7 @@ const newQuests = data.map(HydrateQuest);
MutateCollection("quests.json", (quests) => [...quests, ...newQuests]);
MutateCollection("goals.json", (goals) => {
MutateCollection("goals.json", (goals: SEEDS_GoalDocument[]) => {
// don't duplicate goals
const goalIDs = new Set(goals.map((e) => e.goalID));
@@ -36,7 +36,7 @@ to adjust the constants for the version you're adding.
3. Deactivate old tables using `seeds/scripts/rerunners/toggle-table-inactive.js`:
```
bun toggle-table-inactive.js -t maimaidx-Single-<VERSION>-levels
bun toggle-table-inactive.ts -t maimaidx-Single-<VERSION>-levels
```
Keep tables for the current and last (aka n-1) versions.
@@ -1,7 +1,7 @@
import { spawnSync } from "child_process";
import { Command } from "commander";
import fs from "fs";
import { type QuestDocument, type QuestlineDocument } from "tachi-common";
import { SEEDS_QuestDocument, SEEDS_QuestlineDocument } from "tachi-common";
import { MutateCollection, ReadCollection } from "../util";
@@ -16,19 +16,16 @@ import { MutateCollection, ReadCollection } from "../util";
*/
const program = new Command();
program.requiredOption("-g, --game <game>").requiredOption("-p, --playtype <playtype>");
program.requiredOption("-g, --game <game>");
program.parse(process.argv);
const options = program.opts();
const { game } = options;
const { game, playtype } = options;
const quests = ReadCollection("quests.json").filter(
(e) => e.game === game && e.playtype === playtype,
) as Array<QuestDocument>;
const quests = ReadCollection<SEEDS_QuestDocument>("quests.json").filter((e) => e.game === game);
if (quests.length === 0) {
console.log(`No quests found for game ${game} and playtype ${playtype}.`);
console.log(`No quests found for game ${game}.`);
process.exit(1);
}
@@ -51,7 +48,7 @@ ${quests.map((e) => `${e.questID} ${e.name}`).join("\n")}
);
try {
const r = spawnSync(process.env.EDITOR || "nano", [file], {
const r = spawnSync(process.env.EDITOR || "vim", [file], {
stdio: "inherit",
});
@@ -109,9 +106,8 @@ if (questIDs.length === 0) {
process.exit(1);
}
const questline: QuestlineDocument = {
const questline: SEEDS_QuestlineDocument = {
game,
playtype,
desc,
name,
quests: questIDs,
@@ -6,7 +6,7 @@
3. Parse song durations
1. Download `vgmstream-cli` and put it in `seeds/scripts/personal`.
2. Put opts in `seeds/scripts/personal`.
3. Run `bun parse-song-duration.ts -v /tachi/typescript/seeds-scripts/personal/vgmstream-cli -d ../../personal/vgmstream-cli -g ongeki`.
3. Run `bun parse-song-duration.ts -v /tachi/typescript/seeds-scripts/personal/vgmstream-cli -d ../../personal/opts -g ongeki`.
4. Scrape sdvx.in links (optional)
1. Run `bun scrape-sdvx-in.ts`.
2. Fix any missing entries manually in `seeds/collections/charts-ongeki.json` (refer to `songs-ongeki.json` for `songID`s).
@@ -1,9 +1,13 @@
import { Command } from "commander";
import { GetGameConfig } from "tachi-common";
import { SEEDS_FolderDocument, SEEDS_TableDocument } from "tachi-common/types/seeds-documents-zod";
import crypto from "crypto";
import { CreateFolderID, CreateTableID, MutateCollection } from "../../util";
import {
CreateFolderID,
CreateRandomLegacyFolderID,
CreateTableID,
MutateCollection,
} from "../../util";
const LEVELS = [
"0",
@@ -73,7 +77,7 @@ for (const level of LEVELS) {
game: "ongeki",
id: CreateFolderID(),
inactive: false,
legacyFolderID: `F${crypto.randomBytes(32).toString("hex")}`,
legacyFolderID: CreateRandomLegacyFolderID(),
searchTerms: [],
slug,
title: `Level ${level} (${versionName})`,
@@ -89,7 +93,7 @@ for (const [fullName, safeName] of DIFFICULTIES) {
game: "ongeki",
id: CreateFolderID(),
inactive: false,
legacyFolderID: `F${crypto.randomBytes(32).toString("hex")}`,
legacyFolderID: CreateRandomLegacyFolderID(),
searchTerms: [],
slug,
title: `${fullName} (${versionName})`,
@@ -105,7 +109,7 @@ for (const [fullName, safeName] of GENRES) {
game: "ongeki",
id: CreateFolderID(),
inactive: false,
legacyFolderID: `F${crypto.randomBytes(32).toString("hex")}`,
legacyFolderID: CreateRandomLegacyFolderID(),
searchTerms: [],
slug,
title: `${fullName} (${versionName})`,
@@ -115,8 +119,8 @@ for (const [fullName, safeName] of GENRES) {
genreFolderSlugs.push(slug);
}
MutateCollection("tables.json", (ts) => {
(ts as SEEDS_TableDocument[]).push(
MutateCollection("tables.json", (ts: SEEDS_TableDocument[]) => {
ts.push(
{
default: false,
description: `Levels for O.N.G.E.K.I. in ${versionName}.`,
@@ -137,8 +137,8 @@ const main = async () => {
if (game !== "ongeki" && game !== "chunithm") {
throw new InvalidArgumentError("Bad game");
}
const charts = ReadCollection(`charts-${game}.json`);
const songs = ReadCollection(`songs-${game}.json`);
const charts = ReadCollection<SEEDS_ChartDocument<"ongeki">>(`charts-${game}.json`);
const songs = ReadCollection<SEEDS_SongDocument<"ongeki">>(`songs-${game}.json`);
const dir = await fs.readdir(optPath);
const promises: Promise<void>[] = [];
for (const opt of dir) {
@@ -1,42 +0,0 @@
import { Command } from "commander";
import { MutateCollection, ReadCollection, WriteCollection } from "../util.js";
const program = new Command();
program.requiredOption("-t, --tableID <tableID>");
program.parse(process.argv);
const options = program.opts();
const tables = ReadCollection("tables.json");
let table;
for (const findTable of tables) {
if (findTable.tableID === options.tableID) {
table = findTable;
findTable.inactive = !findTable.inactive;
break;
}
}
if (!table) {
throw new Error(`No such table ${options.tableID} exists.`);
}
MutateCollection("folders.json", (folders) => {
for (const folder of folders) {
const id = folder.folderID ?? folder.id;
const matchesRef =
(folder.slug !== undefined && table.folders.includes(folder.slug)) ||
(id !== undefined && table.folders.includes(id));
if (matchesRef) {
folder.inactive = !folder.inactive;
}
}
return folders;
});
WriteCollection("tables.json", tables);
@@ -0,0 +1,58 @@
import { Command } from "commander";
import { MutateCollection, ReadCollection, WriteCollection } from "../util.js";
import { SEEDS_FolderDocument, SEEDS_TableDocument } from "tachi-common";
import { log as logger } from "../log.ts";
const program = new Command();
program.requiredOption("-t, --tableID <id>");
program.parse(process.argv);
const options = program.opts();
const tables = ReadCollection<SEEDS_TableDocument>("tables.json");
let table;
let toggleValue: boolean;
for (const findTable of tables) {
let targetID;
if (/^T[0-9a-fA-F]+$/u.test(options.tableID)) {
targetID = findTable.id;
} else {
targetID = findTable.legacyTableID;
}
if (options.tableID === targetID) {
table = findTable;
findTable.inactive = !findTable.inactive;
toggleValue = findTable.inactive;
logger.info(`Toggling table ${table.id} (${table.title}) to inactive=${toggleValue}`);
break;
}
}
if (!table) {
throw new Error(`No such table ${options.tableID} exists.`);
}
MutateCollection("folders.json", (folders: SEEDS_FolderDocument[]) => {
for (const folder of folders) {
const matchesRef =
(folder.slug !== undefined && table.folders.includes(folder.slug)) ||
(folder.id !== undefined && table.folders.includes(folder.id));
if (matchesRef) {
folder.inactive = toggleValue;
logger.info(`Toggling folder ${folder.id} (${folder.title})`);
}
}
return folders;
});
if (table.default === true && toggleValue! === true) {
logger.warn(`${table.id} has been deactivated, but it is currently the default table.`);
}
WriteCollection("tables.json", tables);
+13 -1
View File
@@ -10,6 +10,7 @@ import {
CreateSongID,
CreateTableID,
} from "tachi-common";
import crypto from "crypto";
import DeterministicCollectionSort from "./sort-seeds.js";
@@ -37,6 +38,12 @@ export function IterateCollections(cb) {
DeterministicCollectionSort();
}
/**
* @template T
* @param {string} name
* @param {boolean} throwIfNotFound
* @returns {T[]}
*/
export function ReadCollection(name, throwIfNotFound = false) {
const p = path.join(COLLECTIONS_DIR, name);
if (!fs.existsSync(p)) {
@@ -62,8 +69,9 @@ export function WriteCollection(name, data) {
}
/**
* @template T
* @param {string} name
* @param {(collection: unknown) => unknown} cb
* @param {(collection: T[]) => T[]} cb
*/
export function MutateCollection(name, cb) {
const data = cb(ReadCollection(name));
@@ -82,6 +90,10 @@ export function CreateLegacyFolderID(query, game, playtype) {
return `F${fjsh.hash(Object.assign({ game, playtype }, query), "SHA256")}`;
}
export function CreateRandomLegacyFolderID() {
return `F${crypto.randomBytes(32).toString("hex")}`;
}
export function CreateLegacyFolderIDFromFolder(folder) {
return CreateLegacyFolderID(folder.data, folder.game, folder.playtype);
}