From b57726f4344968f254eb5d3dbe6a30548e850359 Mon Sep 17 00:00:00 2001 From: FlipWind Date: Sun, 9 Aug 2026 00:12:13 +0800 Subject: [PATCH] feat: add Linux/Unix scripts and Makefile entry --- Makefile | 39 +++++++ scripts/README.md | 32 ++++- scripts/build.sh | 20 ++++ scripts/dev.sh | 37 ++++++ scripts/sync-items.py | 264 ++++++++++++++++++++++++++++++++++++++++++ scripts/sync-items.sh | 18 +++ 6 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 scripts/build.sh create mode 100644 scripts/dev.sh create mode 100644 scripts/sync-items.py create mode 100644 scripts/sync-items.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..651b329 --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +# SFCraft 文档 · 构建 / 开发入口 (跨平台) +# +# Linux / macOS: make build / make dev / make sync +# Windows(有 make 时): 同样可用, 自动调用 PowerShell 脚本 +# +# 可附加 hugo 参数: make dev HUGO_ARGS="--port 1314" + +.PHONY: help sync build dev + +HUGO_ARGS ?= + +ifeq ($(OS),Windows_NT) + PS := powershell -NoProfile -ExecutionPolicy Bypass -File + BUILD_CMD := $(PS) scripts/build.ps1 + DEV_CMD := $(PS) scripts/dev.ps1 + SYNC_CMD := $(PS) scripts/sync-items.ps1 +else + BUILD_CMD := ./scripts/build.sh + DEV_CMD := ./scripts/dev.sh + SYNC_CMD := ./scripts/sync-items.sh +endif + +help: + @echo "SFCraft 文档" + @echo " make build 同步物品贴图并执行 hugo 构建" + @echo " make dev 同步物品贴图并启动 hugo 开发服务器" + @echo " make sync 仅同步物品贴图" + @echo " make help 显示本帮助" + @echo "" + @echo "可附加 hugo 参数: make dev HUGO_ARGS=\"--port 1314\"" + +sync: + $(SYNC_CMD) + +build: + $(BUILD_CMD) $(HUGO_ARGS) + +dev: + $(DEV_CMD) $(HUGO_ARGS) diff --git a/scripts/README.md b/scripts/README.md index e2c40d9..7a4bcd8 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -26,13 +26,43 @@ powershell -ExecutionPolicy Bypass -File scripts\dev.ps1 脚本通过 `$env:HUGO` 或 PATH 查找 hugo, 找不到时也会探测 winget 安装位置。 +## Linux / macOS 使用 + +Unix 环境下使用同名的 bash 脚本(同步逻辑为 Python 3 实现, 需要 `python3` 与 `hugo`): + +```bash +./scripts/build.sh # 同步贴图 + hugo 构建 +./scripts/dev.sh # 同步贴图 + hugo server +./scripts/dev.sh -NoSync # 仅启动开发服务器 +``` + +也可以使用 Makefile(Windows / Unix 通用, 自动选择对应脚本): + +```bash +make build # 同步贴图 + hugo 构建 +make dev # 同步贴图 + hugo server +make sync # 仅同步贴图 +make dev HUGO_ARGS="--port 1314" +``` + +首次使用前请确认脚本有执行权限: `chmod +x scripts/*.sh`。 + +## 组件写法 + +`crafting` 组件支持两种设置方式, 详见 `assets/items/README.md`: + +- 九格直填: `a1="diamond" a2="stick" ... out="diamond_pickaxe"`; +- pattern 别名: `pattern="a a a\nb b b\nc c c"` + `a="diamond" b="stick"`。 + +贴图同步扫描器对两种写法都会收集物品 id。 + ## 规则细节 - sfcraft 自定义贴图: 每次构建比对远端文件大小, 缺失或变化时更新; - 原版贴图: 只在本地缺失时下载, **不会覆盖**手动放入 `assets/items/` 的贴图; - 物品 id 即 `assets/items/` 下的文件名(不含 `.png`); - wiki 命名规则: `<英文名>_JE<版本>[_BE<版本>].png`, 特殊命名(如 `TNT`、`Flint_and_Steel`) - 由 `sync-items.ps1` 中的变体生成与别名表处理。 + 由 `sync-items.ps1` / `sync-items.py` 中的变体生成与别名表处理。 ## 网络要求 diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..d1b6cad --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# SFCraft 文档构建入口 (Linux / macOS / Unix): 先同步贴图, 再执行 hugo 构建。 +# 贴图同步失败(有物品找不到)时终止构建。 +# 用法: ./scripts/build.sh [hugo 参数...] +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo +echo "[1/2] 同步物品贴图" +"$DIR/sync-items.sh" + +if ! command -v hugo >/dev/null 2>&1; then + echo "[错误] 未找到 hugo, 请安装 Hugo 或将其加入 PATH。" >&2 + exit 1 +fi + +echo +echo "[2/2] hugo 构建" +exec hugo "$@" diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100644 index 0000000..8f0a692 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# SFCraft 文档开发服务器入口 (Linux / macOS / Unix): 先同步贴图, 再启动 hugo server。 +# 贴图同步失败(有物品找不到)时终止启动。 +# 用法: ./scripts/dev.sh [-NoSync] [hugo server 参数...] +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +NOSYNC=0 +HUGO_ARGS=() +for arg in "$@"; do + case "$arg" in + -NoSync|--no-sync|-nosync) + NOSYNC=1 + ;; + *) + HUGO_ARGS+=("$arg") + ;; + esac +done + +if [ "$NOSYNC" -eq 0 ]; then + echo + echo "[1/2] 同步物品贴图" + "$DIR/sync-items.sh" +else + echo "[跳过] 已指定 -NoSync, 不执行贴图同步" +fi + +if ! command -v hugo >/dev/null 2>&1; then + echo "[错误] 未找到 hugo, 请安装 Hugo 或将其加入 PATH。" >&2 + exit 1 +fi + +echo +echo "[2/2] 启动 hugo server" +exec hugo server "${HUGO_ARGS[@]}" diff --git a/scripts/sync-items.py b/scripts/sync-items.py new file mode 100644 index 0000000..ff2c4a0 --- /dev/null +++ b/scripts/sync-items.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +"""SFCraft 文档 · 物品贴图自动同步 (Linux / macOS / Unix 版) + +与 scripts/sync-items.ps1 逻辑一致: + 1. 下载 sfcraft 仓库 textures/item 全部贴图到 assets/items + 2. 扫描 content 中 crafting 组件用到的物品 id (支持九格直填与 pattern 别名两种写法) + 3. 本地缺失的 id 从 Minecraft wiki 自动获取(自动挑选最新版本渲染) + 4. 仍找不到 -> 列出缺失物品并以退出码 1 结束 + +用法: + python3 scripts/sync-items.py [-f] +""" + +import argparse +import json +import re +import sys +import time +import urllib.parse +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +ITEMS_DIR = ROOT / "assets" / "items" +CONTENT_DIR = ROOT / "content" +UA = "sfcraft-docs-sync/1.0 (https://github.com/saltedfishclub/sfcraft-docs)" + +SF_OWNER = "saltedfishclub" +SF_REPO = "sfcraft" +SF_REF = "rev/26.2" +SF_DIR = "src/main/resources/assets/sfcraft/textures/item" +SF_LIST_URL = ( + f"https://api.github.com/repos/{SF_OWNER}/{SF_REPO}/contents/{SF_DIR}?ref={SF_REF}" +) +WIKI_API = "https://zh.minecraft.wiki/api.php" +WIKI_IMG = "https://zh.minecraft.wiki/images/" + +SHORTCODE_RE = re.compile(r"\{\{<\s*crafting\b(.*?)\}\}", re.S) +PAIR_RE = re.compile(r'(\w+)="([^"]*)"') +SLOT_KEYS = ["a1", "a2", "a3", "b1", "b2", "b3", "c1", "c2", "c3", "out"] + +CONNECTORS = {"and", "of", "the", "for", "with", "to", "from", "by", "in", "on", "at"} +ALIASES = { + "tnt": "TNT", + "tnt_minecart": "TNT_Minecart", + "dragon_breath": "Dragon's_Breath", + "slime_ball": "Slime", + "snow_golem": "Snow_Golem", + "ocelot": "Ocelot", +} + + +def http_get_json(url, retries=2): + last = None + for i in range(retries + 1): + try: + req = urllib.request.Request(url, headers={"User-Agent": UA}) + with urllib.request.urlopen(req, timeout=30) as resp: + return json.load(resp) + except Exception as e: # noqa: BLE001 + last = e + if i < retries: + time.sleep(0.8) + raise last + + +def http_download(url, target): + req = urllib.request.Request(url, headers={"User-Agent": UA}) + with urllib.request.urlopen(req, timeout=60) as resp, open(target, "wb") as f: + f.write(resp.read()) + with open(target, "rb") as f: + if f.read(4) != b"\x89PNG": + raise ValueError("下载内容不是有效 PNG") + + +def add_item_id(ids, raw): + v = raw.strip() + if not v: + return + item = v.split(":", 1)[0].strip() + if item: + ids.add(item) + + +def scan_item_ids(): + """扫描 content 下所有 crafting 组件, 收集物品 id。""" + ids = set() + for path in sorted(CONTENT_DIR.rglob("*.md")): + try: + text = path.read_text(encoding="utf-8") + except OSError: + continue + for m in SHORTCODE_RE.finditer(text): + params = dict(PAIR_RE.findall(m.group(1))) + for k in SLOT_KEYS: + if k in params: + add_item_id(ids, params[k]) + if "pattern" in params: + pat = re.sub(r"\\n|;|\r?\n", " ", params["pattern"]) + for tok in pat.split(): + if tok == ".": + continue + if tok in params: + add_item_id(ids, params[tok]) + elif re.fullmatch(r"[a-z0-9_]+", tok): + ids.add(tok) + return ids + + +def name_variants(item_id): + """由物品 id 生成可能的 wiki 英文名(含特殊命名变体与别名)。""" + words = [w for w in item_id.split("_") if w] + title = "_".join(w[0].upper() + w[1:] for w in words) + mixed = "_".join(w if w in CONNECTORS else w[0].upper() + w[1:] for w in words) + variants = [title] + if mixed != title: + variants.append(mixed) + if len(item_id) <= 4 and not re.search(r"[aeiou]", item_id): + variants.append(item_id.upper()) + one_word = "".join(w[0].upper() + w[1:] for w in words) + if one_word != title: + variants.append(one_word) + if item_id in ALIASES: + variants.insert(0, ALIASES[item_id]) + seen = set() + out = [] + for v in variants: + if v not in seen: + seen.add(v) + out.append(v) + return out + + +def wiki_candidates(name): + """查询 wiki 图片列表, 返回符合 <英文名>_JE..[_BE..].png 规则的文件名。""" + regex = re.compile( + r"^" + re.escape(name) + r"(_\(item\))?_JE\d+(\.\d+)*(_BE\d+)?\.png$" + ) + urls = [ + WIKI_API + + "?action=query&list=allimages&aiprefix=" + + urllib.parse.quote(name + "_") + + "&ailimit=500&format=json", + WIKI_API + + "?action=query&list=allimages&aiprefix=" + + urllib.parse.quote(name + "_JE") + + "&ailimit=200&format=json", + ] + seen = set() + all_names = [] + for url in urls: + try: + data = http_get_json(url) + for item in data.get("query", {}).get("allimages", []) or []: + nm = item["name"] + if nm not in seen: + seen.add(nm) + all_names.append(nm) + except Exception as e: # noqa: BLE001 + print(f" [警告] wiki 查询失败 ({name}): {e}", file=sys.stderr) + time.sleep(0.25) + return [n for n in all_names if regex.match(n)] + + +def select_best(name, candidates): + """挑选最新版本: 优先 (item) 物品图标, 再比 JE / BE 版本号。""" + regex = re.compile( + r"^" + re.escape(name) + r"(_\(item\))?_JE(\d+)(\.\d+)*(_BE(\d+))?\.png$" + ) + best = None + best_key = None + for cand in candidates: + m = regex.match(cand) + if not m: + continue + key = ( + 1 if m.group(1) else 0, + int(m.group(2)), + int(m.group(5)) if m.group(5) else -1, + ) + if best_key is None or key > best_key: + best, best_key = cand, key + return best + + +def main(): + ap = argparse.ArgumentParser(description="SFCraft 物品贴图同步") + ap.add_argument("-f", "--force", action="store_true", + help="强制重新下载 sfcraft 自定义贴图") + args = ap.parse_args() + + ITEMS_DIR.mkdir(parents=True, exist_ok=True) + + # 1. sfcraft 自定义贴图 + print("\n== 1/4 同步 sfcraft 自定义贴图") + sf_count = sf_new = 0 + try: + listing = http_get_json(SF_LIST_URL) + for f in listing: + target = ITEMS_DIR / f["name"] + need = ( + args.force + or not target.exists() + or target.stat().st_size != int(f["size"]) + ) + if need: + http_download(f["download_url"], target) + sf_new += 1 + sf_count += 1 + print(f" {sf_count} 个文件, 本次更新 {sf_new} 个") + except Exception as e: # noqa: BLE001 + print(f" [警告] 无法访问 GitHub, 跳过 sfcraft 贴图同步: {e}", file=sys.stderr) + + # 2. 扫描组件物品 id + print("\n== 2/4 扫描组件物品 id") + ids = scan_item_ids() + all_ids = sorted(ids) + need = [i for i in all_ids if not (ITEMS_DIR / f"{i}.png").exists()] + print(f" 使用 {len(all_ids)} 个物品 id, 本地已有 {len(all_ids) - len(need)} 个") + + # 3. 从 Minecraft wiki 获取缺失贴图 + print("\n== 3/4 从 Minecraft wiki 获取缺失贴图") + failed = [] + for item_id in need: + file = None + for name in name_variants(item_id): + cands = wiki_candidates(name) + if cands: + file = select_best(name, cands) + break + if file: + try: + http_download(WIKI_IMG + urllib.parse.quote(file), ITEMS_DIR / f"{item_id}.png") + print(f" ok {item_id} <- {file}") + continue + except Exception as e: # noqa: BLE001 + print(f" fail {item_id} 下载失败: {e}", file=sys.stderr) + else: + print(f" fail {item_id} 在 wiki 上未找到", file=sys.stderr) + failed.append(item_id) + + # 4. 汇总错误 + if failed: + print("\n[错误] 以下物品贴图无法自动获取:", file=sys.stderr) + for i in sorted(failed): + print(f" - {i}", file=sys.stderr) + print( + "\n可能原因:\n" + " 1. 物品 id 拼写错误(组件中填写的是 assets/items 下的文件名)\n" + " 2. 该物品是 sfcraft 自定义物品, 但 textures/item 目录中没有同名贴图\n" + " 3. 原版物品在 Minecraft wiki 上的命名特殊, 未被自动规则覆盖\n" + "\n解决办法:\n" + " - 将贴图手动放入 assets/items/.png 后重新运行\n" + " - 修正组件中的物品 id\n" + " - 若是 wiki 命名特例, 可在 sync-items.py 的 ALIASES 表中补充", + file=sys.stderr, + ) + sys.exit(1) + + print("\n全部贴图就绪") + + +if __name__ == "__main__": + main() diff --git a/scripts/sync-items.sh b/scripts/sync-items.sh new file mode 100644 index 0000000..f8e0800 --- /dev/null +++ b/scripts/sync-items.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# SFCraft 文档 · 物品贴图自动同步 (Linux / macOS / Unix) +# 用法: ./scripts/sync-items.sh [-f] +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +PY="" +if command -v python3 >/dev/null 2>&1; then + PY="python3" +elif command -v python >/dev/null 2>&1; then + PY="python" +else + echo "[错误] 未找到 python3 / python, 请先安装 Python 3。" >&2 + exit 1 +fi + +exec "$PY" "$DIR/sync-items.py" "$@"