#requires -Version 5.1 <# BadWolf 过滤器构建脚本 build.ps1 v1.8 ------------------------------------------------------------------ 用途:把模块目录下的 txt 按文件名排序拼接成 BadWolf-LootFilter.txt, 并完成一致性校验与行号换算。纯 PowerShell,无需安装任何软件。 v1.8 相对 v1.7 的变化 ------------------------------------------------------------------ · 自检新增「别名互为子串」校验 → 「错误」。 硬规则:别名名不得是另一别名名的子串(必须严格不同)。引擎按子串 扫描别名表,短名会抢先命中长名,把长名切成「前缀残渣 + 短名」, 残渣是无效 token → 整条条件恒假,静默失效,原 check 完全查不出。 实例(2026-10-30 修复):裸名 CAST_4_1 / DMG_4_1 是 SHIELD_DMG_4_1、 AMU_DMG_4_1、MELEE_DMG_4_1、GLOVE_DMG_4_1 等 24 个别名的子串 —— 盔甲档位用裸名所以正常,其余部位的长名全部被吃掉。 修法:给裸名加类型前缀(CAST_4_1 → CHEST_CAST_4_1)。 v1.7 相对 v1.6 的变化 ------------------------------------------------------------------ · 自检新增 3 条校验,全部来自 2026-10-30 的全库手工审计,都是 「游戏内静默失效、check 原本查不出」的类型: A) STAT 编号白名单 —— 读 ItemStatCost.txt 的 Id 列,条件里出现 STAT 而 n 不在表内 → 「错误」。写错的编号不会报错,只会让 该条件恒为 0、规则永不命中,肉眼几乎查不出来。 B) PREFIX / SUFFIX 编号越界 —— 与 MagicPrefix / MagicSuffix 的 数据行数比对(当前上界 990 / 1043)→ 「错误」。支持 = 与 ~ 两种写法。 C) 同条件 + 模板只差配色 + 都带 CONTINUE —— 「提示」。这类规则会让 同一件物品把同一段内容并排显示两遍,而既有的「完全重复规则」 (要求模板一字不差)与「死规则」(要求前一条不带 CONTINUE) 都抓不到它。★ 判据必须收在「只差配色」:模板内容不同的同条件组 绝大多数是**分段拼接**(箭袋「升级后需求:(」+ 数值 + ")", 护身符/地图的多行说明等),放开判据会立刻膨胀成 66 组噪音。 数据表位置按 PD2_EXCEL_DIR → build.ps1 同目录 excel\ → 游戏 Data\global\excel → 桌面 data\global\excel 依次探测;找不到就跳过 这两条校验并给提示,不阻断构建。 ★ 不要指向 ZYLPD2\Data\global\magicprefix.txt(911 条残表)。 v1.6 相对 v1.5 的变化 ------------------------------------------------------------------ · 「非法占位符」放行已定义别名:`%别名%` 不再判错误,改判「提示」。 原因:别名是**条件宏**、不是文本占位符——三个过滤器(BadWolf 479 个、 Kryszard 160 个)中 `%别名%` 形态的实际用法为 0,别名定义体也全部是 条件表达式,没有文本型别名。所以模板里写 `%别名%` 在游戏内只会原样 显示成 `%别名%`。 处理:错误降为提示,并明确写出「别名是条件宏,模板里不会被替换成文本」, 既不再误报,也不会让真正的显示错误被静默吞掉。 实现:扫描完成后统一判定($badPh 里名字若在 $aliasNames 内则移入 $aliasPh)—— 别名在文件中的定义位置有先有后,必须等全量收集完再判,不能边扫边判。 v1.5 相对 v1.4 的变化 ------------------------------------------------------------------ · 自检新增「别名 0 引用」检查,补上 v1.4 留下的盲区。 ★ 别名与公式的引用语法完全不同:别名在条件里是**裸名** (`!KNOWN_CODES`、`(SHIELD_SKILL_5_1 OR ...)`),只有公式才是 `%FORMULA名%`。用 `%别名名%` 去统计会全部漏检——2026-10-30 就因此 把 6 个盾牌星级别名误判为全死(实际都在 34 模块 L38/L39 使用)。 实现:复用 $tokRe 抓全大写 token 再查字典,480 个别名 × 4,470 行 只需一次线性扫描,不逐名做正则。 排除:值为裸 TRUE / FALSE 的条目(现 105 个)是设置菜单的开关与 分组标题(JUST_TAG1~4 之类),本就不需要被任何规则引用。 级别:「提示」,不阻断构建。 v1.4 相对 v1.3 的变化 ------------------------------------------------------------------ · 「公式 lint」由 3 条扩到 5 条,依据 2026-10-30 游戏内四轮实测: 0) 公式名合法性 —— 名字必须匹配 ^[A-Za-z_]+$。实测 Formula[TESTA] 的 %FORMULATESTA% 正常出 7,Formula[TEST1] 的 %FORMULATEST1% 原样输出文字 → 带数字会让占位符 静默查不到值。「错误」 4) 公式体含 % —— 实测 %FORMULATESTA%*3 编译不过、该公式作废 (% 只对 ItemDisplay 模板生效)。「错误」 · 自检新增「公式交叉引用」:某条公式的表达式里出现另一个公式名 (如 Formula[ZZ]: XPA+1)直接「错误」。实测裸名引用 / %FORMULA…% 包裹 / $f() / Alias 宏四条路全不通,且失败不报错、不连坐其它公式——只能靠构建期拦。 · 结论性事实:Formula 之间完全隔离,69 个公式必须保持自包含; 「抽公共子公式省体积」这条路不存在,不要再尝试。 v1.3 相对 v1.2 的变化 ------------------------------------------------------------------ · 新增三条「公式 lint」,只作用于 Formula[...] 行,把 2026-10-30 审计 发现的问题变成自动拦截(先剥掉行尾 // 注释,注释里的中文不参与): 1) IF 元数 —— 按顶层逗号切参数,必须正好 3 个;MULTI, 的 双参逗号是变量自身的一部分,先掩码再切,不算分隔符。 不满足则「提示」(build 不中断)。 2) AND/OR 单参 —— 只有一个参数时等价于给表达式加括号,写法冗余; 「提示」。 3) 公式体污染 —— 出现非 ASCII / 单等号(应为 ==)/ ~(BETWEEN 只能 用在 ItemDisplay 条件)→ 直接「错误」,构建不通过。 · 第 4 节自检摘要新增一行「公式 lint 错误/提示」计数。 · build 在写盘前先跑同样的错误级检查:有 lint 错误就中止,不会把 错公式写进合并版(提示级不阻断)。 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) # 兼容「模块与脚本平铺在同一目录」的布局(仓库里可能没有子文件夹) $flat = @(Get-ChildItem -LiteralPath $Root -Filter '*.txt' -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^\d{2}-' }).Count if ($flat -ge 10) { return $Root } 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, "(?; Max = <最大合法编号> } # Id 仅在给了 -IdCol 时填(ItemStatCost 用 Id 列) # Max = 数据行数 - 1,即编号上界(MagicPrefix 992 行含表头 → 编号 0..990) function Get-ExcelMeta([string]$Dir, [string]$File, [string]$IdCol) { $rows = Read-ExcelRows (Join-Path $Dir $File) if ($null -eq $rows) { return $null } $res = @{ Id = $null; Max = ($rows.Count - 2) } if ($IdCol) { $hdr = $rows[0] $ci = -1 for ($k = 0; $k -lt $hdr.Count; $k++) { if ($hdr[$k].Trim() -eq $IdCol) { $ci = $k; break } } if ($ci -ge 0) { $h = @{} for ($r = 1; $r -lt $rows.Count; $r++) { $row = $rows[$r] if ($row.Count -le $ci) { continue } $v = 0 if ([int]::TryParse($row[$ci].Trim(), [ref]$v)) { $h[$v] = $true } } $res.Id = $h } } return $res } # ---------------------------------------------------------------- 孤立 % / 未闭合 % # 半截百分号不会让过滤器报错,只会吞掉颜色或把 %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() } # ---------------------------------------------------------------- 公式 lint # 五条轻量规则,只作用于 Formula[...] 行。入口先切掉行尾 // 注释: # 注释里写中文是合法的,代码体里写中文才是错的。 # 0 公式名 必须匹配 ^[A-Za-z_]+$(错误) # 2026-10-30 游戏内实测:名字带数字时 %FORMULA名字% 查不到值, # 占位符原样输出文字、不报错、不连坐其它公式——纯静默失效, # 所以必须在构建期拦死 # 1 IF 元数 顶层逗号切参数必须正好 3 个(提示) # MULTI, 的双参逗号属于变量自身,先整体掩码再切 # 2 AND/OR 单参 只有 1 个参数(提示)——等价于给表达式加括号,写法冗余 # 3 公式体污染 非 ASCII / 单等号(应为 ==)/ ~(BETWEEN 只能用在 # ItemDisplay 条件)→ 错误,构建不通过 # 4 公式体含 % 错误。同一轮实测:公式之间完全隔离,裸名引用、%FORMULA…% # 包裹、Alias 宏四条路全不通。% 只对 ItemDisplay 模板生效, # 写进公式体必然让该条公式编译失败 # 数「顶层逗号」得出参数个数。注意不要返回数组:PS 里 `return ,$arr` 再被 @() 包裹 # 会变成嵌套数组,Count 恒为 1(本轮踩过)。直接返回整数最省心。 function Get-TopLevelArgCount([string]$s) { if ($s.Trim() -eq '') { return 0 } $n = 1 $depth = 0 foreach ($ch in $s.ToCharArray()) { if ($ch -eq '(') { $depth++ } elseif ($ch -eq ')') { $depth-- } elseif ($ch -eq ',' -and $depth -eq 0) { $n++ } } return $n } function Get-FormulaLint([string]$Body, [string]$Name = '') { $res = New-Object System.Collections.ArrayList $code = $Body $cut = $code.IndexOf('//') if ($cut -ge 0) { $code = $code.Substring(0, $cut) } # ---- 规则 0:公式名只允许字母和下划线(错误) # 实测:Formula[TESTA] 的 %FORMULATESTA% 正常,Formula[TEST1] 的 %FORMULATEST1% # 原样输出。名字里带数字会让占位符查不到值,且完全无声。 if ($Name -ne '' -and $Name -notmatch '^[A-Za-z_]+$') { [void]$res.Add([pscustomobject]@{ Level = 'E'; Msg = "公式名 '$Name' 含非法字符(只允许字母和下划线)。带数字会让 %FORMULA$Name% 静默失效" }) } # ---- 规则 4:公式体内不能出现 % (错误) # 实测:%FORMULATESTA%*3 这类写法编译不过,该公式作废。% 仅对 ItemDisplay 模板生效。 if ($code.Contains('%')) { $qp = $code.IndexOf('%') $qa = [Math]::Max(0, $qp - 16) $ql = [Math]::Min(36, $code.Length - $qa) [void]$res.Add([pscustomobject]@{ Level = 'E'; Msg = "公式体内出现 %(公式之间不能互相引用,%FORMULA…% 只在 ItemDisplay 模板里生效):$($code.Substring($qa, $ql).Trim())" }) } # ---- 规则 3:非 ASCII / 单等号 / ~ (错误) $nonAscii = $null foreach ($ch in $code.ToCharArray()) { if ([int]$ch -gt 127) { $nonAscii = $ch; break } } if ($null -ne $nonAscii) { [void]$res.Add([pscustomobject]@{ Level = 'E'; Msg = "公式体内出现非 ASCII 字符 '$nonAscii'(中文请写在 // 注释里)" }) } $eqRe = [regex]'(?=])=(?!=)' $em = $eqRe.Match($code) if ($em.Success) { $a = [Math]::Max(0, $em.Index - 16) $l = [Math]::Min(36, $code.Length - $a) [void]$res.Add([pscustomobject]@{ Level = 'E'; Msg = "公式体内出现单等号(比较应写 ==):$($code.Substring($a, $l).Trim())" }) } if ($code.Contains('~')) { $pp = $code.IndexOf('~') $a = [Math]::Max(0, $pp - 16) $l = [Math]::Min(36, $code.Length - $a) [void]$res.Add([pscustomobject]@{ Level = 'E'; Msg = "公式体内出现 ~(BETWEEN 只用于 ItemDisplay 条件,公式请写 >= / <=):$($code.Substring($a, $l).Trim())" }) } # ---- 规则 1 / 2:IF 元数、AND·OR 单参数(提示) $multiRe = [regex]'(?i)MULTI\d+\s*,\s*\d+' $callRe = [regex]'(?i)\b(IF|AND|OR)\s*\(' foreach ($m in $callRe.Matches($code)) { $fn = $m.Groups[1].Value.ToUpperInvariant() $op = $m.Index + $m.Length - 1 $d = 0 $cl = -1 for ($k = $op; $k -lt $code.Length; $k++) { $c = $code[$k] if ($c -eq '(') { $d++ } elseif ($c -eq ')') { $d--; if ($d -eq 0) { $cl = $k; break } } } if ($cl -lt 0) { [void]$res.Add([pscustomobject]@{ Level = 'E'; Msg = "$fn( 括号未闭合" }) continue } $inner = $code.Substring($op + 1, $cl - $op - 1) $masked = $multiRe.Replace($inner, 'MULTI_V') $n = Get-TopLevelArgCount $masked $a = [Math]::Max(0, $op - 4) $l = [Math]::Min(52, $code.Length - $a) $snip = $code.Substring($a, $l).Trim() if ($fn -eq 'IF') { if ($n -ne 3) { [void]$res.Add([pscustomobject]@{ Level = 'W'; Msg = "IF 参数数 $n(应为 3):$snip" }) } } elseif ($n -le 1) { [void]$res.Add([pscustomobject]@{ Level = 'W'; Msg = "$fn 只有 $n 个参数(等价于加括号,建议直接写 (...)):$snip" }) } } # 不要写 return , $res.ToArray():外层 @() 会得到「嵌套数组」, # foreach 拿到的元素是空数组,反而每个公式都吐一条空提示(本轮踩过) 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]'(?.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 # 写盘前先拦公式 lint 错误(非 ASCII / 单等号 / ~):过了才生成合并版, # 避免把明知有错的公式写进 BadWolf-LootFilter.txt $preLines = Get-Lines ([System.Text.Encoding]::UTF8.GetString($cat)) $preFormulaRe = [regex]'^Formula\[([^\]]+)\]:(.*)$' $blockers = New-Object System.Collections.ArrayList for ($i = 0; $i -lt $preLines.Count; $i++) { $pf = $preFormulaRe.Match($preLines[$i]) if (-not $pf.Success) { continue } foreach ($q in @(Get-FormulaLint $pf.Groups[2].Value $pf.Groups[1].Value)) { if ($q.Level -eq 'E') { [void]$blockers.Add("L$($i + 1) 公式 $($pf.Groups[1].Value) $($q.Msg)") } } } if ($blockers.Count -gt 0) { Write-Line " [中止] 公式 lint 发现 $($blockers.Count) 项错误,未写入合并版:" foreach ($b in ($blockers | Select-Object -First 20)) { Write-Line " x $b" } return 1 } $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 } }