diff --git a/.gitignore b/.gitignore index d59b8ba..f0abb8b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,3 @@ public themes/hugo-book/** resources/_gen *.log -assets/items/*.png -__pycache__/ diff --git a/Makefile b/Makefile deleted file mode 100644 index 2eea786..0000000 --- a/Makefile +++ /dev/null @@ -1,34 +0,0 @@ -# SFCraft 文档 · 构建 / 开发入口 (跨平台, 脚本为 Python 3) -# -# make build / make dev / make sync / make help -# -# 可附加 hugo 参数: make dev HUGO_ARGS="--port 1314" -# 可指定解释器: make PY=python3 build - -.PHONY: help sync build dev - -HUGO_ARGS ?= - -ifeq ($(OS),Windows_NT) - PY ?= python -else - PY ?= python3 -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: - $(PY) scripts/sync-items.py - -build: - $(PY) scripts/build.py $(HUGO_ARGS) - -dev: - $(PY) scripts/dev.py $(HUGO_ARGS) diff --git a/assets/css/sfcraft-crafting.css b/assets/css/sfcraft-crafting.css new file mode 100644 index 0000000..9f0cf57 --- /dev/null +++ b/assets/css/sfcraft-crafting.css @@ -0,0 +1,117 @@ +.sfc-crafting { + position: relative; + width: 100%; + max-width: 540px; + aspect-ratio: 320 / 160; + margin: 1rem 0 1.25rem; + background-repeat: no-repeat; + background-size: 100% 100%; + container-type: inline-size; +} + +.sfc-slot { + position: absolute; + display: flex; + align-items: center; + justify-content: center; +} + +.sfc-grid { + width: 10.625%; + height: 21.25%; +} + +.sfc-out { + width: 15.625%; + height: 31.25%; +} + +.sfc-item { + width: 88%; + height: 88%; + object-fit: contain; + image-rendering: pixelated; + filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.35)); +} + +.sfc-count { + position: absolute; + right: 5%; + bottom: 3%; + font-family: "Minecraft", ui-monospace, Consolas, monospace; + font-size: 12px; + font-size: 2.8cqw; + font-weight: 700; + line-height: 1; + color: #fff; + text-shadow: + 0 1px 0 #3f3f3f, + 1px 0 0 #3f3f3f, + 0 -1px 0 #3f3f3f, + -1px 0 0 #3f3f3f, + 0 2px 3px rgba(0, 0, 0, 0.55); +} + +/* 贴图缺失时的占位符 */ +.sfc-missing { + display: flex; + align-items: center; + justify-content: center; + width: 86%; + height: 86%; + font-size: 4cqw; + font-weight: 700; + color: rgba(0, 0, 0, 0.28); + border: 2px dashed rgba(0, 0, 0, 0.18); + border-radius: 4px; + background: repeating-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.22), + rgba(255, 255, 255, 0.22) 4px, + rgba(0, 0, 0, 0.04) 4px, + rgba(0, 0, 0, 0.04) 8px + ); +} + +/* 悬停显示物品 id */ +.sfc-slot[data-name]::after { + content: attr(data-name); + position: absolute; + bottom: calc(100% + 6px); + left: 50%; + transform: translateX(-50%); + padding: 3px 9px; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 5px; + background: rgba(18, 18, 18, 0.92); + color: #fff; + font-size: 12px; + white-space: nowrap; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.35); + opacity: 0; + pointer-events: none; + transition: opacity 0.12s ease; + z-index: 30; +} + +.sfc-slot[data-name]:hover::after { + opacity: 1; +} + +.sfc-caption { + margin: 0.35rem 0 0; + text-align: center; + font-size: 0.85rem; + color: var(--gray-600, #6c757d); +} + +.sfc-error { + max-width: 540px; + margin: 1rem 0; + padding: 0.6rem 1rem; + border: 1px solid #f5c2c7; + border-radius: 6px; + background: #f8d7da; + color: #842029; + font-size: 0.9rem; +} diff --git a/assets/items/README.md b/assets/items/README.md index 83e420f..220848b 100644 --- a/assets/items/README.md +++ b/assets/items/README.md @@ -1,26 +1,68 @@ -# 物品贴图库 +# 物品贴图 -本目录存放 `crafting` 合成组件使用的物品贴图(PNG)。 +`crafting` 合成组件里的物品贴图**不需要手工准备**:构建时由 Hugo 自己解析、下载并缓存。 +写 `a1="diamond"` 就会显示钻石,不用先把 `diamond.png` 放进仓库,也不用跑任何同步脚本。 -## 命名规则 +直接 `hugo` / `hugo server` 即可。 -一个物品对应一个文件:`<名称>.png`,文件名(不含 `.png`)就是引用时的名字。 +## 解析顺序 -例如放入 `diamond.png` 后,在合成组件中写 `a1="diamond"` 即可显示钻石。 +每个物品 id 依次尝试,命中即止(实现见 `layouts/_partials/sfcraft/item-texture.html`): -> 推荐使用 16×16(或 32×32)的 Minecraft 像素风贴图,`image-rendering: pixelated` 会保证缩放后依然清晰锐利。 +1. **本目录** `assets/items/.png` —— 手动放入的贴图,优先级最高; +2. **sfcraft 仓库** `textures/item/.png` —— 自定义物品,版本由 `hugo.yaml` + 中 `params.itemTextures.repo.baseURL` 的 ref 决定; +3. **Minecraft wiki** —— 原版物品,按 `<英文名>_JE<版本>[_BE<版本>].png` 的命名 + 列出候选并自动挑选最新版本渲染。 -## 自动同步(推荐) +无论来自哪一层,贴图最终都发布到 `/sfc/items/.png`, +所以把某个物品从 wiki 换成手动贴图不会改变页面里的 URL。 -构建 / 开发时由 `scripts/sync-items.py` 自动维护本目录: +## 缓存 -- sfcraft 自定义贴图(`amethyst_cauldron_blank`、`exp_totem`、`lunch_box`、`pearl_token` 等) - 从 [sfcraft 仓库](https://github.com/saltedfishclub/sfcraft/tree/rev/26.2/src/main/resources/assets/sfcraft/textures/item) - 自动下载,缺失或远端变更时更新; -- 原版物品(`diamond`、`stick` 等)在本地缺失时从 Minecraft wiki 自动获取; -- 手动放入的贴图不会被覆盖;找不到的贴图会报错并列出物品 id,阻止构建 / 开发。 +抓取结果写进 Hugo 的 `getresource` 文件缓存,`hugo.yaml` 里设为永不过期, +因此每个 URL 全局只下载一次: -请使用 `python scripts\build.py` / `python scripts\dev.py` 而非直接运行 `hugo`,详见 `scripts/README.md`。 +- 首次构建需要联网(本仓库当前的配方约需数秒); +- 之后的构建全部命中缓存,**离线也能完整构建**; +- 需要拉取上游更新(sfcraft 换了贴图、wiki 出了新版本渲染)时执行 `hugo --ignoreCache`。 + +缓存默认在 `hugo config | grep cachedir` 指向的目录。CI 里建议把它固定下来并缓存该目录: + +```bash +hugo --cacheDir "$PWD/.hugo-cache" +``` + +这样只有第一次构建需要访问 `raw.githubusercontent.com` 与 `zh.minecraft.wiki`。 +若 CI 完全不允许联网,把需要的贴图提交到本目录即可(第 1 层优先级最高)。 + +## 手动放入贴图 + +需要覆盖上游贴图,或某个物品自动解析不到时,把文件放到 `assets/items/.png`, +文件名(不含 `.png`)就是引用时的 id。这些文件会提交进仓库。 + +> 推荐 16×16 或 32×32 的像素风贴图,`image-rendering: pixelated` 会保证放大后依然锐利。 + +## 解析不到怎么办 + +构建会报错并指出页面与物品 id: + +``` +ERROR /vanilla/items/xxx 引用的物品 "diamnod" 没有对应贴图。请检查 id 拼写, 或把贴图放进 assets/items/diamnod.png +``` + +常见原因与处理: + +- **id 拼错** —— 改正 id; +- **wiki 上的英文名不符合自动推导规则**(如 `TNT`、`Dragon's_Breath`) + —— 在 `data/sfcraft/wiki_aliases.yaml` 里补一条 id → 英文名的映射; +- **确实没有现成贴图** —— 手动放进 `assets/items/`。 + +网络故障、被限流或上游异常时不会被当成「id 写错」:这类情况只告警并显示 `?` 占位符, +不阻断构建,方便离线时继续写文档。 + +想让缺失贴图也不阻断构建,可把 `hugo.yaml` 中 `params.itemTextures.onMissing` +从 `error` 改成 `warn` 或 `ignore`。 ## 组件用法 @@ -35,10 +77,9 @@ ``` - `a1`~`c3`:3×3 合成格的九个格子,`out`:输出格; -- 值 = 贴图文件名(不含 `.png`),**留空 / 省略 = 空格子**; +- 值 = 物品 id,**留空 / 省略 / `.` = 空格子**; - 需要显示堆叠数量时写 `名称:数量`,例如 `b2="stick:2"`; -- `caption` 可选,显示在组件下方居中说明文字; -- 未运行自动同步且贴图缺失时,格子内会显示一个 `?` 占位,方便发现写错的名字。 +- `caption` 可选,显示在组件下方居中说明文字。 ### pattern 别名模式(另一种写法) @@ -58,5 +99,5 @@ - `pattern` 共 9 个 token(3 行 × 3 列),**必须写在一行内**,行间用 `\n` 表示; - 每个 token 对应一个同名参数(`a`、`b`、`c`…),参数值就是物品 id; - token 也可以直接写物品 id:`pattern="diamond stick ."`; -- `.` 表示空格子; +- `.` 表示空格子(写在 pattern 里,或作为别名的值,都算空格子); - 同时给出 `pattern` 和 `a1`~`c3` 时,以 `pattern` 为准(`out` 两者通用)。 diff --git a/data/sfcraft/wiki_aliases.yaml b/data/sfcraft/wiki_aliases.yaml new file mode 100644 index 0000000..6d0e911 --- /dev/null +++ b/data/sfcraft/wiki_aliases.yaml @@ -0,0 +1,29 @@ +# Minecraft wiki 图片命名特例表 +# +# 原版物品贴图的英文名默认由物品 id 自动推导 (逐词首字母大写), +# 例如 diamond_pickaxe -> Diamond_Pickaxe。 +# +# 自动规则覆盖不到的命名写在下面, 左边是物品 id, 右边是 wiki 上的英文名。 +# 构建时报告某个原版物品"未找到", 但它在 wiki 上确实存在时, 在这里补一条即可。 + +aliases: + tnt: TNT + tnt_minecart: TNT_Minecart + dragon_breath: Dragon's_Breath + slime_ball: Slime + snow_golem: Snow_Golem + ocelot: Ocelot + +# 英文名中保持小写的连接词, 例如 flint_and_steel -> Flint_and_Steel +connectors: + - and + - of + - the + - for + - with + - to + - from + - by + - in + - on + - at diff --git a/hugo.yaml b/hugo.yaml index 0fa709f..6d99c37 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -4,12 +4,39 @@ defaultContentLanguage: zh title: SFCraft 文档 theme: hugo-book +module: + hugoVersion: + # layouts/_partials 与 layouts/_shortcodes 目录需要 0.146+, + # 贴图解析用到的 try 需要 0.141+, hugo.Data 需要 0.156+ + min: 0.156.0 + params: BookTheme: auto # 明/暗跟随系统 BookSection: '*' # 每个顶层 section 渲染为侧边栏一个栏目 BookToC: true BookSearch: true + # crafting 组件的物品贴图解析, 见 layouts/_partials/sfcraft/item-texture.html + itemTextures: + localDir: items # 手动放置贴图的目录 (assets//.png) + publishDir: sfc/items # 贴图发布到站点内的路径 + userAgent: sfcraft-docs (+https://github.com/saltedfishclub/sfcraft-docs) + onMissing: error # 贴图确实不存在时: error 阻断构建 / warn 仅告警 / ignore 静默 + repo: + enable: true + # sfcraft 自定义物品贴图; 换版本时改这里的 ref 即可 + baseURL: https://raw.githubusercontent.com/saltedfishclub/sfcraft/rev/26.2/src/main/resources/assets/sfcraft/textures/item/ + wiki: + enable: true + api: https://zh.minecraft.wiki/api.php + +# 远程贴图与 wiki 查询的抓取缓存: 永不过期, 落在 cacheDir 下。 +# 首次构建后即可离线构建; 需要拉取上游更新时执行 hugo --ignoreCache。 +caches: + getresource: + dir: :cacheDir/:project + maxAge: -1 + markup: goldmark: renderer: diff --git a/layouts/_partials/crafting-slot.html b/layouts/_partials/crafting-slot.html deleted file mode 100644 index 77a9604..0000000 --- a/layouts/_partials/crafting-slot.html +++ /dev/null @@ -1,29 +0,0 @@ -{{- $val := .val -}} -{{- $item := "" -}} -{{- $count := 0 -}} -{{- if $val -}} - {{- $parts := split $val ":" -}} - {{- $item = index $parts 0 -}} - {{- if gt (len $parts) 1 -}} - {{- $count = int (index $parts 1) -}} - {{- end -}} -{{- end -}} - -{{- $img := "" -}} -{{- if $item -}} - {{- $img = resources.Get (printf "items/%s.png" $item) -}} -{{- end -}} - -
- {{- if $img -}} - {{ $item }} - {{- if gt $count 1 -}} - {{ $count }} - {{- end -}} - {{- else if $item -}} - ? - {{- end -}} -
diff --git a/layouts/_partials/sfcraft/crafting-slot.html b/layouts/_partials/sfcraft/crafting-slot.html new file mode 100644 index 0000000..ba01f74 --- /dev/null +++ b/layouts/_partials/sfcraft/crafting-slot.html @@ -0,0 +1,53 @@ +{{- /* 合成表中的一个格子 + + 入参: + val 格子内容, "物品id" 或 "物品id:数量", 空字符串表示空格子 + cls "grid" (九宫格) 或 "out" (输出格) + x, y 在合成表底图上的定位 + page 所在页面, 仅用于报错定位 +*/ -}} + +{{- $val := .val -}} +{{- $item := "" -}} +{{- $count := 0 -}} +{{- if $val -}} + {{- $parts := split $val ":" -}} + {{- $item = index $parts 0 -}} + {{- if gt (len $parts) 1 -}} + {{- $count = int (index $parts 1) -}} + {{- end -}} +{{- end -}} + +{{- $img := false -}} +{{- if $item -}} + {{- /* 以 id 为 key, 同一物品每次构建只解析一次 */ -}} + {{- $texture := partialCached "sfcraft/item-texture.html" (dict "id" $item) $item -}} + {{- $img = $texture.image -}} + + {{- if ne $texture.status "ok" -}} + {{- $where := printf "%s 引用的物品 %q" .page.Path $item -}} + {{- $onMissing := (site.Params.itemTextures | default dict).onMissing | default "error" -}} + {{- if eq $texture.status "degraded" -}} + {{- /* 离线 / 限流 / 上游异常: 结论不可靠, 只告警, 不阻断构建 */ -}} + {{- warnf "%s 贴图解析失败, 已降级为占位符: %s" $where $texture.detail -}} + {{- else if eq $onMissing "error" -}} + {{- errorf "%s 没有对应贴图。请检查 id 拼写, 或把贴图放进 assets/items/%s.png" $where $item -}} + {{- else if eq $onMissing "warn" -}} + {{- warnf "%s 没有对应贴图" $where -}} + {{- end -}} + {{- end -}} +{{- end -}} + +
+ {{- if $img -}} + {{ $item }} + {{- if gt $count 1 -}} + {{ $count }} + {{- end -}} + {{- else if $item -}} + ? + {{- end -}} +
diff --git a/layouts/_partials/sfcraft/item-texture.html b/layouts/_partials/sfcraft/item-texture.html new file mode 100644 index 0000000..2e564f2 --- /dev/null +++ b/layouts/_partials/sfcraft/item-texture.html @@ -0,0 +1,93 @@ +{{- /* SFCraft 物品贴图解析器 + + 把物品 id 解析成一个图片 Resource, 全程由 Hugo 完成, 不需要外部同步脚本。 + + 查找顺序 (命中即止): + 1. assets/items/.png 手动放入的贴图, 优先级最高 + 2. sfcraft 仓库 textures/item/.png 自定义物品 + 3. Minecraft wiki 原版物品, 自动挑选最新版本渲染 + + 抓取结果由 Hugo 的 getresource 文件缓存持久化 (hugo.yaml 中 maxAge: -1), + 因此每个 URL 全局只下载一次; 后续构建离线也能完成。 + + 入参: + id 物品 id (assets/items 下的文件名, 不含 .png) + + 返回 dict: + image 图片 Resource, 未解析出来时为 nil + source "local" / "sfcraft" / "wiki", 未解析出来时为 "" + status "ok" 已解析 + "missing" 各来源都明确不存在该贴图 (id 很可能写错了) + "degraded" 有来源访问失败 (离线 / 限流 / 服务异常), 结论不可靠 + detail status 非 ok 时的诊断信息 + + 调用方应使用 partialCached 并以 id 作为 key, 使同一 id 每次构建只解析一次。 +*/ -}} + +{{- $id := .id -}} +{{- $cfg := site.Params.itemTextures | default dict -}} +{{- $repo := $cfg.repo | default dict -}} +{{- $wiki := $cfg.wiki | default dict -}} +{{- $localDir := $cfg.localDir | default "items" -}} +{{- $publishDir := $cfg.publishDir | default "sfc/items" -}} +{{- $opts := dict "headers" (dict "User-Agent" ($cfg.userAgent | default "sfcraft-docs")) -}} + +{{- $image := false -}} +{{- $source := "" -}} +{{- $notes := slice -}} + +{{- /* 1. 本地贴图 (手动放入的永远优先, 便于覆盖上游) */ -}} +{{- with resources.Get (printf "%s/%s.png" $localDir $id) -}} + {{- $image = . -}} + {{- $source = "local" -}} +{{- end -}} + +{{- /* 2. sfcraft 仓库的自定义物品贴图 + 404 时 GetRemote 返回 nil 且不报错, 正好作为"该物品不是自定义物品"的信号, + 可以直接落到下一个来源; 其余状态码 (403/429/5xx) 与网络故障会置 .Err。 */ -}} +{{- if and (not $image) (ne $repo.enable false) $repo.baseURL -}} + {{- $url := printf "%s%s.png" $repo.baseURL $id -}} + {{- with try (resources.GetRemote $url $opts) -}} + {{- with .Err -}} + {{- $notes = $notes | append (printf "sfcraft 仓库访问失败: %s" (replaceRE `^.*error calling GetRemote: ` "" (printf "%s" .))) -}} + {{- else with .Value -}} + {{- $image = . -}} + {{- $source = "sfcraft" -}} + {{- end -}} + {{- end -}} +{{- end -}} + +{{- /* 3. Minecraft wiki 的原版物品渲染图 */ -}} +{{- if and (not $image) (ne $wiki.enable false) $wiki.api -}} + {{- $lookup := partialCached "sfcraft/wiki-lookup.html" + (dict "id" $id "api" $wiki.api "opts" $opts) $id -}} + {{- with $lookup.notes -}}{{- $notes = $notes | append . -}}{{- end -}} + {{- with $lookup.url -}} + {{- with try (resources.GetRemote . $opts) -}} + {{- with .Err -}} + {{- $notes = $notes | append (printf "wiki 贴图下载失败 (%s): %s" $lookup.name (replaceRE `^.*error calling GetRemote: ` "" (printf "%s" .))) -}} + {{- else with .Value -}} + {{- $image = . -}} + {{- $source = "wiki" -}} + {{- else -}} + {{- $notes = $notes | append (printf "wiki 贴图不存在 (%s)" $lookup.name) -}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- end -}} + +{{- /* 统一发布路径, 使贴图来源变化 (wiki -> 手动放入) 时页面上的 URL 不变 */ -}} +{{- $status := "missing" -}} +{{- if $image -}} + {{- $status = "ok" -}} + {{- $image = resources.Copy (printf "%s/%s.png" $publishDir $id) $image -}} +{{- else if $notes -}} + {{- /* 有来源没能给出确定答案, 不能断言 id 写错了 */ -}} + {{- $status = "degraded" -}} +{{- end -}} + +{{- return dict + "image" (cond (eq $status "ok") $image nil) + "source" $source + "status" $status + "detail" (delimit $notes "; ") -}} diff --git a/layouts/_partials/sfcraft/wiki-lookup.html b/layouts/_partials/sfcraft/wiki-lookup.html new file mode 100644 index 0000000..31ac03d --- /dev/null +++ b/layouts/_partials/sfcraft/wiki-lookup.html @@ -0,0 +1,116 @@ +{{- /* 在 Minecraft wiki 上查出某个物品最新版本渲染图的 URL + + wiki 的物品图片按 <英文名>_JE<版本>[_BE<版本>].png 命名, 同一物品会有多个历史版本, + 这里用 allimages API 按前缀列出候选, 再挑版本号最大的一张。 + + 英文名由物品 id 自动推导 (逐词首字母大写), 推不出来的特例写在 + data/sfcraft/wiki_aliases.yaml 里。 + + 入参: + id 物品 id + api wiki api.php 的地址 + opts 传给 resources.GetRemote 的选项 + + 返回 dict: + url 图片 URL, 没找到时为 "" + name 命中的 wiki 英文名 + notes 访问失败等诊断信息 (字符串, 无异常时为 "") +*/ -}} + +{{- $id := .id -}} +{{- $api := .api -}} +{{- $opts := .opts -}} + +{{- $data := (hugo.Data.sfcraft | default dict).wiki_aliases | default dict -}} +{{- $aliases := $data.aliases | default dict -}} +{{- $connectors := $data.connectors | default slice -}} + +{{- /* 生成英文名候选 */ -}} +{{- $words := slice -}} +{{- range split $id "_" -}} + {{- if ne . "" -}}{{- $words = $words | append . -}}{{- end -}} +{{- end -}} +{{- $titleWords := slice -}} +{{- $mixedWords := slice -}} +{{- range $words -}} + {{- $upper := strings.FirstUpper . -}} + {{- $titleWords = $titleWords | append $upper -}} + {{- $mixedWords = $mixedWords | append (cond (in $connectors .) . $upper) -}} +{{- end -}} + +{{- $variants := slice -}} +{{- /* 特例表优先 */ -}} +{{- with index $aliases $id -}}{{- $variants = $variants | append . -}}{{- end -}} +{{- /* diamond_pickaxe -> Diamond_Pickaxe */ -}} +{{- $variants = $variants | append (delimit $titleWords "_") -}} +{{- /* flint_and_steel -> Flint_and_Steel */ -}} +{{- $variants = $variants | append (delimit $mixedWords "_") -}} +{{- /* tnt -> TNT (短且无元音的 id 多为缩写) */ -}} +{{- if and (le (len $id) 4) (not (findRE "[aeiou]" $id)) -}} + {{- $variants = $variants | append (upper $id) -}} +{{- end -}} +{{- /* end_rod -> EndRod */ -}} +{{- $variants = $variants | append (delimit $titleWords "") -}} +{{- $variants = $variants | uniq -}} + +{{- $bestURL := "" -}} +{{- $bestName := "" -}} +{{- $bestScore := -1 -}} +{{- $notes := slice -}} + +{{- range $variants -}} + {{- $name := . -}} + {{- if not $bestURL -}} + {{- $query := querify + "action" "query" + "list" "allimages" + "aiprefix" (printf "%s_" $name) + "ailimit" 500 + "format" "json" -}} + {{- $url := printf "%s?%s" $api $query -}} + {{- $images := slice -}} + {{- with try (resources.GetRemote $url $opts) -}} + {{- with .Err -}} + {{- $notes = $notes | append (printf "wiki 查询失败 (%s): %s" $name (replaceRE `^.*error calling GetRemote: ` "" (printf "%s" .))) -}} + {{- else with .Value -}} + {{- $images = (index (. | transform.Unmarshal) "query" "allimages") | default slice -}} + {{- else -}} + {{- /* API 对合法查询总会返回 200, 拿到 nil 说明请求没真正到达 */ -}} + {{- $notes = $notes | append (printf "wiki 查询无响应 (%s)" $name) -}} + {{- end -}} + {{- end -}} + + {{- /* 从候选里挑版本最新的一张: 优先 _(item) 物品图标, 再比 JE 版本, 最后比 BE 版本 */ -}} + {{- range $images -}} + {{- $prefixItem := printf "%s_(item)_JE" $name -}} + {{- $prefixPlain := printf "%s_JE" $name -}} + {{- $rest := "" -}} + {{- $isItem := 0 -}} + {{- if hasPrefix .name $prefixItem -}} + {{- $rest = strings.TrimPrefix $prefixItem .name -}} + {{- $isItem = 1 -}} + {{- else if hasPrefix .name $prefixPlain -}} + {{- $rest = strings.TrimPrefix $prefixPlain .name -}} + {{- end -}} + {{- /* $rest 形如 "2_BE2.png" / "1.20.png" */ -}} + {{- if and $rest (findRE `^[0-9]+(\.[0-9]+)*(_BE[0-9]+)?\.png$` $rest) -}} + {{- $je := int (index (findRE `^[0-9]+` $rest) 0) -}} + {{- $be := -1 -}} + {{- with findRE `_BE[0-9]+` $rest -}} + {{- $be = int (strings.TrimPrefix "_BE" (index . 0)) -}} + {{- end -}} + {{- $score := add (mul $isItem 1000000) (add (mul $je 1000) (add $be 1)) -}} + {{- if gt $score $bestScore -}} + {{- $bestScore = $score -}} + {{- $bestURL = .url -}} + {{- $bestName = .name -}} + {{- end -}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- end -}} + +{{- return dict + "url" $bestURL + "name" $bestName + "notes" (delimit $notes "; ") -}} diff --git a/layouts/_shortcodes/crafting.html b/layouts/_shortcodes/crafting.html index 94384b2..be29b90 100644 --- a/layouts/_shortcodes/crafting.html +++ b/layouts/_shortcodes/crafting.html @@ -1,4 +1,5 @@ {{- /* SFCraft 合成配方组件 + 两种用法: 1) 九格直填: a1..c3 为 3x3 合成格, out 为输出格 2) pattern 别名模式(提供 pattern 时优先): @@ -6,9 +7,9 @@ a="diamond" b="stick" c="" 每个 token 对应一个同名参数, 值为物品 id token 也可以直接写物品 id, 如 pattern="diamond stick ." "." 表示空格子 - - 值为 assets/items/ 下贴图的文件名(不含 .png), 留空表示空格子 - - 名称后可加 ":数量" 显示堆叠数, 例如 a1="stick:2" - - 引用的贴图不存在时, 格子内显示 ? 占位提示 + + - 值为物品 id, 留空表示空格子; 名称后可加 ":数量" 显示堆叠数, 例如 a1="stick:2" + - 贴图由 partials/sfcraft/item-texture.html 自动解析并缓存, 无需手工准备 - caption 参数可选, 显示在组件下方 */ -}} @@ -36,100 +37,12 @@ {{- end -}} {{- end -}} -{{- $cssOnce := .Page.Store.Get "sfc_crafting_css" -}} -{{- if not $cssOnce -}} +{{- /* 每页只输出一次样式 */ -}} +{{- if not (.Page.Store.Get "sfc_crafting_css") -}} {{- .Page.Store.Set "sfc_crafting_css" true -}} - + {{- with resources.Get "css/sfcraft-crafting.css" -}} + + {{- end -}} {{- end -}} {{- if and $pattern (ne (len $tokens) 9) -}} @@ -156,12 +69,15 @@ {{- else -}} {{- $val = trim ($ctx.Get $slot.key) " " -}} {{- end -}} - {{- partial "crafting-slot.html" + {{- /* "." 表示空格子, 无论它写在 pattern 里还是写成别名的值 */ -}} + {{- if eq $val "." -}}{{- $val = "" -}}{{- end -}} + {{- partial "sfcraft/crafting-slot.html" (dict - "val" $val - "cls" $slot.cls - "x" $slot.x - "y" $slot.y + "val" $val + "cls" $slot.cls + "x" $slot.x + "y" $slot.y + "page" $ctx.Page ) -}} {{- end -}} {{- with $ctx.Get "caption" -}} diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index a809637..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# 构建 / 开发脚本 - -## 用途 - -`scripts/` 下全部为 Python 3 脚本(无其他语言): - -- `sync-items.py` —— 物品贴图自动同步(核心); -- `build.py` —— 同步贴图 + hugo 构建; -- `dev.py` —— 同步贴图 + hugo 开发服务器。 - -同步流程: - -1. 下载 [sfcraft 仓库](https://github.com/saltedfishclub/sfcraft/tree/rev/26.2/src/main/resources/assets/sfcraft/textures/item) 的 - 全部自定义贴图到 `assets/items/`; -2. 扫描 `content/` 中所有 `crafting` 组件用到的物品 id(支持九格直填与 pattern 别名两种写法); -3. 本地缺失的原版物品从 Minecraft wiki 自动获取(自动挑选最新版本渲染); -4. 有任何物品无法找到时, 打印缺失清单并以非零码退出, 阻断构建 / 开发启动。 - -## 使用 - -Windows: - -```powershell -python scripts\build.py # 同步贴图 + hugo 构建 -python scripts\dev.py # 同步贴图 + hugo server -python scripts\dev.py --no-sync # 仅启动开发服务器, 不重新同步 -python scripts\sync-items.py -f # 仅同步贴图(强制刷新 sfcraft 贴图) -``` - -Linux / macOS: - -```bash -python3 scripts/build.py -python3 scripts/dev.py -python3 scripts/dev.py --no-sync -``` - -也可以用 Makefile(Windows / Unix 通用): - -```bash -make build # 同步贴图 + hugo 构建 -make dev # 同步贴图 + hugo server -make sync # 仅同步贴图 -make dev HUGO_ARGS="--port 1314" -make PY=python build # 指定 Python 解释器 -``` - -脚本按 `$HUGO` 环境变量、PATH、winget 安装目录的顺序查找 hugo。 - -## 组件写法 - -`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.py` 中的变体生成与别名表处理。 - -## 网络要求 - -需要能访问 `api.github.com`、`raw.githubusercontent.com` 与 `zh.minecraft.wiki`。 -若 CI 或离线环境不允许联网, 建议把 `assets/items/` 下的贴图提交进仓库。 diff --git a/scripts/build.py b/scripts/build.py deleted file mode 100644 index 79f961b..0000000 --- a/scripts/build.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -"""SFCraft 文档构建入口: 先同步物品贴图, 再执行 hugo 构建。 - -贴图同步失败(有物品找不到)时终止构建。 - -用法: - python scripts/build.py [hugo 参数...] -""" - -import os -import subprocess -import sys -from pathlib import Path - -SCRIPTS = Path(__file__).resolve().parent - - -def find_hugo(): - """按 HUGO 环境变量、PATH、winget 安装目录依次查找 hugo。""" - env = os.environ.get("HUGO") - if env and Path(env).is_file(): - return env - - name = "hugo.exe" if os.name == "nt" else "hugo" - for d in os.environ.get("PATH", "").split(os.pathsep): - if d: - cand = Path(d) / name - if cand.is_file(): - return str(cand) - - local = os.environ.get("LOCALAPPDATA") - if local: - base = Path(local) / "Microsoft" / "WinGet" / "Packages" - if base.is_dir(): - for p in sorted(base.glob("*/hugo.exe")): - return str(p) - return None - - -def main(): - print("\n[1/2] 同步物品贴图") - r = subprocess.run([sys.executable, str(SCRIPTS / "sync-items.py")]) - if r.returncode != 0: - print("[构建终止] 物品贴图同步失败, 未执行 hugo 构建。", file=sys.stderr) - sys.exit(r.returncode) - - hugo = find_hugo() - if not hugo: - print( - "[错误] 未找到 hugo, 请安装 Hugo、将其加入 PATH, 或设置环境变量 HUGO。", - file=sys.stderr, - ) - sys.exit(1) - - print("\n[2/2] hugo 构建") - sys.exit(subprocess.run([hugo, *sys.argv[1:]]).returncode) - - -if __name__ == "__main__": - main() diff --git a/scripts/dev.py b/scripts/dev.py deleted file mode 100644 index fb6a73e..0000000 --- a/scripts/dev.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -"""SFCraft 文档开发服务器入口: 先同步物品贴图, 再启动 hugo server。 - -贴图同步失败(有物品找不到)时终止启动。 - -用法: - python scripts/dev.py [--no-sync] [hugo server 参数...] -""" - -import argparse -import os -import subprocess -import sys -from pathlib import Path - -SCRIPTS = Path(__file__).resolve().parent - - -def find_hugo(): - """按 HUGO 环境变量、PATH、winget 安装目录依次查找 hugo。""" - env = os.environ.get("HUGO") - if env and Path(env).is_file(): - return env - - name = "hugo.exe" if os.name == "nt" else "hugo" - for d in os.environ.get("PATH", "").split(os.pathsep): - if d: - cand = Path(d) / name - if cand.is_file(): - return str(cand) - - local = os.environ.get("LOCALAPPDATA") - if local: - base = Path(local) / "Microsoft" / "WinGet" / "Packages" - if base.is_dir(): - for p in sorted(base.glob("*/hugo.exe")): - return str(p) - return None - - -def main(): - parser = argparse.ArgumentParser(description="SFCraft 开发服务器") - parser.add_argument( - "--no-sync", "-NoSync", action="store_true", - help="跳过贴图同步", - ) - args, rest = parser.parse_known_args() - - if not args.no_sync: - print("\n[1/2] 同步物品贴图") - r = subprocess.run([sys.executable, str(SCRIPTS / "sync-items.py")]) - if r.returncode != 0: - print("[启动终止] 物品贴图同步失败, 未启动 hugo server。", file=sys.stderr) - sys.exit(r.returncode) - else: - print("[跳过] 已指定 --no-sync, 不执行贴图同步") - - hugo = find_hugo() - if not hugo: - print( - "[错误] 未找到 hugo, 请安装 Hugo、将其加入 PATH, 或设置环境变量 HUGO。", - file=sys.stderr, - ) - sys.exit(1) - - print("\n[2/2] 启动 hugo server") - sys.exit(subprocess.run([hugo, "server", *rest]).returncode) - - -if __name__ == "__main__": - main() diff --git a/scripts/sync-items.py b/scripts/sync-items.py deleted file mode 100644 index 411e6d7..0000000 --- a/scripts/sync-items.py +++ /dev/null @@ -1,264 +0,0 @@ -#!/usr/bin/env python3 -"""SFCraft 文档 · 物品贴图自动同步 (Python 3) - - 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: str, retries: int = 2) -> dict: - 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: # noqa: BLE001 - if i < retries: - time.sleep(0.8) - else: - raise - # 理论上不可达(retries 为负时 range 为空才可能走到), 兜底避免隐式返回 None - raise RuntimeError(f"请求失败(无可用异常信息): {url}") - - -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[str] = 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()