feat: add Linux/Unix scripts and Makefile entry

This commit is contained in:
FlipWind
2026-08-09 00:12:13 +08:00
parent cd19dddad1
commit b57726f434
6 changed files with 409 additions and 1 deletions
+39
View File
@@ -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)
+31 -1
View File
@@ -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` 中的变体生成与别名表处理。
## 网络要求
+20
View File
@@ -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 "$@"
+37
View File
@@ -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[@]}"
+264
View File
@@ -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/<id>.png 后重新运行\n"
" - 修正组件中的物品 id\n"
" - 若是 wiki 命名特例, 可在 sync-items.py 的 ALIASES 表中补充",
file=sys.stderr,
)
sys.exit(1)
print("\n全部贴图就绪")
if __name__ == "__main__":
main()
+18
View File
@@ -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" "$@"