874 lines
38 KiB
PowerShell
874 lines
38 KiB
PowerShell
#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 }
|
||
}
|