feat: TS 构建管线、Spine 抓取器与 wallpapers/ 唯一真相来源
把项目从「手写 dist/」改成「wallpapers/ 是唯一真相来源,dist/ 由 pnpm build 生成」,
并补上配套的类型、门禁与抓取器。一次提交落地整条管线,因为拆开会留下不能构建的中间态。
- src/:运行时与模拟器源码(TS,strict),编译到 build/ 再拷进各分发
- tools/:build / dev / check-{syntax,paths,dist},以及抓取器与回归门禁 tools/checks/
(.scratch/ 下那批一次性脚本移入 tools/checks/ 并入库为长期门禁)
- wallpapers/:七档壁纸的源数据 + README.md(id/音频/预设的完整规范)
- docs/adr/0005-0008:构建管线与分发拓扑、模拟器契约、自包含 sim、每骨架资源布局
- .gitignore:排除 .scratch/ 的参考资料副本(上游 spine 整仓克隆 ~1.2 GB、
抓取侦查数据 ~680 MB)与调试转储;这些是本地调查材料,补偿会让仓库无法克隆
- 归一化 .gitignore/CONTEXT.md 行尾(工作区 CRLF、索引 LF 造成的整文件假 diff)
同时修掉三档卡住构建的未完工壁纸:
- kv45 的 meta.json 里 id 还是抓取期场景名 scene_main,经 downloader promote 正名为 kv45
- shajin / zhigengniao_juheye 的 meta.json 误用了骨架描述文件(name/spine/animations/pages)、
且都缺 preset.template.json;现按规范重建:骨架沉到 spines/<名>/(spine-ts 按 atlas 所在
目录解析贴图页)、补上元数据与单骨架预设,并清掉 zhigengniao 骨架里指向作者机的绝对路径
- 顺带 promote 已在 sources.yml 里的 kv46(月升之前,与兽共舞)
pnpm check 五道门全绿:10 个分发 / 7 档壁纸 / 141 处引用自包含。
This commit is contained in:
1 parent
b8eee05d78
commit
3f11426964
297 files changed
+216627
-1926
No files matched your search
@@ -0,0 +1,310 @@
|
||||
# `wallpapers/` 目录规范
|
||||
|
||||
这里是**唯一真相来源**。`dist/` 下的每一个分发目录都由它生成,不要手工改产物——
|
||||
`dist/` 是 `pnpm build` 的产物、已 gitignore、随时可以整份删掉重来。
|
||||
|
||||
改完这里的东西,跑:
|
||||
|
||||
```bash
|
||||
pnpm dev # 本地调试(带热更新,保存即生效)
|
||||
pnpm check # 类型 + 构建 + 语法 + 路径 + 产物自包含,五道门
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、目录结构
|
||||
|
||||
```
|
||||
wallpapers/
|
||||
├── meta.json ← 全局元数据 = 「所有壁纸合集」这一档分发的文案
|
||||
├── preview.gif ← 全局预览图(可选;被 collection-all 用)
|
||||
├── sources.txt ← 来源 URL 清单(**不是构建输入**,纯留档)
|
||||
│
|
||||
└── <游戏id>/ ← 例:hsr。目录名必须等于它 meta.json 里的 id
|
||||
├── meta.json ← 游戏级元数据(文案 + 显示名 + 共享音频声明)
|
||||
├── audios/ ← 游戏级**共享**音频(可选,只在合集分发里出现)
|
||||
│
|
||||
└── <壁纸id>/ ← 例:xilian / kv37。目录名必须等于 meta.json 里的 id
|
||||
├── meta.json ← 壁纸级元数据(文案 + 音源清单)
|
||||
├── preset.template.json ← 运行时配置(背景图 / 骨架 / 取景)
|
||||
├── preview.gif ← 本壁纸的预览图(可选)
|
||||
├── spines/<骨架名>/ ← **一具骨架一组**:<名>.json + <名>.atlas + 贴图页
|
||||
├── scene/ ← 场景图与背景图(非骨架的图)
|
||||
└── audios/ ← 音源文件
|
||||
```
|
||||
|
||||
三层的 `meta.json` 各自管一段:**全局**给「全部合集」写文案,**游戏**给「该游戏合集」写文案并声明共享音频,**壁纸**给单档分发写文案并声明自己的音源。
|
||||
|
||||
---
|
||||
|
||||
## 二、id 与命名规则(最容易踩的一条)
|
||||
|
||||
| 规则 | 说明 |
|
||||
| --- | --- |
|
||||
| **目录名 = id** | `wallpapers/hsr/xilian/` 的 `meta.json` 里 `"id"` 必须也是 `xilian`,否则构建直接报错 |
|
||||
| **字符集** | 只能用字母、数字、`_`、`-`,且不能以 `-`/`_` 开头:`/^[a-z0-9][a-z0-9_-]*$/i` |
|
||||
| **不能是保留名** | `audios`、`meta.json`、`preview.gif`、`preset.template.json` 不能当 id |
|
||||
| **壁纸 id 全局唯一** | 跨游戏也不能重名——它是 WE combo 的 value,撞了会让后一档静默覆盖前一档 |
|
||||
| **永远不要改已发布的 id** | 老用户的设置按 id 保存。改了 = 他们的选择失效。显示名(`name`)随便改,id 不能动 |
|
||||
|
||||
**id 一律 ASCII,不要中文。** 显示名(`name` / `title` / `description`)保留中文,那是产品内容。
|
||||
|
||||
---
|
||||
|
||||
## 三、`meta.json` 字段
|
||||
|
||||
### 1. 全局 `wallpapers/meta.json`(= 「所有壁纸合集」)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "所有壁纸合集",
|
||||
"title": "【崩坏:星穹铁道】昔涟",
|
||||
"description": "…WE 的 BBCode 文案…",
|
||||
"defaultPresetId": "xilian",
|
||||
"preview": "preview.gif",
|
||||
"workshopid": "3604974793",
|
||||
"workshopurl": "steam://url/CommunityFilePage/3604974793"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| `name` | ✓ | 调试服索引页上的显示名 |
|
||||
| `title` | ✓ | 写进 `project.json` 的 `title`(创意工坊标题) |
|
||||
| `description` | ✓ | 写进 `project.json`,支持 WE 的 BBCode |
|
||||
| `defaultPresetId` | | **决定 `preset` 下拉的第一项与 `preset.value`**。改它 = 改所有用户的默认观感,不是重构 |
|
||||
| `preview` | | 相对 `wallpapers/` 的文件名。缺省 → `project.json` 整个省略 `preview` 字段 |
|
||||
| `workshopid` / `workshopurl` | | 上传后由 WE 生成,**没上传就别写**——缺省时字段整个不出现 |
|
||||
|
||||
### 2. 游戏 `wallpapers/<游戏id>/meta.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "hsr",
|
||||
"name": "崩坏:星穹铁道",
|
||||
"title": "【崩坏:星穹铁道】壁纸合集",
|
||||
"audios": []
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| `id` | ✓ | 必须等于目录名 |
|
||||
| `name` | ✓ | 显示名(多游戏时用来给 `preset` 下拉加前缀) |
|
||||
| `title` | ✓ | **游戏合集**的创意工坊标题。游戏合集的文案只认这一层——**不回落到全局 `meta.json`**(那层是给「全部合集」写的,回落会让 `collection-ys` 顶着《崩坏:星穹铁道》昔涟,见 `issues/20`) |
|
||||
| `description` | | 游戏合集的文案,WE 的 BBCode。缺省时构建用「共 N 档:…」的模板自动生成 |
|
||||
| `audios` | | **游戏级共享音频**:整个游戏所有壁纸都能选到的曲子。只在「合集」类分发里出现,单档分发不会带上它。形状与壁纸的 `audio.choices` 相同(`{name, file}`),`file` 相对 `wallpapers/<游戏id>/` |
|
||||
|
||||
### 3. 壁纸 `wallpapers/<游戏id>/<壁纸id>/meta.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "xilian",
|
||||
"name": "昔涟立绘",
|
||||
"title": "【崩坏:星穹铁道】昔涟",
|
||||
"description": "…WE 的 BBCode 文案…",
|
||||
"preview": "preview.gif",
|
||||
"audio": {
|
||||
"choices": [
|
||||
{ "name": "「再度和你」", "file": "audios/zaiduheni.flac" },
|
||||
{ "name": "昔涟", "file": "audios/xilian-src.flac" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| `id` | ✓ | 必须等于目录名,且全局唯一 |
|
||||
| `name` | ✓ | 显示名。单档分发里它就是 `title` 的来源之一,也是 `preset` 下拉的选项文字 |
|
||||
| `title` / `description` | ✓ | 单档分发的 `project.json` 文案 |
|
||||
| `preview` | | 相对本目录的文件名。**声明了就必须真实存在**,否则报错 |
|
||||
| `audio.choices` | ✓ | 音源清单,见下 |
|
||||
| `audio.default` | | 默认音源的位置,**从 1 起**。省略 = 第一条 |
|
||||
| `workshopid` / `workshopurl` | | 同全局:没上传就别写 |
|
||||
|
||||
---
|
||||
|
||||
## 四、音频规范
|
||||
|
||||
### `choices` 里写什么
|
||||
|
||||
每条只有两个字段:**`name`(显示名,中文原样)** 和 **`file`(相对声明者目录的路径)**。
|
||||
|
||||
```json
|
||||
{ "name": "「再度和你」", "file": "audios/zaiduheni.flac" }
|
||||
```
|
||||
|
||||
- **不要写 `id`**:id 由构建按固定顺序**自增分配**(`"1"`、`"2"`、`"3"`…),全项目唯一。
|
||||
之所以必须全项目唯一,是因为 `bgm` 下拉会把一个分发里**所有**壁纸的音源平铺进同一个列表,
|
||||
两档都叫 `"1"` 就分不清选的是哪个了。
|
||||
- **顺序即默认**:第一条就是这档壁纸的默认音乐。要换默认,把那条挪到第一位
|
||||
(或显式写 `"default": 2`)。
|
||||
|
||||
### 文件放哪、什么格式
|
||||
|
||||
| 位置 | 谁用 |
|
||||
| --- | --- |
|
||||
| `wallpapers/<游戏>/<壁纸>/audios/` | 该壁纸自己的音源,单档与合集分发都会带上 |
|
||||
| `wallpapers/<游戏>/audios/` | 游戏级共享音源,**只在合集分发里出现** |
|
||||
|
||||
**扩展名白名单**(WE 内置的 Chromium 146 实测能解的):
|
||||
|
||||
```
|
||||
.flac .mp3 .ogg .opus .wav
|
||||
```
|
||||
|
||||
`.m4a` / `.aac` **会被直接拒**——CEF 的 FFmpeg 构建里没有 aac 解码器。
|
||||
文件名可以保留中文,但路径里不能有 `..`(音频会被搬进分发目录内部)也不能用 `\`。
|
||||
|
||||
### 孤儿检查
|
||||
|
||||
**`audios/` 下的每个文件都必须在 `choices` 里被声明。** 一个没声明的 `.flac` 会安静地躺在仓库里:
|
||||
类型检查过、构建过、产物自包含,只是那首歌永远选不到——所以 `pnpm check` 会把它揪出来。
|
||||
反过来,`choices` 里声明了但目录不存在也会报错。
|
||||
|
||||
---
|
||||
|
||||
## 五、`preset.template.json`(运行时配置)
|
||||
|
||||
```json
|
||||
{
|
||||
"backgroundImage": "./scene/ava.jpg",
|
||||
"spineConfig": {
|
||||
"jsonUrl": "./spines/xilian/xilian.json",
|
||||
"atlasUrl": "./spines/xilian/xilian.atlas",
|
||||
"animation": "idle",
|
||||
"viewport": { "padLeft": "-25%", "padRight": "-28%", "padTop": "-30%", "padBottom": "-23%" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 路径**一律写成相对本目录**(以 `./` 开头)。构建会把它解析成 `import.meta.url` 推导出的绝对 URL,
|
||||
于是整个分发目录可以整体搬走而不用改任何一行路径。
|
||||
- `backgroundImage`、`spineConfig.jsonUrl`、`spineConfig.atlasUrl` **三个都必须真实存在**,构建会检查。
|
||||
- `viewport` 的 pad 可以用百分比或像素,**可以是负数**(表示裁切)。
|
||||
- `framing: "author"` = 保留作者原始取景,不跟随背景缩放。
|
||||
|
||||
### 场景壁纸(`sceneConfig`)
|
||||
|
||||
一档壁纸也可以是**一整页场景**:N 具骨架 + M 块贴图平面按抓取期定下的摆放与层级合成。
|
||||
|
||||
```json
|
||||
{
|
||||
"backgroundImage": "./scene/cover.jpg",
|
||||
"sceneConfig": {
|
||||
"ui": [2500, 1080],
|
||||
"parts": [
|
||||
{ "kind": "image", "id": "main_sky_jpg", "image": "./scene/main_sky_jpg.jpg",
|
||||
"order": 0, "position": [0, 0, 0], "scale": [1, 1, 1] },
|
||||
{ "kind": "spine", "id": "main_nike",
|
||||
"jsonUrl": "./spines/main_nike/main_nike.json",
|
||||
"atlasUrl": "./spines/main_nike/main_nike.atlas",
|
||||
"animation": "眨眼",
|
||||
"order": 3, "position": [-120.5, 480.25, 0], "scale": [1.09, 1.09, 1] }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 规则 | 说明 |
|
||||
| --- | --- |
|
||||
| **二选一** | `spineConfig`(单骨架)与 `sceneConfig`(场景)**只能写一个**;都写或都不写,构建期直接报错 |
|
||||
| `kind` | `spine`(骨架)/ `image`(贴图平面)/ `solid`(纯色平面,只给 `width`/`height`) |
|
||||
| `order` | 绘制层级,抓取期由场景树遍历序给出 |
|
||||
| `position` / `scale` | **世界变换**(抓取期已按引擎语义逐层合成),页面坐标系(y 向上);运行时按 `flipY` 处理方向 |
|
||||
| `animation` | 缺省 = 该骨架的第一个动画 |
|
||||
| 路径 | 与单骨架同一条规则(`./` 开头、相对本目录),构建会解析成 `import.meta.url` 推导的绝对 URL |
|
||||
|
||||
这些字段由抓取器从页面场景树抄来(见 `tools/downloader/README.md`),构建期烘焙进 `preset.js`,
|
||||
运行时不再读侧车。当前已知近似:glTF 网格(`geometry.type: 1`)没有尺寸,平面回落贴图原始尺寸。
|
||||
|
||||
> `tools/downloader` 现在**每个场景直接产出一份终态的壁纸目录**(`_out/<游戏>/<页面>/<场景id>/`):
|
||||
> 形状与本节完全一致(`meta.json` + `preset.template.json` + `spines/` + `scene/` + `audios/`),
|
||||
> 整个目录拷进 `wallpapers/<游戏id>/` 即可(**目录名要等于 `meta.json.id`**),
|
||||
> 或者用 `python -m tools.downloader promote` 改名 + 校验后搬进来。
|
||||
|
||||
> 这个文件里**不放**显示名、文案、音源清单——那些都在 `meta.json`。
|
||||
> 两份数据在构建时合成 `preset.js`(运行时)与 `project.json`(发布)。
|
||||
|
||||
---
|
||||
|
||||
## 六、图片与骨架
|
||||
|
||||
- **一具骨架的东西放在一起**:`spines/<骨架名>/` 里放它的 `.json`、`.atlas` 与全部贴图页。
|
||||
这不是风格问题——spine-ts 的 `AssetManager` 按 **`<atlas 所在目录>/<页名>`** 解析贴图页
|
||||
(页名来自 atlas 第一行),页跑到别处就读不到。
|
||||
- **骨架 `.json` 里作者的目录字段要留空串**:`skeleton.images`(以及 `skeleton.audio`)指的是导出机的
|
||||
目录(`../images/`、`../audios`),搬进分发后一定指错。现在运行时不读它们(页按 atlas 所在目录解析),
|
||||
但留着就是一颗定时炸弹:以后真用到共享贴图或事件音时会静默指向不存在的路径。下载器产出的骨架
|
||||
(`promote.py`)已经归一化,存量骨架要自己补——迁移时就漏了 hsr 那两具。
|
||||
- **场景图与背景图放 `scene/`**。注意一个例外:`backgroundImage` 也可以指向某个骨架的贴图页
|
||||
(kv37 就是这样,它用 atlas 里的 `kv37_xilian.webp` 当背景),那种情况路径写进 `spines/<名>/` 即可。
|
||||
- 目录名固定(`spines` / `scene` / `audios`),构建按它们搬文件——改名要同步改
|
||||
`tools/lib/generate.ts` 的 `copyWallpaperAssets`。
|
||||
- `.atlas` 是文本文件:**第一行是贴图页的文件名**,接着 `size:` / `filter:` / `scale:`,
|
||||
之后才是各区域的 `bounds` / `offsets` / `rotate`。
|
||||
**改名贴图页时 atlas 第一行要一起改**,否则运行时会去请求一个不存在的页。
|
||||
- 大图建议转 WebP(本项目现用)。**atlas 页要整组一起转**,并同步改 `.atlas` 里的页名。
|
||||
|
||||
---
|
||||
|
||||
## 七、常见任务
|
||||
|
||||
### 加一档新壁纸
|
||||
|
||||
1. 建目录 `wallpapers/<游戏id>/<新id>/`(**id 必须全局唯一**)
|
||||
2. 放 `meta.json`:`id` 与目录名一致,填 `name` / `title` / `description`
|
||||
3. 放 `preset.template.json`,路径都写 `./` 开头
|
||||
4. 把骨架、atlas、贴图页放进 `spines/<骨架名>/`;场景图与背景图放进 `scene/`(规则见第六节)
|
||||
5. 有音源就放进 `audios/` 并在 `meta.json` 的 `audio.choices` 里声明
|
||||
6. 有预览图就放 `preview.gif` 并在 `meta.json` 里写 `"preview": "preview.gif"`
|
||||
7. `pnpm check`
|
||||
|
||||
### 加一个新游戏
|
||||
|
||||
同上,外加 `wallpapers/<游戏id>/meta.json`(`id` + `name` + `title`——`title` 是「该游戏合集」的创意工坊标题,必填,见第三节)。
|
||||
「该游戏合集」这一档分发的目录名与文案会自动从它生成。
|
||||
|
||||
### 加一首音乐
|
||||
|
||||
把文件放进对应的 `audios/`,在 `meta.json` 的 `choices` **末尾**追加一条 `{name, file}`。
|
||||
**追加到末尾**,不要插在中间——id 是自增的,插在中间会让后面所有音源的 id 后移,
|
||||
老用户已保存的 `bgm` 选择会指向别的曲子。
|
||||
|
||||
### 换默认音乐
|
||||
|
||||
把 `choices` 里那条挪到第一位(或写 `"default": N`)。
|
||||
|
||||
### 加/换预览图
|
||||
|
||||
壁纸级:放 `<壁纸id>/preview.gif` 并写 `"preview": "preview.gif"`。
|
||||
全局(「所有合集」那一档):放 `wallpapers/preview.gif`,在 `wallpapers/meta.json` 里写 `"preview": "preview.gif"`。
|
||||
|
||||
---
|
||||
|
||||
## 八、`pnpm check` 会替你检查什么
|
||||
|
||||
| 门 | 内容 |
|
||||
| --- | --- |
|
||||
| `typecheck` | `tsc` |
|
||||
| `build` | 四个分发全部构建成功 |
|
||||
| `check:syntax` | 会被 Node 直接执行的 `.ts` 都是 strip-safe |
|
||||
| `check:paths` | **音频孤儿**:每个 `audios/` 文件都被声明、每条声明都指向存在的文件 |
|
||||
| `check:dist` | 产物**自包含**:分发目录里没有绝对链接、没有跨目录相对链接、引用的文件都在 |
|
||||
|
||||
构建期还会 fail-fast 校验:id 与目录名一致、id 字符集、保留名、壁纸 id 全局唯一、
|
||||
`preview` 存在、`preset.template.json` 三个路径存在、音频扩展名在白名单内。
|
||||
|
||||
---
|
||||
|
||||
## 九、不要放在这里的东西
|
||||
|
||||
| 东西 | 该去哪 |
|
||||
| --- | --- |
|
||||
| 无损母带(几十 MB 的原始 flac) | `masters/`(已 gitignore,只在本地留存) |
|
||||
| 手写的 `id` / `index` / `order` | 不用写。音源 id 由构建自增分配;属性的 `index`/`order` 也由构建按属性表顺序编号 |
|
||||
| 构建产物(`dist/` 的拷贝) | 不要。`dist/` 完全由 `pnpm build` 生成 |
|
||||
| 抓取脚本、临时下载 | 别处。这里只放**最终要发布**的资源 |
|
||||
|
||||
`wallpapers/sources.txt` 是留档用的来源 URL 清单,**不参与构建**——删掉不影响产物。
|
||||
Reference in new issue
Block a user