Crusader_Decomp/patch_crusader_cheat_menu.ps1

715 lines
26 KiB
PowerShell
Raw Normal View History

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