diablo2-web/scripts/publish-packs.ts

185 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Publish `samples/d2-packs` to the asset repository.
*
* The baked atlas packs are ~66 MB of indexed PNG. PNG is already deflate
* compressed, so git's delta compression buys almost nothing: every re-bake changes
* nearly every file, and a normal commit would grow the repository by another ~60 MB
* each time (measured: the code repository went 63 MB → 117 MB over two bakes).
*
* So the asset repository deliberately keeps **exactly one commit**: every publish
* creates an orphan history (a brand new repository in a temp directory), commits the
* current packs, and force-pushes it over the remote. The repository therefore stays
* at "one snapshot" (~66 MB) no matter how often the packs are re-baked, and the code
* repository — which no longer contains the packs — clones in a couple of megabytes.
*
* Usage:
* ```
* node scripts/publish-packs.ts # default remote
* node scripts/publish-packs.ts <git-remote-url> # explicit remote
* ```
*
* The remote needs credentials; either pass a URL with them, or run
* `git config --global credential.helper store` once so `git push` does not prompt.
* Nothing here writes a password to disk.
*
* @module
*/
import { execFileSync } from 'node:child_process'
import {
cpSync, existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync,
} from 'node:fs'
import { createHash } from 'node:crypto'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
/** Where the packs live inside the code repository. */
const PACKS_DIR = resolve(import.meta.dirname, '..', 'samples', 'd2-packs')
/** The asset repository, without credentials. */
const DEFAULT_REMOTE = 'https://git.projectdiablo2.cn/troytt/diablo2-web-assets.git'
/** The code repository, linked from the generated README. */
const CODE_REPO = 'https://git.projectdiablo2.cn/troytt/diablo2-web'
/** The archive mirror served next to the deployment. */
const ARCHIVE_BASE = 'https://www.laiseek.xyz/diablo2/assets'
/** Raised when the packs are missing or git fails. */
class PublishError extends Error {}
/**
* Run one git command in `cwd`, inheriting stdio so progress is visible.
*
* @param args - git arguments.
* @param cwd - directory to run in.
*/
function git(args: readonly string[], cwd: string): void {
try {
execFileSync('git', args, { cwd, stdio: ['ignore', 'inherit', 'inherit'] })
} catch {
throw new PublishError(`git ${args.join(' ')} failed`)
}
}
/**
* Collect the statistics that go into the generated README.
*
* @returns file count, total bytes, map-block count and the `index.json` hash.
*/
function measure(): { files: number; bytes: number; maps: number; indexSha: string } {
const indexPath = join(PACKS_DIR, 'index.json')
if (!existsSync(indexPath)) {
throw new PublishError(`no packs at ${PACKS_DIR} — run: npm run pack:data`)
}
const index = JSON.parse(readFileSync(indexPath, 'utf8')) as { entries?: unknown[] }
const indexSha = createHash('sha256').update(readFileSync(indexPath)).digest('hex')
let files = 0
let bytes = 0
const walk = (dir: string): void => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name)
if (entry.isDirectory()) walk(path)
else {
files += 1
bytes += statSync(path).size
}
}
}
walk(PACKS_DIR)
return { files, bytes, maps: index.entries?.length ?? 0, indexSha }
}
/**
* Build the README that ships inside the asset repository.
*
* @param stats - output of {@link measure}.
* @param stamp - `YYYY-MM-DD` bake date.
* @returns the markdown text.
*/
function readme(stats: ReturnType<typeof measure>, stamp: string): string {
const mb = (stats.bytes / 1048576).toFixed(1)
return `# d2web 烘焙图集包(${stamp})
这是 [\`troytt/diablo2-web\`](${CODE_REPO}) 的**烘焙产物**:把原版暗黑破坏神 II(1.13c)的地图
解包成 web 原生格式——索引色 PNG 图集页 + \`scene.json\`(画家序绘制列表、碰撞 RLE、出生点、
对象元数据)+ \`index.json\`(含各 act 调色板)。
- 地图块 **${stats.maps} 个**(35 个预置关卡,act1–act5),文件 **${stats.files} 个**,合计 **${mb} MB**
- \`index.json\` sha256:\`${stats.indexSha}\`
- 生成时间:${stamp}
## 本仓库为什么只有一个提交
图集页是索引色 PNG(deflate 已压缩),git 的 delta 压缩对它几乎无收益:每次重烘几乎全部文件
都会变。若按普通方式提交,每烘焙一次仓库就再长 ~60 MB(实测代码仓库因此从 63 MB 涨到 117 MB)。
所以这里刻意**只保留一个提交**:每次重烘用 orphan 提交 + force push 覆盖,仓库体积恒定在
「一份快照」≈${Math.round(stats.bytes / 1048576)} MB。历史没有价值——需要旧版请从代码仓库
对应提交重新烘焙。
## 怎么用
\`\`\`bash
# 放进代码仓库的 samples/d2-packs/,页面默认就吃这个目录
git clone ${DEFAULT_REMOTE} samples/d2-packs
\`\`\`
只要一份压缩包(不需要 git):
\`\`\`bash
curl -O ${ARCHIVE_BASE}/d2-packs-${stamp}.tar.xz
tar xJf d2-packs-${stamp}.tar.xz
\`\`\`
## 怎么重新生成并发布
在代码仓库里(需要自备原版 MPQ,放在 \`samples/d2/\`):
\`\`\`bash
npm run pack:data # scripts/pack-act-assets.ts → samples/d2-packs(约 2 分钟)
npm run verify:packs # 与"现读现解"逐项比对:绘制序、碰撞栅格逐字节、每帧像素哈希
npm run publish:packs # orphan 提交 + force push 覆盖本仓库
\`\`\`
## 许可
内容为原版游戏美术的解码产物,仅供**持有正版游戏数据**的人在本项目内使用;请勿再分发。
原版 MPQ 与第三方参考源码都不在本仓库内。
`
}
const remote = process.argv[2] ?? process.env['D2_ASSETS_REMOTE'] ?? DEFAULT_REMOTE
const stats = measure()
const stamp = new Date().toISOString().slice(0, 10)
const work = mkdtempSync(join(tmpdir(), 'd2-assets-'))
try {
console.log(`图集包:${String(stats.maps)} 块地图 / ${String(stats.files)} 个文件 / ${(stats.bytes / 1048576).toFixed(1)} MB`)
console.log(`index.json sha256:${stats.indexSha}`)
console.log(`临时仓库:${work}`)
git(['init', '-q', '-b', 'main'], work)
cpSync(PACKS_DIR, work, { recursive: true })
writeFileSync(join(work, 'README.md'), readme(stats, stamp))
writeFileSync(join(work, '.gitattributes'), '*.png binary -diff\n*.json text eol=lf\n')
git(['add', '-A'], work)
git([
'-c', 'user.name=d2web', '-c', 'user.email=d2web@localhost',
'commit', '-q', '-m',
`烘焙图集包 ${stamp}:${String(stats.maps)} 个地图块(${(stats.bytes / 1048576).toFixed(1)} MB)
本仓库刻意只保留一个提交:图集页是索引色 PNG,git 的 delta 压缩对它几乎无收益,
每次重烘几乎全部文件都变;普通提交会让仓库每烘焙一次再长 ~60 MB。所以每次重烘都
用 orphan 提交 + force push 覆盖(见 README)。
index.json sha256: ${stats.indexSha}
由 troytt/diablo2-web 的 npm run pack:data 生成。`,
], work)
console.log(`推送到 ${remote}(force,覆盖历史)…`)
git(['push', '--force', remote, 'main'], work)
console.log('完成。使用方:git clone ' + remote + ' samples/d2-packs')
} finally {
rmSync(work, { recursive: true, force: true })
}