TH1/Tools/CheckSkillDataAssetsIntegrity.ps1

379 lines
13 KiB
PowerShell

param(
[string]$AssetPath = "Unity/Assets/BundleResources/DataAssets/SkillDataAssets.asset",
[string]$ExportPath = "Unity/Assets/BundleResources/Export/SkillDataAssets.asset",
[string]$MultilingualPath = "Unity/Assets/BundleResources/Export/Multilingual.asset",
[string]$MultilingualTxtPath = "Tools/MultilingualTxt.txt",
[switch]$CheckExport
)
$ErrorActionPreference = "Stop"
$repoRoot = git rev-parse --show-toplevel 2>$null
if (-not $repoRoot) {
throw "Not inside a git repository."
}
$repoRoot = [System.IO.Path]::GetFullPath($repoRoot.Trim())
$requiredSkillTypes = @(
296, 297, 298, 299,
300, 301, 302, 303, 304, 305, 306,
307, 308, 309, 310,
313, 314, 315, 316, 317, 318, 319, 320, 321, 322,
327, 328, 329,
330, 331, 332
)
$leakTokens = @(
"NotShow:",
"ShowOnUnitMono:",
"SkillIcon:",
"HasShowList:",
"SkillShowList:",
"skillPriority:",
"ReserveOnCarry:",
"ReserveLeaveCarry:",
"ReserveGiantUpgrade:",
"ReserveCommonTransform:",
"- SkillType:",
"SkillViewType:"
)
$requiredSkillIconGuids = @{
296 = "a994762e362347569aaac57eda527bbd"
297 = "a557b1e216994dbc9aba71daa4b6cd7f"
298 = "d0c8d1956592444eb96e533d0b0ec728"
299 = "6b94561a0cb3413499cb61ee8788d4e0"
330 = "a994762e362347569aaac57eda527bbd"
331 = "6f47452c0c494c579c2d939697037cd6"
332 = "4e8a40912d3d450c92996e337cad1067"
}
$knownBadTextTokens = @(
'\u5E76\u975E\u9644\u8FD1',
'\u518D\u9644\u8FD1',
([char]0x947E).ToString(),
([char]0x934F).ToString(),
(([char]0x6D93).ToString() + ([char]0x5D86).ToString() + ([char]0x6D86).ToString()),
([char]0x9286).ToString(),
([char]0xFFFD).ToString(),
([char]0x00C3).ToString(),
([char]0x00C2).ToString()
)
$requiredMultilingualSnippetsById = @{
21828 = '\u5E76\u4E3A\u9644\u8FD1\u53CB\u65B9'
21829 = '\u5728\u9644\u8FD1\u751F\u6210'
21857 = '\u5728\u9644\u8FD1\u751F\u6210'
}
function Resolve-RepoPath([string]$Path) {
if ([System.IO.Path]::IsPathRooted($Path)) {
return [System.IO.Path]::GetFullPath($Path)
}
return [System.IO.Path]::GetFullPath((Join-Path $repoRoot $Path))
}
function Read-TextUtf8([string]$Path) {
$fullPath = Resolve-RepoPath $Path
if (-not (Test-Path -LiteralPath $fullPath)) {
throw "File not found: $fullPath"
}
return [System.IO.File]::ReadAllText($fullPath, [System.Text.Encoding]::UTF8)
}
function Get-SkillEntries([string]$Path) {
$fullPath = Resolve-RepoPath $Path
if (-not (Test-Path -LiteralPath $fullPath)) {
throw "SkillDataAssets file not found: $fullPath"
}
$lines = Get-Content -LiteralPath $fullPath -Encoding UTF8
$entries = @()
$current = $null
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
if ($line -match '^ - SkillType: (\d+)') {
if ($current) {
$entries += [pscustomobject]$current
}
$current = [ordered]@{
SkillType = [int]$Matches[1]
Line = $i + 1
Text = $line + "`n"
}
continue
}
if ($current) {
if ($line -match '^ SkillViewTypeColorList:') {
$entries += [pscustomobject]$current
$current = $null
break
}
$current.Text += $line + "`n"
}
}
if ($current) {
$entries += [pscustomobject]$current
}
return $entries
}
function Get-UnityMetaGuidSet {
if ($script:UnityMetaGuidSet) {
return $script:UnityMetaGuidSet
}
$assetsPath = Resolve-RepoPath "Unity/Assets"
$script:UnityMetaGuidSet = @{}
Get-ChildItem -LiteralPath $assetsPath -Filter "*.meta" -Recurse -File | ForEach-Object {
$metaText = [System.IO.File]::ReadAllText($_.FullName, [System.Text.Encoding]::UTF8)
if ($metaText -match '(?m)^guid: ([0-9a-f]{32})') {
$script:UnityMetaGuidSet[$Matches[1]] = $_.FullName
}
}
return $script:UnityMetaGuidSet
}
function Get-SkillFieldValue($Entry, [string]$FieldName) {
$escapedField = [regex]::Escape($FieldName)
if ($Entry.Text -match "(?m)^ ${escapedField}: (.*)$") {
return $Matches[1].Trim()
}
return $null
}
function Get-SkillIconInfo($Entry) {
$iconValue = Get-SkillFieldValue $Entry "SkillIcon"
if ([string]::IsNullOrWhiteSpace($iconValue)) {
return $null
}
if ($iconValue -match '^\{fileID: ([^,}]+)(?:, guid: ([0-9a-f]{32}), type: ([0-9]+))?\}$') {
return [pscustomobject]@{
FileID = $Matches[1]
Guid = if ($Matches.Count -ge 3) { $Matches[2] } else { "" }
Type = if ($Matches.Count -ge 4) { $Matches[3] } else { "" }
}
}
throw "SkillType $($Entry.SkillType) has malformed SkillIcon field near line $($Entry.Line): $iconValue"
}
function Assert-NoSerializedLeakInSkillText([string]$Label, $Entries) {
foreach ($entry in $Entries) {
$entryLines = @($entry.Text -split "`n")
for ($i = 0; $i -lt $entryLines.Count; $i++) {
$line = $entryLines[$i]
if ($line -notmatch '^ (SkillName|SkillDesc):[ \t]*(.*)$') {
continue
}
$fieldName = $Matches[1]
$fieldText = $Matches[2]
$j = $i + 1
while ($j -lt $entryLines.Count) {
$nextLine = $entryLines[$j]
if ($nextLine -match '^ [A-Za-z_][A-Za-z0-9_]*:' -or
$nextLine -match '^ - SkillType:' -or
$nextLine -match '^ SkillViewTypeColorList:') {
break
}
$fieldText += "`n" + $nextLine
$j++
}
foreach ($token in $leakTokens) {
if ($fieldText.Contains($token)) {
throw "$Label SkillType $($entry.SkillType) $fieldName contains serialized field token '$token' near line $($entry.Line)."
}
}
foreach ($token in $knownBadTextTokens) {
if ($fieldText.Contains($token)) {
throw "$Label SkillType $($entry.SkillType) $fieldName contains known-bad text token '$token' near line $($entry.Line)."
}
}
}
}
}
function Assert-SkillIcons([string]$Label, $Entries) {
$metaGuids = Get-UnityMetaGuidSet
foreach ($entry in $Entries) {
$iconInfo = Get-SkillIconInfo $entry
if ($null -eq $iconInfo) {
throw "$Label SkillType $($entry.SkillType) has no SkillIcon field near line $($entry.Line)."
}
if ($iconInfo.FileID -ne "0") {
if ([string]::IsNullOrWhiteSpace($iconInfo.Guid)) {
throw "$Label SkillType $($entry.SkillType) has a non-empty SkillIcon fileID but no guid near line $($entry.Line)."
}
if (-not $metaGuids.ContainsKey($iconInfo.Guid)) {
throw "$Label SkillType $($entry.SkillType) SkillIcon guid $($iconInfo.Guid) has no .meta file under Unity/Assets."
}
}
$showOnUnitMono = Get-SkillFieldValue $entry "ShowOnUnitMono"
if ($showOnUnitMono -eq "1" -and ($iconInfo.FileID -eq "0" -or [string]::IsNullOrWhiteSpace($iconInfo.Guid))) {
throw "$Label SkillType $($entry.SkillType) is ShowOnUnitMono but has an empty SkillIcon near line $($entry.Line)."
}
if ($requiredSkillIconGuids.ContainsKey($entry.SkillType)) {
$expectedGuid = $requiredSkillIconGuids[$entry.SkillType]
if ($iconInfo.Guid -ne $expectedGuid) {
throw "$Label SkillType $($entry.SkillType) SkillIcon guid is $($iconInfo.Guid); expected $expectedGuid."
}
}
}
}
function Assert-SkillIconSync($SourceEntries, $ExportEntries) {
$sourceByType = @{}
foreach ($entry in $SourceEntries) {
$sourceByType[$entry.SkillType] = $entry
}
foreach ($exportEntry in $ExportEntries) {
if (-not $sourceByType.ContainsKey($exportEntry.SkillType)) {
continue
}
$sourceIcon = Get-SkillIconInfo $sourceByType[$exportEntry.SkillType]
$exportIcon = Get-SkillIconInfo $exportEntry
if ($sourceIcon.FileID -ne $exportIcon.FileID -or $sourceIcon.Guid -ne $exportIcon.Guid) {
throw "Export SkillType $($exportEntry.SkillType) SkillIcon does not match source. source=$($sourceIcon.FileID)/$($sourceIcon.Guid); export=$($exportIcon.FileID)/$($exportIcon.Guid)."
}
}
}
function Assert-SkillDataAsset([string]$Label, [string]$Path) {
$entries = @(Get-SkillEntries $Path)
if ($entries.Count -lt 290) {
throw "$Label SkillDataAssets has only $($entries.Count) skill rows; expected at least 290."
}
$counts = @{}
foreach ($entry in $entries) {
if (-not $counts.ContainsKey($entry.SkillType)) {
$counts[$entry.SkillType] = 0
}
$counts[$entry.SkillType]++
}
$duplicates = @($counts.Keys | Where-Object { $counts[$_] -gt 1 } | Sort-Object)
if ($duplicates.Count -gt 0) {
throw "$Label SkillDataAssets has duplicate SkillType rows: $($duplicates -join ', ')."
}
$missing = @($requiredSkillTypes | Where-Object { -not $counts.ContainsKey($_) })
if ($missing.Count -gt 0) {
throw "$Label SkillDataAssets is missing required SkillType rows: $($missing -join ', ')."
}
Assert-NoSerializedLeakInSkillText $Label $entries
Assert-SkillIcons $Label $entries
Write-Host "$Label SkillDataAssets OK: checked $($entries.Count) skill rows."
return $entries
}
function Get-MultilingualBlocksById([string]$Path) {
$text = Read-TextUtf8 $Path
$blocksById = @{}
$matches = [regex]::Matches($text, '(?ms)^ - ID: (\d+)\r?\n.*?(?=^ - ID: |\z)')
foreach ($match in $matches) {
$blocksById[[int]$match.Groups[1].Value] = $match.Value
}
return $blocksById
}
function Assert-ExportMultilingualReferences($ExportEntries, [string]$MultilingualAssetPath) {
$blocksById = Get-MultilingualBlocksById $MultilingualAssetPath
foreach ($entry in $ExportEntries) {
foreach ($fieldName in @("SkillName", "SkillDesc")) {
$fieldValue = Get-SkillFieldValue $entry $fieldName
if ($fieldValue -notmatch '^\d+$') {
continue
}
$id = [int]$fieldValue
if (-not $blocksById.ContainsKey($id)) {
throw "Export SkillType $($entry.SkillType) $fieldName references missing multilingual ID $id."
}
$block = $blocksById[$id]
if ($block -match '(?m)^ ZH:\s*$') {
throw "Export SkillType $($entry.SkillType) $fieldName references multilingual ID $id with empty ZH."
}
}
}
foreach ($id in $requiredMultilingualSnippetsById.Keys) {
if (-not $blocksById.ContainsKey($id)) {
throw "Export Multilingual.asset is missing required ID $id."
}
$snippet = $requiredMultilingualSnippetsById[$id]
if (-not $blocksById[$id].Contains($snippet)) {
throw "Export Multilingual.asset ID $id does not contain required snippet $snippet."
}
}
}
function Assert-NoSerializedLeakInTextFile([string]$Label, [string]$Path) {
$text = Read-TextUtf8 $Path
foreach ($token in $leakTokens) {
if ($text.Contains($token)) {
throw "$Label contains serialized field token '$token': $Path"
}
}
foreach ($token in $knownBadTextTokens) {
if ($text.Contains($token)) {
throw "$Label contains known-bad text token '$token': $Path"
}
}
if ($text -match '(^|!@#\$%)21861%\$#@!') {
throw "$Label still contains polluted multilingual ID 21861: $Path"
}
Write-Host "$Label OK: no serialized field leakage detected."
}
$sourceEntries = Assert-SkillDataAsset "Source" $AssetPath
if ($CheckExport) {
$exportEntries = Assert-SkillDataAsset "Export" $ExportPath
$sourceSet = @{}
foreach ($entry in $sourceEntries) {
$sourceSet[$entry.SkillType] = $true
}
$exportSet = @{}
foreach ($entry in $exportEntries) {
$exportSet[$entry.SkillType] = $true
}
$missingInExport = @($sourceSet.Keys | Where-Object { -not $exportSet.ContainsKey($_) } | Sort-Object)
if ($missingInExport.Count -gt 0) {
throw "Export SkillDataAssets is stale; missing source SkillType rows: $($missingInExport -join ', ')."
}
Assert-SkillIconSync $sourceEntries $exportEntries
Assert-ExportMultilingualReferences $exportEntries $MultilingualPath
Assert-NoSerializedLeakInTextFile "Export Multilingual.asset" $MultilingualPath
Assert-NoSerializedLeakInTextFile "Tools MultilingualTxt.txt" $MultilingualTxtPath
}