feat: update arcaea for v3 + 6.14 seeds/data (#1478)

* fix: update arcaea's CCs

* fix: proper ID check

* rest of the owl

* I remembered to run tests

* there is no pale purple

* add phase 12

* remove particle arts from the mobile folder

* make arcaea an honorary GCM game it always wanted to be

* songpack=genre

* move displayVersion over to chart data

* disable FR/PM score validators

* Revert "disable FR/PM score validators"

This reverts commit da1cfbaa62.

* bullshit

* bullshit 2

* bullshit 2
This commit is contained in:
nairobi
2026-05-20 22:13:05 +01:00
committed by GitHub
parent 8a57df288e
commit 5606732ea1
18 changed files with 13513 additions and 831 deletions
+11252 -362
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -43,6 +43,17 @@
"versionFilter": ["mobile"],
"where": "chart.level = '11'"
},
{
"game": "arcaea",
"id": "F19e3cdc05007035d8d0",
"inactive": false,
"legacyFolderID": "Ffe7f2dc36cbcc0e0880fff765f4e580f877af220b63e441d0eb661e92411fd19",
"searchTerms": [],
"slug": "11p-mobile",
"title": "Level 11+ (Mobile)",
"versionFilter": ["mobile"],
"where": "chart.level = '11+'"
},
{
"game": "arcaea",
"id": "F19d35f0cf25b7ac2d8b",
@@ -120,6 +131,17 @@
"versionFilter": ["mobile"],
"where": "chart.level = '7'"
},
{
"game": "arcaea",
"id": "F19e3cc9eccdc9552061",
"inactive": false,
"legacyFolderID": "Fe247b9ea00af9c1ea12f24d0b10e949b454a8eb2df5ec3c9f55f789569a868c7",
"searchTerms": [],
"slug": "7p-mobile",
"title": "Level 7+ (Mobile)",
"versionFilter": ["mobile"],
"where": "chart.level = '7+'"
},
{
"game": "arcaea",
"id": "F19d35f0cf25f9312476",
@@ -131,6 +153,17 @@
"versionFilter": ["mobile"],
"where": "chart.level = '8'"
},
{
"game": "arcaea",
"id": "F19e3cc9eccd254cfb80",
"inactive": false,
"legacyFolderID": "Ff348f630a49f29d3a694d85efdc4a36f53d63877ea512935925aaf4d1a3188dc",
"searchTerms": [],
"slug": "8p-mobile",
"title": "Level 8+ (Mobile)",
"versionFilter": ["mobile"],
"where": "chart.level = '8+'"
},
{
"game": "arcaea",
"id": "F19d35f0cf258374e9b3",
+2106 -359
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -10,12 +10,15 @@
"5-mobile",
"6-mobile",
"7-mobile",
"7p-mobile",
"8-mobile",
"8p-mobile",
"9-mobile",
"9p-mobile",
"10-mobile",
"10p-mobile",
"11-mobile",
"11p-mobile",
"12-mobile"
],
"game": "arcaea",
+2 -1
View File
@@ -63,6 +63,7 @@ The following judgements are defined:
- `Past`
- `Present`
- `Future`
- `Eternal`
- `Beyond`
## Classes
@@ -70,7 +71,7 @@ The following judgements are defined:
| Name | Type | Values |
| :: | :: | :: |
| `badge` | DERIVED | BLUE, GREEN, ASH_PURPLE, PURPLE, RED, ONE_STAR, TWO_STARS, THREE_STARS
| `courseBanner` | PROVIDED | PHASE_1, PHASE_2, PHASE_3, PHASE_4, PHASE_5, PHASE_6, PHASE_7, PHASE_8, PHASE_9, PHASE_10, PHASE_11
| `courseBanner` | PROVIDED | PHASE_1, PHASE_2, PHASE_3, PHASE_4, PHASE_5, PHASE_6, PHASE_7, PHASE_8, PHASE_9, PHASE_10, PHASE_11, PHASE_12
## Versions
@@ -14,12 +14,20 @@ export default function IIDXStyleSongChartInfoFormat({
}: {
chart: ChartDocument | null;
game: V3Game;
song: SongDocument<"bms" | "chunithm" | "iidx" | "maimaidx" | "ongeki" | "pms" | "popn">;
song: SongDocument<
"arcaea" | "bms" | "chunithm" | "iidx" | "maimaidx" | "ongeki" | "pms" | "popn"
>;
}) {
const genre =
game === "ongeki"
? ((song as SongDocument<"ongeki">).data.flavorGenre ?? song.data.genre)
: song.data.genre;
let genre: string;
if (game === "ongeki") {
genre =
(song as SongDocument<"ongeki">).data.flavorGenre ??
(song as SongDocument<"ongeki">).data.genre;
} else if (game === "arcaea") {
genre = (song as SongDocument<"arcaea">).data.songPack;
} else {
genre = (song as any).data.genre;
}
return (
<>
@@ -28,12 +28,17 @@ export default function SongChartInfoFormat({
/>
);
}
if (gameGroup === "ongeki" || gameGroup === "chunithm" || gameGroup === "maimaidx") {
if (["arcaea", "chunithm", "maimaidx", "ongeki"].includes(gameGroup)) {
let displayVersion =
(chart?.data as any).displayVersion ?? (song.data as any).displayVersion;
if (gameGroup === "arcaea") {
displayVersion = `version ${displayVersion}`;
}
return (
<>
<IIDXStyleSongChartInfoFormat
{...{
song: song as SongDocument<"chunithm" | "maimaidx" | "ongeki">,
song: song as SongDocument<"arcaea" | "chunithm" | "maimaidx" | "ongeki">,
chart,
game,
}}
@@ -48,17 +53,9 @@ export default function SongChartInfoFormat({
prefix="Internal Level: "
/>
</h6>
{"displayVersion" in chart.data ? (
<h6>
<Muted>From {chart.data.displayVersion}</Muted>
</h6>
) : (
"displayVersion" in song.data && (
<h6>
<Muted>From {song.data.displayVersion}</Muted>
</h6>
)
)}
<h6>
<Muted>From {displayVersion}</Muted>
</h6>
</>
)}
</>
+4 -2
View File
@@ -13,9 +13,10 @@ import { bgc } from "./_util";
const ARCAEA_DIFFICULTY_COLORS: GPTClientImplementation<
GamesForGroup["arcaea"]
>["difficultyColours"] = {
Past: COLOUR_SET.paleBlue,
Present: COLOUR_SET.paleGreen,
Past: COLOUR_SET.blue,
Present: COLOUR_SET.green,
Future: COLOUR_SET.purple,
Eternal: COLOUR_SET.paleBlue,
Beyond: COLOUR_SET.vibrantRed,
};
@@ -62,6 +63,7 @@ const ARCAEA_COLORS: GPTClientImplementation<GamesForGroup["arcaea"]>["classColo
PHASE_9: bgc("indigo", "var(--bs-light)"),
PHASE_10: bgc("firebrick", "var(--bs-light)"),
PHASE_11: bgc("darkred", "var(--bs-light)"),
PHASE_12: bgc("blueviolet", "var(--bs-light)"),
},
};
@@ -13,7 +13,6 @@ export const GAME_GROUP_ARCAEA_CONF = {
// Potential future controller playtype support?
playtypes: ["Touch"],
songData: z.strictObject({
displayVersion: z.string(),
songPack: z.string(),
}),
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
@@ -41,6 +40,7 @@ const ArcaeaClasses = [
ClassValue("PHASE_9", "Phase 9", "Ego's Demise"),
ClassValue("PHASE_10", "Phase 10", "A Torrent of Light and Conflict"),
ClassValue("PHASE_11", "Phase 11", "Radiant Genesis"),
ClassValue("PHASE_12", "Phase 12", "Irruption of New Color"),
];
export const GAME_ARCAEA_CONF = {
@@ -101,11 +101,12 @@ export const GAME_ARCAEA_CONF = {
difficulties: {
type: "FIXED",
order: ["Past", "Present", "Future", "Beyond"],
order: ["Past", "Present", "Future", "Eternal", "Beyond"],
formatShort: {
Past: "PST",
Present: "PRS",
Future: "FTR",
Eternal: "ETR",
Beyond: "BYD",
},
formatLong: {},
@@ -132,6 +133,7 @@ export const GAME_ARCAEA_CONF = {
chartData: z.strictObject({
inGameStrID: z.string(),
displayVersion: z.string(),
notecount: zodNonNegativeInt,
}),
+1
View File
@@ -572,6 +572,7 @@ export enum ARCAEA_COURSE_BANNERS {
PHASE_9 = 8,
PHASE_10 = 9,
PHASE_11 = 10,
PHASE_12 = 11,
}
export enum ONGEKI_NOTE_LAMPS {
+2 -3
View File
@@ -1,8 +1,7 @@
export function FindChartWithPTDFVersion(collection, songID, playtype, difficulty, version) {
export function FindChartWithDFVersion(collection, songID, difficulty, version) {
return collection.find(
(chart) =>
chart.song.id === songID &&
chart.playtype === playtype &&
chart.songID === songID &&
chart.difficulty === difficulty &&
chart.versions.includes(version),
);
@@ -0,0 +1 @@
wikiwiki-arcaea/*.json
@@ -7,9 +7,11 @@ import {
type integer,
type SongDocument,
} from "tachi-common";
import crypto from "crypto";
import {
CreateChartID,
CreateSongID,
GetFreshSongIDGenerator,
ReadCollection,
WriteCollection,
@@ -40,6 +42,7 @@ interface SonglistEntry {
search_artist?: LocalizedSearchTerms;
version: string;
difficulties: Array<SonglistChart>;
deleted?: boolean;
}
interface PacklistEntry {
@@ -100,6 +103,8 @@ function convertDifficulty(input: integer): Difficulties["arcaea"] {
return "Future";
case 3:
return "Beyond";
case 4:
return "Eternal";
}
throw new Error(
@@ -158,9 +163,9 @@ const inGameIDToSongsMap: MultiMapUniqueValues<
const existingCharts: Map<string, ChartDocument<"arcaea">> = new Map();
for (const chart of existingChartDocs) {
const song = existingSongDocsById.get(chart.song.id);
const song = existingSongDocsById.get(chart.songID);
if (!song) {
console.warn(`Chart ${chart.song.id} does not belong to any song?`);
console.warn(`Chart ${chart.songID} does not belong to any song?`);
continue;
}
inGameIDToSongsMap.set(chart.data.inGameStrID, song);
@@ -173,6 +178,9 @@ const newSongs: Array<SongDocument<"arcaea">> = [];
const newCharts: Array<ChartDocument<"arcaea">> = [];
for (const entry of data.songs) {
if (entry.deleted) {
continue;
}
const inGameID = entry.id;
let possibleSongs = inGameIDToSongsMap.get(inGameID);
@@ -195,9 +203,9 @@ for (const entry of data.songs) {
artist: entry.artist,
altTitles,
searchTerms,
id: getNewSongID(),
id: CreateSongID(),
legacySongID: getNewSongID(),
data: {
displayVersion: entry.version,
songPack: convertPackName(packsByID, entry.set),
},
};
@@ -247,9 +255,9 @@ for (const entry of data.songs) {
artist: chart.artist ?? entry.artist,
altTitles,
searchTerms,
id: getNewSongID(),
id: CreateSongID(),
legacySongID: getNewSongID(),
data: {
displayVersion: chart.version ?? entry.version,
songPack: convertPackName(packsByID, entry.set),
},
};
@@ -270,17 +278,17 @@ for (const entry of data.songs) {
}
const chartDoc: ChartDocument<"arcaea"> = {
game: "arcaea",
chartID: CreateChartID(),
id: CreateChartID(),
songID: song.id,
legacyChartID: crypto.randomBytes(20).toString("hex"),
difficulty,
isPrimary: true,
level: `${chart.rating}${chart.ratingPlus ? "+" : ""}`,
levelNum: chart.rating + (chart.ratingPlus ? 0.7 : 0),
versions: ["mobile"],
playtype: "Touch",
data: {
inGameStrID: inGameID,
displayVersion: chart.version ?? entry.version,
// Filled in later, but not by this script
notecount: 0,
},
@@ -1,6 +1,6 @@
## Chart constants
Get the data by going to [譜面定数表](https://wikiwiki.jp/arcaea/%E8%AD%9C%E9%9D%A2%E5%AE%9A%E6%95%B0%E8%A1%A8)
or [譜面定数表 (Level 7以下)](https://wikiwiki.jp/arcaea/譜面定数表/譜面定数表 (Level 7以下)) and paste in the contents
Get the data by going to [譜面定数表](https://wikiwiki.jp/arcaea/%E8%AD%9C%E9%9D%A2%E5%AE%9A%E6%95%B0%E8%A1%A8)
or [譜面定数表 (Level 7以下)](https://wikiwiki.jp/arcaea/%E8%AD%9C%E9%9D%A2%E5%AE%9A%E6%95%B0%E8%A1%A8/%E8%AD%9C%E9%9D%A2%E5%AE%9A%E6%95%B0%E8%A1%A8%20%28Level%207%E4%BB%A5%E4%B8%8B%29) and paste in the contents
of this script in the browser console:
```js
@@ -15,6 +15,8 @@ of this script in the browser console:
return "Future";
case "firebrick":
return "Beyond";
case "slateblue":
return "Eternal"
default:
throw new Error(
`Unknown difficulty color ${cssColor}. Update the script and try again.`
@@ -113,9 +115,9 @@ into the browser console:
const EXPECTED_HEADER_CELLS = ["Notes", "Song", "Composer", "Diff.", "Lv.", "F", "L", "A", "S"];
const results = [];
const tables = document.querySelectorAll("table");
for (const table of tables) {
const header = [...table.querySelectorAll("thead th")].map((th) => th.textContent);
@@ -138,7 +140,7 @@ into the browser console:
const difficulty = cells[3].textContent;
const level = cells[4].textContent;
const notecount = Number(cells[0].textContent);
if (Number.isNaN(notecount)) {
console.error(`Could not parse notecount ${
cells[0].textContent
@@ -2,9 +2,8 @@ import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { FindChartWithPTDFVersion } from "../../../finders.js";
import { ApplyMutations } from "../../../mutations.js";
import { ReadCollection } from "../../../util.js";
import { FindChartWithDFVersion } from "../../../finders.js";
import { ReadCollection, WriteCollection } from "../../../util.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -27,12 +26,31 @@ const MANUAL_TITLE_MAP = {
"ベースラインやってる?w": "Can I Friend You on Bassbook? Lol",
"光速神授説- Divine Light of Myriad -": "Divine Light of Myriad",
"妖艶魔女-trappola bewitching-": "trappola bewitching",
"緋色月下、狂咲ノ絶(nayuta 2017 ver.)": "Hiiro Gekka, Kyoushou no Zetsu (nayuta 2017 ver.)",
// They use some fucked up delta character on wikiwiki
"LIVHT MY W​Δ​Y": "LIVHT MY WΔY",
"Rain of Conflictin a Radiant Abyss": "Rain of Conflict in a Radiant Abyss",
"MEGALOVANIA(Camellia Remix)": "MEGALOVANIA (Camellia Remix)",
"DA'AT-The First Seeker of Souls-": "DA'AT -The First Seeker of Souls-",
"Signal feat. Such": "Signal",
"キャラメルポップコーンたべたいよ~": "キャラメルポップコーンたべたいよ〜",
"患部で止まってすぐ溶ける ~ 狂気の優曇華院": "患部で止まってすぐ溶ける 〜 狂気の優曇華院",
"患部で止まってすぐ溶ける~ 狂気の優曇華院": "患部で止まってすぐ溶ける 〜 狂気の優曇華院",
};
// Multiple different songs with the same title, requiring artist search.
const NEEDS_ARTIST_SEARCH = ["Quon", "Genesis"];
const AF_FTR_BLACKLIST = [
"Singularity VVVIP",
"Ignotus Afterburn",
"overdead.",
"Red and Blue and Green",
"0xe0e1ccull",
"HIVEMIND INTERLINKED",
"Live Faster Die Younger",
"Mistempered Malignance",
];
const songs = ReadCollection("songs-arcaea.json");
function findSong(collection, ccEntry) {
@@ -50,9 +68,12 @@ function parseScrapedData(file, mutationCallback) {
const charts = ReadCollection("charts-arcaea.json");
const ccData = JSON.parse(fs.readFileSync(path.join(__dirname, file), "utf-8"));
const mutations = [];
for (const entry of ccData) {
if (AF_FTR_BLACKLIST.includes(entry.title) && entry.difficulty === "Future") {
continue;
}
const song = findSong(songs, entry);
if (!song) {
@@ -60,36 +81,26 @@ function parseScrapedData(file, mutationCallback) {
continue;
}
const chart = FindChartWithPTDFVersion(
charts,
song.id,
"Touch",
entry.difficulty,
"mobile",
);
const chart = FindChartWithDFVersion(charts, song.id, entry.difficulty, "mobile");
if (!chart) {
console.warn(`${song.title} [${entry.difficulty}] - Couldn't find chart?`);
continue;
}
mutations.push(mutationCallback(chart, entry));
mutationCallback(chart, entry);
}
console.info(`Finished parsing ${file}`);
ApplyMutations("charts-arcaea.json", mutations);
WriteCollection("charts-arcaea.json", charts);
}
function chartConstantMutationCallback(chart, entry) {
return {
data: {
level: entry.level,
levelNum: entry.levelNum,
},
match: {
chartID: chart.chartID,
},
};
if (chart.levelNum !== entry.levelNum) {
console.info(`${entry.title} ${entry.difficulty} ${chart.levelNum} -> ${entry.levelNum}`);
}
chart.level = entry.level;
chart.levelNum = entry.levelNum;
}
if (fs.existsSync(path.join(__dirname, "lower.json"))) {
@@ -101,14 +112,19 @@ if (fs.existsSync(path.join(__dirname, "upper.json"))) {
}
if (fs.existsSync(path.join(__dirname, "notecount.json"))) {
parseScrapedData("notecount.json", (chart, entry) => ({
data: {
data: {
notecount: entry.notecount,
},
},
match: {
chartID: chart.chartID,
},
}));
parseScrapedData("notecount.json", (chart, entry) => {
if (
chart.data.notecount !== undefined &&
chart.data.notecount > 0 &&
chart.data.notecount !== entry.notecount
) {
console.warn(
`${entry.title} ${entry.difficulty} ${chart.notecount} -> ${entry.notecount}`,
);
}
chart.data = {
...chart.data,
notecount: entry.notecount,
};
});
}
@@ -1,45 +1,17 @@
import crypto from "crypto";
import { log as logger } from "../log.ts";
import { CreateFolderID, MutateCollection, ReadCollection, WriteCollection } from "../util.js";
const translateMap = new Map();
const origFolders = ReadCollection("folders.json", true);
import { CreateFolderID, MutateCollection } from "../util.js";
MutateCollection("folders.json", (folders) => {
logger.info("Updating Folders.");
for (const folder of folders) {
const newFolderID = CreateFolderID(folder.data, folder.game, folder.playtype);
translateMap.set(folder.folderID, newFolderID);
folder.folderID = newFolderID;
if (folder.id === undefined || !folder.id.startsWith("F")) {
folder.id = CreateFolderID();
folder.legacyFolderID = `F${crypto.randomBytes(32).toString("hex")}`;
logger.info(`Updating ${folder.slug}`);
}
}
return folders;
});
try {
MutateCollection("tables.json", (tables) => {
logger.info("Updating Tables.");
for (const table of tables) {
table.folders = table.folders.map((e) => {
const newFolderID = translateMap.get(e);
if (!newFolderID) {
throw new Error(`${e} doesn't exist as a folderID? Can't autofix.`);
}
return newFolderID;
});
}
logger.info("Done.");
return tables;
});
} catch (err) {
logger.error("Failed to update tables.json, reverting all auto-folder fixes.", { err });
WriteCollection("folders.json", origFolders);
}
+1 -1
View File
@@ -131,7 +131,7 @@ function GetSongCollectionGameGroup(filename) {
export function GetFreshSongIDGenerator(gameGroup) {
const existing = ReadCollection(`songs-${gameGroup}.json`);
let max = existing.reduce((acc, s) => Math.max(acc, s.id ?? 0), 0);
let max = existing.reduce((acc, s) => Math.max(acc, s.legacySongID ?? 0), 0);
return () => ++max;
}
@@ -1445,7 +1445,6 @@ export const TestingArcaeaSheriruthSong: SongDocument<"arcaea"> = {
altTitles: [],
artist: "Team Grimoire",
data: {
displayVersion: "1.0",
songPack: "Eternal Core",
},
id: "s19",
@@ -1467,6 +1466,7 @@ export const TestingArcaeaSheriruthFTR: ChartDocument<"arcaea"> = {
chartID: "48de13e0be17a282eed6ef97e6e1478eb59bc55e",
legacyChartID: "48de13e0be17a282eed6ef97e6e1478eb59bc55e",
data: {
displayVersion: "1.0",
inGameStrID: "sheriruth",
notecount: 1151,
},