<# 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