diff --git a/.gitignore b/.gitignore index cb77605..56d7dc2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ public themes/hugo-book/** resources/_gen +*.log +assets/items/*.png \ No newline at end of file diff --git a/assets/items/README.md b/assets/items/README.md new file mode 100644 index 0000000..ec2f838 --- /dev/null +++ b/assets/items/README.md @@ -0,0 +1,41 @@ +# 物品贴图库 + +本目录存放 `crafting` 合成组件使用的物品贴图(PNG)。 + +## 命名规则 + +一个物品对应一个文件:`<名称>.png`,文件名(不含 `.png`)就是引用时的名字。 + +例如放入 `diamond.png` 后,在合成组件中写 `a1="diamond"` 即可显示钻石。 + +> 推荐使用 16×16(或 32×32)的 Minecraft 像素风贴图,`image-rendering: pixelated` 会保证缩放后依然清晰锐利。 + +## 自动同步(推荐) + +构建 / 开发时由 `scripts/sync-items.ps1` 自动维护本目录: + +- 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,阻止构建 / 开发。 + +请使用 `scripts\build.ps1` / `scripts\dev.ps1` 而非直接运行 `hugo`,详见 `scripts/README.md`。 + +## 组件用法 + +```markdown +{{< crafting + a1="diamond" a2="diamond" a3="diamond" + b1="" b2="stick" b3="" + c1="" c2="stick" c3="" + out="diamond_pickaxe" + caption="钻石镐(可选)" +>}} +``` + +- `a1`~`c3`:3×3 合成格的九个格子,`out`:输出格; +- 值 = 贴图文件名(不含 `.png`),**留空 / 省略 = 空格子**; +- 需要显示堆叠数量时写 `名称:数量`,例如 `b2="stick:2"`; +- `caption` 可选,显示在组件下方居中说明文字; +- 未运行自动同步且贴图缺失时,格子内会显示一个 `?` 占位,方便发现写错的名字。 diff --git a/assets/table.png b/assets/table.png new file mode 100644 index 0000000..aba9000 Binary files /dev/null and b/assets/table.png differ diff --git a/layouts/_partials/crafting-slot.html b/layouts/_partials/crafting-slot.html new file mode 100644 index 0000000..37e6b8b --- /dev/null +++ b/layouts/_partials/crafting-slot.html @@ -0,0 +1,29 @@ +{{- $val := trim (.ctx.Get .key) " " -}} +{{- $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/_shortcodes/crafting.html b/layouts/_shortcodes/crafting.html new file mode 100644 index 0000000..687539f --- /dev/null +++ b/layouts/_shortcodes/crafting.html @@ -0,0 +1,124 @@ +{{- /* SFCraft 合成配方组件 + - 调用: 页面内写 shortcode, 名为 crafting + - a1..c3 为 3x3 合成格, out 为输出格 + - 值为 assets/items/ 下贴图的文件名(不含 .png), 留空表示空格子 + - 名称后可加 ":数量" 显示堆叠数, 例如 a1="stick:2" + - 引用的贴图不存在时, 格子内显示 ? 占位提示 + - caption 参数可选, 显示在组件下方 +*/ -}} + +{{- $table := resources.Get "table.png" -}} +{{- $slots := slice + (dict "key" "a1" "cls" "grid" "x" "13.75%" "y" "20%") + (dict "key" "a2" "cls" "grid" "x" "25%" "y" "20%") + (dict "key" "a3" "cls" "grid" "x" "36.25%" "y" "20%") + (dict "key" "b1" "cls" "grid" "x" "13.75%" "y" "42.5%") + (dict "key" "b2" "cls" "grid" "x" "25%" "y" "42.5%") + (dict "key" "b3" "cls" "grid" "x" "36.25%" "y" "42.5%") + (dict "key" "c1" "cls" "grid" "x" "13.75%" "y" "65%") + (dict "key" "c2" "cls" "grid" "x" "25%" "y" "65%") + (dict "key" "c3" "cls" "grid" "x" "36.25%" "y" "65%") + (dict "key" "out" "cls" "out" "x" "70%" "y" "37.5%") +-}} + +{{- $cssOnce := .Page.Store.Get "sfc_crafting_css" -}} +{{- if not $cssOnce -}} + {{- .Page.Store.Set "sfc_crafting_css" true -}} + +{{- end -}} + +
+ {{- range $slots -}} + {{- partial "crafting-slot.html" + (dict + "ctx" $ + "key" .key + "cls" .cls + "x" .x + "y" .y + ) -}} + {{- end -}} + {{- with .Get "caption" -}} +
{{ . }}
+ {{- end -}} +
diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..e2c40d9 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,40 @@ +# 构建 / 开发脚本 + +## 用途 + +`sync-items.ps1` 会在构建和开发前自动保证物品贴图齐全: + +1. 下载 [sfcraft 仓库](https://github.com/saltedfishclub/sfcraft/tree/rev/26.2/src/main/resources/assets/sfcraft/textures/item) 的 + 全部自定义贴图到 `assets/items/`; +2. 扫描 `content/` 中所有 `crafting` 组件用到的物品 id; +3. 本地缺失的原版物品从 Minecraft wiki 自动获取(自动挑选最新版本渲染); +4. 有任何物品无法找到时, 打印缺失清单并以非零码退出, 阻断构建 / 开发启动。 + +## 使用 + +```powershell +.\scripts\build.ps1 # 同步贴图 + hugo 构建 +.\scripts\dev.ps1 # 同步贴图 + hugo server +.\scripts\dev.ps1 -NoSync # 仅启动开发服务器, 不重新同步 +``` + +若 PowerShell 执行策略阻止脚本, 请用: + +```powershell +powershell -ExecutionPolicy Bypass -File scripts\dev.ps1 +``` + +脚本通过 `$env:HUGO` 或 PATH 查找 hugo, 找不到时也会探测 winget 安装位置。 + +## 规则细节 + +- sfcraft 自定义贴图: 每次构建比对远端文件大小, 缺失或变化时更新; +- 原版贴图: 只在本地缺失时下载, **不会覆盖**手动放入 `assets/items/` 的贴图; +- 物品 id 即 `assets/items/` 下的文件名(不含 `.png`); +- wiki 命名规则: `<英文名>_JE<版本>[_BE<版本>].png`, 特殊命名(如 `TNT`、`Flint_and_Steel`) + 由 `sync-items.ps1` 中的变体生成与别名表处理。 + +## 网络要求 + +需要能访问 `api.github.com`、`raw.githubusercontent.com` 与 `zh.minecraft.wiki`。 +若 CI 或离线环境不允许联网, 建议把 `assets/items/` 下的贴图提交进仓库。 diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 0000000..60b06b5 --- /dev/null +++ b/scripts/build.ps1 @@ -0,0 +1,35 @@ +<# + SFCraft 文档构建入口: 先同步物品贴图, 再执行 hugo 构建。 + 贴图同步失败(有物品找不到)时终止构建。 + 用法: powershell -ExecutionPolicy Bypass -File scripts\build.ps1 [hugo 参数...] +#> +[CmdletBinding()] +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$HugoArgs +) + +$ErrorActionPreference = 'Stop' +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +function Get-HugoPath { + if ($env:HUGO) { return $env:HUGO } + $cmd = Get-Command hugo -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + $p = Get-ChildItem -Path (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages') -Recurse -Filter hugo.exe -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending | Select-Object -First 1 + if ($p) { return $p.FullName } + throw '未找到 Hugo。请安装 Hugo, 或将 hugo 可执行文件路径写入环境变量 HUGO。' +} + +Write-Host "`n[1/2] 同步物品贴图" -ForegroundColor Cyan +& (Join-Path $scriptDir 'sync-items.ps1') +if ($LASTEXITCODE -ne 0) { + Write-Host "[构建终止] 物品贴图同步失败, 未执行 hugo 构建。" -ForegroundColor Red + exit $LASTEXITCODE +} + +Write-Host "`n[2/2] hugo 构建" -ForegroundColor Cyan +$hugo = Get-HugoPath +& $hugo @HugoArgs +exit $LASTEXITCODE diff --git a/scripts/dev.ps1 b/scripts/dev.ps1 new file mode 100644 index 0000000..6e0d9ae --- /dev/null +++ b/scripts/dev.ps1 @@ -0,0 +1,42 @@ +<# + SFCraft 文档开发服务器入口: 先同步物品贴图, 再启动 hugo server。 + 贴图同步失败(有物品找不到)时终止启动。 + 用法: powershell -ExecutionPolicy Bypass -File scripts\dev.ps1 [-NoSync] [hugo server 参数...] +#> +[CmdletBinding()] +param( + [switch]$NoSync, + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$HugoArgs +) + +$ErrorActionPreference = 'Stop' +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +function Get-HugoPath { + if ($env:HUGO) { return $env:HUGO } + $cmd = Get-Command hugo -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + $p = Get-ChildItem -Path (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages') -Recurse -Filter hugo.exe -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending | Select-Object -First 1 + if ($p) { return $p.FullName } + throw '未找到 Hugo。请安装 Hugo, 或将 hugo 可执行文件路径写入环境变量 HUGO。' +} + +if (-not $NoSync) { + Write-Host "`n[1/2] 同步物品贴图" -ForegroundColor Cyan + & (Join-Path $scriptDir 'sync-items.ps1') + if ($LASTEXITCODE -ne 0) { + Write-Host "[启动终止] 物品贴图同步失败, 未启动 hugo server。" -ForegroundColor Red + exit $LASTEXITCODE + } +} else { + Write-Host "[跳过] 已指定 -NoSync, 不执行贴图同步" -ForegroundColor Yellow +} + +Write-Host "`n[2/2] 启动 hugo server" -ForegroundColor Cyan +$hugo = Get-HugoPath +$serverArgs = @('server') +if ($HugoArgs.Count -gt 0) { $serverArgs += $HugoArgs } +& $hugo @serverArgs +exit $LASTEXITCODE diff --git a/scripts/sync-items.ps1 b/scripts/sync-items.ps1 new file mode 100644 index 0000000..45c506d --- /dev/null +++ b/scripts/sync-items.ps1 @@ -0,0 +1,233 @@ +<# + SFCraft 文档 · 物品贴图自动同步 + + 流程: + 1. 将 sfcraft 仓库 textures/item 目录下所有贴图同步到 assets/items + 2. 扫描 content 中所有 crafting 组件, 收集用到的物品 id + 3. 对本地缺失的 id, 从 Minecraft wiki 自动获取(按命名规则挑选最新版本) + 4. 仍有无法获取的 id 时, 列出并退出(返回码 1), 阻断 build / dev + + 用法: + powershell -ExecutionPolicy Bypass -File scripts\sync-items.ps1 [-Force] + + 说明: + - sfcraft 自定义贴图每次与远端比对大小, 缺失或变更时更新 + - 原版贴图只在本地缺失时下载, 不会覆盖手动放入 assets/items 的贴图 + - 需要联网访问 api.github.com / zh.minecraft.wiki +#> +[CmdletBinding()] +param( + [switch]$Force +) + +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path -Parent $PSScriptRoot +$itemsDir = Join-Path $repoRoot 'assets\items' +$contentDir = Join-Path $repoRoot 'content' +$ua = 'sfcraft-docs-sync/1.0 (https://github.com/saltedfishclub/sfcraft-docs)' + +New-Item -ItemType Directory -Path $itemsDir -Force | Out-Null + +# ---------- 数据源 ---------- +$sfOwner = 'saltedfishclub' +$sfRepo = 'sfcraft' +$sfRef = 'rev/26.2' +$sfDir = 'src/main/resources/assets/sfcraft/textures/item' +$sfListUrl = "https://api.github.com/repos/$sfOwner/$sfRepo/contents/$sfDir`?ref=$sfRef" +$wikiApi = 'https://zh.minecraft.wiki/api.php' +$wikiImg = 'https://zh.minecraft.wiki/images/' + +function Write-Step([string]$s) { Write-Host "`n== $s" -ForegroundColor Cyan } + +function Invoke-Get([string]$url, [int]$maxRetry = 2) { + for ($i = 0; ; $i++) { + try { + return Invoke-RestMethod -Uri $url -Headers @{ 'User-Agent' = $ua } -TimeoutSec 30 + } catch { + if ($i -ge $maxRetry) { throw } + Start-Sleep -Milliseconds 800 + } + } +} + +# ---------- 1. sfcraft 自定义贴图 ---------- +Write-Step '1/4 同步 sfcraft 自定义贴图' +$sfCount = 0 +$sfNew = 0 +try { + $list = Invoke-Get $sfListUrl + foreach ($f in $list) { + $target = Join-Path $itemsDir $f.name + $need = $Force -or + -not (Test-Path -LiteralPath $target) -or + ((Get-Item -LiteralPath $target).Length -ne [int64]$f.size) + if ($need) { + Invoke-WebRequest -Uri $f.download_url -Headers @{ 'User-Agent' = $ua } -OutFile $target -TimeoutSec 60 + $sfNew++ + } + $sfCount++ + } + Write-Host " $sfCount 个文件, 本次更新 $sfNew 个" +} catch { + Write-Host " [警告] 无法访问 GitHub, 跳过 sfcraft 贴图同步: $($_.Exception.Message)" -ForegroundColor Yellow +} + +# ---------- 2. 扫描 crafting 组件 ---------- +Write-Step '2/4 扫描组件物品 id' +$ids = @{} +$shortcodePat = [regex]'(?s){{<[ ]*crafting\b(.*?)}}' +$paramPat = [regex]'(?:a[123]|b[123]|c[123]|out)="([^"]*)"' + +Get-ChildItem -Path $contentDir -Recurse -Filter '*.md' -File | ForEach-Object { + $text = Get-Content -LiteralPath $_.FullName -Raw -Encoding UTF8 + foreach ($m in $shortcodePat.Matches($text)) { + foreach ($p in $paramPat.Matches($m.Groups[1].Value)) { + $val = $p.Groups[1].Value.Trim() + if ($val.Length -gt 0) { + $id = ($val -split ':')[0].Trim() + if ($id.Length -gt 0) { $ids[$id] = $true } + } + } + } +} + +$all = @($ids.Keys | Sort-Object) +$need = @($all | Where-Object { -not (Test-Path -LiteralPath (Join-Path $itemsDir "${_}.png")) }) +Write-Host " 使用 $($all.Count) 个物品 id, 本地已有 $($all.Count - $need.Count) 个" + +# ---------- 3. 从 Minecraft wiki 获取缺失贴图 ---------- +Write-Step '3/4 从 Minecraft wiki 获取缺失贴图' + +function Get-WikiCandidates([string]$name) { + $regex = '^' + [regex]::Escape($name) + '(_\(item\))?_JE\d+(\.\d+)*(_BE\d+)?\.png$' + # 两个查询互补: + # _ 宽前缀, 覆盖 (item) 等变体, 如 Diamond_Boots_(item)_JE3_BE2.png + # _JE 窄前缀, 在宽前缀因数量被截断时兜底(裸物品如 diamond 的 JE 渲染排序靠前) + $urls = @( + "$wikiApi`?action=query&list=allimages&aiprefix=$([uri]::EscapeDataString($name + '_'))&ailimit=500&format=json", + "$wikiApi`?action=query&list=allimages&aiprefix=$([uri]::EscapeDataString($name + '_JE'))&ailimit=200&format=json" + ) + $seen = @{} + $all = New-Object System.Collections.ArrayList + foreach ($u in $urls) { + try { + $r = Invoke-Get $u + if ($null -ne $r.query.allimages) { + foreach ($n in $r.query.allimages) { + if (-not $seen.ContainsKey($n.name)) { + [void]$seen.Add($n.name, $true) + [void]$all.Add($n.name) + } + } + } + } catch { + Write-Host " [警告] wiki 查询失败 ($name): $($_.Exception.Message)" -ForegroundColor Yellow + } + Start-Sleep -Milliseconds 250 + } + return @($all | Where-Object { $_ -match $regex }) +} + +function Select-BestCandidate([string]$name, [string[]]$candidates) { + $regex = '^' + [regex]::Escape($name) + '(_\(item\))?_JE(\d+)(\.\d+)*(_BE(\d+))?\.png$' + $best = $null + $bestKey = $null + foreach ($c in $candidates) { + $m = [regex]::Match($c, $regex) + if (-not $m.Success) { continue } + $key = @( + $(if ($m.Groups[1].Success) { 1 } else { 0 }), # 优先物品图标 (item) + [int]$m.Groups[2].Value, # JE 版本 + $(if ($m.Groups[5].Success) { [int]$m.Groups[5].Value } else { -1 }) # BE 版本 + ) + if ($null -eq $bestKey) { $best = $c; $bestKey = $key; continue } + for ($i = 0; $i -lt 3; $i++) { + if ($key[$i] -ne $bestKey[$i]) { + if ($key[$i] -gt $bestKey[$i]) { $best = $c; $bestKey = $key } + break + } + } + } + return $best +} + +function Get-NameVariants([string]$id) { + $words = @($id -split '_' | Where-Object { $_ }) + $title = @($words | ForEach-Object { $_.Substring(0, 1).ToUpper() + $_.Substring(1) }) -join '_' + + $connectors = @('and', 'of', 'the', 'for', 'with', 'to', 'from', 'by', 'in', 'on', 'at') + $mixed = @($words | ForEach-Object { + if ($connectors -contains $_) { $_ } else { $_.Substring(0, 1).ToUpper() + $_.Substring(1) } + }) -join '_' + + $variants = New-Object System.Collections.ArrayList + [void]$variants.Add($title) + if ($mixed -cne $title) { [void]$variants.Add($mixed) } + if ($id.Length -le 4 -and $id -notmatch '[aeiou]') { [void]$variants.Add($id.ToUpper()) } + $oneWord = @($words | ForEach-Object { $_.Substring(0, 1).ToUpper() + $_.Substring(1) }) -join '' + if ($oneWord -cne $title) { [void]$variants.Add($oneWord) } + + $aliases = @{ + 'tnt' = 'TNT' + 'tnt_minecart' = 'TNT_Minecart' + 'dragon_breath' = "Dragon's_Breath" + 'slime_ball' = 'Slime' + 'snow_golem' = 'Snow_Golem' + 'ocelot' = 'Ocelot' + } + if ($aliases.ContainsKey($id)) { [void]$variants.Insert(0, $aliases[$id]) } + return @($variants | Select-Object -Unique) +} + +$failed = @() +foreach ($id in $need) { + $file = $null + foreach ($name in Get-NameVariants $id) { + $cands = Get-WikiCandidates $name + if ($cands.Count -gt 0) { $file = Select-BestCandidate $name $cands; break } + } + + if ($file) { + $target = Join-Path $itemsDir "$id.png" + try { + Invoke-WebRequest -Uri ($wikiImg + [uri]::EscapeDataString($file)) -Headers @{ 'User-Agent' = $ua } -OutFile $target -TimeoutSec 60 + $fs = [System.IO.File]::OpenRead($target) + $sig = New-Object byte[] 8 + [void]$fs.Read($sig, 0, 8) + $fs.Close() + if (-not ($sig[0] -eq 137 -and $sig[1] -eq 80 -and $sig[2] -eq 78 -and $sig[3] -eq 71)) { + throw '下载内容不是有效 PNG' + } + Write-Host " ok $id <- $file" -ForegroundColor Green + continue + } catch { + Write-Host " fail $id 下载失败: $($_.Exception.Message)" -ForegroundColor Yellow + } + } else { + Write-Host " fail $id 在 wiki 上未找到" -ForegroundColor Yellow + } + $failed += $id +} + +# ---------- 4. 汇总错误 ---------- +if ($failed.Count -gt 0) { + Write-Host "`n[错误] 以下物品贴图无法自动获取:" -ForegroundColor Red + $failed | Sort-Object | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + Write-Host @" + +可能原因: + 1. 物品 id 拼写错误(组件中填写的是 assets/items 下的文件名) + 2. 该物品是 sfcraft 自定义物品, 但 textures/item 目录中没有同名贴图 + 3. 原版物品在 Minecraft wiki 上的命名特殊, 未被自动规则覆盖 + +解决办法: + - 将贴图手动放入 assets/items/.png 后重新运行 + - 修正组件中的物品 id + - 若是 wiki 命名特例, 可在 scripts/sync-items.ps1 的 Get-NameVariants 别名表中补充 +"@ -ForegroundColor Yellow + exit 1 +} + +Write-Host "`n全部贴图就绪 ✓" -ForegroundColor Green +exit 0 diff --git a/themes/hugo-book b/themes/hugo-book new file mode 160000 index 0000000..bc3454c --- /dev/null +++ b/themes/hugo-book @@ -0,0 +1 @@ +Subproject commit bc3454c1db956f651dbcc4cf5056861fd9128817