Files
Shuery 3f11426964 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 处引用自包含。
2026-10-02 01:27:02 +08:00

125 lines
4.7 KiB
Python
Raw Permalink 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.
"""一次性迁移:把 `wallpapers/<游戏>/<壁纸>/` 从「按文件类型分组」改成「一具骨架一组」。
旧:
effects/<名>.json ← 骨架数据
images/<名>.atlas images/<名>*.webp ← atlas 与页(扁平)
images/<名>/<名>.atlas + <名>.png ← 另一档用的「一骨架一目录」
images/scene/<图> ← 场景图
images/ava.jpg ← 背景图
新:
spines/<名>/<名>.json | <名>.atlas | <页…>
scene/<图> ← 场景图与背景图
audios/… ← 不动
规则(页的判定用 **atlas 自己声明的页名**,与运行时同一份真相,不做名字启发式):
1. 每个 `<atlas>` 的 stem 建 `spines/<stem>/`,把该 atlas 与它声明的页搬进去;
2. `effects/*.json` → `spines/<stem>/<stem>.json`;
3. `images/` 里剩下的文件(含 `images/scene/*`)→ `scene/`;
4. 按实际搬动结果重写 `preset.template.json` 里所有 `./…` 路径。
用法:
python .scratch/migrate-walls.py # dry-run,只打印
python .scratch/migrate-walls.py --apply # 真搬 + 改预设
"""
from __future__ import annotations
import json
import re
import shutil
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
WALLS = ROOT / "wallpapers"
APPLY = "--apply" in sys.argv
PAGE_EXT = re.compile(r"\.(png|webp|jpg|jpeg)$", re.IGNORECASE)
def atlas_pages(text: str) -> list[str]:
"""取 atlas 声明的页名(非属性行、以图片扩展名结尾的行)。"""
pages = []
for raw in text.splitlines():
line = raw.strip()
if line and ":" not in line and PAGE_EXT.search(line):
pages.append(line)
return pages
def migrate(wall: Path) -> dict[str, str]:
"""搬一档壁纸,返回 {旧相对路径: 新相对路径}(相对壁纸目录,不带 ./)。"""
moves: dict[str, str] = {}
# 1) atlas 及其声明的页
atlases = list((wall / "images").rglob("*.atlas")) + list((wall / "spines").rglob("*.atlas"))
for atlas in atlases:
stem = atlas.stem
dest_dir = wall / "spines" / stem
pages = atlas_pages(atlas.read_text(encoding="utf-8"))
moves[str(atlas.relative_to(wall))] = f"spines/{stem}/{atlas.name}"
for page in pages:
src = atlas.parent / page
if src.exists():
moves[str(src.relative_to(wall))] = f"spines/{stem}/{page}"
# 2) 骨架 json
for js in (wall / "effects").glob("*.json") if (wall / "effects").is_dir() else []:
stem = js.stem
moves[str(js.relative_to(wall))] = f"spines/{stem}/{js.name}"
# 3) images/ 下剩下的(含 images/scene/*)→ scene/
if (wall / "images").is_dir():
for path in sorted((wall / "images").rglob("*")):
if path.is_dir():
continue
rel = str(path.relative_to(wall))
if rel in moves:
continue
moves[rel] = f"scene/{path.name}"
# 4) 重写预设(按映射精确替换 ./ 路径)
preset_path = wall / "preset.template.json"
preset = json.loads(preset_path.read_text(encoding="utf-8"))
replaced: list[str] = []
def rewrite(value: object) -> object:
if isinstance(value, str) and value.startswith("./"):
old = value[2:]
new = moves.get(old)
if new:
replaced.append(f"{value} → ./{new}")
return f"./{new}"
if isinstance(value, dict):
return {k: rewrite(v) for k, v in value.items()}
if isinstance(value, list):
return [rewrite(v) for v in value]
return value
new_preset = rewrite(preset)
print(f"\n== {wall.relative_to(ROOT)}")
for old, new in sorted(moves.items()):
print(f" {old:52} → {new}")
for line in replaced:
print(f" 预设: {line}")
if APPLY:
for old, new in moves.items():
src, dst = wall / old, wall / new
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), str(dst))
# 清掉空目录
for empty in ["effects", "images"]:
d = wall / empty
if d.is_dir():
shutil.rmtree(d, ignore_errors=True)
preset_path.write_text(json.dumps(new_preset, ensure_ascii=False, indent="\t") + "\n", encoding="utf-8")
return moves
total = 0
for wall in sorted(p for p in WALLS.rglob("*") if (p / "preset.template.json").is_file()):
total += len(migrate(wall))
print(f"\n{'已搬动' if APPLY else '将搬动'} {total} 个文件;{'(dry-run,加 --apply 才真搬)' if not APPLY else ''}")