S14V2.08
This commit is contained in:
parent
00c561a642
commit
3640c6dc42
555
build/build.ps1
555
build/build.ps1
|
|
@ -1,10 +1,96 @@
|
|||
#requires -Version 5.1
|
||||
<#
|
||||
BadWolf 过滤器构建脚本 build.ps1 v1.2
|
||||
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> 而 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<n>,<l> 的
|
||||
双参逗号是变量自身的一部分,先掩码再切,不算分隔符。
|
||||
不满足则「提示」(build 不中断)。
|
||||
2) AND/OR 单参 —— 只有一个参数时等价于给表达式加括号,写法冗余;
|
||||
「提示」。
|
||||
3) 公式体污染 —— 出现非 ASCII / 单等号(应为 ==)/ ~(BETWEEN 只能
|
||||
用在 ItemDisplay 条件)→ 直接「错误」,构建不通过。
|
||||
· 第 4 节自检摘要新增一行「公式 lint 错误/提示」计数。
|
||||
· build 在写盘前先跑同样的错误级检查:有 lint 错误就中止,不会把
|
||||
错公式写进合并版(提示级不阻断)。
|
||||
|
||||
v1.2 相对 v1.1 的变化
|
||||
------------------------------------------------------------------
|
||||
· 自检新增:孤立 % / 未闭合 % 扫描(半截百分号会吞颜色或漏出明文)
|
||||
|
|
@ -237,6 +323,67 @@ function Get-CodeWhitelist {
|
|||
return , $set
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- 数据表读取(v1.7 新增)
|
||||
# 用途:给「STAT 编号白名单」「词缀编号越界」两条校验提供权威数据。
|
||||
# 为什么必须查表:编号写错时游戏内是**静默失效**——该条件恒为 0,规则永不命中,
|
||||
# 不报错、不连坐,肉眼极难发现。2026-10-30 手工审计才把 139 种 STAT 全核了一遍。
|
||||
# 表目录按优先级探测,都找不到就跳过这两条校验(只给提示,不阻断构建):
|
||||
# 1. 环境变量 PD2_EXCEL_DIR
|
||||
# 2. build.ps1 同目录下的 excel\
|
||||
# 3. 游戏目录 ..\..\Data\global\excel
|
||||
# 4. 桌面 data\global\excel
|
||||
# ★ 不要读 ZYLPD2\Data\global\magicprefix.txt:那是 911 条残表,比正式表少 79 条。
|
||||
function Get-ExcelDir {
|
||||
$cands = New-Object System.Collections.ArrayList
|
||||
if ($env:PD2_EXCEL_DIR) { [void]$cands.Add($env:PD2_EXCEL_DIR) }
|
||||
[void]$cands.Add((Join-Path $Root 'excel'))
|
||||
[void]$cands.Add((Join-Path $Root '..\..\Data\global\excel'))
|
||||
$desk = [Environment]::GetFolderPath('Desktop')
|
||||
if ($desk) { [void]$cands.Add((Join-Path $desk 'data\global\excel')) }
|
||||
foreach ($d in $cands) {
|
||||
if (-not $d) { continue }
|
||||
if (Test-Path -LiteralPath (Join-Path $d 'ItemStatCost.txt')) { return $d }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Read-ExcelRows([string]$Path) {
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return $null }
|
||||
$txt = [System.IO.File]::ReadAllText($Path, [System.Text.Encoding]::UTF8)
|
||||
$rows = New-Object System.Collections.ArrayList
|
||||
foreach ($l in ($txt -split "`r?`n")) {
|
||||
if ($l.Trim() -eq '') { continue }
|
||||
[void]$rows.Add(($l -split "`t"))
|
||||
}
|
||||
if ($rows.Count -lt 2) { return $null }
|
||||
return , $rows.ToArray()
|
||||
}
|
||||
|
||||
# 返回 @{ Id = <编号哈希>; 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% 的区间,再逐个检查剩下的 %:
|
||||
|
|
@ -300,6 +447,115 @@ function Get-PercentFault([string[]]$Lines) {
|
|||
return $res.ToArray()
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- 公式 lint
|
||||
# 五条轻量规则,只作用于 Formula[...] 行。入口先切掉行尾 // 注释:
|
||||
# 注释里写中文是合法的,代码体里写中文才是错的。
|
||||
# 0 公式名 必须匹配 ^[A-Za-z_]+$(错误)
|
||||
# 2026-10-30 游戏内实测:名字带数字时 %FORMULA名字% 查不到值,
|
||||
# 占位符原样输出文字、不报错、不连坐其它公式——纯静默失效,
|
||||
# 所以必须在构建期拦死
|
||||
# 1 IF 元数 顶层逗号切参数必须正好 3 个(提示)
|
||||
# MULTI<n>,<l> 的双参逗号属于变量自身,先整体掩码再切
|
||||
# 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
|
||||
|
|
@ -318,8 +574,14 @@ function Invoke-SelfCheck([string[]]$Lines, $Mods) {
|
|||
$continueCount = 0
|
||||
$phSeen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
$badPh = @{}
|
||||
# 以 %别名% 形式出现的名字:别名是条件宏、不是文本占位符,游戏内不会替换成文字。
|
||||
# 因此不判「非法占位符」(那是错误),只作提示,避免写错别名导致界面出现裸 %名%。
|
||||
$aliasPh = @{}
|
||||
$filtlvl = 0
|
||||
$trail = New-Object System.Collections.ArrayList
|
||||
$lintErr = 0
|
||||
$lintWarn = 0
|
||||
$lintShown = 0
|
||||
|
||||
for ($i = 0; $i -lt $Lines.Count; $i++) {
|
||||
$ln = $i + 1
|
||||
|
|
@ -362,7 +624,23 @@ function Invoke-SelfCheck([string[]]$Lines, $Mods) {
|
|||
}
|
||||
}
|
||||
$mf = $formulaRe.Match($s)
|
||||
if ($mf.Success) { $formulaCount++; [void]$formulaNames.Add($mf.Groups[1].Value) }
|
||||
if ($mf.Success) {
|
||||
$formulaCount++
|
||||
$fkey = $mf.Groups[1].Value
|
||||
[void]$formulaNames.Add($fkey)
|
||||
foreach ($q in @(Get-FormulaLint $mf.Groups[2].Value $fkey)) {
|
||||
if ($q.Level -eq 'E') {
|
||||
$lintErr++
|
||||
[void]$errors.Add("L$ln 公式 $fkey $($q.Msg)")
|
||||
} else {
|
||||
$lintWarn++
|
||||
if ($lintShown -lt 12) {
|
||||
$lintShown++
|
||||
[void]$warns.Add("L$ln 公式 $fkey $($q.Msg)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$continueCount += ([regex]::Matches($s, '%CONTINUE%')).Count
|
||||
|
||||
|
|
@ -392,12 +670,40 @@ function Invoke-SelfCheck([string[]]$Lines, $Mods) {
|
|||
if ($seenAlias[$k] -gt 1) { [void]$errors.Add("别名重复定义:$k") }
|
||||
}
|
||||
|
||||
# 非法占位符
|
||||
# 非法占位符(前缀是已定义别名的不算非法:记入 $aliasPh 作提示,见下)
|
||||
$aliasSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
foreach ($a in $aliasNames) { [void]$aliasSet.Add($a) }
|
||||
|
||||
# 别名互为子串(v1.8 新增)—— 命名红线:别名名不得是另一别名名的子串
|
||||
# 引擎按子串扫描别名表,短名会抢先命中长名,把长名切成「前缀残渣 + 短名」,
|
||||
# 残渣是无效 token → 整条条件恒假,静默失效、原 check 完全查不出。
|
||||
# 实例:裸名 DMG_4_1 是 SHIELD_DMG_4_1 / AMU_DMG_4_1 / MELEE_DMG_4_1 … 的子串。
|
||||
$aliasByLen = @($aliasSet | Sort-Object -Property @{Expression={$_.Length}}, @{Expression={$_}})
|
||||
for ($i = 0; $i -lt $aliasByLen.Count; $i++) {
|
||||
$shortName = $aliasByLen[$i]
|
||||
for ($j = $i + 1; $j -lt $aliasByLen.Count; $j++) {
|
||||
$longName = $aliasByLen[$j]
|
||||
if ($longName.Length -le $shortName.Length) { continue }
|
||||
if ($longName.IndexOf($shortName, [System.StringComparison]::Ordinal) -ge 0) {
|
||||
[void]$errors.Add("别名互为子串:$shortName 是 $longName 的一部分(短名会抢先命中,长名失效)")
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in ($badPh.Keys | Sort-Object)) {
|
||||
if ($aliasSet.Contains($k)) {
|
||||
$aliasPh[$k] = $badPh[$k]
|
||||
continue
|
||||
}
|
||||
$lns = ($badPh[$k] | Select-Object -First 8) -join ','
|
||||
[void]$errors.Add("非法占位符 %$k%($($badPh[$k].Count) 处,行 $lns)")
|
||||
}
|
||||
|
||||
# 别名写成 %别名% 形态:不算错误,但游戏内不会被替换成文字,只会原样显示 %别名%
|
||||
foreach ($k in ($aliasPh.Keys | Sort-Object)) {
|
||||
$lns = ($aliasPh[$k] | Select-Object -First 6) -join ','
|
||||
[void]$warns.Add("别名以占位符形态出现 %$k%($($aliasPh[$k].Count) 处,行 $lns):别名是条件宏,模板里不会被替换成文本")
|
||||
}
|
||||
|
||||
# 未定义标识(只判别名风格:含下划线或数字)
|
||||
$defined = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
foreach ($n in $aliasNames) { [void]$defined.Add($n.ToUpperInvariant()) }
|
||||
|
|
@ -450,6 +756,124 @@ function Invoke-SelfCheck([string[]]$Lines, $Mods) {
|
|||
}
|
||||
if ($deadCount -gt 12) { [void]$warns.Add("... 另有 $($deadCount - 12) 条死规则") }
|
||||
|
||||
# ---------------------------------------------------------------- 编号体系校验(v1.7 新增)
|
||||
# 三条都是「游戏内静默失效、check 原本查不出」的类型:
|
||||
# A STAT 编号不在 ItemStatCost → 条件恒为 0,规则永不命中
|
||||
# B PREFIX / SUFFIX 编号越界 → 同上
|
||||
# C 同条件 + 都带 CONTINUE → 一件物品并排输出多条标题
|
||||
# 扫描对象是全部非注释行(不只是 ItemDisplay),这样别名体内的编号也能覆盖到。
|
||||
$excelDir = Get-ExcelDir
|
||||
$statIds = $null
|
||||
$pfMax = -1
|
||||
$sfMax = -1
|
||||
if ($null -eq $excelDir) {
|
||||
[void]$warns.Add('未找到 ItemStatCost.txt / MagicPrefix.txt,STAT 与词缀编号校验已跳过(可用环境变量 PD2_EXCEL_DIR 指定 excel 目录)')
|
||||
} else {
|
||||
$mc = Get-ExcelMeta $excelDir 'ItemStatCost.txt' 'Id'
|
||||
if ($null -ne $mc -and $null -ne $mc.Id) { $statIds = $mc.Id }
|
||||
$mp = Get-ExcelMeta $excelDir 'MagicPrefix.txt' ''
|
||||
if ($null -ne $mp) { $pfMax = $mp.Max }
|
||||
$ms = Get-ExcelMeta $excelDir 'MagicSuffix.txt' ''
|
||||
if ($null -ne $ms) { $sfMax = $ms.Max }
|
||||
}
|
||||
|
||||
$statBad = @{}
|
||||
$pfBad = @{}
|
||||
$sfBad = @{}
|
||||
$statN = 0
|
||||
if ($null -ne $statIds) { $statN = $statIds.Count }
|
||||
if ($null -ne $statIds -or $pfMax -ge 0 -or $sfMax -ge 0) {
|
||||
$pfRe = [regex]'\bPREFIX(?:=(\d+)|~(\d+)-(\d+))'
|
||||
$sfRe = [regex]'\bSUFFIX(?:=(\d+)|~(\d+)-(\d+))'
|
||||
for ($i = 0; $i -lt $Lines.Count; $i++) {
|
||||
$sl = $Lines[$i]
|
||||
if ($sl.TrimStart().StartsWith('//')) { continue }
|
||||
if ($null -ne $statIds) {
|
||||
foreach ($m in [regex]::Matches($sl, '\bSTAT(\d+)\b')) {
|
||||
$v = [int]$m.Groups[1].Value
|
||||
if ($statIds.ContainsKey($v)) { continue }
|
||||
if (-not $statBad.ContainsKey($v)) { $statBad[$v] = New-Object System.Collections.ArrayList }
|
||||
[void]$statBad[$v].Add($i + 1)
|
||||
}
|
||||
}
|
||||
if ($pfMax -ge 0) {
|
||||
foreach ($m in $pfRe.Matches($sl)) {
|
||||
$vs = @()
|
||||
if ($m.Groups[1].Success) { $vs = @([int]$m.Groups[1].Value) }
|
||||
else { $vs = @([int]$m.Groups[2].Value, [int]$m.Groups[3].Value) }
|
||||
foreach ($v in $vs) {
|
||||
if ($v -le $pfMax) { continue }
|
||||
if (-not $pfBad.ContainsKey($v)) { $pfBad[$v] = New-Object System.Collections.ArrayList }
|
||||
[void]$pfBad[$v].Add($i + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($sfMax -ge 0) {
|
||||
foreach ($m in $sfRe.Matches($sl)) {
|
||||
$vs = @()
|
||||
if ($m.Groups[1].Success) { $vs = @([int]$m.Groups[1].Value) }
|
||||
else { $vs = @([int]$m.Groups[2].Value, [int]$m.Groups[3].Value) }
|
||||
foreach ($v in $vs) {
|
||||
if ($v -le $sfMax) { continue }
|
||||
if (-not $sfBad.ContainsKey($v)) { $sfBad[$v] = New-Object System.Collections.ArrayList }
|
||||
[void]$sfBad[$v].Add($i + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($v in ($statBad.Keys | Sort-Object)) {
|
||||
$lns = ($statBad[$v] | Select-Object -First 4) -join ','
|
||||
[void]$errors.Add("STAT$v 不在 ItemStatCost 中($($statBad[$v].Count) 处,行 $lns)")
|
||||
}
|
||||
foreach ($v in ($pfBad.Keys | Sort-Object)) {
|
||||
$lns = ($pfBad[$v] | Select-Object -First 4) -join ','
|
||||
[void]$errors.Add("PREFIX=$v 越界(MagicPrefix 最大编号 $pfMax,$($pfBad[$v].Count) 处,行 $lns)")
|
||||
}
|
||||
foreach ($v in ($sfBad.Keys | Sort-Object)) {
|
||||
$lns = ($sfBad[$v] | Select-Object -First 4) -join ','
|
||||
[void]$errors.Add("SUFFIX=$v 越界(MagicSuffix 最大编号 $sfMax,$($sfBad[$v].Count) 处,行 $lns)")
|
||||
}
|
||||
}
|
||||
|
||||
# C 同条件 + 都带 CONTINUE → 并排输出多条标题。
|
||||
# 与既有两个检查正好互补、零重叠:
|
||||
# 「完全重复规则」抓 条件+模板全同;「死规则」抓 同条件但前面那条不带 CONTINUE;
|
||||
# 这里抓 同条件、都带 CONTINUE、模板又不完全相同的那一类(2026-10-30 审计的 02/03 段
|
||||
# 7 条就是这种:模板只差一个 %GRAY%,两边都带 CONTINUE,前两个检查都漏)。
|
||||
$condMap = @{}
|
||||
foreach ($r in $rules) {
|
||||
$k = ($r.Cond -replace '\s+', ' ').Trim().ToUpperInvariant()
|
||||
if ($k -eq '') { continue }
|
||||
if (-not $condMap.ContainsKey($k)) { $condMap[$k] = New-Object System.Collections.ArrayList }
|
||||
[void]$condMap[$k].Add($r)
|
||||
}
|
||||
$sameCont = 0
|
||||
$colorRe = [regex]('%(' + ($Colors -join '|') + ')%')
|
||||
foreach ($k in ($condMap.Keys | Sort-Object)) {
|
||||
$grp = $condMap[$k]
|
||||
if ($grp.Count -lt 2) { continue }
|
||||
$allCont = $true
|
||||
$outs = @{}
|
||||
$lns = New-Object System.Collections.ArrayList
|
||||
foreach ($g in $grp) {
|
||||
if (-not $g.Out.ToUpperInvariant().Contains('%CONTINUE%')) { $allCont = $false; break }
|
||||
[void]$lns.Add($g.Line)
|
||||
# 归一化:先剥掉配色占位符再比。判据必须是「只差颜色」——
|
||||
# 模板内容不同的绝大多数是**分段拼接**(前一段以「升级后需求:(」收尾、
|
||||
# 后一段接数值、末段以 ")}" 闭口;护身符三段说明、地图两行信息同理),
|
||||
# 那是正常设计,一旦误报就变成 66 组噪音(首版实测)。
|
||||
$no = $colorRe.Replace($g.Out, '')
|
||||
$outs[($no -replace '\s+', ' ').Trim()] = $true
|
||||
}
|
||||
if (-not $allCont) { continue }
|
||||
if ($outs.Count -ge 2) { continue }
|
||||
$sameCont++
|
||||
$cs = $k
|
||||
if ($cs.Length -gt 70) { $cs = $cs.Substring(0, 70) }
|
||||
[void]$warns.Add("同条件近似重复 L$($lns -join ',')(模板只差配色、都带 CONTINUE,同一件物品会并排显示两遍):$cs")
|
||||
}
|
||||
|
||||
|
||||
# 物品代码白名单
|
||||
$wl = Get-CodeWhitelist
|
||||
$wlCount = 0
|
||||
|
|
@ -499,6 +923,92 @@ function Invoke-SelfCheck([string[]]$Lines, $Mods) {
|
|||
[void]$warns.Add("Formula 定义后 0 引用:$($fUnused -join '、')")
|
||||
}
|
||||
|
||||
# 别名 0 引用(v1.5 新增)
|
||||
# ★ 别名与公式的引用语法完全不同:别名在条件里是**裸名**(如 `!KNOWN_CODES`、
|
||||
# `(SHIELD_SKILL_5_1 OR ...)`),只有公式才是 `%FORMULA名%`。曾经用 `%别名名%`
|
||||
# 去查别名引用,导致 6 个盾牌星级别名被误判为全死(实际都在 34 模块 L38/L39 使用)。
|
||||
# 实现:借用已有的 $tokRe 抓全大写 token 再查字典,避免 480×4470 次正则的开销。
|
||||
# 排除:值为裸 TRUE / FALSE 的条目是设置菜单的开关与分组标题(JUST_TAG1~4 之类),
|
||||
# 本就不需要被任何规则引用。
|
||||
$aliasBody = @{}
|
||||
for ($i = 0; $i -lt $Lines.Count; $i++) {
|
||||
$ca = $aliasRe.Match($Lines[$i])
|
||||
if (-not $ca.Success) { continue }
|
||||
$ab = $ca.Groups[2].Value
|
||||
$ac = $ab.IndexOf('//')
|
||||
if ($ac -ge 0) { $ab = $ab.Substring(0, $ac) }
|
||||
$aliasBody[$ca.Groups[1].Value] = $ab.Trim()
|
||||
}
|
||||
$aliasRef = @{}
|
||||
for ($i = 0; $i -lt $Lines.Count; $i++) {
|
||||
$s2 = $Lines[$i]
|
||||
if ($s2.TrimStart().StartsWith('//')) { continue }
|
||||
$ca = $aliasRe.Match($s2)
|
||||
$body2 = $s2
|
||||
$selfName = ''
|
||||
if ($ca.Success) { $body2 = $ca.Groups[2].Value; $selfName = $ca.Groups[1].Value }
|
||||
$cut2 = $body2.IndexOf('//')
|
||||
if ($cut2 -ge 0) { $body2 = $body2.Substring(0, $cut2) }
|
||||
foreach ($tm2 in $tokRe.Matches($body2)) {
|
||||
$t2 = $tm2.Groups[1].Value
|
||||
if ($t2 -eq $selfName) { continue }
|
||||
if ($aliasBody.ContainsKey($t2)) { $aliasRef[$t2] = $true }
|
||||
}
|
||||
}
|
||||
# $tokRe 只认 [A-Z] 开头的 token。名字以数字或下划线开头的别名(现存 `7CR_INFUSION`)
|
||||
# 永远抓不到,会被误判成 0 引用——这里对这少数几个名字单独补扫一遍。
|
||||
$oddNames = @($aliasBody.Keys | Where-Object { -not ($_ -cmatch '^[A-Z][A-Z0-9_]+$') })
|
||||
if ($oddNames.Count -gt 0) {
|
||||
for ($i = 0; $i -lt $Lines.Count; $i++) {
|
||||
$s3 = $Lines[$i]
|
||||
if ($s3.TrimStart().StartsWith('//')) { continue }
|
||||
$ca3 = $aliasRe.Match($s3)
|
||||
$b3 = if ($ca3.Success) { $ca3.Groups[2].Value } else { $s3 }
|
||||
$c3 = $b3.IndexOf('//')
|
||||
if ($c3 -ge 0) { $b3 = $b3.Substring(0, $c3) }
|
||||
foreach ($on in $oddNames) {
|
||||
if ($ca3.Success -and $ca3.Groups[1].Value -eq $on) { continue }
|
||||
$pat3 = '(?<![A-Za-z0-9_])' + [regex]::Escape($on) + '(?![A-Za-z0-9_])'
|
||||
if ([regex]::IsMatch($b3, $pat3)) { $aliasRef[$on] = $true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$aUnused = @()
|
||||
foreach ($an in ($aliasBody.Keys | Sort-Object)) {
|
||||
if ($aliasRef.ContainsKey($an)) { continue }
|
||||
$av = $aliasBody[$an].ToUpperInvariant()
|
||||
if ($av -eq 'TRUE' -or $av -eq 'FALSE') { continue }
|
||||
$aUnused += $an
|
||||
}
|
||||
if ($aUnused.Count -gt 0) {
|
||||
$aShow = $aUnused
|
||||
if ($aShow.Count -gt 10) { $aShow = $aShow[0..9] }
|
||||
$aMore = if ($aUnused.Count -gt 10) { "…(共 $($aUnused.Count) 个)" } else { '' }
|
||||
[void]$warns.Add("别名定义后 0 引用:$($aShow -join '、')$aMore")
|
||||
}
|
||||
|
||||
# 公式之间不可互相引用(2026-10-30 游戏内实测四条路全不通:裸名 / %FORMULA…% /
|
||||
# $f() / Alias 宏)。失败是静默的——该条公式作废、占位符原样输出、不报错。
|
||||
# 这里抓「公式体里出现另一个公式名」这种最容易犯的写法;公式名(SMA/XPA/C_HELM…)
|
||||
# 与 STAT/MULTI 等变量名不重叠,误报风险极低。
|
||||
for ($i = 0; $i -lt $Lines.Count; $i++) {
|
||||
$cf = $formulaRe.Match($Lines[$i])
|
||||
if (-not $cf.Success) { continue }
|
||||
$own = $cf.Groups[1].Value
|
||||
$cbody = $cf.Groups[2].Value
|
||||
$ccut = $cbody.IndexOf('//')
|
||||
if ($ccut -ge 0) { $cbody = $cbody.Substring(0, $ccut) }
|
||||
foreach ($fn2 in $formulaNames) {
|
||||
if ($fn2 -eq $own) { continue }
|
||||
$pat = '(?<![A-Za-z0-9_])' + [regex]::Escape($fn2) + '(?![A-Za-z0-9_])'
|
||||
if ([regex]::IsMatch($cbody, $pat)) {
|
||||
[void]$errors.Add("L$($i + 1) 公式 $own 的表达式里引用了公式 $fn2(公式之间不能互相引用,游戏内会静默失效)")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 版本行联动:版本行日期 vs 最晚模块改动
|
||||
$verRe = [regex]'更新于\s*(\d{2,4})[/-](\d{1,2})[/-](\d{1,2})'
|
||||
foreach ($l in $Lines) {
|
||||
|
|
@ -538,7 +1048,16 @@ function Invoke-SelfCheck([string[]]$Lines, $Mods) {
|
|||
Dead = $deadCount
|
||||
FRef = $fRef.Count
|
||||
FUnused = $fUnused.Count
|
||||
ARef = $aliasRef.Count
|
||||
AUnused = $aUnused.Count
|
||||
Pct = $pctFaults.Count
|
||||
LintE = $lintErr
|
||||
LintW = $lintWarn
|
||||
Excel = $excelDir
|
||||
StatN = $statN
|
||||
PfMax = $pfMax
|
||||
SfMax = $sfMax
|
||||
SameCont = $sameCont
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -681,14 +1200,23 @@ function Invoke-Check($Mods) {
|
|||
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.ARef)/$($chk.Aliases)(未引用 $($chk.AUnused),不含 TRUE/FALSE 开关项)"
|
||||
Write-Line " 符号 孤立 % / 未闭合 % $($chk.Pct) 处"
|
||||
if ($null -ne $chk.Excel) {
|
||||
Write-Line " 编号 STAT 白名单 $($chk.StatN) 个 | PREFIX <= $($chk.PfMax) | SUFFIX <= $($chk.SfMax) | 同条件近似重复 $($chk.SameCont) 组"
|
||||
} else {
|
||||
Write-Line " 编号 数据表未加载,STAT / 词缀编号校验已跳过"
|
||||
}
|
||||
$lintFlag = 'OK'
|
||||
if ($chk.LintE -gt 0) { $lintFlag = 'x' } elseif ($chk.LintW -gt 0) { $lintFlag = '!' }
|
||||
Write-Line " 公式 lint $lintFlag 错误 $($chk.LintE) / 提示 $($chk.LintW)(公式名 / 公式体 % / IF 元数 / AND·OR 单参 / 非 ASCII·单等号·~)"
|
||||
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 " 无错误(括号 / 占位符 / 别名重复 / 物品代码 / 公式引用)"
|
||||
Write-Line " 无错误(括号 / 占位符 / 别名重复 / 物品代码 / 公式引用 / 别名引用 / 公式 lint)"
|
||||
}
|
||||
if ($chk.Warns.Count -gt 0) {
|
||||
Write-Line ""
|
||||
|
|
@ -746,6 +1274,25 @@ function Invoke-Build($Mods) {
|
|||
}
|
||||
|
||||
$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) }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue