投降逻辑,联机广播逻辑等

This commit is contained in:
wuwenbo 2026-05-17 00:21:43 +08:00
parent ea7d6a5060
commit 2b9cf05f5a
9 changed files with 1091 additions and 60 deletions

View 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%"
)

View 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
}

View 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`:指定其他映射文件。

View File

@ -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()

View File

@ -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;

View File

@ -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)

View File

@ -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)

View File

@ -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
}
}

View File

@ -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)
{