图库改走独立仓库 + 单提交历史(每次重烘 orphan + force push)

背景:Gitea 的 Release 附件在本实例不可用(1KB..59.5MB 全部 HTTP 500),所以图集包改为
发布到独立仓库 troytt/diablo2-web-assets,并刻意只保留一个提交——每次重烘在临时目录 git init
后提交当前包并 force push,仓库体积恒定在「一份快照」~66 MB;代码仓库 clone 保持 2.7 MB。

新增 scripts/publish-packs.ts(npm run publish:packs):
  - 统计块数/文件数/体积与 index.json sha256,生成图库仓库里的 README(用法、许可、重建步骤)
  - 写 .gitattributes(*.png binary -diff)
  - 在临时目录 init -b main → 拷贝 samples/d2-packs → 提交 → force push
  - 不落任何凭据到磁盘;remote 可用参数或 D2_ASSETS_REMOTE 覆盖

README「资源包从哪来」改为以图库仓库为主、归档镜像为辅;HANDOVER 记录两种发布方式、
服务端 gc 的注意事项(Gitea 的 cron.git_gc_repos 默认关闭,force push 后的旧对象需 gc 才回收)。
This commit is contained in:
troytt 2026-09-14 12:03:27 +08:00
parent a1af1d7304
commit cd81a7e686
4 changed files with 205 additions and 7 deletions

View File

@ -42,9 +42,13 @@ npm run net-server # 等价于 node scripts/net-server.ts 8787
```bash ```bash
npm run pack:data # scripts/pack-act-assets.ts → samples/d2-packs(62 个地图块 / 35 关,约 67 MB) npm run pack:data # scripts/pack-act-assets.ts → samples/d2-packs(62 个地图块 / 35 关,约 67 MB)
# 图集包不在 Git 里(每次重烘会让仓库再长 ~60 MB,PNG 无法 delta 压缩)。发布归档: # 图集包不在代码仓库里(每次重烘会让仓库再长 ~60 MB,PNG 无法 delta 压缩)。发布方式:
# https://www.laiseek.xyz/diablo2/assets/d2-packs-<日期>.tar.xz (nginx → /var/www/d2assets/) # 1) 图库仓库 troytt/diablo2-web-assets:npm run publish:packs
# Gitea 的 Release 附件在本实例(124.221.104.39)是坏的:1KB..59.5MB 全部 HTTP 500 # → 每次 orphan 提交 + force push,仓库恒定"一份快照"(~66 MB),代码仓库 clone 仅 ~2.7 MB
# 2) 归档镜像(可选):https://www.laiseek.xyz/diablo2/assets/d2-packs-<日期>.tar.xz
# (nginx location /diablo2/assets/ → /var/www/d2assets/)
# 注意:force push 后旧提交对象要服务端 git gc 才回收;Gitea 的 [cron.git_gc_repos] 默认关闭,
# 而 Gitea 的 Release 附件在本实例(124.221.104.39)是坏的:1KB..59.5MB 全部 HTTP 500。
npm run verify:packs # 与现读现解逐项比对:绘制序/坐标、碰撞栅格逐字节、每帧像素 FNV 哈希(992/992) npm run verify:packs # 与现读现解逐项比对:绘制序/坐标、碰撞栅格逐字节、每帧像素 FNV 哈希(992/992)
npm run verify:listfile # 用社区 1.13c listfile 逐归档判定"这个成员到底存不存在" npm run verify:listfile # 用社区 1.13c listfile 逐归档判定"这个成员到底存不存在"
npm run verify:deploy # 线上 /diablo2/ 页面 + 资源包 + Range 206 + 旧入口 410(17/17) npm run verify:deploy # 线上 /diablo2/ 页面 + 资源包 + Range 206 + 旧入口 410(17/17)

View File

@ -46,7 +46,16 @@ scripts/ 资源包烘焙、格式/数据/部署校验、无头浏览器检
### 2. 资源包从哪来(不在 Git 里) ### 2. 资源包从哪来(不在 Git 里)
图集包(62 个地图块 / 35 关,索引色 PNG + `scene.json`)以归档形式发布,**不入 Git**: 图集包(62 个地图块 / 35 关,索引色 PNG + `scene.json`)**不在本仓库里**——它是 ~66 MB 的索引色
PNG,每次重烘几乎全部文件都变,而 PNG 无法被 git 的 delta 压缩,随仓库分发会让每烘焙一次仓库再长
~60 MB。它放在**图库仓库**,那里刻意**只保留一个提交**(每次重烘 orphan 提交 + force push),
体积恒定在「一份快照」:
```bash
git clone https://git.projectdiablo2.cn/troytt/diablo2-web-assets.git samples/d2-packs
```
只想要一份压缩包(不需要 git):
```bash ```bash
curl -O https://www.laiseek.xyz/diablo2/assets/d2-packs-2026-09-14.tar.xz curl -O https://www.laiseek.xyz/diablo2/assets/d2-packs-2026-09-14.tar.xz
@ -54,8 +63,8 @@ sha256sum d2-packs-2026-09-14.tar.xz # 应为 9cdfd0c19ac68de842c6725070b65d05
tar xJf d2-packs-2026-09-14.tar.xz # 解出 samples/d2-packs/ tar xJf d2-packs-2026-09-14.tar.xz # 解出 samples/d2-packs/
``` ```
同目录还有 `d2-packs-2026-09-14.sha256`。仓库里只保留 `.gitignore` 规则与本文档; 仓库里只保留 `.gitignore` 规则与本文档;要自己生成包就照第 3 步 `npm run pack:data` 从原版 MPQ
要自己生成包就照第 3 步 `npm run pack:data` 从原版 MPQ 烘焙。 烘焙,再用 `npm run publish:packs` 发布到图库仓库(脚本会重建单提交历史)。
### 3. 本地跑起来 ### 3. 本地跑起来

View File

@ -24,7 +24,8 @@
"verify:object-lookup": "node scripts/verify-object-lookup.ts", "verify:object-lookup": "node scripts/verify-object-lookup.ts",
"port:object-lookup": "node scripts/port-object-lookup.ts", "port:object-lookup": "node scripts/port-object-lookup.ts",
"verify:alignment": "node scripts/verify-tile-alignment.ts", "verify:alignment": "node scripts/verify-tile-alignment.ts",
"verify:tiles": "node scripts/verify-tiles.ts" "verify:tiles": "node scripts/verify-tiles.ts",
"publish:packs": "node scripts/publish-packs.ts"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.20.2", "@types/node": "^22.20.2",

184
scripts/publish-packs.ts Normal file
View File

@ -0,0 +1,184 @@
/**
* 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 })
}