'use strict'; // Version-pinned, complete official export sets for manual Eagle testing. const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { execFile } = require('child_process'); const root = path.resolve(process.argv[2] || path.join(__dirname, '.fixtures')); const revisions = { '3.4': 'ef5013143178918ddfc7cabfbddeae7aec7cf84e', '3.5': '2cd9467d2736b7f3a0b81382f117c73823bfb44b', '3.6': '654c20e5b0e523040b6366bbd1042510d2645134', '3.7': '9639bcc81722d7178fd9d1cdc1a3d55a4c91f989', '3.8': '8b4844bd4b193ba9e54487ed397a777993cbad56', '4.0': '425ce416bb218b28caeec47b317aa57cd7140375', '4.1': '77a5db0ec6d16331f5efbaa7662bba9355bd3424', '4.2': 'e7dc1435fa4a0083ab431f1b28e083c14a1f5c68', '4.3': 'ba17cf88b198c6531c6c51d8d4e1e4b3c3ab47dd' }; const names = ['spineboy', 'goblins', 'vine', 'stretchyman', 'tank', 'coin', 'raptor']; const curl = (url, file) => new Promise((resolve, reject) => execFile('curl', ['--http1.1', '-LfsS', '--retry', '2', '--max-time', '60', url, '-o', file], error => error ? reject(error) : resolve())); function blobHash(file) { const bytes = fs.readFileSync(file); return crypto.createHash('sha1').update('blob ' + bytes.length + '\0').update(bytes).digest('hex'); } async function pool(jobs) { let cursor = 0; await Promise.all(Array.from({ length: 6 }, async () => { while (cursor < jobs.length) await jobs[cursor++](); })); } (async () => { fs.mkdirSync(root, { recursive: true }); const inventory = []; for (const [version, revision] of Object.entries(revisions)) { const folder = path.join(root, version); fs.mkdirSync(folder, { recursive: true }); let previous = []; try { previous = JSON.parse(fs.readFileSync(path.join(folder, '.sources.json'), 'utf8')).files; } catch (_) {} const treeFile = path.join(folder, '.tree.json'); await curl(`https://api.github.com/repos/EsotericSoftware/spine-runtimes/git/trees/${revision}?recursive=1`, treeFile); const tree = JSON.parse(fs.readFileSync(treeFile, 'utf8')); if (tree.truncated || !tree.tree || tree.sha !== revision) throw new Error('Incomplete source tree: ' + version); const license = tree.tree.find(f => f.path === 'LICENSE'); if (license) { const dest = path.join(folder, 'LICENSE.txt'); if (!fs.existsSync(dest) || blobHash(dest) !== license.sha) await curl(`https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/${revision}/LICENSE`, dest); if (blobHash(dest) !== license.sha) throw new Error('License checksum mismatch: ' + version); } const files = tree.tree.filter(f => f.type === 'blob' && names.some(n => f.path.startsWith('examples/' + n + '/export/')) && /\.(json|skel|atlas|png)$/.test(f.path) && !/-pma/.test(f.path)); for (const old of previous.filter(old => !files.some(f => f.path === old.path))) { const parts = old.path.split('/'), dest = path.join(folder, parts[1], parts.slice(3).join('/')); if (fs.existsSync(dest) && blobHash(dest) === old.sha) { fs.unlinkSync(dest); if (!fs.readdirSync(path.dirname(dest)).length) fs.rmdirSync(path.dirname(dest)); } } await pool(files.map(f => async () => { const parts = f.path.split('/'), dest = path.join(folder, parts[1], parts.slice(3).join('/')); fs.mkdirSync(path.dirname(dest), { recursive: true }); if (fs.existsSync(dest) && blobHash(dest) === f.sha) return; const temp = dest + '.download'; await curl(`https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/${revision}/${f.path}`, temp); if (blobHash(temp) !== f.sha) throw new Error('Checksum mismatch: ' + f.path); fs.renameSync(temp, dest); })); for (const f of files.filter(f => f.path.endsWith('.json'))) { const parts = f.path.split('/'); const data = JSON.parse(fs.readFileSync(path.join(folder, parts[1], parts.slice(3).join('/')), 'utf8')); if (!data.skeleton || !data.skeleton.spine.startsWith(version + '.')) throw new Error('Wrong export version: ' + f.path); } const entry = { version, revision, files: files.map(f => ({ path: f.path, sha: f.sha, size: f.size })) }; fs.writeFileSync(path.join(folder, '.sources.json'), JSON.stringify(entry, null, 2)); inventory.push(entry); console.log(version + ': ' + files.length + ' official files verified'); } const rows = inventory.map(e => '| ' + e.version + ' | ' + new Set(e.files.map(f => f.path.split('/')[1])).size + ' | ' + e.files.length + ' | `' + e.revision + '` |').join('\n'); fs.writeFileSync(path.join(root, 'README.md'), '# Spine 官方测试资源\n\n按版本和示例分目录,骨骼、Atlas 和贴图均来自官方仓库,所有文件已校验 Git blob SHA-1。这里下载的是普通 Alpha 导出,未混入同名 PMA 图集。\n\n| 版本系列 | 示例组 | 文件数 | 固定提交 |\n|---|---:|---:|---|\n' + rows + '\n\n将单个示例文件夹导入 Eagle。JSON 与 SKEL 可分别测试;测试复合后缀时请复制文件后将 `.atlas` 改为 `.atlas.txt`、`.skel` 改为 `.skel.bytes`。保留原始版本作为对照。每个版本的 `.sources.json` 记录来源文件及校验值。\n\n官方仓库:https://github.com/EsotericSoftware/spine-runtimes\n'); console.log('Saved to ' + root); })().catch(error => { console.error(error); process.exitCode = 1; });