89 lines
6.1 KiB
JavaScript
89 lines
6.1 KiB
JavaScript
// Fetch public wf-calc chart snapshots for comparison, without changing service data.
|
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
import { createHash } from 'node:crypto';
|
|
import vm from 'node:vm';
|
|
import { prepareHolodoriDbSource } from './holodoridb-source.mjs';
|
|
|
|
const source = prepareHolodoriDbSource('kor', { review: true });
|
|
const origin = 'https://dreams.wf-calc.net';
|
|
const directory = 'data/wf-audit';
|
|
await mkdir(`${directory}/charts`, { recursive: true });
|
|
async function get(url) {
|
|
const response = await fetch(url, { signal: AbortSignal.timeout(60000) });
|
|
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
|
|
return response.text();
|
|
}
|
|
const home = await get(origin);
|
|
const mainPath = home.match(/<script type="module" crossorigin src="([^"]+)"/)?.[1];
|
|
if (!mainPath) throw new Error('Main module not found');
|
|
const main = await get(new URL(mainPath, origin));
|
|
await writeFile(`${directory}/main.js`, main);
|
|
const simulatorPath = main.match(/\.\/UnitSimulatorPage-([^"']+\.js)/)?.[1];
|
|
if (!simulatorPath) throw new Error('Simulator module not found');
|
|
const simulatorUrl = `${origin}/assets/UnitSimulatorPage-${simulatorPath}`;
|
|
const simulator = await get(simulatorUrl);
|
|
await writeFile(`${directory}/simulator.js`, simulator);
|
|
const imports = [...simulator.matchAll(/charts\/([^"/]+\.json)"[^\n]{0,180}?import\("\.\/([^"/]+\.js)"/g)].map(([,name,asset]) => {
|
|
const match = name.match(/^(m\d{4})\.(easy|normal|hard|expert)-(\d+)\.json$/i);
|
|
return match ? { songId: match[1], difficulty: match[2].toLowerCase(), level: Number(match[3]), name, asset } : null;
|
|
}).filter(Boolean);
|
|
if (!imports.length) throw new Error('No chart modules found');
|
|
if (new Set(imports.map(i=>`${i.songId}.${i.difficulty}`)).size !== imports.length) throw new Error('Duplicate chart modules');
|
|
// Reuse only the existing local data decoder/adapter; remote JS is never executed.
|
|
const collector = await readFile('scripts/collect-exact-charts.mjs', 'utf8');
|
|
const context = vm.createContext({});
|
|
vm.runInContext(collector.slice(collector.indexOf('function requiredMatch'), collector.indexOf('function legacyCalibrationNotes')), context);
|
|
const local = JSON.parse(await readFile('src/lib/data/exact-chart-catalog.json', 'utf8'));
|
|
const sus = JSON.parse(await readFile('data/sus-audit/latest.json', 'utf8'));
|
|
if (sus.source.commit !== source.commit) throw new Error('SUS audit source changed: rerun it first');
|
|
const rows = [];
|
|
let cursor = 0;
|
|
console.log(`wf-calc ${imports.length}개 채보 비교 시작`);
|
|
const counts = notes => ({ regular:notes.filter(n=>n.type!=='mid'&&!n.isFlick).length, flick:notes.filter(n=>n.isFlick).length, mid:notes.filter(n=>n.type==='mid').length });
|
|
function compare(a,b,kind=true) {
|
|
let extra=0,missing=0;
|
|
for(const type of kind?['regular','flick','mid']:['all']) {
|
|
const filter=n=>type==='all'||(n.type==='mid'?'mid':n.isFlick?'flick':'regular')===type;
|
|
const x=a.filter(filter).map(n=>n.timeSeconds).sort((a,b)=>a-b), y=b.filter(filter).map(n=>n.timeSeconds).sort((a,b)=>a-b);
|
|
let i=0,j=0;
|
|
while(i<x.length||j<y.length) {
|
|
if(i<x.length&&j<y.length&&Math.abs(x[i]-y[j])<=1e-6){i++;j++;}
|
|
else if(j>=y.length||(i<x.length&&x[i]<y[j])){extra++;i++;}else{missing++;j++;}
|
|
}
|
|
}
|
|
return {extra,missing};
|
|
}
|
|
async function worker() {
|
|
while(cursor<imports.length) {
|
|
const item=imports[cursor++];
|
|
const url=`${origin}/assets/${item.asset}`;
|
|
const module=await get(url);
|
|
context.input=module;
|
|
const data=JSON.parse(vm.runInContext('extractJsonParseArgument(input)',context));
|
|
await writeFile(`${directory}/charts/${item.songId}.${item.difficulty}.json`,JSON.stringify(data));
|
|
context.input=data;
|
|
const notes=vm.runInContext('flattenEvents(input)',context);
|
|
const entry=local.charts.find(c=>c.songId===item.songId&&c.difficulty===item.difficulty);
|
|
if(!entry){rows.push({...item,localMissing:true});continue;}
|
|
const chart=JSON.parse(await readFile(`static${entry.path}`,'utf8'));
|
|
const reference=sus.rows.find(r=>r.songId===item.songId&&r.difficulty===item.difficulty);
|
|
const special=(data.sp_activation_events??[]).toSorted((a,b)=>a.slot-b.slot).map(e=>e.time_seconds);
|
|
const category=counts(notes);
|
|
const differentSpecial=other=>special.length!==other.length||special.some((t,i)=>Math.abs(t-other[i])>1e-6);
|
|
rows.push({...item,title:reference.title,url,moduleSha256:createHash('sha256').update(module).digest('hex'),
|
|
wfSource:data.source,localSource:chart.source,wfCounts:category,susExpected:reference.expected,
|
|
wfCategoryMatchesSus:Object.keys(category).every(k=>category[k]===reference.expected[k]),
|
|
localVsWf:compare(chart.notes,notes),localVsWfTimesOnly:compare(chart.notes,notes,false),
|
|
wfSpecial:special,localSpecialMismatch:differentSpecial(chart.specialStartSeconds),susSpecialMismatch:differentSpecial(reference.referenceSpecial),
|
|
metadataKeys:Object.keys(data).filter(k=>k!=='note_events')});
|
|
if(rows.length%100===0||rows.length===imports.length)console.log(`${rows.length}/${imports.length}`);
|
|
}
|
|
}
|
|
await Promise.all(Array.from({length:6},worker));
|
|
rows.sort((a,b)=>a.songId.localeCompare(b.songId)||a.difficulty.localeCompare(b.difficulty));
|
|
const missing=local.charts.filter(c=>!rows.some(r=>r.songId===c.songId&&r.difficulty===c.difficulty)).map(c=>({songId:c.songId,title:c.title,difficulty:c.difficulty}));
|
|
const shared=rows.filter(r=>!r.localMissing);
|
|
const summary={wfCharts:rows.length,shared:shared.length,missingOnWf:missing.length,localTimelineMatches:shared.filter(r=>!r.localVsWf.extra&&!r.localVsWf.missing).length,localTimesOnlyMatches:shared.filter(r=>!r.localVsWfTimesOnly.extra&&!r.localVsWfTimesOnly.missing).length,wfCategoryMismatchSus:shared.filter(r=>!r.wfCategoryMatchesSus).length,localSpecialMismatch:shared.filter(r=>r.localSpecialMismatch).length,wfSpecialMismatchSus:shared.filter(r=>r.susSpecialMismatch).length};
|
|
await writeFile(`${directory}/latest.json`,JSON.stringify({generatedAt:new Date().toISOString(),source,simulatorUrl,summary,missingOnWf:missing,adapter:'Existing local flattenEvents adapter; adapter-derived classifications must not be attributed to the wf-calc scoring engine.',rows},null,2));
|
|
console.log(JSON.stringify(summary,null,2));
|