feat: add Linux/Unix scripts and Makefile entry
This commit is contained in:
@@ -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
@@ -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` 中的变体生成与别名表处理。
|
||||
|
||||
## 网络要求
|
||||
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -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[@]}"
|
||||
@@ -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()
|
||||
@@ -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" "$@"
|
||||
Reference in New Issue
Block a user