投降逻辑,联机广播逻辑等
This commit is contained in:
parent
ea7d6a5060
commit
2b9cf05f5a
9
Tools/ObfuscatedExceptionDecoder.bat
Normal file
9
Tools/ObfuscatedExceptionDecoder.bat
Normal file
@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set "SCRIPT=%~dp0ObfuscatedExceptionDecoder.ps1"
|
||||
where pwsh.exe >nul 2>nul
|
||||
if %errorlevel%==0 (
|
||||
pwsh.exe -NoProfile -ExecutionPolicy Bypass -STA -File "%SCRIPT%"
|
||||
) else (
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -STA -File "%SCRIPT%"
|
||||
)
|
||||
949
Tools/ObfuscatedExceptionDecoder.ps1
Normal file
949
Tools/ObfuscatedExceptionDecoder.ps1
Normal file
@ -0,0 +1,949 @@
|
||||
param(
|
||||
[string]$MappingPath = (Join-Path $PSScriptRoot 'OPSFile.txt'),
|
||||
[string]$Text,
|
||||
[string]$InputPath,
|
||||
[string]$OutputPath,
|
||||
[ValidateSet('Annotate', 'Replace')]
|
||||
[string]$Mode = 'Annotate',
|
||||
[Alias('AggressiveShortNames')]
|
||||
[switch]$IncludeOneCharNames,
|
||||
[switch]$NoGui
|
||||
)
|
||||
|
||||
Set-StrictMode -Version 2.0
|
||||
|
||||
function New-ObjectList {
|
||||
return New-Object 'System.Collections.Generic.List[object]'
|
||||
}
|
||||
|
||||
function New-StringHashtable {
|
||||
return New-Object System.Collections.Hashtable -ArgumentList ([System.StringComparer]::Ordinal)
|
||||
}
|
||||
|
||||
function Add-LookupValue {
|
||||
param(
|
||||
[hashtable]$Lookup,
|
||||
[string]$Key,
|
||||
[object]$Value
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Key)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (-not $Lookup.ContainsKey($Key)) {
|
||||
$Lookup[$Key] = New-ObjectList
|
||||
}
|
||||
|
||||
[void]$Lookup[$Key].Add($Value)
|
||||
}
|
||||
|
||||
function Remove-AssemblyPrefix {
|
||||
param([string]$Key)
|
||||
|
||||
$pipeIndex = $Key.IndexOf('|')
|
||||
if ($pipeIndex -ge 0 -and $pipeIndex -lt ($Key.Length - 1)) {
|
||||
return $Key.Substring($pipeIndex + 1)
|
||||
}
|
||||
|
||||
return $Key
|
||||
}
|
||||
|
||||
function Get-JsonValue {
|
||||
param(
|
||||
[object]$Object,
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
if ($null -eq $Object) {
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($Object -is [System.Collections.IDictionary]) {
|
||||
return $Object[$Name]
|
||||
}
|
||||
|
||||
$property = $Object.PSObject.Properties[$Name]
|
||||
if ($null -eq $property) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return $property.Value
|
||||
}
|
||||
|
||||
function Get-JsonEntries {
|
||||
param([object]$Object)
|
||||
|
||||
$entries = New-ObjectList
|
||||
if ($null -eq $Object) {
|
||||
return $entries
|
||||
}
|
||||
|
||||
if ($Object -is [System.Collections.IDictionary]) {
|
||||
foreach ($entry in $Object.GetEnumerator()) {
|
||||
[void]$entries.Add([pscustomobject]@{
|
||||
Name = [string]$entry.Key
|
||||
Value = $entry.Value
|
||||
})
|
||||
}
|
||||
return $entries
|
||||
}
|
||||
|
||||
foreach ($property in $Object.PSObject.Properties) {
|
||||
[void]$entries.Add([pscustomobject]@{
|
||||
Name = [string]$property.Name
|
||||
Value = $property.Value
|
||||
})
|
||||
}
|
||||
|
||||
return $entries
|
||||
}
|
||||
|
||||
function ConvertFrom-OpsJson {
|
||||
param([string]$Json)
|
||||
|
||||
if ($PSVersionTable.PSEdition -eq 'Desktop') {
|
||||
Add-Type -AssemblyName System.Web.Extensions
|
||||
$serializer = New-Object System.Web.Script.Serialization.JavaScriptSerializer
|
||||
$serializer.MaxJsonLength = [int]::MaxValue
|
||||
$serializer.RecursionLimit = 128
|
||||
return $serializer.DeserializeObject($Json)
|
||||
}
|
||||
|
||||
$convertCommand = Get-Command ConvertFrom-Json
|
||||
if ($convertCommand.Parameters.ContainsKey('AsHashtable')) {
|
||||
return ($Json | ConvertFrom-Json -AsHashtable)
|
||||
}
|
||||
|
||||
return ($Json | ConvertFrom-Json)
|
||||
}
|
||||
|
||||
function ConvertFrom-JsonStringLiteral {
|
||||
param([string]$Text)
|
||||
|
||||
if ($null -eq $Text) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if ($Text.IndexOf('\') -lt 0) {
|
||||
return $Text
|
||||
}
|
||||
|
||||
return [System.Text.RegularExpressions.Regex]::Unescape($Text)
|
||||
}
|
||||
|
||||
function Get-OpsVersion {
|
||||
param([string]$Json)
|
||||
|
||||
$stringMatch = [regex]::Match($Json, '"Version"\s*:\s*"(?<version>[^"]*)"')
|
||||
if ($stringMatch.Success) {
|
||||
return $stringMatch.Groups['version'].Value
|
||||
}
|
||||
|
||||
$match = [regex]::Match($Json, '"Version"\s*:\s*\[(?<version>[^\]]*)\]')
|
||||
if (-not $match.Success) {
|
||||
return ''
|
||||
}
|
||||
|
||||
$parts = @(
|
||||
$match.Groups['version'].Value.Split(',') |
|
||||
ForEach-Object { $_.Trim().Trim('"') } |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||||
)
|
||||
return ($parts -join '.')
|
||||
}
|
||||
|
||||
function Get-OpsMappingEntries {
|
||||
param(
|
||||
[string]$Json,
|
||||
[string]$SectionName
|
||||
)
|
||||
|
||||
$entries = New-ObjectList
|
||||
$sectionPattern = '"' + [regex]::Escape($SectionName) + '"\s*:\s*\{\s*"MemberType"\s*:\s*"[^"]*"\s*,\s*"Mapping"\s*:\s*\{(?<body>.*?)\}\s*\}'
|
||||
$sectionMatch = [regex]::Match($Json, $sectionPattern, [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
||||
if (-not $sectionMatch.Success) {
|
||||
return $entries
|
||||
}
|
||||
|
||||
$body = $sectionMatch.Groups['body'].Value
|
||||
$entryRegex = New-Object System.Text.RegularExpressions.Regex('"\s*(?<key>(?:\\.|[^"\\])*)"\s*:\s*"(?<value>(?:\\.|[^"\\])*)"', [System.Text.RegularExpressions.RegexOptions]::Compiled)
|
||||
foreach ($entryMatch in $entryRegex.Matches($body)) {
|
||||
[void]$entries.Add([pscustomobject]@{
|
||||
Name = (ConvertFrom-JsonStringLiteral $entryMatch.Groups['key'].Value)
|
||||
Value = (ConvertFrom-JsonStringLiteral $entryMatch.Groups['value'].Value)
|
||||
})
|
||||
}
|
||||
|
||||
return $entries
|
||||
}
|
||||
|
||||
function Get-SymbolInfo {
|
||||
param(
|
||||
[string]$SectionName,
|
||||
[string]$OriginalKey
|
||||
)
|
||||
|
||||
$raw = Remove-AssemblyPrefix $OriginalKey
|
||||
$info = [ordered]@{
|
||||
Display = $raw
|
||||
OriginalType = $null
|
||||
MemberName = $null
|
||||
}
|
||||
|
||||
if ($SectionName -eq 'Type' -or $SectionName -eq 'Namespace') {
|
||||
$info.Display = $raw
|
||||
$info.OriginalType = $raw
|
||||
return [pscustomobject]$info
|
||||
}
|
||||
|
||||
if ($SectionName -eq 'Method' -or $SectionName -eq 'Property') {
|
||||
$match = [regex]::Match($raw, '^(?<return>.+?)\s+(?<declaringType>.+)::(?<member>[^\(]+)(?<params>\(.*\))$')
|
||||
if ($match.Success) {
|
||||
$declaringType = $match.Groups['declaringType'].Value
|
||||
$member = $match.Groups['member'].Value
|
||||
$parameters = $match.Groups['params'].Value
|
||||
$info.Display = "$declaringType.$member$parameters"
|
||||
$info.OriginalType = $declaringType
|
||||
$info.MemberName = $member
|
||||
return [pscustomobject]$info
|
||||
}
|
||||
}
|
||||
|
||||
if ($SectionName -eq 'Field' -or $SectionName -eq 'Event') {
|
||||
$match = [regex]::Match($raw, '^(?<fieldType>.+?)\s+(?<declaringType>.+)::(?<member>.+)$')
|
||||
if ($match.Success) {
|
||||
$declaringType = $match.Groups['declaringType'].Value
|
||||
$member = $match.Groups['member'].Value
|
||||
$info.Display = "$declaringType.$member"
|
||||
$info.OriginalType = $declaringType
|
||||
$info.MemberName = $member
|
||||
return [pscustomobject]$info
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]$info
|
||||
}
|
||||
|
||||
function Get-SectionPriority {
|
||||
param(
|
||||
[string]$Section,
|
||||
[string]$PreviousChar,
|
||||
[string]$NextChar
|
||||
)
|
||||
|
||||
if ($NextChar -eq '(') {
|
||||
switch ($Section) {
|
||||
'Method' { return 0 }
|
||||
'Property' { return 1 }
|
||||
'Type' { return 2 }
|
||||
default { return 5 }
|
||||
}
|
||||
}
|
||||
|
||||
if ($PreviousChar -eq '.' -or $PreviousChar -eq ':' -or $PreviousChar -eq '/') {
|
||||
switch ($Section) {
|
||||
'Method' { return 0 }
|
||||
'Property' { return 1 }
|
||||
'Field' { return 2 }
|
||||
'Event' { return 3 }
|
||||
'Type' { return 4 }
|
||||
default { return 5 }
|
||||
}
|
||||
}
|
||||
|
||||
if ($NextChar -eq ':') {
|
||||
switch ($Section) {
|
||||
'Type' { return 0 }
|
||||
'Method' { return 1 }
|
||||
'Property' { return 2 }
|
||||
default { return 5 }
|
||||
}
|
||||
}
|
||||
|
||||
switch ($Section) {
|
||||
'Type' { return 0 }
|
||||
'Method' { return 1 }
|
||||
'Property' { return 2 }
|
||||
'Field' { return 3 }
|
||||
'Event' { return 4 }
|
||||
default { return 5 }
|
||||
}
|
||||
}
|
||||
|
||||
function Format-Candidate {
|
||||
param([object]$Candidate)
|
||||
|
||||
return "$($Candidate.Section): $($Candidate.Display)"
|
||||
}
|
||||
|
||||
function Resolve-Candidates {
|
||||
param(
|
||||
[object[]]$Candidates,
|
||||
[string]$PreviousChar,
|
||||
[string]$NextChar
|
||||
)
|
||||
|
||||
return @(
|
||||
$Candidates |
|
||||
Sort-Object `
|
||||
@{ Expression = { Get-SectionPriority -Section $_.Section -PreviousChar $PreviousChar -NextChar $NextChar } },
|
||||
@{ Expression = { $_.Display } }
|
||||
)
|
||||
}
|
||||
|
||||
function Format-Replacement {
|
||||
param(
|
||||
[string]$SourceName,
|
||||
[object[]]$Candidates,
|
||||
[string]$Mode,
|
||||
[string]$PreviousChar = '',
|
||||
[string]$NextChar = ''
|
||||
)
|
||||
|
||||
$ordered = @(Resolve-Candidates -Candidates $Candidates -PreviousChar $PreviousChar -NextChar $NextChar)
|
||||
if ($ordered.Count -eq 0) {
|
||||
return $SourceName
|
||||
}
|
||||
|
||||
$primary = $ordered[0]
|
||||
if ($Mode -eq 'Replace') {
|
||||
return $primary.Display
|
||||
}
|
||||
|
||||
$label = Format-Candidate $primary
|
||||
if ($ordered.Count -gt 1) {
|
||||
$label = "$label; +$($ordered.Count - 1) more"
|
||||
}
|
||||
|
||||
return "$SourceName [$label]"
|
||||
}
|
||||
|
||||
function Add-Hit {
|
||||
param(
|
||||
[hashtable]$Hits,
|
||||
[string]$Name,
|
||||
[object[]]$Candidates
|
||||
)
|
||||
|
||||
if (-not $Hits.ContainsKey($Name)) {
|
||||
$Hits[$Name] = [ordered]@{
|
||||
Count = 0
|
||||
Candidates = $Candidates
|
||||
}
|
||||
}
|
||||
|
||||
$Hits[$Name].Count = [int]$Hits[$Name].Count + 1
|
||||
}
|
||||
|
||||
function ConvertTo-ObjectArray {
|
||||
param([object]$Value)
|
||||
|
||||
$items = New-ObjectList
|
||||
if ($null -eq $Value) {
|
||||
return $items.ToArray()
|
||||
}
|
||||
|
||||
if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) {
|
||||
foreach ($item in $Value) {
|
||||
[void]$items.Add($item)
|
||||
}
|
||||
}
|
||||
else {
|
||||
[void]$items.Add($Value)
|
||||
}
|
||||
|
||||
return $items.ToArray()
|
||||
}
|
||||
|
||||
function Get-RegexFromNames {
|
||||
param([string[]]$Names)
|
||||
|
||||
$filteredNames = @(
|
||||
$Names |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||
Sort-Object @{ Expression = { $_.Length }; Descending = $true }, @{ Expression = { $_ } } -Unique
|
||||
)
|
||||
|
||||
if ($filteredNames.Count -eq 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$escapedNames = @($filteredNames | ForEach-Object { [regex]::Escape($_) })
|
||||
$pattern = '(?<![A-Za-z0-9_])(?:' + ($escapedNames -join '|') + ')(?![A-Za-z0-9_])'
|
||||
return New-Object System.Text.RegularExpressions.Regex($pattern, [System.Text.RegularExpressions.RegexOptions]::Compiled)
|
||||
}
|
||||
|
||||
function New-DecoderIndex {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "Mapping file was not found: $Path"
|
||||
}
|
||||
|
||||
$resolvedPath = (Resolve-Path -LiteralPath $Path).Path
|
||||
$json = Get-Content -LiteralPath $resolvedPath -Raw -Encoding UTF8
|
||||
$version = Get-OpsVersion -Json $json
|
||||
|
||||
$allCandidates = New-ObjectList
|
||||
$byObfuscatedName = New-StringHashtable
|
||||
$byCompositeName = New-StringHashtable
|
||||
$typeOriginalToObfuscated = New-StringHashtable
|
||||
$sectionCounts = New-StringHashtable
|
||||
$sectionNames = @('Namespace', 'Type', 'Method', 'Field', 'Property', 'Event')
|
||||
|
||||
foreach ($sectionName in $sectionNames) {
|
||||
$mappingEntries = Get-OpsMappingEntries -Json $json -SectionName $sectionName
|
||||
if ($mappingEntries.Count -eq 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
$count = 0
|
||||
foreach ($property in $mappingEntries) {
|
||||
$originalKey = [string]$property.Name
|
||||
$obfuscatedName = [string]$property.Value
|
||||
if ([string]::IsNullOrWhiteSpace($obfuscatedName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$symbolInfo = Get-SymbolInfo -SectionName $sectionName -OriginalKey $originalKey
|
||||
$candidate = [pscustomobject]@{
|
||||
Section = $sectionName
|
||||
Obfuscated = $obfuscatedName
|
||||
OriginalKey = $originalKey
|
||||
Display = $symbolInfo.Display
|
||||
OriginalType = $symbolInfo.OriginalType
|
||||
MemberName = $symbolInfo.MemberName
|
||||
}
|
||||
|
||||
[void]$allCandidates.Add($candidate)
|
||||
Add-LookupValue -Lookup $byObfuscatedName -Key $obfuscatedName -Value $candidate
|
||||
$count++
|
||||
|
||||
if ($sectionName -eq 'Type' -and -not [string]::IsNullOrWhiteSpace($symbolInfo.OriginalType)) {
|
||||
$typeOriginalToObfuscated[$symbolInfo.OriginalType] = $obfuscatedName
|
||||
}
|
||||
}
|
||||
|
||||
$sectionCounts[$sectionName] = $count
|
||||
}
|
||||
|
||||
foreach ($candidate in $allCandidates) {
|
||||
if ($candidate.Section -eq 'Type' -or $candidate.Section -eq 'Namespace') {
|
||||
continue
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($candidate.OriginalType)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $typeOriginalToObfuscated.ContainsKey($candidate.OriginalType)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$typeObfuscatedName = [string]$typeOriginalToObfuscated[$candidate.OriginalType]
|
||||
if ([string]::IsNullOrWhiteSpace($typeObfuscatedName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
Add-LookupValue -Lookup $byCompositeName -Key "$typeObfuscatedName.$($candidate.Obfuscated)" -Value $candidate
|
||||
Add-LookupValue -Lookup $byCompositeName -Key "$typeObfuscatedName::$($candidate.Obfuscated)" -Value $candidate
|
||||
Add-LookupValue -Lookup $byCompositeName -Key "$typeObfuscatedName/$($candidate.Obfuscated)" -Value $candidate
|
||||
}
|
||||
|
||||
$standaloneNames = @($byObfuscatedName.Keys | ForEach-Object { [string]$_ })
|
||||
$commonShortWords = @(
|
||||
'am', 'an', 'as', 'at', 'be', 'by', 'do', 'go', 'he', 'if',
|
||||
'in', 'is', 'it', 'me', 'my', 'no', 'of', 'on', 'or', 'so',
|
||||
'to', 'up', 'us', 'we'
|
||||
)
|
||||
$standaloneDefaultSet = New-StringHashtable
|
||||
foreach ($entry in $byObfuscatedName.GetEnumerator()) {
|
||||
$name = [string]$entry.Key
|
||||
if ($name.Length -ge 3) {
|
||||
$standaloneDefaultSet[$name] = $true
|
||||
continue
|
||||
}
|
||||
|
||||
if ($name.Length -eq 2 -and -not $commonShortWords.Contains($name.ToLowerInvariant())) {
|
||||
$candidates = ConvertTo-ObjectArray -Value $entry.Value
|
||||
foreach ($candidate in $candidates) {
|
||||
if ($candidate.Section -eq 'Type') {
|
||||
$standaloneDefaultSet[$name] = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$tokenRegex = New-Object System.Text.RegularExpressions.Regex('(?<![A-Za-z0-9_])(?<name>[A-Za-z_][A-Za-z0-9_]*)(?![A-Za-z0-9_])')
|
||||
$compositeRegex = New-Object System.Text.RegularExpressions.Regex('(?<![A-Za-z0-9_])(?<left>[A-Za-z_][A-Za-z0-9_]*)(?<sep>::|\.|/)(?<right>[A-Za-z_][A-Za-z0-9_]*)(?![A-Za-z0-9_])')
|
||||
|
||||
return [pscustomobject]@{
|
||||
MappingPath = $resolvedPath
|
||||
Version = $version
|
||||
AllCandidates = $allCandidates
|
||||
ByObfuscatedName = $byObfuscatedName
|
||||
ByCompositeName = $byCompositeName
|
||||
SectionCounts = $sectionCounts
|
||||
DefaultStandaloneNames = $standaloneDefaultSet
|
||||
StandaloneRegexDefault = $tokenRegex
|
||||
StandaloneRegexAll = $tokenRegex
|
||||
CompositeRegex = $compositeRegex
|
||||
}
|
||||
}
|
||||
|
||||
function New-SummaryText {
|
||||
param(
|
||||
[hashtable]$Hits,
|
||||
[object]$Index,
|
||||
[int]$ReplacementCount
|
||||
)
|
||||
|
||||
$lines = New-Object 'System.Collections.Generic.List[string]'
|
||||
[void]$lines.Add('')
|
||||
[void]$lines.Add('---- OPS decode summary ----')
|
||||
[void]$lines.Add("Mapping: $($Index.MappingPath)")
|
||||
if (-not [string]::IsNullOrWhiteSpace($Index.Version)) {
|
||||
[void]$lines.Add("Version: $($Index.Version)")
|
||||
}
|
||||
[void]$lines.Add("Hits: $ReplacementCount, unique names: $($Hits.Count)")
|
||||
|
||||
if ($Hits.Count -eq 0) {
|
||||
[void]$lines.Add('No obfuscated names were matched.')
|
||||
return ($lines -join [Environment]::NewLine)
|
||||
}
|
||||
|
||||
[void]$lines.Add('')
|
||||
foreach ($name in @($Hits.Keys | Sort-Object)) {
|
||||
$hit = $Hits[$name]
|
||||
$candidates = @($hit.Candidates)
|
||||
$ordered = @(Resolve-Candidates -Candidates $candidates -PreviousChar '' -NextChar '')
|
||||
$primary = if ($ordered.Count -gt 0) { Format-Candidate $ordered[0] } else { '(unknown)' }
|
||||
$suffix = ''
|
||||
if ($ordered.Count -gt 1) {
|
||||
$suffix = "; ambiguous candidates: $($ordered.Count)"
|
||||
}
|
||||
[void]$lines.Add("$name x$($hit.Count) -> $primary$suffix")
|
||||
|
||||
if ($ordered.Count -gt 1) {
|
||||
foreach ($candidate in @($ordered | Select-Object -Skip 1 -First 5)) {
|
||||
[void]$lines.Add(" - $(Format-Candidate $candidate)")
|
||||
}
|
||||
if ($ordered.Count -gt 6) {
|
||||
[void]$lines.Add(" - ... $($ordered.Count - 6) more")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ($lines -join [Environment]::NewLine)
|
||||
}
|
||||
|
||||
function Invoke-RegexTransform {
|
||||
param(
|
||||
[string]$SourceText,
|
||||
[System.Text.RegularExpressions.Regex]$Regex,
|
||||
[scriptblock]$Replacement
|
||||
)
|
||||
|
||||
$builder = New-Object System.Text.StringBuilder
|
||||
$lastIndex = 0
|
||||
|
||||
foreach ($match in $Regex.Matches($SourceText)) {
|
||||
if ($match.Index -lt $lastIndex) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($match.Index -gt $lastIndex) {
|
||||
[void]$builder.Append($SourceText.Substring($lastIndex, $match.Index - $lastIndex))
|
||||
}
|
||||
|
||||
[void]$builder.Append((& $Replacement $match))
|
||||
$lastIndex = $match.Index + $match.Length
|
||||
}
|
||||
|
||||
if ($lastIndex -lt $SourceText.Length) {
|
||||
[void]$builder.Append($SourceText.Substring($lastIndex))
|
||||
}
|
||||
|
||||
return $builder.ToString()
|
||||
}
|
||||
|
||||
function Convert-ObfuscatedText {
|
||||
param(
|
||||
[string]$SourceText,
|
||||
[object]$Index,
|
||||
[ValidateSet('Annotate', 'Replace')]
|
||||
[string]$Mode = 'Annotate',
|
||||
[switch]$IncludeOneCharNames,
|
||||
[switch]$AppendSummary
|
||||
)
|
||||
|
||||
if ($null -eq $SourceText) {
|
||||
$SourceText = ''
|
||||
}
|
||||
|
||||
$hits = New-StringHashtable
|
||||
$stats = @{
|
||||
ReplacementCount = 0
|
||||
PlaceholderIndex = 0
|
||||
}
|
||||
$result = $SourceText
|
||||
$placeholders = @{}
|
||||
$byCompositeName = $Index.ByCompositeName
|
||||
$byObfuscatedName = $Index.ByObfuscatedName
|
||||
|
||||
if ($null -ne $Index.CompositeRegex) {
|
||||
$result = Invoke-RegexTransform -SourceText $result -Regex $Index.CompositeRegex -Replacement {
|
||||
param($match)
|
||||
$name = [string]$match.Value
|
||||
$candidateList = $byCompositeName.get_Item($name)
|
||||
$candidates = @(ConvertTo-ObjectArray -Value $candidateList)
|
||||
if ($candidates.Count -eq 0) {
|
||||
return $name
|
||||
}
|
||||
|
||||
Add-Hit -Hits $hits -Name $name -Candidates $candidates
|
||||
$stats.ReplacementCount = [int]$stats.ReplacementCount + 1
|
||||
$replacement = Format-Replacement -SourceName $name -Candidates $candidates -Mode $Mode
|
||||
$placeholder = "__OPS_DECODER_PLACEHOLDER_$($stats.PlaceholderIndex)__"
|
||||
$stats.PlaceholderIndex = [int]$stats.PlaceholderIndex + 1
|
||||
$placeholders[$placeholder] = $replacement
|
||||
return $placeholder
|
||||
}
|
||||
}
|
||||
|
||||
$standaloneRegex = if ($IncludeOneCharNames) { $Index.StandaloneRegexAll } else { $Index.StandaloneRegexDefault }
|
||||
if ($null -ne $standaloneRegex) {
|
||||
$sourceForContext = $result
|
||||
$result = Invoke-RegexTransform -SourceText $result -Regex $standaloneRegex -Replacement {
|
||||
param($match)
|
||||
$name = [string]$match.Groups['name'].Value
|
||||
$shouldDecode = if ($IncludeOneCharNames) {
|
||||
$byObfuscatedName.ContainsKey($name)
|
||||
}
|
||||
else {
|
||||
$Index.DefaultStandaloneNames.ContainsKey($name)
|
||||
}
|
||||
|
||||
if (-not $shouldDecode) {
|
||||
return $name
|
||||
}
|
||||
|
||||
$previousChar = ''
|
||||
$nextChar = ''
|
||||
if ($match.Index -gt 0) {
|
||||
$previousChar = $sourceForContext.Substring($match.Index - 1, 1)
|
||||
}
|
||||
if (($match.Index + $match.Length) -lt $sourceForContext.Length) {
|
||||
$nextChar = $sourceForContext.Substring($match.Index + $match.Length, 1)
|
||||
}
|
||||
|
||||
$candidateList = $byObfuscatedName.get_Item($name)
|
||||
$candidates = @(ConvertTo-ObjectArray -Value $candidateList)
|
||||
Add-Hit -Hits $hits -Name $name -Candidates $candidates
|
||||
$stats.ReplacementCount = [int]$stats.ReplacementCount + 1
|
||||
return Format-Replacement -SourceName $name -Candidates $candidates -Mode $Mode -PreviousChar $previousChar -NextChar $nextChar
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($placeholder in @($placeholders.Keys)) {
|
||||
$result = $result.Replace($placeholder, [string]$placeholders[$placeholder])
|
||||
}
|
||||
|
||||
$summary = New-SummaryText -Hits $hits -Index $Index -ReplacementCount ([int]$stats.ReplacementCount)
|
||||
$fullText = if ($AppendSummary) { $result + [Environment]::NewLine + $summary } else { $result }
|
||||
|
||||
return [pscustomobject]@{
|
||||
Text = $result
|
||||
Summary = $summary
|
||||
FullText = $fullText
|
||||
ReplacementCount = [int]$stats.ReplacementCount
|
||||
UniqueHitCount = $hits.Count
|
||||
}
|
||||
}
|
||||
|
||||
function Show-DecoderGui {
|
||||
param([string]$InitialMappingPath)
|
||||
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
[System.Windows.Forms.Application]::EnableVisualStyles()
|
||||
|
||||
$form = New-Object System.Windows.Forms.Form
|
||||
$form.Text = 'OPS Exception Decoder'
|
||||
$form.StartPosition = 'CenterScreen'
|
||||
$form.Width = 1180
|
||||
$form.Height = 760
|
||||
$form.MinimumSize = New-Object System.Drawing.Size(940, 600)
|
||||
|
||||
$font = New-Object System.Drawing.Font('Microsoft YaHei UI', 9)
|
||||
$monoFont = New-Object System.Drawing.Font('Consolas', 10)
|
||||
$form.Font = $font
|
||||
|
||||
$main = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$main.Dock = 'Fill'
|
||||
$main.ColumnCount = 1
|
||||
$main.RowCount = 4
|
||||
[void]$main.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 44)))
|
||||
[void]$main.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 100)))
|
||||
[void]$main.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 44)))
|
||||
[void]$main.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 26)))
|
||||
$form.Controls.Add($main)
|
||||
|
||||
$topPanel = New-Object System.Windows.Forms.TableLayoutPanel
|
||||
$topPanel.Dock = 'Fill'
|
||||
$topPanel.ColumnCount = 7
|
||||
[void]$topPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 58)))
|
||||
[void]$topPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 100)))
|
||||
[void]$topPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 72)))
|
||||
[void]$topPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 72)))
|
||||
[void]$topPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 168)))
|
||||
[void]$topPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 130)))
|
||||
[void]$topPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Absolute, 120)))
|
||||
$main.Controls.Add($topPanel, 0, 0)
|
||||
|
||||
$mappingLabel = New-Object System.Windows.Forms.Label
|
||||
$mappingLabel.Text = 'Mapping'
|
||||
$mappingLabel.TextAlign = 'MiddleRight'
|
||||
$mappingLabel.Dock = 'Fill'
|
||||
$topPanel.Controls.Add($mappingLabel, 0, 0)
|
||||
|
||||
$mappingTextBox = New-Object System.Windows.Forms.TextBox
|
||||
$mappingTextBox.Text = $InitialMappingPath
|
||||
$mappingTextBox.Dock = 'Fill'
|
||||
$mappingTextBox.Anchor = 'Left,Right'
|
||||
$topPanel.Controls.Add($mappingTextBox, 1, 0)
|
||||
|
||||
$browseButton = New-Object System.Windows.Forms.Button
|
||||
$browseButton.Text = 'Browse'
|
||||
$browseButton.Dock = 'Fill'
|
||||
$topPanel.Controls.Add($browseButton, 2, 0)
|
||||
|
||||
$reloadButton = New-Object System.Windows.Forms.Button
|
||||
$reloadButton.Text = 'Reload'
|
||||
$reloadButton.Dock = 'Fill'
|
||||
$topPanel.Controls.Add($reloadButton, 3, 0)
|
||||
|
||||
$modeBox = New-Object System.Windows.Forms.ComboBox
|
||||
$modeBox.DropDownStyle = 'DropDownList'
|
||||
[void]$modeBox.Items.Add('Annotate')
|
||||
[void]$modeBox.Items.Add('Replace')
|
||||
$modeBox.SelectedIndex = 0
|
||||
$modeBox.Dock = 'Fill'
|
||||
$topPanel.Controls.Add($modeBox, 4, 0)
|
||||
|
||||
$oneCharCheckBox = New-Object System.Windows.Forms.CheckBox
|
||||
$oneCharCheckBox.Text = 'Aggressive short names'
|
||||
$oneCharCheckBox.Checked = $false
|
||||
$oneCharCheckBox.Dock = 'Fill'
|
||||
$topPanel.Controls.Add($oneCharCheckBox, 5, 0)
|
||||
|
||||
$convertButtonTop = New-Object System.Windows.Forms.Button
|
||||
$convertButtonTop.Text = 'Convert'
|
||||
$convertButtonTop.Dock = 'Fill'
|
||||
$topPanel.Controls.Add($convertButtonTop, 6, 0)
|
||||
|
||||
$split = New-Object System.Windows.Forms.SplitContainer
|
||||
$split.Dock = 'Fill'
|
||||
$split.Orientation = 'Vertical'
|
||||
$split.SplitterDistance = 545
|
||||
$main.Controls.Add($split, 0, 1)
|
||||
|
||||
$inputGroup = New-Object System.Windows.Forms.GroupBox
|
||||
$inputGroup.Text = 'Input: exception message / stack trace'
|
||||
$inputGroup.Dock = 'Fill'
|
||||
$split.Panel1.Controls.Add($inputGroup)
|
||||
|
||||
$inputTextBox = New-Object System.Windows.Forms.TextBox
|
||||
$inputTextBox.Multiline = $true
|
||||
$inputTextBox.AcceptsTab = $true
|
||||
$inputTextBox.AcceptsReturn = $true
|
||||
$inputTextBox.ScrollBars = 'Both'
|
||||
$inputTextBox.WordWrap = $false
|
||||
$inputTextBox.Dock = 'Fill'
|
||||
$inputTextBox.Font = $monoFont
|
||||
$inputGroup.Controls.Add($inputTextBox)
|
||||
|
||||
$outputGroup = New-Object System.Windows.Forms.GroupBox
|
||||
$outputGroup.Text = 'Output: decoded result'
|
||||
$outputGroup.Dock = 'Fill'
|
||||
$split.Panel2.Controls.Add($outputGroup)
|
||||
|
||||
$outputTextBox = New-Object System.Windows.Forms.TextBox
|
||||
$outputTextBox.Multiline = $true
|
||||
$outputTextBox.AcceptsTab = $true
|
||||
$outputTextBox.AcceptsReturn = $true
|
||||
$outputTextBox.ScrollBars = 'Both'
|
||||
$outputTextBox.WordWrap = $false
|
||||
$outputTextBox.ReadOnly = $false
|
||||
$outputTextBox.Dock = 'Fill'
|
||||
$outputTextBox.Font = $monoFont
|
||||
$outputGroup.Controls.Add($outputTextBox)
|
||||
|
||||
$buttonPanel = New-Object System.Windows.Forms.FlowLayoutPanel
|
||||
$buttonPanel.Dock = 'Fill'
|
||||
$buttonPanel.FlowDirection = 'LeftToRight'
|
||||
$buttonPanel.Padding = New-Object System.Windows.Forms.Padding(8, 5, 8, 5)
|
||||
$main.Controls.Add($buttonPanel, 0, 2)
|
||||
|
||||
$pasteButton = New-Object System.Windows.Forms.Button
|
||||
$pasteButton.Text = 'Paste'
|
||||
$pasteButton.Width = 118
|
||||
$buttonPanel.Controls.Add($pasteButton)
|
||||
|
||||
$convertButton = New-Object System.Windows.Forms.Button
|
||||
$convertButton.Text = 'Convert'
|
||||
$convertButton.Width = 86
|
||||
$buttonPanel.Controls.Add($convertButton)
|
||||
|
||||
$copyButton = New-Object System.Windows.Forms.Button
|
||||
$copyButton.Text = 'Copy'
|
||||
$copyButton.Width = 92
|
||||
$buttonPanel.Controls.Add($copyButton)
|
||||
|
||||
$saveButton = New-Object System.Windows.Forms.Button
|
||||
$saveButton.Text = 'Save'
|
||||
$saveButton.Width = 92
|
||||
$buttonPanel.Controls.Add($saveButton)
|
||||
|
||||
$clearButton = New-Object System.Windows.Forms.Button
|
||||
$clearButton.Text = 'Clear'
|
||||
$clearButton.Width = 74
|
||||
$buttonPanel.Controls.Add($clearButton)
|
||||
|
||||
$sampleButton = New-Object System.Windows.Forms.Button
|
||||
$sampleButton.Text = 'Sample'
|
||||
$sampleButton.Width = 92
|
||||
$buttonPanel.Controls.Add($sampleButton)
|
||||
|
||||
$statusLabel = New-Object System.Windows.Forms.Label
|
||||
$statusLabel.Text = 'Ready'
|
||||
$statusLabel.Dock = 'Fill'
|
||||
$statusLabel.TextAlign = 'MiddleLeft'
|
||||
$statusLabel.Padding = New-Object System.Windows.Forms.Padding(8, 0, 8, 0)
|
||||
$main.Controls.Add($statusLabel, 0, 3)
|
||||
|
||||
$state = @{
|
||||
Index = $null
|
||||
}
|
||||
|
||||
$loadMapping = {
|
||||
try {
|
||||
$form.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
|
||||
$statusLabel.Text = 'Loading mapping...'
|
||||
$form.Refresh()
|
||||
$state.Index = New-DecoderIndex -Path $mappingTextBox.Text
|
||||
$counts = $state.Index.SectionCounts
|
||||
$statusLabel.Text = "Mapping loaded: Type $($counts.Type), Method $($counts.Method), Field $($counts.Field), Property $($counts.Property), Event $($counts.Event)"
|
||||
}
|
||||
catch {
|
||||
$state.Index = $null
|
||||
$statusLabel.Text = "Failed to load mapping: $($_.Exception.Message)"
|
||||
[System.Windows.Forms.MessageBox]::Show($form, $_.Exception.Message, 'OPS Exception Decoder', 'OK', 'Error') | Out-Null
|
||||
}
|
||||
finally {
|
||||
$form.Cursor = [System.Windows.Forms.Cursors]::Default
|
||||
}
|
||||
}
|
||||
|
||||
$runConvert = {
|
||||
if ($null -eq $state.Index) {
|
||||
& $loadMapping
|
||||
}
|
||||
if ($null -eq $state.Index) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
$form.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
|
||||
$selectedMode = if ($modeBox.SelectedIndex -eq 1) { 'Replace' } else { 'Annotate' }
|
||||
$result = Convert-ObfuscatedText -SourceText $inputTextBox.Text -Index $state.Index -Mode $selectedMode -IncludeOneCharNames:$oneCharCheckBox.Checked -AppendSummary
|
||||
$outputTextBox.Text = $result.FullText
|
||||
$statusLabel.Text = "Done: $($result.ReplacementCount) hits, $($result.UniqueHitCount) unique names"
|
||||
}
|
||||
catch {
|
||||
$statusLabel.Text = "Convert failed: $($_.Exception.Message)"
|
||||
[System.Windows.Forms.MessageBox]::Show($form, $_.Exception.Message, 'OPS Exception Decoder', 'OK', 'Error') | Out-Null
|
||||
}
|
||||
finally {
|
||||
$form.Cursor = [System.Windows.Forms.Cursors]::Default
|
||||
}
|
||||
}
|
||||
|
||||
$browseButton.Add_Click({
|
||||
$dialog = New-Object System.Windows.Forms.OpenFileDialog
|
||||
$dialog.Title = 'Select OPSFile.txt'
|
||||
$dialog.Filter = 'OPS mapping (*.txt;*.json)|*.txt;*.json|All files (*.*)|*.*'
|
||||
if (Test-Path -LiteralPath $mappingTextBox.Text) {
|
||||
$dialog.InitialDirectory = Split-Path -Parent (Resolve-Path -LiteralPath $mappingTextBox.Text).Path
|
||||
$dialog.FileName = Split-Path -Leaf $mappingTextBox.Text
|
||||
}
|
||||
if ($dialog.ShowDialog($form) -eq 'OK') {
|
||||
$mappingTextBox.Text = $dialog.FileName
|
||||
& $loadMapping
|
||||
}
|
||||
})
|
||||
|
||||
$reloadButton.Add_Click({ & $loadMapping })
|
||||
$convertButton.Add_Click({ & $runConvert })
|
||||
$convertButtonTop.Add_Click({ & $runConvert })
|
||||
|
||||
$pasteButton.Add_Click({
|
||||
if ([System.Windows.Forms.Clipboard]::ContainsText()) {
|
||||
$inputTextBox.Text = [System.Windows.Forms.Clipboard]::GetText()
|
||||
}
|
||||
})
|
||||
|
||||
$copyButton.Add_Click({
|
||||
if (-not [string]::IsNullOrEmpty($outputTextBox.Text)) {
|
||||
[System.Windows.Forms.Clipboard]::SetText($outputTextBox.Text)
|
||||
$statusLabel.Text = 'Copied to clipboard'
|
||||
}
|
||||
})
|
||||
|
||||
$saveButton.Add_Click({
|
||||
$dialog = New-Object System.Windows.Forms.SaveFileDialog
|
||||
$dialog.Title = 'Save decoded result'
|
||||
$dialog.Filter = 'Text file (*.txt)|*.txt|All files (*.*)|*.*'
|
||||
$dialog.FileName = 'decoded_exception.txt'
|
||||
if ($dialog.ShowDialog($form) -eq 'OK') {
|
||||
[System.IO.File]::WriteAllText($dialog.FileName, $outputTextBox.Text, [System.Text.Encoding]::UTF8)
|
||||
$statusLabel.Text = "Saved: $($dialog.FileName)"
|
||||
}
|
||||
})
|
||||
|
||||
$clearButton.Add_Click({
|
||||
$inputTextBox.Clear()
|
||||
$outputTextBox.Clear()
|
||||
$statusLabel.Text = 'Cleared'
|
||||
})
|
||||
|
||||
$sampleButton.Add_Click({
|
||||
$inputTextBox.Text = "error fu: host broadcast failed" + [Environment]::NewLine + "error blu: send to host failed" + [Environment]::NewLine + "at gn.blu(RuntimeData.MapData, RuntimeData.PlayerData)"
|
||||
})
|
||||
|
||||
$form.Add_Shown({ & $loadMapping })
|
||||
[void]$form.ShowDialog()
|
||||
}
|
||||
|
||||
$shouldRunCli = $NoGui -or -not [string]::IsNullOrEmpty($Text) -or -not [string]::IsNullOrEmpty($InputPath) -or -not [string]::IsNullOrEmpty($OutputPath)
|
||||
|
||||
if ($shouldRunCli) {
|
||||
$sourceText = $Text
|
||||
if (-not [string]::IsNullOrEmpty($InputPath)) {
|
||||
$sourceText = Get-Content -LiteralPath $InputPath -Raw -Encoding UTF8
|
||||
}
|
||||
|
||||
$index = New-DecoderIndex -Path $MappingPath
|
||||
$result = Convert-ObfuscatedText -SourceText $sourceText -Index $index -Mode $Mode -IncludeOneCharNames:$IncludeOneCharNames -AppendSummary
|
||||
|
||||
if (-not [string]::IsNullOrEmpty($OutputPath)) {
|
||||
[System.IO.File]::WriteAllText($OutputPath, $result.FullText, [System.Text.Encoding]::UTF8)
|
||||
}
|
||||
else {
|
||||
Write-Output $result.FullText
|
||||
}
|
||||
}
|
||||
else {
|
||||
Show-DecoderGui -InitialMappingPath $MappingPath
|
||||
}
|
||||
41
Tools/ObfuscatedExceptionDecoder_README.md
Normal file
41
Tools/ObfuscatedExceptionDecoder_README.md
Normal file
@ -0,0 +1,41 @@
|
||||
# OPS Exception Decoder
|
||||
|
||||
外部异常反混淆工具,直接放在 `Tools` 目录使用,不需要放进 Unity。
|
||||
|
||||
## 图形界面
|
||||
|
||||
双击:
|
||||
|
||||
```bat
|
||||
Tools\ObfuscatedExceptionDecoder.bat
|
||||
```
|
||||
|
||||
工具会自动读取同目录的 `OPSFile.txt`。把后台异常消息或堆栈贴到左侧,点 `Convert`,右侧会输出反混淆结果和命中摘要。
|
||||
|
||||
默认模式会保留混淆名并在后面加注释,例如:
|
||||
|
||||
```text
|
||||
fu [Type: Logic.InputLogic]: 房主广播失败
|
||||
blu [Method: Logic.PlayerLogic.StartNextTurn(RuntimeData.MapData,RuntimeData.PlayerData)]: 发送给房主失败
|
||||
```
|
||||
|
||||
如果想得到更干净的文本,可以把模式切到 `Replace`。
|
||||
|
||||
## 命令行
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File Tools\ObfuscatedExceptionDecoder.ps1 -NoGui -Text "异常消息 fu: 房主广播失败"
|
||||
```
|
||||
|
||||
批量转换文件:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File Tools\ObfuscatedExceptionDecoder.ps1 -NoGui -InputPath crash.txt -OutputPath decoded_crash.txt
|
||||
```
|
||||
|
||||
常用参数:
|
||||
|
||||
- `-Mode Annotate`:保留混淆名并注释原名,默认。
|
||||
- `-Mode Replace`:直接替换成原名。
|
||||
- `-IncludeOneCharNames` / `-AggressiveShortNames`:更激进地转换极短名称;可能误伤 `to` / `in` 这类普通英文,默认关闭。
|
||||
- `-MappingPath Tools\OPSFile.txt`:指定其他映射文件。
|
||||
@ -1565,6 +1565,7 @@ namespace RuntimeData
|
||||
public static bool SaveMapData(MapData map, bool isBegin=false, bool isEnd=false)
|
||||
{
|
||||
if (map == null) return false;
|
||||
if (ShouldSkipMapArchive(map)) return false;
|
||||
|
||||
// 改为二进制文件扩展名
|
||||
string path = Application.persistentDataPath + "/../Config/map_archive";
|
||||
@ -1664,30 +1665,40 @@ namespace RuntimeData
|
||||
private static MapData GetLatestReadableMapArchive(bool isMulti, MapArchiveKind kind, uint mapId)
|
||||
{
|
||||
var files = GetMapArchiveCandidates(isMulti, kind, mapId);
|
||||
var targetFile = files.FirstOrDefault();
|
||||
if (targetFile == null) return null;
|
||||
|
||||
if (!TryParseMapArchiveFileName(targetFile, out _, out _, out var archiveMapId)) return null;
|
||||
var endArchiveTimes = kind == MapArchiveKind.Continue
|
||||
? GetLatestEndMapArchiveTimes(isMulti)
|
||||
: null;
|
||||
|
||||
foreach (var targetFile in files)
|
||||
if (endArchiveTimes != null
|
||||
&& endArchiveTimes.TryGetValue(archiveMapId, out var endWriteTime)
|
||||
&& endWriteTime >= GetMapArchiveLastWriteTime(targetFile))
|
||||
{
|
||||
if (!TryParseMapArchiveFileName(targetFile, out _, out _, out var archiveMapId)) continue;
|
||||
if (endArchiveTimes != null
|
||||
&& endArchiveTimes.TryGetValue(archiveMapId, out var endWriteTime)
|
||||
&& endWriteTime >= GetMapArchiveLastWriteTime(targetFile))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var mapData = ReadMapDataWithBackup(targetFile);
|
||||
if (mapData == null) continue;
|
||||
|
||||
var expectedMode = isMulti ? NetMode.Multi : NetMode.Single;
|
||||
if (mapData.Net.Mode == expectedMode) return mapData;
|
||||
|
||||
LogSystem.LogWarning($"存档模式与文件名不一致,跳过: {targetFile}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
var mapData = ReadMapDataWithBackup(targetFile);
|
||||
if (mapData == null) return null;
|
||||
if (ShouldSkipMapArchive(mapData)) return null;
|
||||
|
||||
var expectedMode = isMulti ? NetMode.Multi : NetMode.Single;
|
||||
if (mapData.Net.Mode != expectedMode)
|
||||
{
|
||||
LogSystem.LogWarning($"存档模式与文件名不一致,跳过: {targetFile}");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (kind == MapArchiveKind.Continue && mapData.PlayerMap.SelfPlayerData?.IsSurrender == true)
|
||||
return null;
|
||||
|
||||
return mapData;
|
||||
}
|
||||
|
||||
private static bool ShouldSkipMapArchive(MapData map)
|
||||
{
|
||||
return map?.MapConfig?.MatchSettlement == MatchSettlementType.Story;
|
||||
}
|
||||
|
||||
private static void InvalidateMapArchiveAvailabilityCache()
|
||||
|
||||
@ -2424,9 +2424,23 @@ namespace Logic.Action
|
||||
protected override bool Execute(CommonActionParams actionParams)
|
||||
{
|
||||
actionParams.PlayerData.Surrender(actionParams.MapData);
|
||||
SaveLocalSurrenderEndArchive(actionParams);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SaveLocalSurrenderEndArchive(CommonActionParams actionParams)
|
||||
{
|
||||
var map = actionParams.MapData;
|
||||
if (map == null || map != Main.MapData) return;
|
||||
if (actionParams.PlayerData == null || actionParams.PlayerData != map.PlayerMap.SelfPlayerData) return;
|
||||
if (map.MapConfig?.MatchSettlement == MatchSettlementType.Story) return;
|
||||
|
||||
if (map.Net.Mode == NetMode.Multi) PlayerPrefs.SetInt("MultiArchive", 0);
|
||||
if (map.Net.Mode == NetMode.Single) PlayerPrefs.SetInt("Archive", 0);
|
||||
PlayerPrefs.Save();
|
||||
MapData.SaveMapData(map, false, true);
|
||||
}
|
||||
|
||||
public override bool CheckCan(CommonActionParams actionParams)
|
||||
{
|
||||
if (actionParams.PlayerData == null) return false;
|
||||
|
||||
@ -336,16 +336,8 @@ namespace TH1_Logic.Core
|
||||
// 收集数据刷新
|
||||
CollectManager.Instance.Init();
|
||||
|
||||
//清空MapRenderer,然后重新初始化
|
||||
|
||||
//TODO 和文波确认流程
|
||||
MapRenderer.Dispose();
|
||||
MapRenderer.Initialize(this,MapData);
|
||||
UIManager.Instance.OnMatchStart();
|
||||
|
||||
//初始化交互相关的logic
|
||||
InputLogic = new InputLogic(this,MapData);
|
||||
MapInteractionLogic = new MapInteraction(this,MapData);
|
||||
//step #3 初始化map相关的模块
|
||||
InitMapAddtion();
|
||||
|
||||
//初始化地图生成器
|
||||
MapGeneratorLogic = new MapGenerator(this,MapData);
|
||||
@ -504,15 +496,8 @@ namespace TH1_Logic.Core
|
||||
CollectManager.Instance.Init();
|
||||
AIActionScoreCalculator.RefreshCalMap(MapData, true);
|
||||
AchievementDataManager.Instance.OnGameStart(MapData);
|
||||
//清空MapRenderer,然后重新初始化
|
||||
MapRenderer.Dispose();
|
||||
MapRenderer.Initialize(this,MapData);
|
||||
|
||||
UIManager.Instance.OnMatchStart();
|
||||
|
||||
//初始化交互相关的logic
|
||||
InputLogic = new InputLogic(this,MapData);
|
||||
MapInteractionLogic = new MapInteraction(this,MapData);
|
||||
//step #3 初始化map相关的模块
|
||||
InitMapAddtion();
|
||||
MapRenderer.Instance.FirstRenderMap();
|
||||
|
||||
// 对齐其他 Match 入口:补 AfterMapAddtion,触发 UIManager.AfterMatchStart
|
||||
@ -562,13 +547,8 @@ namespace TH1_Logic.Core
|
||||
AudioManager.Instance.StopMusic();
|
||||
AudioManager.Instance.InGameAudioInit(this,MapData);
|
||||
AchievementDataManager.Instance.OnGameStart(MapData);
|
||||
//清空MapRenderer,然后重新初始化
|
||||
MapRenderer.Dispose();
|
||||
MapRenderer.Initialize(this,MapData);
|
||||
UIManager.Instance.OnMatchStart();
|
||||
//初始化交互相关的logic
|
||||
InputLogic = new InputLogic(this,MapData);
|
||||
MapInteractionLogic = new MapInteraction(this,MapData);
|
||||
//step #3 初始化map相关的模块
|
||||
InitMapAddtion();
|
||||
MapRenderer.Instance.FirstRenderMap();
|
||||
|
||||
// 对齐其他 Match 入口:补 AfterMapAddtion,触发 UIManager.AfterMatchStart
|
||||
@ -636,8 +616,9 @@ namespace TH1_Logic.Core
|
||||
return;
|
||||
}
|
||||
|
||||
MapRenderer.Initialize(this, previousMap);
|
||||
MapRenderer.OnMatchStart(this, previousMap);
|
||||
UIManager.Instance.OnMatchStart();
|
||||
PresentationManager.OnMatchStart();
|
||||
MapRenderer.Instance.FirstRenderMap();
|
||||
}
|
||||
catch (System.Exception restoreException)
|
||||
|
||||
@ -78,6 +78,7 @@ namespace TH1_Logic.Steam
|
||||
private bool _steamServerConnected = false;
|
||||
private int _steamSessionFailCount;
|
||||
private bool _isHandlingSessionLoss;
|
||||
private int _suppressP2PSendFailureLobbyErrors;
|
||||
private const float SteamSessionCheckInterval = 1f;
|
||||
private const int SteamSessionFailThreshold = 2;
|
||||
private string RoomName;
|
||||
@ -1168,7 +1169,7 @@ namespace TH1_Logic.Steam
|
||||
{
|
||||
var error = $"P2P message send failed: target={steamID}, reason={reason}";
|
||||
LogSystem.LogError(error);
|
||||
OnLobbyErrorEvent?.Invoke(error);
|
||||
if (_suppressP2PSendFailureLobbyErrors <= 0) OnLobbyErrorEvent?.Invoke(error);
|
||||
}
|
||||
|
||||
// 发送P2P消息
|
||||
@ -1215,26 +1216,39 @@ namespace TH1_Logic.Steam
|
||||
}
|
||||
|
||||
if (targets.Count == 0) return true;
|
||||
if (!SimpleP2P.Instance.CanQueueMessages(targets, data.Length, out var failedTarget, out var reason))
|
||||
var successCount = 0;
|
||||
var failedCount = 0;
|
||||
_suppressP2PSendFailureLobbyErrors++;
|
||||
try
|
||||
{
|
||||
if (failedTarget.IsValid()) OnP2PMessageSendFailed(failedTarget, reason);
|
||||
else
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var error = $"P2P broadcast preflight failed: {reason}";
|
||||
LogSystem.LogError(error);
|
||||
OnLobbyErrorEvent?.Invoke(error);
|
||||
if (SimpleP2P.Instance.SendTo(target, data, reliable))
|
||||
{
|
||||
successCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
failedCount++;
|
||||
LogSystem.LogWarning($"P2P broadcast enqueue failed: target={target}, bytes={data.Length}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var target in targets)
|
||||
finally
|
||||
{
|
||||
if (SimpleP2P.Instance.SendTo(target, data, reliable)) continue;
|
||||
OnP2PMessageSendFailed(target, $"P2P broadcast enqueue failed after preflight: bytes={data.Length}");
|
||||
return false;
|
||||
_suppressP2PSendFailureLobbyErrors--;
|
||||
}
|
||||
|
||||
return true;
|
||||
if (successCount > 0)
|
||||
{
|
||||
if (failedCount > 0)
|
||||
LogSystem.LogWarning($"P2P broadcast partially succeeded: success={successCount}, failed={failedCount}, bytes={data.Length}");
|
||||
return true;
|
||||
}
|
||||
|
||||
var error = $"P2P broadcast failed for all targets: targets={targets.Count}, bytes={data.Length}";
|
||||
LogSystem.LogError(error);
|
||||
OnLobbyErrorEvent?.Invoke(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ReportP2PSendPrecheckFailed(ulong member, string reason)
|
||||
|
||||
@ -154,6 +154,7 @@ namespace TH1Renderer
|
||||
//InGame游戏开始的生命周期
|
||||
public static void OnMatchStart(Main main, MapData mapData)
|
||||
{
|
||||
UIBlockCameraDrag.ResetState();
|
||||
Dispose();
|
||||
Initialize(main,mapData);
|
||||
_instance.InGameBubbleManager.OnGameStart();
|
||||
@ -168,6 +169,7 @@ namespace TH1Renderer
|
||||
//InGame游戏结束时的生命周期
|
||||
public void OnMatchEnd()
|
||||
{
|
||||
UIBlockCameraDrag.ResetState();
|
||||
InGameBubbleManager.OnGameClosed();
|
||||
}
|
||||
private void ClearAllChildren(Transform parent)
|
||||
@ -1138,4 +1140,4 @@ namespace TH1Renderer
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,16 @@ public class UIBlockCameraDrag : MonoBehaviour, IPointerEnterHandler, IPointerEx
|
||||
|
||||
//是否ban点击
|
||||
public bool BanClick = false;
|
||||
|
||||
public static void ResetState()
|
||||
{
|
||||
IsPointerOnUI = false;
|
||||
ShouldBlockDrag = false;
|
||||
MoveEvent = false;
|
||||
DownUpEvent = false;
|
||||
DragOrigin = Vector3.zero;
|
||||
MoveVector = Vector3.zero;
|
||||
}
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user