ZYLPD2-Filter-Lonelywolf/buildbuild.ps1

874 lines
38 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#requires -Version 5.1
<#
BadWolf 过滤器构建脚本 build.ps1 v1.2
------------------------------------------------------------------
用途:把模块目录下的 txt 按文件名排序拼接成 BadWolf-LootFilter.txt,
并完成一致性校验与行号换算。纯 PowerShell,无需安装任何软件。
v1.2 相对 v1.1 的变化
------------------------------------------------------------------
· 自检新增:孤立 % / 未闭合 % 扫描(半截百分号会吞颜色或漏出明文)
判定:先整体屏蔽合法占位符 %NAME%,剩下的 % 只放行四种写字面百分号的情形
(数值后 40%ED / 右括号后 [4-6]%物理减伤 / 夹在两个占位符之间 /
「物理减伤%」「魔法减伤%」后的百分比示意符)
v1.1 相对 v1.0 的变化
------------------------------------------------------------------
· 模块白名单:只拼 NN-*.txt,其它 txt 一律跳过并黄字警告(不再静默混入)
· 备份轮转:build 只保留最近 2 份 .bak,可用 -KeepBackups N 调整
· 临时文件:写盘失败也会清理 .tmp
· 自检新增:物品代码白名单(item_codes.txt)/ 死规则 / Formula 引用完整性
· 版本行联动:版本行日期落后于最晚模块改动时间时提示
用法(二选一)
------------------------------------------------------------------
A. 双击 build.bat → 直接校验(窗口不关)
B. 命令行:
powershell -NoProfile -ExecutionPolicy Bypass -File build.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File build.ps1 build
powershell -NoProfile -ExecutionPolicy Bypass -File build.ps1 map 1939
powershell -NoProfile -ExecutionPolicy Bypass -File build.ps1 map 16:139
powershell -NoProfile -ExecutionPolicy Bypass -File build.ps1 export
命令
------------------------------------------------------------------
check 校验(默认,不写任何文件)
build 重建合并版(自动备份 .bak,只保留最近 2 份;-NoBackup 关闭备份)
map 行号换算: 1939(合并→模块) 或 16:139(模块→合并)
export 导出行号映射表
参数
------------------------------------------------------------------
-NoBackup build 时不备份原文件
-KeepBackups N 备份保留份数,默认 2
-Strict 有提示项也算不通过(退出码 1)
退出码:0 一致 / 1 有问题 / 2 环境问题
#>
param(
[Parameter(Position = 0)][string]$Command = 'check',
[Parameter(Position = 1)][string]$Arg = '',
[switch]$NoBackup,
[switch]$Strict,
[int]$KeepBackups = 2
)
$ErrorActionPreference = 'Stop'
# ---------------------------------------------------------------- 基础路径
$Root = $PSScriptRoot
$MergedName = 'BadWolf-LootFilter.txt'
$MergedPath = Join-Path $Root $MergedName
$MapName = [char]0x884C + [char]0x53F7 + [char]0x6620 + [char]0x5C04 + [char]0x8868 + '.md'
function Write-Line([string]$s) { Write-Host $s }
function Format-N([long]$n) { return $n.ToString('N0', [System.Globalization.CultureInfo]::InvariantCulture) }
function Get-Bar { return ('=' * 74) }
function Write-Head([string]$t) { Write-Host ""; Write-Host "[$t]"; Write-Host ('-' * 74) }
function Get-Md5Hex([byte[]]$Bytes) {
$md5 = [System.Security.Cryptography.MD5]::Create()
try { $h = $md5.ComputeHash($Bytes) } finally { $md5.Dispose() }
return (-join ($h | ForEach-Object { $_.ToString('x2') }))
}
# ---------------------------------------------------------------- 模块目录
# 只认「≥10 个 数字-xxx.txt」的目录;多个候选直接报错,绝不猜
function Get-ModuleDir {
$cands = @()
foreach ($d in @(Get-ChildItem -LiteralPath $Root -Directory -ErrorAction SilentlyContinue)) {
$n = @(Get-ChildItem -LiteralPath $d.FullName -Filter '*.txt' -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match '^\d{2}-' }).Count
if ($n -gt 0) { $cands += [pscustomobject]@{ Path = $d.FullName; Count = $n } }
}
$good = @($cands | Where-Object { $_.Count -ge 10 } | Sort-Object -Descending Count)
if ($good.Count -eq 0) {
$tip = ''
if ($cands.Count -gt 0) {
$tip = ';候选目录(文件数不足):' + (($cands | ForEach-Object { "$($_.Path)($($_.Count)个)" }) -join ',')
}
throw "找不到模块目录:$Root 下没有包含 ≥10 个 `00-xxx.txt` 形式文件的文件夹$tip"
}
if ($good.Count -gt 1) {
$list = ($good | ForEach-Object { "$($_.Path)($($_.Count) 个)" }) -join ';'
throw "检测到多个可能的模块目录,为避免用错目录构建,已停止:$list。请把多余的移走或改名。"
}
return $good[0].Path
}
# ---------------------------------------------------------------- 载入模块
# 白名单:只认「NN-名称.txt」;名字里带 副本/备份/copy/bak/(1) 的视为复制品,同样跳过
$script:SkippedModules = @()
$CopyNameRe = '副本|备份|复制|copy|bak|backup|\(\d+\)'
function Get-Modules([string]$Dir) {
$allTxt = @(Get-ChildItem -LiteralPath $Dir -Filter '*.txt' -File)
$files = @($allTxt | Where-Object {
$_.Name -match '^\d{2}-.*\.txt$' -and $_.Name -notmatch $CopyNameRe
} | Sort-Object Name)
$script:SkippedModules = @($allTxt | Where-Object {
-not ($_.Name -match '^\d{2}-.*\.txt$') -or ($_.Name -match $CopyNameRe)
} | Sort-Object Name | ForEach-Object { $_.Name })
$list = New-Object System.Collections.ArrayList
$cursor = 1
$strict = [System.Text.UTF8Encoding]::new($false, $true)
foreach ($f in $files) {
$bytes = [System.IO.File]::ReadAllBytes($f.FullName)
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
$errors = New-Object System.Collections.ArrayList
try { [void]$strict.GetString($bytes) }
catch { [void]$errors.Add('不是合法的 UTF-8 编码') }
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
[void]$errors.Add('含 UTF-8 BOM')
}
$lfOnly = ([regex]::Matches($text, "(?<!\r)\n")).Count
if ($lfOnly -gt 0) { [void]$errors.Add("存在裸 LF 换行($lfOnly 处)") }
if ($bytes.Length -eq 0) { [void]$errors.Add('空文件') }
elseif ($bytes[$bytes.Length - 1] -ne 10) { [void]$errors.Add('文件末尾缺少换行') }
$lineCount = ([regex]::Matches($text, "\n")).Count
$m = [pscustomobject]@{
Name = $f.Name
Code = if ($f.Name.Length -ge 2) { $f.Name.Substring(0, 2) } else { '??' }
Path = $f.FullName
Bytes = $bytes
ByteLen = $bytes.Length
Text = $text
LineCount = $lineCount
Start = $cursor
MTime = $f.LastWriteTime
Errors = $errors
}
$m | Add-Member -MemberType ScriptProperty -Name End -Value { $this.Start + $this.LineCount - 1 }
[void]$list.Add($m)
$cursor += $lineCount
}
return , $list.ToArray()
}
function Get-Lines([string]$Text) {
$a = $Text -split "`r`n"
if ($a.Count -gt 0 -and $a[-1] -eq '') {
if ($a.Count -eq 1) { return @() }
return $a[0..($a.Count - 2)]
}
return $a
}
function Find-ModuleOfLine($Mods, [int]$Line) {
foreach ($m in $Mods) { if ($Line -ge $m.Start -and $Line -le $m.End) { return $m } }
return $null
}
function Join-Bytes($Mods) {
$ms = New-Object System.IO.MemoryStream
try {
foreach ($m in $Mods) { $ms.Write($m.Bytes, 0, $m.ByteLen) }
return , $ms.ToArray()
} finally { $ms.Dispose() }
}
# ---------------------------------------------------------------- 白名单
$Colors = @('BLACK','WHITE','GRAY','LIGHT_GRAY','DARK_GRAY','RED','DARK_RED','ORANGE','GOLD',
'YELLOW','GREEN','DARK_GREEN','SAGE','TEAL','BLUE','DARK_BLUE','PURPLE','CORAL',
'TAN','LIGHT_BLUE','VIOLET')
$Special = @('NAME','CL','NL','CONTINUE')
$InfoCodes = @('ED','EDAM','EDEF','RES','FRES','CRES','LRES','PRES','IAS','FCR','MFIND','LIFE',
'STR','DEX','AR','RANGE','PRICE','QTY','GEMTYPE','SOCKETS','MAXSOCKETS','BASENAME',
'BASEMINKICK','BASEMAXKICK','CRAFTALVL','REROLLALVL','UPLVL','UPSTR','UPDEX',
'WPNSPD','MAXDMG','ITEMCODE')
$PhSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
foreach ($x in @($Colors + $Special + $InfoCodes)) { [void]$PhSet.Add($x) }
$NumPrefix = @('SK','STAT','CHARSTAT','TABSK','FORMULA')
$KwPrefix = @('SK','STAT','CHARSTAT','TABSK','MULTI','CLSK','FORMULA')
$IgnoreUndef = @('SPEARBASE2_3')
$MaxLineLen = 1024
function Test-Placeholder([string]$name) {
if ($PhSet.Contains($name)) { return $true }
foreach ($p in $NumPrefix) {
if (-not $name.StartsWith($p, [System.StringComparison]::Ordinal)) { continue }
$tail = $name.Substring($p.Length)
if ($tail -eq '') { return $true }
if ($tail -match '^\d+$') { return $true }
if ($p -eq 'FORMULA' -and $tail -match '^[A-Z0-9_]+$') { return $true }
}
return $false
}
function Test-KeywordToken([string]$t) {
foreach ($p in $KwPrefix) {
if (-not $t.StartsWith($p, [System.StringComparison]::Ordinal)) { continue }
$tail = $t.Substring($p.Length)
if ($tail -eq '') { return $true }
if ($tail -match '^\d+$') { return $true }
if ($p -eq 'FORMULA' -and $tail -match '^[A-Z0-9_]+$') { return $true }
}
return $false
}
# ---------------------------------------------------------------- 物品代码白名单
# item_codes.txt 由 data\global\excel 生成:[codes] 物品代码 / [types] 类型代码。
# 文件不存在则跳过该项检查(不视为错误)。
function Get-CodeWhitelist {
$p = Join-Path $Root 'item_codes.txt'
if (-not (Test-Path -LiteralPath $p)) { return $null }
$set = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($l in [System.IO.File]::ReadAllLines($p, [System.Text.Encoding]::UTF8)) {
$t = $l.Trim()
if ($t -eq '') { continue }
if ($t.StartsWith('#') -or $t.StartsWith('[')) { continue }
[void]$set.Add($t)
}
if ($set.Count -eq 0) { return $null }
return , $set
}
# ---------------------------------------------------------------- 孤立 % / 未闭合 %
# 半截百分号不会让过滤器报错,只会吞掉颜色或把 %XXX 漏成明文,所以只作「提示」。
# 做法:先找出所有合法占位符 %NAME% 的区间,再逐个检查剩下的 %:
# 放行 前一位是数字 40%ED、/35%%TAN%、10%移动速度
# 放行 前一位是 ] ) / - [4-6]%物理减伤、+[50-80]% 增强伤害
# 放行 前一位是占位符结尾,且后一位是占位符开头 / } / ] %EDEF%%%GRAY%、%STAT258%%}
# 放行 前四位是「物理减伤」「魔法减伤」 [4-6]%物理减伤% ← 百分比减伤的专用示意符,
# 刻意与数值减伤 [2-5]物理减伤 区分(2026-10-28 用户定义,不要删、不要报)
# 报出 其余全部(%GRAY%%Steel( 缺右侧、抽动挣扎%%CL% 多一个、神蛇之羽CL% 缺左侧)
function Get-PercentFault([string[]]$Lines) {
$phMask = [regex]'%[A-Za-z][A-Za-z0-9_\-]*%'
$nextNameRe = [regex]'^([A-Za-z][A-Za-z0-9_\-]*)'
$prevNameRe = [regex]'([A-Za-z][A-Za-z0-9_\-]*)$'
$res = New-Object System.Collections.ArrayList
for ($i = 0; $i -lt $Lines.Count; $i++) {
$s = $Lines[$i]
if (-not $s.Contains('%')) { continue }
if ($s.TrimStart().StartsWith('//')) { continue }
# 行尾 // 注释里的 % 不参与显示,先切掉
$cut = $s.IndexOf('//')
$body = if ($cut -ge 0) { $s.Substring(0, $cut) } else { $s }
if (-not $body.Contains('%')) { continue }
$inPh = [System.Collections.Generic.HashSet[int]]::new()
$phStart = [System.Collections.Generic.HashSet[int]]::new()
$phEnd = [System.Collections.Generic.HashSet[int]]::new()
foreach ($mm in $phMask.Matches($body)) {
[void]$phStart.Add($mm.Index)
[void]$phEnd.Add($mm.Index + $mm.Length - 1)
for ($k = $mm.Index; $k -lt ($mm.Index + $mm.Length); $k++) { [void]$inPh.Add($k) }
}
$j = $body.IndexOf('%')
while ($j -ge 0) {
if ($inPh.Contains($j)) { $j = $body.IndexOf('%', $j + 1); continue }
$prev = if ($j -gt 0) { $body[$j - 1] } else { [char]0 }
$next = if ($j + 1 -lt $body.Length) { $body[$j + 1] } else { [char]0 }
$ok = $false
if ([char]::IsDigit($prev)) { $ok = $true }
elseif ('])/-'.IndexOf($prev) -ge 0) { $ok = $true }
elseif ($phEnd.Contains($j - 1) -and ($phStart.Contains($j + 1) -or $next -eq '}' -or $next -eq ']')) { $ok = $true }
elseif ($j -ge 4 -and ($body.Substring($j - 4, 4) -eq '物理减伤' -or $body.Substring($j - 4, 4) -eq '魔法减伤')) { $ok = $true }
if (-not $ok) {
$kind = '孤立 %'
$nw = $nextNameRe.Match($body.Substring($j + 1))
$pw = $prevNameRe.Match($body.Substring(0, $j))
if ($pw.Success -and (Test-Placeholder $pw.Groups[1].Value)) { $kind = '缺左侧 %' }
elseif ($nw.Success) { $kind = '缺右侧 %' }
elseif ($phStart.Contains($j + 1) -or $next -eq '%') { $kind = '多余 %' }
$a = [Math]::Max(0, $j - 16)
$len = [Math]::Min(38, $body.Length - $a)
$snip = $body.Substring($a, $len)
[void]$res.Add([pscustomobject]@{ Line = $i + 1; Kind = $kind; Snippet = $snip })
}
$j = $body.IndexOf('%', $j + 1)
}
}
return $res.ToArray()
}
# ---------------------------------------------------------------- 自检
function Invoke-SelfCheck([string[]]$Lines, $Mods) {
$errors = New-Object System.Collections.ArrayList
$warns = New-Object System.Collections.ArrayList
$ruleRe = [regex]'^ItemDisplay\[([^\]]*)\]:(.*)$'
$aliasRe = [regex]'^Alias\[([^\]]+)\]:(.*)$'
$formulaRe = [regex]'^Formula\[([^\]]+)\]:(.*)$'
$phRe = [regex]'%([A-Za-z][A-Za-z0-9_]*)%'
$tokRe = [regex]'(?<![A-Za-z0-9_])([A-Z][A-Z0-9_]{1,})(?![A-Za-z0-9_])'
$rules = New-Object System.Collections.ArrayList
$aliasNames = New-Object System.Collections.ArrayList
$formulaNames = New-Object System.Collections.ArrayList
$formulaCount = 0
$continueCount = 0
$phSeen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
$badPh = @{}
$filtlvl = 0
$trail = New-Object System.Collections.ArrayList
for ($i = 0; $i -lt $Lines.Count; $i++) {
$ln = $i + 1
$s = $Lines[$i]
$mr = $ruleRe.Match($s)
if ($mr.Success) {
$cond = $mr.Groups[1].Value
$out = $mr.Groups[2].Value
[void]$rules.Add([pscustomobject]@{ Line = $ln; Cond = $cond; Out = $out })
if ($cond.Contains('FILTLVL')) { $filtlvl++ }
if ($cond.Contains('(') -or $cond.Contains(')')) {
$open = ([regex]::Matches($cond, '\(')).Count
$close = ([regex]::Matches($cond, '\)')).Count
if ($open -ne $close) {
[void]$errors.Add("L$ln 条件括号不平衡(( $open / ) $close):$cond")
}
}
}
$ma = $aliasRe.Match($s)
if ($ma.Success) { [void]$aliasNames.Add($ma.Groups[1].Value) }
$mf = $formulaRe.Match($s)
if ($mf.Success) { $formulaCount++; [void]$formulaNames.Add($mf.Groups[1].Value) }
$continueCount += ([regex]::Matches($s, '%CONTINUE%')).Count
foreach ($pm in $phRe.Matches($s)) {
$nm = $pm.Groups[1].Value
[void]$phSeen.Add($nm)
if (-not (Test-Placeholder $nm)) {
if (-not $badPh.ContainsKey($nm)) { $badPh[$nm] = New-Object System.Collections.ArrayList }
[void]$badPh[$nm].Add($ln)
}
}
if ($s -ne $s.TrimEnd()) { [void]$trail.Add($ln) }
if ($s.Length -gt $MaxLineLen) { [void]$warns.Add("L$ln 超长($($s.Length) 字符)") }
}
# 孤立 % / 未闭合 %
$pctFaults = @(Get-PercentFault $Lines)
foreach ($f in $pctFaults) {
[void]$warns.Add("$($f.Kind) L$($f.Line):$($f.Snippet)")
}
# 别名重复
$seenAlias = @{}
foreach ($n in $aliasNames) { if ($seenAlias.ContainsKey($n)) { $seenAlias[$n]++ } else { $seenAlias[$n] = 1 } }
foreach ($k in ($seenAlias.Keys | Sort-Object)) {
if ($seenAlias[$k] -gt 1) { [void]$errors.Add("别名重复定义:$k") }
}
# 非法占位符
foreach ($k in ($badPh.Keys | Sort-Object)) {
$lns = ($badPh[$k] | Select-Object -First 8) -join ','
[void]$errors.Add("非法占位符 %$k%($($badPh[$k].Count) 处,行 $lns)")
}
# 未定义标识(只判别名风格:含下划线或数字)
$defined = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
foreach ($n in $aliasNames) { [void]$defined.Add($n.ToUpperInvariant()) }
$refs = @{}
foreach ($r in $rules) {
foreach ($tm in $tokRe.Matches($r.Cond)) {
$t = $tm.Groups[1].Value
if (-not $refs.ContainsKey($t)) { $refs[$t] = New-Object System.Collections.ArrayList }
[void]$refs[$t].Add($r.Line)
}
}
foreach ($t in ($refs.Keys | Sort-Object)) {
if ($defined.Contains($t)) { continue }
if ($IgnoreUndef -contains $t) { continue }
if (Test-KeywordToken $t) { continue }
if ($t -match '[_\d]') {
$lns = ($refs[$t] | Select-Object -First 4) -join ','
[void]$warns.Add("未定义标识 $t(L$lns)")
}
}
# 完全重复规则
$dupMap = @{}
foreach ($r in $rules) {
$k = (($r.Cond -replace '\s+', ' ').Trim().ToLowerInvariant()) + '|' + (($r.Out -replace '\s+', ' ').Trim())
if ($dupMap.ContainsKey($k)) { $dupMap[$k] += ",$($r.Line)" } else { $dupMap[$k] = "$($r.Line)" }
}
foreach ($k in $dupMap.Keys) {
if ($dupMap[$k] -match ',') {
$c = $k.Split('|')[0]
if ($c.Length -gt 70) { $c = $c.Substring(0, 70) }
[void]$warns.Add("完全重复规则 L$($dupMap[$k]):$c")
}
}
# 死规则:同一条件重复出现,且更早的一条没有 %CONTINUE% → 后一条永不命中
$stopAt = @{}
$deadCount = 0
foreach ($r in $rules) {
$k = ($r.Cond -replace '\s+', ' ').Trim().ToUpperInvariant()
if ($k -eq '') { continue }
if ($stopAt.ContainsKey($k)) {
$deadCount++
if ($deadCount -le 12) {
[void]$warns.Add("死规则 L$($r.Line) 永不命中(同条件 L$($stopAt[$k]) 未带 CONTINUE,已终结判定):$($r.Cond)")
}
} elseif (-not $r.Out.ToUpperInvariant().Contains('%CONTINUE%')) {
$stopAt[$k] = $r.Line
}
}
if ($deadCount -gt 12) { [void]$warns.Add("... 另有 $($deadCount - 12) 条死规则") }
# 物品代码白名单
$wl = Get-CodeWhitelist
$wlCount = 0
if ($null -eq $wl) {
[void]$warns.Add('未找到 item_codes.txt,跳过物品代码校验')
} else {
$wlCount = $wl.Count
$codeRe = [regex]'^[a-z][a-z0-9]{1,5}$'
$badCode = @{}
foreach ($r in $rules) {
$c = [regex]::Replace($r.Cond, '%[^%]*%', ' ')
$c = $c.Replace('(', ' ').Replace(')', ' ').Replace('!', ' ')
foreach ($t in ($c -split '\s+')) {
if ($t -eq '') { continue }
if (-not $codeRe.IsMatch($t)) { continue }
if ($wl.Contains($t)) { continue }
if ($defined.Contains($t.ToUpperInvariant())) { continue }
if (-not $badCode.ContainsKey($t)) { $badCode[$t] = New-Object System.Collections.ArrayList }
[void]$badCode[$t].Add($r.Line)
}
}
foreach ($k in ($badCode.Keys | Sort-Object)) {
$lns = ($badCode[$k] | Select-Object -First 4) -join ','
[void]$errors.Add("疑似无效物品代码 $k($($badCode[$k].Count) 处,行 $lns)")
}
}
# Formula 引用完整性
$fDef = @{}
foreach ($n in $formulaNames) { $fDef[$n] = $true }
$fRef = @{}
for ($i = 0; $i -lt $Lines.Count; $i++) {
if ($formulaRe.IsMatch($Lines[$i])) { continue }
foreach ($fm in [regex]::Matches($Lines[$i], 'FORMULA([A-Za-z0-9_]+)')) {
$n = $fm.Groups[1].Value
if (-not $fRef.ContainsKey($n)) { $fRef[$n] = New-Object System.Collections.ArrayList }
[void]$fRef[$n].Add($i + 1)
}
}
foreach ($n in ($fRef.Keys | Sort-Object)) {
if ($fDef.ContainsKey($n)) { continue }
$lns = ($fRef[$n] | Select-Object -First 4) -join ','
[void]$errors.Add("Formula 引用无定义:FORMULA$n($($fRef[$n].Count) 处,行 $lns)")
}
$fUnused = @($formulaNames | Where-Object { -not $fRef.ContainsKey($_) } | Sort-Object)
if ($fUnused.Count -gt 0) {
[void]$warns.Add("Formula 定义后 0 引用:$($fUnused -join '、')")
}
# 版本行联动:版本行日期 vs 最晚模块改动
$verRe = [regex]'更新于\s*(\d{2,4})[/-](\d{1,2})[/-](\d{1,2})'
foreach ($l in $Lines) {
$mv = $verRe.Match($l)
if (-not $mv.Success) { continue }
$y = [int]$mv.Groups[1].Value
if ($y -lt 100) { $y += 2000 }
$verOk = $true
$vd = [datetime]::MinValue
try { $vd = [datetime]::new($y, [int]$mv.Groups[2].Value, [int]$mv.Groups[3].Value) }
catch { $verOk = $false }
if ($verOk -and $null -ne $Mods -and $Mods.Count -gt 0) {
$newest = $Mods | Sort-Object MTime -Descending | Select-Object -First 1
if ($vd.Date -lt $newest.MTime.Date) {
[void]$warns.Add("版本行停在 $($vd.ToString('yyyy-MM-dd')),但最晚模块改动是 $($newest.MTime.ToString('yyyy-MM-dd'))($($newest.Name))—— 记得更新版本号")
}
}
break
}
if ($trail.Count -gt 0) {
$head = ($trail | Select-Object -First 6) -join ','
[void]$warns.Add("行尾空白 $($trail.Count) 处(L$head…)")
}
return [pscustomobject]@{
Errors = $errors
Warns = $warns
Rules = $rules.Count
Aliases = $aliasNames.Count
Formulas = $formulaCount
Continue = $continueCount
Lines = $Lines.Count
PhKinds = $phSeen.Count
FiltLvl = $filtlvl
WlCount = $wlCount
Dead = $deadCount
FRef = $fRef.Count
FUnused = $fUnused.Count
Pct = $pctFaults.Count
}
}
# ---------------------------------------------------------------- 差异定位
function Get-DiffBlocks([string[]]$A, [string[]]$B) {
$blocks = New-Object System.Collections.ArrayList
if ($A.Count -eq 0 -or $B.Count -eq 0) { return , $blocks.ToArray() }
if ($A.Count -eq $B.Count) {
# 行数相同:逐行比对,合并连续差异
$cur = $null
for ($i = 0; $i -lt $A.Count; $i++) {
if ($A[$i] -cne $B[$i]) {
if ($null -eq $cur) {
$cur = [pscustomobject]@{
AFrom = $i + 1; ATo = $i + 1
BFrom = $i + 1; BTo = $i + 1
A = @(); B = @()
}
}
$cur.ATo = $i + 1
$cur.BTo = $i + 1
$cur.A += $A[$i]
$cur.B += $B[$i]
} elseif ($null -ne $cur) {
[void]$blocks.Add($cur); $cur = $null
}
}
if ($null -ne $cur) { [void]$blocks.Add($cur) }
return , $blocks.ToArray()
}
# 行数不同:掐掉公共前后缀,报中间
$min = [Math]::Min($A.Count, $B.Count)
$p = 0
while ($p -lt $min -and $A[$p] -ceq $B[$p]) { $p++ }
$s = 0
while ($s -lt ($min - $p) -and $A[$A.Count - 1 - $s] -ceq $B[$B.Count - 1 - $s]) { $s++ }
$blk = [pscustomobject]@{
AFrom = $p + 1
ATo = $A.Count - $s
BFrom = $p + 1
BTo = $B.Count - $s
A = @($A[$p..([Math]::Max($p, $A.Count - $s - 1))])
B = @($B[$p..([Math]::Max($p, $B.Count - $s - 1))])
}
[void]$blocks.Add($blk)
return , $blocks.ToArray()
}
# ---------------------------------------------------------------- check
function Invoke-Check($Mods) {
Write-Line (Get-Bar)
Write-Line " BadWolf 过滤器 构建校验"
Write-Line (Get-Bar)
$mergedRaw = @()
if (Test-Path -LiteralPath $MergedPath) {
$mergedRaw = [System.IO.File]::ReadAllBytes($MergedPath)
}
$mergedText = [System.Text.Encoding]::UTF8.GetString($mergedRaw)
$mergedLines = Get-Lines $mergedText
$cat = Join-Bytes $Mods
$catText = [System.Text.Encoding]::UTF8.GetString($cat)
$catLines = Get-Lines $catText
Write-Line " 模块数 $($Mods.Count)"
Write-Line " 拼接 $(Format-N $cat.Length) B $(Format-N $catLines.Count) 行 MD5 $(Get-Md5Hex $cat)"
Write-Line " 合并 $(Format-N $mergedRaw.Length) B $(Format-N $mergedLines.Count) 行 MD5 $(Get-Md5Hex $mergedRaw)"
# ---- 1 完整性
Write-Head '1/5 模块完整性'
$badMods = 0
foreach ($m in $Mods) {
if ($m.Errors.Count -eq 0) { $flag = 'OK ' } else { $flag = 'ERR'; $badMods++ }
$line = " $flag [{0}] {1,5} 行 {2,10} B {3}" -f $m.Code, $m.LineCount, (Format-N $m.ByteLen), $m.Name
Write-Line $line
foreach ($e in $m.Errors) { Write-Line " ! $e" }
}
# ---- 2 比对
Write-Head '2/5 拼接与比对'
$same = $false
if ($mergedRaw.Length -eq 0) {
Write-Line " 状态 合并版不存在(首次构建?),无基准可比 → 运行 build 生成"
$same = $false
} elseif ($cat.Length -eq $mergedRaw.Length) {
$same = $true
for ($i = 0; $i -lt $cat.Length; $i++) {
if ($cat[$i] -ne $mergedRaw[$i]) { $same = $false; break }
}
} else { $same = $false }
if ($mergedRaw.Length -eq 0) {
# 没有基准文件,跳过差异定位
} elseif ($same) {
Write-Line " 状态 一致(逐字节相同)"
} else {
$delta = $cat.Length - $mergedRaw.Length
$sign = ''
if ($delta -ge 0) { $sign = '+' }
$blocks = Get-DiffBlocks $mergedLines $catLines
Write-Line " 状态 不一致(拼接比合并版 $sign$delta B,$($blocks.Count) 个差异块)"
foreach ($b in $blocks) {
$mod = Find-ModuleOfLine $Mods $b.BFrom
$head = " [差异] 合并版 L$($b.AFrom)-$($b.ATo)"
if ($null -ne $mod) {
$ml = $b.BFrom - $mod.Start + 1
$mh = $b.BTo - $mod.Start + 1
if ($ml -eq $mh) { $head += " ← $($mod.Name) 第 $ml 行" }
else { $head += " ← $($mod.Name) 第 $ml-$mh 行" }
}
Write-Line $head
$n = [Math]::Min($b.A.Count, 3)
for ($i = 0; $i -lt $n; $i++) {
Write-Line " 合并: $($b.A[$i])"
Write-Line " 模块: $($b.B[$i])"
}
if ($b.A.Count -ne $b.B.Count) {
Write-Line " (行数 $($b.A.Count) → $($b.B.Count))"
}
}
}
# ---- 3 行号表
Write-Head '3/5 行号映射表(模块 → 合并版)'
Write-Line " 模块 合并版区间 行数 字节 文件名"
foreach ($m in $Mods) {
$line = " [{0}] {1,5}-{2,-5} {3,8} {4,12} {5}" -f $m.Code, $m.Start, $m.End, $m.LineCount, (Format-N $m.ByteLen), $m.Name
Write-Line $line
}
$last = $Mods[$Mods.Count - 1]
Write-Line (" 合计 1-{0,-9} {1,8} {2,12} ({3} 个模块)" -f $last.End, $last.End, (Format-N $cat.Length), $Mods.Count)
# ---- 4 自检
Write-Head '4/5 静态自检'
$chk = Invoke-SelfCheck $catLines $Mods
Write-Line " 规则 $($chk.Rules) 条 | 别名 $($chk.Aliases) 个 | 公式 $($chk.Formulas) 个 | CONTINUE $($chk.Continue) 处 | FILTLVL $($chk.FiltLvl) 条"
Write-Line " 行 $(Format-N $chk.Lines) | 字节 $(Format-N $cat.Length) | 占位符 $($chk.PhKinds) 种"
$wlTxt = if ($chk.WlCount -gt 0) { "白名单 $($chk.WlCount) 码" } else { '白名单 未加载' }
Write-Line " 语义 $wlTxt | 死规则 $($chk.Dead) | 公式引用 $($chk.FRef)/$($chk.Formulas)(未引用 $($chk.FUnused))"
Write-Line " 符号 孤立 % / 未闭合 % $($chk.Pct) 处"
if ($chk.Errors.Count -gt 0) {
Write-Line ""
Write-Line " 错误 $($chk.Errors.Count) 项:"
foreach ($e in ($chk.Errors | Select-Object -First 30)) { Write-Line " x $e" }
} else {
Write-Line ""
Write-Line " 无错误(括号 / 占位符 / 别名重复 / 物品代码 / 公式引用)"
}
if ($chk.Warns.Count -gt 0) {
Write-Line ""
Write-Line " 提示 $($chk.Warns.Count) 项(不阻断):"
foreach ($w in ($chk.Warns | Select-Object -First 20)) { Write-Line " - $w" }
if ($chk.Warns.Count -gt 20) { Write-Line " ... 另有 $($chk.Warns.Count - 20) 项" }
} else {
Write-Line " 无提示项"
}
# ---- 5 结论
Write-Head '5/5 结论'
$ok = $same -and ($chk.Errors.Count -eq 0) -and ($badMods -eq 0)
if ($ok) {
Write-Line " 通过:合并版与模块一致,自检无错误。"
if ($chk.Warns.Count -gt 0) { Write-Line " (有 $($chk.Warns.Count) 项提示,非阻断)" }
} else {
if (-not $same) { Write-Line " x 模块已改动但合并版未更新 → 运行 build(或双击 build.bat 选 build)" }
if ($badMods -gt 0) { Write-Line " x $badMods 个模块有编码/换行问题,先修复再构建" }
if ($chk.Errors.Count -gt 0) { Write-Line " x 自检发现 $($chk.Errors.Count) 项错误" }
}
Write-Line ""
if ($Strict) { if ($ok -and $chk.Warns.Count -eq 0) { return 0 } else { return 1 } }
if ($ok) { return 0 } else { return 1 }
}
# ---------------------------------------------------------------- 备份轮转
# 只保留最近 $Keep 份 BadWolf-LootFilter.txt.<时间戳>.bak(文件名时间戳可直接按名倒序)
function Remove-OldBackup([int]$Keep) {
$dir = Split-Path -Parent $MergedPath
$pat = (Split-Path -Leaf $MergedPath) + '.*.bak'
$all = @(Get-ChildItem -LiteralPath $dir -Filter $pat -File -ErrorAction SilentlyContinue |
Sort-Object Name -Descending)
if ($all.Count -le $Keep) { return @() }
$gone = @($all | Select-Object -Skip $Keep)
foreach ($g in $gone) { Remove-Item -LiteralPath $g.FullName -Force -ErrorAction SilentlyContinue }
return $gone
}
# ---------------------------------------------------------------- build
function Invoke-Build($Mods) {
Write-Line (Get-Bar)
Write-Line " BadWolf 过滤器 构建"
Write-Line (Get-Bar)
$bad = @($Mods | Where-Object { $_.Errors.Count -gt 0 })
if ($bad.Count -gt 0) {
Write-Line " [中止] 以下模块存在编码/换行问题:"
foreach ($m in $bad) {
Write-Line " $($m.Name)"
foreach ($e in $m.Errors) { Write-Line " ! $e" }
}
return 1
}
$cat = Join-Bytes $Mods
$old = @()
if (Test-Path -LiteralPath $MergedPath) { $old = [System.IO.File]::ReadAllBytes($MergedPath) }
$same = $false
if ($old.Length -eq $cat.Length) {
$same = $true
for ($i = 0; $i -lt $cat.Length; $i++) { if ($cat[$i] -ne $old[$i]) { $same = $false; break } }
}
if ($same) {
Write-Line " 合并版已是最新,无需重建。"
Write-Line ""
return Invoke-Check $Mods
}
if (-not $NoBackup -and (Test-Path -LiteralPath $MergedPath)) {
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$bak = "$MergedPath.$stamp.bak"
Copy-Item -LiteralPath $MergedPath -Destination $bak -Force
Write-Line " 已备份 → $(Split-Path -Leaf $bak)"
if ($KeepBackups -gt 0) {
$gone = @(Remove-OldBackup $KeepBackups)
if ($gone.Count -gt 0) {
Write-Line " 轮转 删除旧备份 $($gone.Count) 份(保留最近 $KeepBackups 份):$(($gone | ForEach-Object { $_.Name }) -join ', ')"
} else {
Write-Line " 轮转 现有备份未超过 $KeepBackups 份,无需删除"
}
}
}
$tmp = "$MergedPath.tmp"
try {
[System.IO.File]::WriteAllBytes($tmp, $cat)
Move-Item -LiteralPath $tmp -Destination $MergedPath -Force
} finally {
if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue }
}
$new = [System.IO.File]::ReadAllBytes($MergedPath)
$lineN = ([regex]::Matches([System.Text.Encoding]::UTF8.GetString($new), "\n")).Count
Write-Line " 已写入 $(Format-N $new.Length) B / $(Format-N $lineN) 行 MD5 $(Get-Md5Hex $new)"
$okWrite = $false
if ($new.Length -eq $cat.Length) {
$okWrite = $true
for ($i = 0; $i -lt $cat.Length; $i++) { if ($new[$i] -ne $cat[$i]) { $okWrite = $false; break } }
}
if ($okWrite) { Write-Line " 写入校验通过(逐字节一致)" } else { Write-Line " [错误] 写入校验失败"; return 1 }
Write-Line ""
return Invoke-Check $Mods
}
# ---------------------------------------------------------------- map
function Invoke-Map($Mods, [string]$Q) {
if ($Q -match ':') {
$parts = $Q.Split(':')
$code = $parts[0]; $ln = 0
if (-not [int]::TryParse($parts[1], [ref]$ln)) { Write-Line "无法解析行号:$Q"; return 1 }
$m = $Mods | Where-Object { $_.Code -eq $code -or $_.Name.StartsWith($code) } | Select-Object -First 1
if ($null -eq $m) { Write-Line "找不到模块:$code"; return 1 }
if ($ln -lt 1 -or $ln -gt $m.LineCount) { Write-Line "$($m.Name) 只有 $($m.LineCount) 行"; return 1 }
$merged = $m.Start + $ln - 1
Write-Line "$($m.Name) 第 $ln 行 → 合并版 L$merged"
$lines = Get-Lines $m.Text
if ($ln -le $lines.Count) { Write-Line " $($lines[$ln - 1])" }
return 0
}
$ln = 0
if (-not [int]::TryParse($Q, [ref]$ln)) { Write-Line "无法解析行号:$Q"; return 1 }
$m = Find-ModuleOfLine $Mods $ln
if ($null -eq $m) { Write-Line "L$ln 超出范围(1-$($Mods[$Mods.Count-1].End))"; return 1 }
Write-Line "合并版 L$ln → $($m.Name) 第 $($ln - $m.Start + 1) 行"
return 0
}
# ---------------------------------------------------------------- export
function Invoke-Export($Mods) {
$cat = Join-Bytes $Mods
$end = $Mods[$Mods.Count - 1].End
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine('# BadWolf 过滤器行号映射表')
[void]$sb.AppendLine('')
[void]$sb.AppendLine("- 生成时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
[void]$sb.AppendLine("- 模块数:$($Mods.Count)")
[void]$sb.AppendLine("- 模块拼接:$(Format-N $cat.Length) 字节 / $(Format-N $end) 行 / MD5 ``$(Get-Md5Hex $cat)``")
[void]$sb.AppendLine('- 换算:`合并行 = 模块起始行 + 模块行 - 1`')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('| 模块 | 合并版起 | 合并版止 | 行数 | 字节 |')
[void]$sb.AppendLine('|---|---:|---:|---:|---:|')
foreach ($m in $Mods) {
[void]$sb.AppendLine("| $($m.Name) | $($m.Start) | $($m.End) | $($m.LineCount) | $(Format-N $m.ByteLen) |")
}
[void]$sb.AppendLine("| **合计** | 1 | $end | $end | $(Format-N $cat.Length) |")
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## 常用锚点')
[void]$sb.AppendLine('')
foreach ($m in $Mods) {
if (@('00','09','16','22','28','37','39') -contains $m.Code) {
[void]$sb.AppendLine("- ``$($m.Code)`` $($m.Name) → L$($m.Start)–L$($m.End)")
}
}
$path = Join-Path $Root $MapName
[System.IO.File]::WriteAllText($path, $sb.ToString(), (New-Object System.Text.UTF8Encoding($false)))
Write-Line "已导出 $path"
return 0
}
# ---------------------------------------------------------------- 主流程
try {
$moduleDir = Get-ModuleDir
} catch {
Write-Line "[错误] $($_.Exception.Message)"
exit 2
}
Write-Line (Get-Bar)
Write-Line " BadWolf 过滤器构建工具(PowerShell 版)"
Write-Line (Get-Bar)
Write-Line " 脚本位置 $PSCommandPath"
Write-Line " 模块目录 $moduleDir"
Write-Line " 合并版 $MergedPath"
$mods = Get-Modules $moduleDir
if ($mods.Count -eq 0) { Write-Line "[错误] 模块目录里没有 NN-*.txt 模块文件"; exit 2 }
if ($script:SkippedModules.Count -gt 0) {
Write-Host ""
Write-Host " [警告] 以下 txt 不符合「NN-名称.txt」,已跳过(不会进入合并版):" -ForegroundColor Yellow
foreach ($n in $script:SkippedModules) { Write-Host " - $n" -ForegroundColor Yellow }
Write-Host " 若是正式模块,请改名成 40-xxx.txt 这种形式。" -ForegroundColor Yellow
}
# 编号必须唯一:两个 16-xxx.txt 会让合并版多出一整段,且顺序取决于文件名,必须人工处理
$dupCodes = @($mods | Group-Object Code | Where-Object { $_.Count -gt 1 })
if ($dupCodes.Count -gt 0) {
Write-Line ""
Write-Line "[错误] 模块编号重复,已停止(避免把两份内容都拼进合并版):"
foreach ($g in $dupCodes) {
$names = ($g.Group | ForEach-Object { $_.Name }) -join ' / '
Write-Line (" [$($g.Name)] $names")
}
Write-Line " 处理办法:把多余的那份移出模块目录,或改成未占用的编号。"
exit 2
}
switch ($Command.ToLowerInvariant()) {
'build' { exit (Invoke-Build $mods) }
'map' { if ([string]::IsNullOrWhiteSpace($Arg)) { Write-Line "用法:build.bat map 1939 或 build.bat map 16:139"; exit 2 }; exit (Invoke-Map $mods $Arg) }
'export' { exit (Invoke-Export $mods) }
'check' { exit (Invoke-Check $mods) }
default { Write-Line "[错误] 未知命令:$Command(可用 check / build / map / export)"; exit 2 }
}