data: add Watcha Gatcha charts from official master
Publish GitHub Pages output / publish (push) Successful in 36s
Publish GitHub Pages output / publish (push) Successful in 36s
This commit is contained in:
@@ -1,17 +1,20 @@
|
||||
// Convert Hololive Dreams' decrypted SUS dialect to this application's compact score timeline.
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readHolodoriDbTable as readMaster } from './holodoridb-source.mjs';
|
||||
import { readHolodoriDbTable as readMaster, prepareHolodoriDbSource } from './holodoridb-source.mjs';
|
||||
import path from 'node:path';
|
||||
import { susScoreWeight } from './sus-score-weight.mjs';
|
||||
import { formulaScoreMetadata } from './song-score-formulas.mjs';
|
||||
|
||||
import { parseSusTimeline } from './sus-timeline.mjs';
|
||||
const difficulties = ['easy', 'normal', 'hard', 'expert'];
|
||||
const overrides = JSON.parse(await readFile(new URL('../data/song-metadata-overrides.json', import.meta.url), 'utf8')).songs;
|
||||
|
||||
const music = new Map(readMaster('kor', 'Music.json').map((row) => { const data = row.data ?? row; return [data.id, data]; }));
|
||||
const translation = new Map(readMaster('kor', 'LangMusic_Kor.json').map((row) => { const data = row.data ?? row; return [data.id, data.text]; }));
|
||||
const levels = new Map(readMaster('kor', 'MusicDifficulty.json').map((row) => { const data = row.data ?? row; return [`${data.musicId}.${data.difficultyType.split('_').at(-1).toLowerCase()}`, data.difficultyLevel]; }));
|
||||
const reviewedSource = prepareHolodoriDbSource('kor');
|
||||
const masterDataSource = { repository: reviewedSource.repository, commit: reviewedSource.commit, locale: 'kor' };
|
||||
const songIds = process.argv.slice(2);
|
||||
if (songIds.length === 0) throw new Error('곡 ID 또는 곡 ID.난이도를 지정하세요.');
|
||||
const selected = songIds.flatMap((value) => {
|
||||
@@ -30,13 +33,37 @@ for (const { songId, difficulty } of selected) {
|
||||
const mids = timeline.notes.filter((note) => note.type === 'mid').length;
|
||||
throw new Error(`${songId}.${difficulty}: 파서 노트 수 ${timeline.notes.length}/${weight.fullComboNoteCount} 불일치 (중간 ${mids}, 헤더 ${weight.counts.LONG_RELAY + weight.counts.LONG_CONTINUE})`);
|
||||
}
|
||||
const master = music.get(songId);
|
||||
const difficultyLevel = levels.get(`${songId}.${difficulty}`);
|
||||
const officialMaster = music.get(songId);
|
||||
const metadataSource = !officialMaster ? overrides[songId] : undefined;
|
||||
let supplemental;
|
||||
if (metadataSource) {
|
||||
const metadataBytes = await readFile(path.resolve(metadataSource.file));
|
||||
if (createHash('sha256').update(metadataBytes).digest('hex') !== metadataSource.sha256) {
|
||||
throw new Error(songId + ': 보충 메타데이터 해시가 다릅니다.');
|
||||
}
|
||||
supplemental = JSON.parse(metadataBytes.toString('utf8'));
|
||||
if (supplemental.id !== songId) throw new Error(songId + ': 보충 메타데이터 곡 ID가 다릅니다.');
|
||||
}
|
||||
// Fresh official master takes precedence over the pinned supplementary response.
|
||||
const master = officialMaster ?? supplemental;
|
||||
const supplementalDifficulty = supplemental?.difficulties.find((row) => row.difficultyType.split('_').at(-1).toLowerCase() === difficulty);
|
||||
const difficultyLevel = levels.get(songId + '.' + difficulty) ?? supplementalDifficulty?.difficultyLevel;
|
||||
const releaseTime = Number(master?.startTime);
|
||||
const durationSeconds = master?.playingSeconds;
|
||||
if (!master || !Number.isInteger(difficultyLevel) || difficultyLevel < 1 || !Number.isFinite(releaseTime) ||
|
||||
!Number.isFinite(durationSeconds) || durationSeconds < timeline.notes.at(-1).timeSeconds ||
|
||||
!Number.isFinite(master.liveScoreCoefficientPermil) || !Array.isArray(master.characterIds) || !master.musicSingerType) {
|
||||
throw new Error(songId + '.' + difficulty + ': 검증된 곡 메타데이터가 필요합니다.');
|
||||
}
|
||||
if (supplementalDifficulty && (supplementalDifficulty.fullComboNoteCount !== weight.fullComboNoteCount || supplementalDifficulty.normalNoteCount !== weight.counts.NORMAL)) {
|
||||
throw new Error(songId + '.' + difficulty + ': 메타데이터와 SUS 노트 수가 다릅니다.');
|
||||
}
|
||||
const susSha256 = createHash('sha256').update(bytes).digest('hex');
|
||||
const formula = formulaScoreMetadata(songId, difficulty, difficultyLevel, timeline.notes.length, master.liveScoreCoefficientPermil, susSha256);
|
||||
const output = { schemaVersion: 1, songId, difficulty, durationSeconds: master.playingSeconds, scoreRatio: formula?.scoreRatio ?? null, scoreCalibrationSource: formula?.scoreCalibrationSource ?? null, ...(formula ?? {}), notes: timeline.notes, specialStartSeconds: timeline.specialStartSeconds, singerType: master.musicSingerType?.split('_').at(-1).toLowerCase() ?? null, singerCharacterIds: master.characterIds ?? [], source: { page: 'Hololive Dreams CDN via holodori-asset-tools', transcriptionStatus: 'exact_from_decrypted_sus', susSha256 } };
|
||||
outputs.push({ filename: path.resolve(`static/data/charts/${songId}.${difficulty}.json`), output });
|
||||
generatedCatalogEntries.push({ songId, title: translation.get(master.titleLangId) ?? songId, difficulty, difficultyLevel, durationSeconds: master.playingSeconds, scoreRatio: output.scoreRatio, scoreCalibrationSource: output.scoreCalibrationSource, scoreFormula: output.scoreFormula, totalNotes: output.notes.length, singerType: output.singerType, singerCharacterIds: output.singerCharacterIds, releaseTime: Number(master.startTime), path: `/data/charts/${songId}.${difficulty}.json` });
|
||||
if (!formula) throw new Error(songId + '.' + difficulty + ': 검증된 점수 공식이 필요합니다.');
|
||||
const output = { schemaVersion: 1, songId, difficulty, durationSeconds, ...formula, notes: timeline.notes, specialStartSeconds: timeline.specialStartSeconds, singerType: master.musicSingerType.split('_').at(-1).toLowerCase(), singerCharacterIds: master.characterIds, source: { page: 'Hololive Dreams CDN via holodori-asset-tools', transcriptionStatus: 'exact_from_decrypted_sus', susSha256, ...(metadataSource ? { metadataSource } : { masterDataSource }) } };
|
||||
outputs.push({ filename: path.resolve('static/data/charts/' + songId + '.' + difficulty + '.json'), output });
|
||||
generatedCatalogEntries.push({ songId, title: translation.get(master.titleLangId) ?? supplemental?.title?.kor ?? songId, difficulty, difficultyLevel, durationSeconds, scoreRatio: output.scoreRatio, scoreCalibrationSource: output.scoreCalibrationSource, scoreFormula: output.scoreFormula, totalNotes: output.notes.length, singerType: output.singerType, singerCharacterIds: output.singerCharacterIds, releaseTime, path: '/data/charts/' + songId + '.' + difficulty + '.json' });
|
||||
console.log(`${songId}.${difficulty}: ${timeline.notes.length} notes ${timeline.specialStartSeconds.join(', ')}`);
|
||||
}
|
||||
const catalogPath = path.resolve('src/lib/data/exact-chart-catalog.json');
|
||||
|
||||
Reference in New Issue
Block a user