param( [ValidateSet('1', '2', '3', '4')] [string]$Choice ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $exePath = Join-Path $PSScriptRoot 'CRUSADER.EXE' $statePath = Join-Path $PSScriptRoot 'patch_crusader_cheat_menu.state.json' $sites = @{ Hook = @{ Label = 'Hidden menu direct hook site' Offset = 0x70D75 Original = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02) Patched = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02) LegacyBadPatched = [byte[]](0x9A, 0x86, 0x9A, 0x0B, 0x00, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90) OriginalPatterns = @( @('68', '03', '01', '9A', 'FF', 'FF', '00', '00', '83', 'C4', '02') ) Fixup = @{ OperandOffset = 0x70D79 OriginalTargetSeg = 92 OriginalTargetOffset = 0x0476 PatchedTargetSeg = 117 PatchedTargetOffset = 0x0086 RetailSourceSegmentIndex = 39 RetailChainOffset = 0x2B79 RetailEntryOffset = 0x71D68 } } Wrapper = @{ Label = 'Current-slot wrapper arg site' Offset = 0xB9A8D Original = [byte[]](0x6A, 0x01, 0xFF, 0x76, 0x08, 0xFF, 0x76, 0x06) Patched = [byte[]](0x6A, 0x01, 0x6A, 0x00, 0x6A, 0x00, 0x90, 0x90) OriginalPatterns = @( @('6A', '01', 'FF', '76', '08', 'FF', '76', '06') ) } LegacyDeferredHook = @{ Label = 'Rejected deferred-event hook site' Offset = 0xC99DD Original = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02) Patched = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02) OriginalPatterns = @( @('68', '03', '01', '9A', 'FF', 'FF', '00', '00', '83', 'C4', '02') ) Fixup = @{ OperandOffset = 0xC99E1 OriginalTargetSeg = 92 OriginalTargetOffset = 0x0476 PatchedTargetSeg = 117 PatchedTargetOffset = 0x020D } } LegacyDeferredWrapper = @{ Label = 'Rejected modal wrapper arg site' Offset = 0xB9C48 Original = [byte[]](0x6A, 0x00, 0xFF, 0x76, 0x08, 0xFF, 0x76, 0x06) Patched = [byte[]](0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0x90, 0x90) OriginalPatterns = @( @('6A', '00', 'FF', '76', '08', 'FF', '76', '06') ) } } function Format-HexBytes { param([byte[]]$Bytes) return (($Bytes | ForEach-Object { $_.ToString('X2') }) -join ' ') } function Get-ByteSlice { param( [byte[]]$Bytes, [int]$Offset, [int]$Count ) if ($Offset -lt 0 -or ($Offset + $Count) -gt $Bytes.Length) { throw "Requested byte range 0x{0:X}-0x{1:X} is outside the file." -f $Offset, ($Offset + $Count - 1) } $slice = New-Object byte[] $Count [Array]::Copy($Bytes, $Offset, $slice, 0, $Count) return $slice } function Get-U16Le { param( [byte[]]$Bytes, [int]$Offset ) return [BitConverter]::ToUInt16($Bytes, $Offset) } function Get-U32Le { param( [byte[]]$Bytes, [int]$Offset ) return [BitConverter]::ToUInt32($Bytes, $Offset) } function Set-U16Le { param( [byte[]]$Bytes, [int]$Offset, [int]$Value ) $raw = [BitConverter]::GetBytes([UInt16]$Value) [Array]::Copy($raw, 0, $Bytes, $Offset, 2) } function Test-ByteArrayEqual { param( [byte[]]$Left, [byte[]]$Right ) if ($Left.Length -ne $Right.Length) { return $false } for ($i = 0; $i -lt $Left.Length; $i++) { if ($Left[$i] -ne $Right[$i]) { return $false } } return $true } function Test-BytePatternMatch { param( [byte[]]$Bytes, [object[]]$Pattern ) if ($Bytes.Length -ne $Pattern.Length) { return $false } for ($i = 0; $i -lt $Bytes.Length; $i++) { $expected = [string]$Pattern[$i] if ($expected -eq '??') { continue } if ($Bytes[$i] -ne [Convert]::ToByte($expected, 16)) { return $false } } return $true } function Convert-BytesToStoredHex { param([byte[]]$Bytes) return (($Bytes | ForEach-Object { $_.ToString('X2') }) -join '') } function Convert-StoredHexToBytes { param([string]$Hex) if ([string]::IsNullOrWhiteSpace($Hex)) { return $null } $normalized = $Hex.Replace(' ', '') if (($normalized.Length % 2) -ne 0) { throw "Invalid stored hex string length: $Hex" } $result = New-Object byte[] ($normalized.Length / 2) for ($i = 0; $i -lt $result.Length; $i++) { $result[$i] = [Convert]::ToByte($normalized.Substring($i * 2, 2), 16) } return $result } function Get-HookFixupInfo { param( [byte[]]$FileBytes, [hashtable]$Site ) $neHeaderOffset = [int](Get-U32Le -Bytes $FileBytes -Offset 0x3C) $neSignature = Get-ByteSlice -Bytes $FileBytes -Offset $neHeaderOffset -Count 2 if ($neSignature[0] -ne 0x4E -or $neSignature[1] -ne 0x45) { throw 'Executable does not contain a valid NE header at e_lfanew.' } $numSegments = [int](Get-U16Le -Bytes $FileBytes -Offset ($neHeaderOffset + 0x1C)) $segmentTableOffset = [int](Get-U16Le -Bytes $FileBytes -Offset ($neHeaderOffset + 0x22)) + $neHeaderOffset $alignmentShift = [int](Get-U16Le -Bytes $FileBytes -Offset ($neHeaderOffset + 0x32)) $operandFileOffset = [int]$Site.Fixup.OperandOffset $segmentInfoByIndex = @{} $sourceSegment = $null for ($index = 1; $index -le $numSegments; $index++) { $segmentEntryOffset = $segmentTableOffset + (($index - 1) * 8) $sectorOffset = [int](Get-U16Le -Bytes $FileBytes -Offset $segmentEntryOffset) if ($sectorOffset -eq 0) { continue } $segmentLength = [int](Get-U16Le -Bytes $FileBytes -Offset ($segmentEntryOffset + 2)) if ($segmentLength -eq 0) { $segmentLength = 0x10000 } $segmentFlags = [int](Get-U16Le -Bytes $FileBytes -Offset ($segmentEntryOffset + 4)) $segmentFileOffset = [int]($sectorOffset -shl $alignmentShift) $segmentInfo = @{ SegmentIndex = $index SegmentEntryOffset = $segmentEntryOffset SegmentFileOffset = $segmentFileOffset SegmentLength = $segmentLength SegmentFlags = $segmentFlags HasReloc = (($segmentFlags -band 0x0100) -ne 0) } $segmentInfoByIndex[$index] = $segmentInfo if ($segmentFileOffset -le $operandFileOffset -and $operandFileOffset -lt ($segmentFileOffset + $segmentLength)) { $sourceSegment = $segmentInfo break } } if ($null -eq $sourceSegment) { $fallbackSegmentIndex = [int]$Site.Fixup.RetailSourceSegmentIndex $fallbackChainOffset = [int]$Site.Fixup.RetailChainOffset if ($segmentInfoByIndex.ContainsKey($fallbackSegmentIndex)) { $fallbackSegment = $segmentInfoByIndex[$fallbackSegmentIndex] if (($fallbackSegment.SegmentFileOffset + $fallbackChainOffset) -eq $operandFileOffset) { $sourceSegment = $fallbackSegment } } } if ($null -eq $sourceSegment) { throw ("Could not locate the hook operand file offset 0x{0:X} in any NE code segment." -f $operandFileOffset) } if (-not $sourceSegment.HasReloc) { throw 'Hook source segment does not advertise a relocation table.' } $segmentIndex = [int]$sourceSegment.SegmentIndex $segmentFileOffset = [int]$sourceSegment.SegmentFileOffset $segmentLength = [int]$sourceSegment.SegmentLength $relocTableOffset = $segmentFileOffset + $segmentLength $relocCount = [int](Get-U16Le -Bytes $FileBytes -Offset $relocTableOffset) $entryOffset = $relocTableOffset + 2 $chainOffset = $operandFileOffset - $segmentFileOffset for ($index = 0; $index -lt $relocCount; $index++) { $addrType = $FileBytes[$entryOffset] $relType = $FileBytes[$entryOffset + 1] $entryChainOffset = Get-U16Le -Bytes $FileBytes -Offset ($entryOffset + 2) if ($entryChainOffset -eq $chainOffset) { return @{ SegmentIndex = $segmentIndex SegmentFileOffset = $segmentFileOffset RelocTableOffset = $relocTableOffset EntryOffset = $entryOffset EntryIndex = $index AddrType = $addrType RelType = $relType ChainOffset = $entryChainOffset TargetSeg = $FileBytes[$entryOffset + 4] Reserved = $FileBytes[$entryOffset + 5] TargetOffset = Get-U16Le -Bytes $FileBytes -Offset ($entryOffset + 6) } } if ($addrType -eq 3 -and (($relType -band 0x03) -eq 0)) { $visited = New-Object 'System.Collections.Generic.HashSet[int]' $currentChainOffset = $entryChainOffset while ($currentChainOffset -ne 0xFFFF -and $currentChainOffset -lt $segmentLength) { if (-not $visited.Add($currentChainOffset)) { break } if ($currentChainOffset -eq $chainOffset) { return @{ SegmentIndex = $segmentIndex SegmentFileOffset = $segmentFileOffset RelocTableOffset = $relocTableOffset EntryOffset = $entryOffset EntryIndex = $index AddrType = $addrType RelType = $relType ChainOffset = $entryChainOffset TargetSeg = $FileBytes[$entryOffset + 4] Reserved = $FileBytes[$entryOffset + 5] TargetOffset = Get-U16Le -Bytes $FileBytes -Offset ($entryOffset + 6) } } $currentChainOffset = Get-U16Le -Bytes $FileBytes -Offset ($segmentFileOffset + $currentChainOffset) } } $entryOffset += 8 } $fallbackEntryOffset = [int]$Site.Fixup.RetailEntryOffset $fallbackChainOffset = [int]$Site.Fixup.RetailChainOffset if ($fallbackEntryOffset -ge 0 -and ($fallbackEntryOffset + 7) -lt $FileBytes.Length) { $addrType = $FileBytes[$fallbackEntryOffset] $relType = $FileBytes[$fallbackEntryOffset + 1] $entryChainOffset = [int](Get-U16Le -Bytes $FileBytes -Offset ($fallbackEntryOffset + 2)) if ( $sourceSegment.SegmentIndex -eq [int]$Site.Fixup.RetailSourceSegmentIndex -and $chainOffset -eq $fallbackChainOffset -and $addrType -eq 3 -and (($relType -band 0x03) -eq 0) -and $entryChainOffset -eq $fallbackChainOffset ) { return @{ SegmentIndex = $segmentIndex SegmentFileOffset = $segmentFileOffset RelocTableOffset = $relocTableOffset EntryOffset = $fallbackEntryOffset EntryIndex = -1 AddrType = $addrType RelType = $relType ChainOffset = $entryChainOffset TargetSeg = $FileBytes[$fallbackEntryOffset + 4] Reserved = $FileBytes[$fallbackEntryOffset + 5] TargetOffset = Get-U16Le -Bytes $FileBytes -Offset ($fallbackEntryOffset + 6) } } } throw ("Could not find hook relocation entry for segment {0} chain offset 0x{1:X}." -f $segmentIndex, $chainOffset) } function Get-StateData { if (-not (Test-Path -LiteralPath $statePath)) { return @{} } $raw = Get-Content -LiteralPath $statePath -Raw if ([string]::IsNullOrWhiteSpace($raw)) { return @{} } $parsed = ConvertFrom-Json -InputObject $raw -AsHashtable if ($null -eq $parsed) { return @{} } return $parsed } function Save-StateData { param([hashtable]$StateData) $json = ConvertTo-Json -InputObject $StateData -Depth 4 Set-Content -LiteralPath $statePath -Value $json -Encoding ASCII } function Get-SavedOriginalBytes { param( [hashtable]$StateData, [string]$SiteKey, [int]$SiteOffset ) if (-not $StateData.ContainsKey($SiteKey)) { return $null } $siteState = $StateData[$SiteKey] if ($siteState -isnot [hashtable]) { return $null } if (-not $siteState.ContainsKey('OriginalBytes')) { return $null } if ($siteState.ContainsKey('Offset') -and ([int]$siteState.Offset -ne $SiteOffset)) { return $null } return Convert-StoredHexToBytes -Hex ([string]$siteState.OriginalBytes) } function Save-OriginalBytesIfMissing { param( [hashtable]$StateData, [string]$SiteKey, [int]$SiteOffset, [byte[]]$Bytes ) if ($StateData.ContainsKey($SiteKey) -and ($StateData[$SiteKey] -is [hashtable])) { $existing = $StateData[$SiteKey] if ($existing.ContainsKey('OriginalBytes') -and $existing.ContainsKey('Offset') -and ([int]$existing.Offset -eq $SiteOffset)) { return $false } } $StateData[$SiteKey] = @{ Offset = $SiteOffset OriginalBytes = Convert-BytesToStoredHex -Bytes $Bytes } return $true } function Get-SiteState { param( [byte[]]$FileBytes, [hashtable]$Site ) $current = Get-ByteSlice -Bytes $FileBytes -Offset $Site.Offset -Count $Site.Original.Length if ($Site.ContainsKey('Fixup')) { $fixupInfo = Get-HookFixupInfo -FileBytes $FileBytes -Site $Site $isFixupOriginal = ($fixupInfo.TargetSeg -eq $Site.Fixup.OriginalTargetSeg) -and ($fixupInfo.TargetOffset -eq $Site.Fixup.OriginalTargetOffset) $isFixupPatched = ($fixupInfo.TargetSeg -eq $Site.Fixup.PatchedTargetSeg) -and ($fixupInfo.TargetOffset -eq $Site.Fixup.PatchedTargetOffset) if ($Site.ContainsKey('LegacyBadPatched') -and (Test-ByteArrayEqual -Left $current -Right $Site.LegacyBadPatched) -and $isFixupOriginal) { return 'LegacyBadPatch' } if ((Test-ByteArrayEqual -Left $current -Right $Site.Patched) -and $isFixupPatched) { return 'Patched' } if ((Test-ByteArrayEqual -Left $current -Right $Site.Original) -and $isFixupOriginal) { return 'Original' } foreach ($pattern in $Site.OriginalPatterns) { if ((Test-BytePatternMatch -Bytes $current -Pattern $pattern) -and $isFixupOriginal) { return 'Original' } } return 'Unknown' } if (Test-ByteArrayEqual -Left $current -Right $Site.Patched) { return 'Patched' } if (Test-ByteArrayEqual -Left $current -Right $Site.Original) { return 'Original' } foreach ($pattern in $Site.OriginalPatterns) { if (Test-BytePatternMatch -Bytes $current -Pattern $pattern) { return 'Original' } } return 'Unknown' } function Assert-SiteStateKnown { param( [byte[]]$FileBytes, [hashtable]$Site ) $state = Get-SiteState -FileBytes $FileBytes -Site $Site if ($state -eq 'Unknown') { $current = Get-ByteSlice -Bytes $FileBytes -Offset $Site.Offset -Count $Site.Original.Length throw ( "{0} at file offset 0x{1:X} does not match either expected byte sequence.`nCurrent : {2}`nOriginal: {3}`nPatched : {4}`n`nRefusing to modify an unknown executable state." -f $Site.Label, $Site.Offset, (Format-HexBytes -Bytes $current), (Format-HexBytes -Bytes $Site.Original), (Format-HexBytes -Bytes $Site.Patched) ) } } function Set-ByteSlice { param( [byte[]]$Bytes, [int]$Offset, [byte[]]$Value ) [Array]::Copy($Value, 0, $Bytes, $Offset, $Value.Length) } function Show-Status { param([byte[]]$FileBytes) $hookState = Get-SiteState -FileBytes $FileBytes -Site $sites.Hook $wrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.Wrapper Write-Host '' Write-Host 'CRUSADER.EXE patch status' Write-Host '------------------------' Write-Host ("EXE: {0}" -f $exePath) Write-Host ("A hook @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState) Write-Host ("B wrapper @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState) Write-Host '' Write-Host '1. Apply supported hidden-menu patch (aliases to Experiment B)' Write-Host '2. Apply Experiment B (retarget + modal arg fix)' Write-Host '3. Restore original bytes' Write-Host '4. Exit' Write-Host '' } function Set-DesiredState { param( [bool]$HookPatched, [bool]$WrapperPatched, [string]$Label ) $fileBytes = [System.IO.File]::ReadAllBytes($exePath) $stateData = Get-StateData Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.Hook Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.Wrapper Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyDeferredHook Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyDeferredWrapper $hookCurrent = Get-ByteSlice -Bytes $fileBytes -Offset $sites.Hook.Offset -Count $sites.Hook.Original.Length $hookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.Hook $wrapperCurrent = Get-ByteSlice -Bytes $fileBytes -Offset $sites.Wrapper.Offset -Count $sites.Wrapper.Original.Length $legacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.LegacyDeferredHook $hookCurrentState = Get-SiteState -FileBytes $fileBytes -Site $sites.Hook $wrapperCurrentState = Get-SiteState -FileBytes $fileBytes -Site $sites.Wrapper $stateChanged = $false if ($hookCurrentState -eq 'Original') { $stateChanged = (Save-OriginalBytesIfMissing -StateData $stateData -SiteKey 'Hook' -SiteOffset $sites.Hook.Offset -Bytes $hookCurrent) -or $stateChanged if (-not $stateData.ContainsKey('HookFixup')) { $stateData['HookFixup'] = @{ TargetSeg = $hookFixupInfo.TargetSeg TargetOffset = $hookFixupInfo.TargetOffset Reserved = $hookFixupInfo.Reserved } $stateChanged = $true } } if ($wrapperCurrentState -eq 'Original') { $stateChanged = (Save-OriginalBytesIfMissing -StateData $stateData -SiteKey 'Wrapper' -SiteOffset $sites.Wrapper.Offset -Bytes $wrapperCurrent) -or $stateChanged } if ($stateChanged) { Save-StateData -StateData $stateData } if ($HookPatched) { $hookBytes = $sites.Hook.Patched $hookTargetSeg = [int]$sites.Hook.Fixup.PatchedTargetSeg $hookTargetOffset = [int]$sites.Hook.Fixup.PatchedTargetOffset $hookReserved = 0 } else { $hookBytes = Get-SavedOriginalBytes -StateData $stateData -SiteKey 'Hook' -SiteOffset $sites.Hook.Offset if ($null -eq $hookBytes) { if ($hookCurrentState -eq 'Original' -or $hookCurrentState -eq 'LegacyBadPatch') { $hookBytes = $hookCurrent } else { throw 'No saved original bytes are available for the Experiment A hook site. Restore requires either a prior patch run with this script or your full executable backup.' } } if ($stateData.ContainsKey('HookFixup')) { $hookTargetSeg = [int]$stateData['HookFixup'].TargetSeg $hookTargetOffset = [int]$stateData['HookFixup'].TargetOffset $hookReserved = [int]$stateData['HookFixup'].Reserved } else { $hookTargetSeg = [int]$sites.Hook.Fixup.OriginalTargetSeg $hookTargetOffset = [int]$sites.Hook.Fixup.OriginalTargetOffset $hookReserved = 0 } } if ($WrapperPatched) { $wrapperBytes = $sites.Wrapper.Patched } else { $wrapperBytes = Get-SavedOriginalBytes -StateData $stateData -SiteKey 'Wrapper' -SiteOffset $sites.Wrapper.Offset if ($null -eq $wrapperBytes) { if ($wrapperCurrentState -eq 'Original') { $wrapperBytes = $wrapperCurrent } else { throw 'No saved original bytes are available for the Experiment B wrapper site. Restore requires either a prior patch run with this script or your full executable backup.' } } } $legacyDeferredHookBytes = $sites.LegacyDeferredHook.Original $legacyDeferredHookTargetSeg = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetSeg $legacyDeferredHookTargetOffset = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetOffset $legacyDeferredHookReserved = 0 $legacyDeferredWrapperBytes = $sites.LegacyDeferredWrapper.Original Set-ByteSlice -Bytes $fileBytes -Offset $sites.Hook.Offset -Value $hookBytes $fileBytes[$hookFixupInfo.EntryOffset + 4] = [byte]$hookTargetSeg $fileBytes[$hookFixupInfo.EntryOffset + 5] = [byte]$hookReserved Set-U16Le -Bytes $fileBytes -Offset ($hookFixupInfo.EntryOffset + 6) -Value $hookTargetOffset Set-ByteSlice -Bytes $fileBytes -Offset $sites.Wrapper.Offset -Value $wrapperBytes Set-ByteSlice -Bytes $fileBytes -Offset $sites.LegacyDeferredHook.Offset -Value $legacyDeferredHookBytes $fileBytes[$legacyDeferredHookFixupInfo.EntryOffset + 4] = [byte]$legacyDeferredHookTargetSeg $fileBytes[$legacyDeferredHookFixupInfo.EntryOffset + 5] = [byte]$legacyDeferredHookReserved Set-U16Le -Bytes $fileBytes -Offset ($legacyDeferredHookFixupInfo.EntryOffset + 6) -Value $legacyDeferredHookTargetOffset Set-ByteSlice -Bytes $fileBytes -Offset $sites.LegacyDeferredWrapper.Offset -Value $legacyDeferredWrapperBytes [System.IO.File]::WriteAllBytes($exePath, $fileBytes) $verifyBytes = [System.IO.File]::ReadAllBytes($exePath) $hookState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Hook $wrapperState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Wrapper $verifiedHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.Hook $verifiedLegacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.LegacyDeferredHook $verifiedHookBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.Hook.Offset -Count $hookBytes.Length $verifiedWrapperBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.Wrapper.Offset -Count $wrapperBytes.Length $verifiedLegacyDeferredHookBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyDeferredHook.Offset -Count $legacyDeferredHookBytes.Length $verifiedLegacyDeferredWrapperBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyDeferredWrapper.Offset -Count $legacyDeferredWrapperBytes.Length if (-not (Test-ByteArrayEqual -Left $verifiedHookBytes -Right $hookBytes)) { throw 'Hook-site verification failed after write.' } if ($verifiedHookFixupInfo.TargetSeg -ne $hookTargetSeg -or $verifiedHookFixupInfo.TargetOffset -ne $hookTargetOffset) { throw 'Hook relocation verification failed after write.' } if (-not (Test-ByteArrayEqual -Left $verifiedWrapperBytes -Right $wrapperBytes)) { throw 'Wrapper-site verification failed after write.' } if (-not (Test-ByteArrayEqual -Left $verifiedLegacyDeferredHookBytes -Right $legacyDeferredHookBytes)) { throw 'Rejected deferred-event hook cleanup verification failed after write.' } if ($verifiedLegacyDeferredHookFixupInfo.TargetSeg -ne $legacyDeferredHookTargetSeg -or $verifiedLegacyDeferredHookFixupInfo.TargetOffset -ne $legacyDeferredHookTargetOffset) { throw 'Rejected deferred-event relocation cleanup verification failed after write.' } if (-not (Test-ByteArrayEqual -Left $verifiedLegacyDeferredWrapperBytes -Right $legacyDeferredWrapperBytes)) { throw 'Rejected modal-wrapper cleanup verification failed after write.' } Write-Host '' Write-Host ("Applied: {0}" -f $Label) Write-Host ("A hook @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState) Write-Host ("B wrapper @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState) Write-Host '' Write-Host 'What this means:' Write-Host '- Experiment A retargets the existing cheat-success far call into cheat_menu_open_from_current_slot while keeping the original event-dispatch framing.' Write-Host '- Experiment B preserves the wrapper mode byte `1` but forces the two ambiguous 16-bit constructor parameters to zero instead of inheriting arbitrary caller-frame values.' Write-Host '- Restore also cleans up the rejected deferred-event patch sites if they were left behind by earlier attempts.' Write-Host '' } function Invoke-MenuChoice { param([string]$SelectedChoice) switch ($SelectedChoice.Trim()) { '1' { Write-Warning 'Experiment A alone is not supported on the cheat-code path. Applying the safer Experiment B patch instead.' Set-DesiredState -HookPatched $true -WrapperPatched $true -Label 'Experiment B (via menu option 1 alias)' } '2' { Set-DesiredState -HookPatched $true -WrapperPatched $true -Label 'Experiment B (A + B)' } '3' { Set-DesiredState -HookPatched $false -WrapperPatched $false -Label 'Restore original bytes' } '4' { return $false } default { Write-Warning 'Invalid selection.' } } return $true } if (-not (Test-Path -LiteralPath $exePath)) { throw "CRUSADER.EXE was not found next to the script. Put this .ps1 file in the same folder as CRUSADER.EXE." } if ($PSBoundParameters.ContainsKey('Choice')) { [void](Invoke-MenuChoice -SelectedChoice $Choice) return } :mainloop while ($true) { $currentBytes = [System.IO.File]::ReadAllBytes($exePath) Show-Status -FileBytes $currentBytes $choice = Read-Host 'Select 1, 2, 3, or 4' if ([string]::IsNullOrEmpty($choice)) { break mainloop } if (-not (Invoke-MenuChoice -SelectedChoice $choice)) { break mainloop } }