Files
HolodoriCalc/scripts/game-db-source.mjs
T
dal4segno 07b98db520
Publish GitHub Pages output / publish (push) Successful in 44s
data: switch to recovered game DB and refresh cards and charts
2026-09-29 22:48:08 +09:00

57 lines
2.9 KiB
JavaScript

import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { gunzipSync } from 'node:zlib';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
export const gameDbRoot = fileURLToPath(new URL('../data/game-db/', import.meta.url));
export const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
export function openGameDb(pointer, root = gameDbRoot) {
if (!/^snapshots\/[a-zA-Z0-9]{64}$/.test(pointer.snapshot ?? '')) throw new Error('Invalid game DB snapshot path');
const directory = path.join(root, pointer.snapshot);
const bytes = readFileSync(path.join(directory, 'manifest.json'));
if (sha256(bytes) !== pointer.manifestSha256) throw new Error('Game DB manifest hash mismatch');
const manifest = JSON.parse(bytes);
if (manifest.schemaVersion !== 1 || manifest.kind !== 'android-master-db' || manifest.locale !== 'kor' ||
pointer.snapshot !== `snapshots/${manifest.masterDataVersion}` || !Object.keys(manifest.tables ?? {}).length) {
throw new Error('Invalid game DB manifest');
}
return { ...manifest, ...pointer, directory };
}
export function sourceMetadata(source) {
return { kind: source.kind, masterDataVersion: source.masterDataVersion, locale: source.locale,
capturedAt: source.capturedAt, manifestSha256: source.manifestSha256 };
}
export function prepareGameDbSource(locale = 'kor', { review = false } = {}) {
if (locale !== 'kor') throw new Error(`Current game DB contains Korean only; no external fallback for ${locale}`);
const pointer = JSON.parse(readFileSync(path.join(gameDbRoot, 'current.json'), 'utf8'));
const source = openGameDb(pointer);
if (!review) {
let report;
try { report = JSON.parse(readFileSync(path.join(gameDbRoot, 'review/latest.json'), 'utf8')); }
catch { throw new Error('Run npm run data:sync and report its changes before data:apply.'); }
if (report.target.manifestSha256 !== source.manifestSha256) throw new Error('Game DB changed after review; run data:sync again.');
}
return source;
}
export function readGameDbText(source, filename) {
const name = filename.replace(/\.json$/, '');
const table = source.tables[name];
if (!table || table.file !== `${name}.json.gz` || !/^[A-Za-z0-9_]+$/.test(name)) throw new Error(`Missing game DB table: ${filename}`);
const packed = readFileSync(path.join(source.directory, table.file));
if (sha256(packed) !== table.gzipSha256) throw new Error(`Game DB archive hash mismatch: ${name}`);
const raw = gunzipSync(packed);
if (sha256(raw) !== table.sha256) throw new Error(`Game DB table hash mismatch: ${name}`);
const rows = JSON.parse(raw);
if (!Array.isArray(rows) || rows.length !== table.rows) throw new Error(`Game DB row count mismatch: ${name}`);
return raw.toString('utf8');
}
export function readGameDbTable(locale, filename) {
return JSON.parse(readGameDbText(prepareGameDbSource(locale), filename));
}