2026-03-24 18:14:20 +01:00
param (
2026-03-25 17:36:16 +01:00
[ ValidateSet ( '1' , '2' , '3' , '4' , 'candidate-i' , 'candidate-j' , 'candidate-m' , 'candidate-n' , 'candidate-o' , 'candidate-p' , 'restore' , 'exit' ) ]
[ string ] $Choice ,
[ string ] $ExePath = $ (
if ( Test-Path -LiteralPath ( Join-Path $PSScriptRoot 'CRUSADER.EXE' ) ) {
Join-Path $PSScriptRoot 'CRUSADER.EXE'
}
elseif ( Test-Path -LiteralPath 'F:\Apps\Crusader No Remorse\CRUSADER.EXE' ) {
'F:\Apps\Crusader No Remorse\CRUSADER.EXE'
}
else {
Join-Path $PSScriptRoot 'CRUSADER.EXE'
}
)
2026-03-24 18:14:20 +01:00
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
2026-03-25 17:36:16 +01:00
$exePath = $ExePath
2026-03-24 18:14:20 +01:00
$statePath = Join-Path $PSScriptRoot 'patch_crusader_cheat_menu.state.json'
$sites = @ {
2026-03-25 17:36:16 +01:00
CtrlQDebuggerInit = @ {
Label = 'Ctrl+Q debugger-state init body'
Offset = 0xC970D
Original = [ byte[] ] (
0xA0 , 0x4F , 0x60 , 0xB4 , 0x00 , 0xF7 , 0xD8 , 0x1B , 0xC0 , 0x40 , 0xA2 , 0x4F , 0x60 , 0x80 , 0x3E , 0x4F ,
0x60 , 0x00 , 0x74 , 0x47 , 0x6A , 0xFF , 0x6A , 0xFF , 0xC4 , 0x1E , 0xD0 , 0x4C , 0x26 , 0x8A , 0x47 , 0x05 ,
0x50 , 0x1E , 0x68 , 0xD2 , 0x60 , 0x6A , 0x00 , 0x6A , 0x00 , 0x83 , 0xEC , 0x06 , 0xC7 , 0x86 , 0x76 , 0xFF ,
0x00 , 0x00 , 0x8B , 0x86 , 0x76 , 0xFF , 0xF7 , 0xD0 , 0x89 , 0x86 , 0x78 , 0xFF , 0xC6 , 0x86 , 0x7A , 0xFF ,
0x00 , 0x6A , 0x00 , 0x6A , 0x00 , 0x9A , 0xFF , 0xFF , 0x00 , 0x00 , 0x83 , 0xC4 , 0x14 , 0x52 , 0x50 , 0x9A ,
0xFF , 0xFF , 0x00 , 0x00 , 0x83 , 0xC4 , 0x08 , 0x5F , 0x5E , 0xC9 , 0xCB , 0x6A , 0xFF , 0x6A , 0xFF , 0xC4 ,
0x1E , 0xD0 , 0x4C , 0x26 , 0x8A , 0x47 , 0x05 , 0x50 , 0x1E , 0x68 , 0xEE , 0x60 , 0x6A , 0x00 , 0x6A , 0x00 ,
0x83 , 0xEC , 0x06 , 0xC7
)
Patched = [ byte[] ] (
0xA1 , 0x9C , 0x65 , 0x0B , 0x06 , 0x9E , 0x65 , 0x74 , 0x10 , 0xC4 , 0x1E , 0x9C , 0x65 , 0xC6 , 0x47 , 0x75 ,
0x00 , 0xC6 , 0x47 , 0x74 , 0x01 , 0x5F , 0x5E , 0xC9 , 0xCB , 0x6A , 0x00 , 0x6A , 0x00 , 0xE9 , 0x25 , 0x00 ,
0x55 , 0x8B , 0xEC , 0xA1 , 0x9C , 0x65 , 0x39 , 0x46 , 0x06 , 0x75 , 0x16 , 0xA1 , 0x9E , 0x65 , 0x39 , 0x46 ,
0x08 , 0x75 , 0x0E , 0xC4 , 0x5E , 0x06 , 0xC7 , 0x47 , 0x74 , 0x00 , 0x00 , 0x6A , 0x00 , 0x6A , 0x00 , 0xEB ,
0x0E , 0x5D , 0xCB , 0x90 , 0x90 , 0x9A , 0xFF , 0xFF , 0x00 , 0x00 , 0x83 , 0xC4 , 0x04 , 0xEB , 0x0A , 0x9A ,
0xFF , 0xFF , 0x00 , 0x00 , 0x83 , 0xC4 , 0x04 , 0x5D , 0xCB , 0x0B , 0xC2 , 0x74 , 0x13 , 0xA3 , 0x9C , 0x65 ,
0x89 , 0x16 , 0x9E , 0x65 , 0x89 , 0xC3 , 0x8E , 0xC2 , 0xC6 , 0x47 , 0x75 , 0x00 , 0xC6 , 0x47 , 0x74 , 0x01 ,
0x5F , 0x5E , 0xC9 , 0xCB
)
LegacyPatched = [ byte[] ] (
0xA1 , 0x9C , 0x65 , 0x0B , 0x06 , 0x9E , 0x65 , 0x75 , 0x52 , 0x31 , 0xC0 , 0x50 , 0x50 , 0xEB , 0x36 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x9A , 0xFF , 0xFF , 0x00 , 0x00 , 0x83 , 0xC4 , 0x04 , 0x0B , 0xC2 , 0x75 ,
0x03 , 0xE9 , 0x46 , 0x06 , 0xA3 , 0x9C , 0x65 , 0x89 , 0x16 , 0x9E , 0x65 , 0xC4 , 0x1E , 0x9C , 0x65 , 0xC6 ,
0x47 , 0x75 , 0x01 , 0xC6 , 0x47 , 0x74 , 0x00 , 0xC7 , 0x47 , 0x76 , 0x00 , 0x00 , 0xC7 , 0x47 , 0x78 , 0x00 ,
0x00 , 0xE9 , 0x26 , 0x06 , 0xC7
)
LegacyPatchedVariants = @ (
[ byte[] ] (
0xA1 , 0x9C , 0x65 , 0x0B , 0x06 , 0x9E , 0x65 , 0x75 , 0x5E , 0x31 , 0xC0 , 0x50 , 0x50 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x9A , 0xFF , 0xFF , 0x00 , 0x00 , 0x83 , 0xC4 , 0x04 ,
0xEB , 0x0D , 0x9A , 0xFF , 0xFF , 0x00 , 0x00 , 0x83 , 0xC4 , 0x04 , 0x5F , 0x5E , 0xC9 , 0xCB , 0x0B , 0xC2 ,
0x74 , 0xF8 , 0xA3 , 0x9C , 0x65 , 0x89 , 0x16 , 0x9E , 0x65 , 0x6A , 0x00 , 0x6A , 0x00 , 0xEB , 0xE2 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0xC7
) ,
[ byte[] ] (
0xA1 , 0x9C , 0x65 , 0x0B , 0x06 , 0x9E , 0x65 , 0x75 , 0x5E , 0x31 , 0xC0 , 0x50 , 0x50 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x9A , 0xFF , 0xFF , 0x00 , 0x00 , 0x83 , 0xC4 , 0x04 , 0xEB , 0x0D , 0x9A ,
0xFF , 0xFF , 0xFF , 0xFF , 0x00 , 0x00 , 0x04 , 0x5F , 0x5E , 0xC9 , 0xCB , 0x0B , 0xC2 , 0x74 , 0xF7 , 0xA3 ,
0x9C , 0x65 , 0x89 , 0x16 , 0x9E , 0x65 , 0x6A , 0x00 , 0x6A , 0x00 , 0xEB , 0xE2 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0xEC , 0x06 , 0xC7
) ,
[ byte[] ] (
0xA1 , 0x9C , 0x65 , 0x0B , 0x06 , 0x9E , 0x65 , 0x75 , 0x55 , 0x31 , 0xC0 , 0x50 , 0x50 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 ,
0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x9A , 0xFF , 0xFF , 0x00 , 0x00 , 0x83 , 0xC4 , 0x04 ,
0x52 , 0x50 , 0x9A , 0xFF , 0xFF , 0x00 , 0x00 , 0x83 , 0xC4 , 0x04 , 0xA3 , 0x9C , 0x65 , 0x89 , 0x16 , 0x9E ,
0x65 , 0xC4 , 0x1E , 0x9C , 0x65 , 0xC7 , 0x07 , 0xAF , 0x65 , 0xC6 , 0x47 , 0x75 , 0x01 , 0xC6 , 0x47 , 0x74 ,
0x00 , 0x5F , 0x5E , 0xC9 , 0xCB , 0x90 , 0x90
)
)
LegacyPatchedFixupState = 'Any'
OriginalPatterns = @ (
@ ( 'A0' , '4F' , '60' , 'B4' , '00' , 'F7' , 'D8' , '1B' , 'C0' , '40' , 'A2' , '4F' , '60' , '80' , '3E' , '4F' , '60' , '00' , '74' , '47' )
)
Fixup = @ {
OperandOffset = 0xC9753
OriginalTargetSeg = 107
OriginalTargetOffset = 0x0046
PatchedTargetSeg = 130
PatchedTargetOffset = 0x0000
}
}
DebuggerCallback = @ {
Label = 'legacy secondary call cleanup'
Offset = 0xC975D
Original = [ byte[] ] ( 0xFF , 0xFF , 0x00 , 0x00 )
Patched = [ byte[] ] ( 0xFF , 0xFF , 0x00 , 0x00 )
OriginalPatterns = @ (
@ ( 'FF' , 'FF' , '00' , '00' )
)
Fixup = @ {
OperandOffset = 0xC975D
OriginalTargetSeg = 101
OriginalTargetOffset = 0x1588
PatchedTargetSeg = 101
PatchedTargetOffset = 0x1588
}
}
PrivateBreakpointMethod0 = @ {
Label = 'private break callback slot'
Offset = 0xEA328
Original = [ byte[] ] ( 0xFF , 0xFF , 0x00 , 0x00 )
Patched = [ byte[] ] ( 0xFF , 0xFF , 0x00 , 0x00 )
OriginalPatterns = @ (
@ ( 'FF' , 'FF' , '00' , '00' )
)
Fixup = @ {
OperandOffset = 0xEA328
OriginalTargetSeg = 133
OriginalTargetOffset = 0x1162
PatchedTargetSeg = 117
PatchedTargetOffset = 0x0086
}
}
PrivateBreakpointMethod1 = @ {
Label = 'private helper callback slot'
Offset = 0xEA32C
Original = [ byte[] ] ( 0xFF , 0xFF , 0x00 , 0x00 )
Patched = [ byte[] ] ( 0xFF , 0xFF , 0x00 , 0x00 )
OriginalPatterns = @ (
@ ( 'FF' , 'FF' , '00' , '00' )
)
Fixup = @ {
OperandOffset = 0xEA32C
OriginalTargetSeg = 133
OriginalTargetOffset = 0x1278
PatchedTargetSeg = 130
PatchedTargetOffset = 0x0474
}
}
LegacyBreakpointCallback = @ {
Label = 'legacy seg1408 break callback target'
Offset = 0xEA1AB
Original = [ byte[] ] ( 0xFF , 0xFF , 0x00 , 0x00 )
Patched = [ byte[] ] ( 0xFF , 0xFF , 0x00 , 0x00 )
OriginalPatterns = @ (
@ ( 'FF' , 'FF' , '00' , '00' )
)
Fixup = @ {
OperandOffset = 0xEA1AB
OriginalTargetSeg = 130
OriginalTargetOffset = 0x046F
PatchedTargetSeg = 117
PatchedTargetOffset = 0x0086
}
}
CallbackGuardCode = @ {
Label = 'seg1408 break-next dispatch patch'
Offset = 0xCEAD3
Original = [ byte[] ] ( 0x26 , 0x8B , 0x1F , 0xFF , 0x1F )
Patched = [ byte[] ] ( 0xFF , 0x1E , 0x97 , 0x65 , 0x90 )
OriginalPatterns = @ (
@ ( '26' , '8B' , '1F' , 'FF' , '1F' )
)
}
CallbackTargetSlot = @ {
Label = 'break-next private stub target slot'
Offset = 0xEA197
Original = [ byte[] ] ( 0xFF , 0xFF , 0x00 , 0x00 )
Patched = [ byte[] ] ( 0xFF , 0xFF , 0x00 , 0x00 )
OriginalPatterns = @ (
@ ( 'FF' , 'FF' , '00' , '00' )
)
Fixup = @ {
OperandOffset = 0xEA197
OriginalTargetSeg = 5
OriginalTargetOffset = 0x08D2
PatchedTargetSeg = 131
PatchedTargetOffset = 0x2318
}
}
InterpreterBreakCall = @ {
Label = 'interpreter debugger break callsite'
Offset = 0xCFAB5
Original = [ byte[] ] ( 0x9A , 0xFF , 0xFF , 0x00 , 0x00 )
Patched = [ byte[] ] ( 0x9A , 0xFF , 0xFF , 0x00 , 0x00 )
OriginalPatterns = @ (
@ ( '9A' , 'FF' , 'FF' , '00' , '00' )
)
Fixup = @ {
OperandOffset = 0xCFAB6
OriginalTargetSeg = 130
OriginalTargetOffset = 0x0053
PatchedTargetSeg = 131
PatchedTargetOffset = 0x232D
}
}
2026-03-24 18:14:20 +01:00
Hook = @ {
2026-03-25 17:36:16 +01:00
Label = 'Rejected direct cheat hook site'
2026-03-24 18:14:20 +01:00
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 = @ {
2026-03-25 17:36:16 +01:00
Label = 'Rejected current-slot wrapper arg site'
2026-03-24 18:14:20 +01:00
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' )
)
}
}
2026-03-25 17:36:16 +01:00
$candidateProfiles = [ ordered ] @ {
'candidate-o' = @ {
MenuKey = '1'
Label = 'Candidate O'
ArmMode = 'interpreter-callsite'
UiTargetSeg = 117
UiTargetOffset = 0x020D
PatchCurrentUnitWrapper = $false
PatchModalWrapper = $true
Summary = 'interpreter callsite retarget -> corrected private modal stub with zeroed modal-wrapper args'
}
'candidate-p' = @ {
MenuKey = '2'
Label = 'Candidate P'
ArmMode = 'interpreter-callsite'
UiTargetSeg = 117
UiTargetOffset = 0x0086
PatchCurrentUnitWrapper = $true
PatchModalWrapper = $false
Summary = 'interpreter callsite retarget -> corrected private current-unit stub with zeroed current-slot args'
}
}
$script:ctrlQPatchedTemplate = [ byte[] ] $sites . CtrlQDebuggerInit . Patched . Clone ( )
$script:configuredProfileKey = 'candidate-o'
function Find-ByteSequenceOffset {
param (
[ byte[] ] $Bytes ,
[ byte[] ] $Pattern
)
$lastStart = $Bytes . Length - $Pattern . Length
for ( $start = 0 ; $start -le $lastStart ; $start + + ) {
$matched = $true
for ( $index = 0 ; $index -lt $Pattern . Length ; $index + + ) {
if ( $Bytes [ $start + $index ] -ne $Pattern [ $index ] ) {
$matched = $false
break
}
}
if ( $matched ) {
return $start
}
}
throw 'Could not find the private-vtable base immediate inside the Ctrl+Q patch template.'
}
function New-CtrlQPatchedBytes {
param ( [ string ] $ArmMode )
switch ( $ArmMode ) {
'interpreter-callsite' {
return [ byte[] ] (
0xA1 , 0x9C , 0x65 , 0x0B , 0x06 , 0x9E , 0x65 , 0x74 , 0x10 , 0xC4 , 0x1E ,
0x9C , 0x65 , 0xC6 , 0x47 , 0x75 , 0x00 , 0xC6 , 0x47 , 0x74 , 0x01 , 0x5F ,
0x5E , 0xC9 , 0xCB , 0x6A , 0x00 , 0x6A , 0x00 , 0xE9 , 0x25 , 0x00 , 0x55 ,
0x8B , 0xEC , 0xA1 , 0x9C , 0x65 , 0x39 , 0x46 , 0x06 , 0x75 , 0x16 , 0xA1 ,
0x9E , 0x65 , 0x39 , 0x46 , 0x08 , 0x75 , 0x0E , 0xC4 , 0x5E , 0x06 , 0xC7 ,
0x47 , 0x74 , 0x00 , 0x00 , 0x6A , 0x00 , 0x6A , 0x00 , 0xEB , 0x0E , 0x5D ,
0xCB , 0x90 , 0x90 , 0x9A , 0xFF , 0xFF , 0x00 , 0x00 , 0x83 , 0xC4 , 0x04 ,
0xEB , 0x0A , 0x9A , 0xFF , 0xFF , 0x00 , 0x00 , 0x83 , 0xC4 , 0x04 , 0x5D ,
0xCB , 0x0B , 0xC2 , 0x74 , 0x13 , 0xA3 , 0x9C , 0x65 , 0x89 , 0x16 , 0x9E ,
0x65 , 0x89 , 0xC3 , 0x8E , 0xC2 , 0xC6 , 0x47 , 0x75 , 0x00 , 0xC6 , 0x47 ,
0x74 , 0x01 , 0x5F , 0x5E , 0xC9 , 0xCB
)
}
default {
throw " Unsupported arm mode ' $ArmMode '. "
}
}
}
function Get-ConfiguredCandidateProfile {
return $candidateProfiles [ $script:configuredProfileKey ]
}
function Set-ConfiguredCandidateProfile {
param ( [ string ] $ProfileKey )
if ( -not $candidateProfiles . Contains ( $ProfileKey ) ) {
throw " Unknown candidate profile ' $ProfileKey '. "
}
$profile = $candidateProfiles [ $ProfileKey ]
$sites . CtrlQDebuggerInit . Patched = New-CtrlQPatchedBytes -ArmMode ( [ string ] $profile . ArmMode )
$sites . DebuggerCallback . Fixup . PatchedTargetSeg = [ int ] $profile . UiTargetSeg
$sites . DebuggerCallback . Fixup . PatchedTargetOffset = [ int ] $profile . UiTargetOffset
$script:configuredProfileKey = $ProfileKey
}
function Test-CandidateApplied {
param (
[ byte[] ] $FileBytes ,
[ string ] $ProfileKey
)
$previousProfileKey = $script:configuredProfileKey
try {
Set-ConfiguredCandidateProfile -ProfileKey $ProfileKey
return (
( Get-SiteState -FileBytes $FileBytes -Site $sites . CtrlQDebuggerInit ) -eq 'Patched' -and
( Get-SiteState -FileBytes $FileBytes -Site $sites . InterpreterBreakCall ) -eq 'Patched' -and
( Get-SiteState -FileBytes $FileBytes -Site $sites . DebuggerCallback ) -eq 'Patched' -and
( Get-SiteState -FileBytes $FileBytes -Site $sites . CallbackGuardCode ) -eq 'Original' -and
( Get-SiteState -FileBytes $FileBytes -Site $sites . CallbackTargetSlot ) -eq 'Original' -and
( Get-SiteState -FileBytes $FileBytes -Site $sites . Wrapper ) -eq $ ( if ( $candidateProfiles [ $ProfileKey ] . PatchCurrentUnitWrapper ) { 'Patched' } else { 'Original' } ) -and
( Get-SiteState -FileBytes $FileBytes -Site $sites . LegacyDeferredWrapper ) -eq $ ( if ( $candidateProfiles [ $ProfileKey ] . PatchModalWrapper ) { 'Patched' } else { 'Original' } )
)
}
finally {
Set-ConfiguredCandidateProfile -ProfileKey $previousProfileKey
}
}
function Get-AppliedCandidateProfileKey {
param ( [ byte[] ] $FileBytes )
foreach ( $profileKey in $candidateProfiles . Keys ) {
if ( Test-CandidateApplied -FileBytes $FileBytes -ProfileKey $profileKey ) {
return $profileKey
}
}
return $null
}
Set-ConfiguredCandidateProfile -ProfileKey $script:configuredProfileKey
2026-03-24 18:14:20 +01:00
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'
}
}
2026-03-25 17:36:16 +01:00
$legacyCandidates = @ ( )
if ( $Site . ContainsKey ( 'LegacyPatched' ) ) {
$legacyCandidates + = , ( [ byte[] ] $Site . LegacyPatched )
}
if ( $Site . ContainsKey ( 'LegacyPatchedVariants' ) ) {
$legacyCandidates + = $Site . LegacyPatchedVariants
}
foreach ( $legacyCandidate in $legacyCandidates ) {
if ( Test-ByteArrayEqual -Left $current -Right $legacyCandidate ) {
$legacyFixupState = if ( $Site . ContainsKey ( 'LegacyPatchedFixupState' ) ) { [ string ] $Site . LegacyPatchedFixupState } else { 'Patched' }
if (
( $legacyFixupState -eq 'Patched' -and $isFixupPatched ) -or
( $legacyFixupState -eq 'Original' -and $isFixupOriginal ) -or
( $legacyFixupState -eq 'Any' -and ( $isFixupPatched -or $isFixupOriginal ) )
) {
return 'LegacyPatched'
}
}
}
2026-03-24 18:14:20 +01:00
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. `n Current : {2} `n Original: {3} `n Patched : {4} `n `n Refusing 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 )
2026-03-25 17:36:16 +01:00
$selectedProfile = Get-ConfiguredCandidateProfile
$appliedProfileKey = Get-AppliedCandidateProfileKey -FileBytes $FileBytes
$appliedProfile = if ( $null -ne $appliedProfileKey ) { $candidateProfiles [ $appliedProfileKey ] } else { $null }
$ctrlQState = Get-SiteState -FileBytes $FileBytes -Site $sites . CtrlQDebuggerInit
$interpreterCallState = Get-SiteState -FileBytes $FileBytes -Site $sites . InterpreterBreakCall
$privateDispatchState = Get-SiteState -FileBytes $FileBytes -Site $sites . DebuggerCallback
$callbackGuardState = Get-SiteState -FileBytes $FileBytes -Site $sites . CallbackGuardCode
$callbackTargetState = Get-SiteState -FileBytes $FileBytes -Site $sites . CallbackTargetSlot
2026-03-24 18:14:20 +01:00
$hookState = Get-SiteState -FileBytes $FileBytes -Site $sites . Hook
$wrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites . Wrapper
2026-03-25 17:36:16 +01:00
$deferredHookState = Get-SiteState -FileBytes $FileBytes -Site $sites . LegacyDeferredHook
$deferredWrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites . LegacyDeferredWrapper
2026-03-24 18:14:20 +01:00
Write-Host ''
Write-Host 'CRUSADER.EXE patch status'
Write-Host '------------------------'
Write-Host ( " EXE: {0} " -f $exePath )
2026-03-25 17:36:16 +01:00
Write-Host ( " Selected candidate : {0} ({1}) " -f $selectedProfile . Label , $selectedProfile . Summary )
if ( $null -ne $appliedProfile ) {
Write-Host ( " Applied candidate : {0} ({1}) " -f $appliedProfile . Label , $appliedProfile . Summary )
}
elseif ( $ctrlQState -eq 'Original' ) {
Write-Host 'Applied candidate : Retail/original'
}
else {
Write-Host 'Applied candidate : Unknown'
}
Write-Host ( " 0x410 init body @ 0x{0:X}: {1} " -f $sites . CtrlQDebuggerInit . Offset , $ctrlQState )
Write-Host ( " Interpreter call @ 0x{0:X}: {1} " -f $sites . InterpreterBreakCall . Offset , $interpreterCallState )
Write-Host ( " Private UI call @ 0x{0:X}: {1} " -f $sites . DebuggerCallback . Offset , $privateDispatchState )
Write-Host ( " Legacy break hook @ 0x{0:X}: {1} " -f $sites . CallbackGuardCode . Offset , $callbackGuardState )
Write-Host ( " Legacy target slot @ 0x{0:X}: {1} " -f $sites . CallbackTargetSlot . Offset , $callbackTargetState )
Write-Host ( " Current-unit args @ 0x{0:X}: {1} " -f $sites . Wrapper . Offset , $wrapperState )
Write-Host ( " Modal wrapper args @ 0x{0:X}: {1} " -f $sites . LegacyDeferredWrapper . Offset , $deferredWrapperState )
Write-Host ( " Deferred hook cleanup @ 0x{0:X}: {1} " -f $sites . LegacyDeferredHook . Offset , $deferredHookState )
Write-Host ( " Direct hook cleanup @ 0x{0:X}: {1} " -f $sites . Hook . Offset , $hookState )
2026-03-24 18:14:20 +01:00
Write-Host ''
2026-03-25 17:36:16 +01:00
foreach ( $profileKey in $candidateProfiles . Keys ) {
$profile = $candidateProfiles [ $profileKey ]
$marker = if ( $appliedProfileKey -eq $profileKey ) { 'applied' } else { 'ready' }
Write-Host ( " {0}. Apply {1} [{2}] {3} " -f $profile . MenuKey , $profile . Label , $marker , $profile . Summary )
}
2026-03-24 18:14:20 +01:00
Write-Host '3. Restore original bytes'
Write-Host '4. Exit'
Write-Host ''
}
function Set-DesiredState {
param (
2026-03-25 17:36:16 +01:00
[ bool ] $CtrlQPatched ,
[ string ] $Label ,
[ string ] $ProfileKey
2026-03-24 18:14:20 +01:00
)
$fileBytes = [ System.IO.File ] :: ReadAllBytes ( $exePath )
2026-03-25 17:36:16 +01:00
$appliedProfileKey = Get-AppliedCandidateProfileKey -FileBytes $fileBytes
$profileKeyForStateCheck = if ( $null -ne $appliedProfileKey ) { $appliedProfileKey } elseif ( $CtrlQPatched ) { $ProfileKey } else { $script:configuredProfileKey }
$profileKeyForWrite = if ( $CtrlQPatched ) { $ProfileKey } else { $profileKeyForStateCheck }
if ( [ string ] :: IsNullOrWhiteSpace ( $profileKeyForStateCheck ) ) {
throw 'No candidate profile is available for state validation.'
}
Set-ConfiguredCandidateProfile -ProfileKey $profileKeyForStateCheck
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites . CtrlQDebuggerInit
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites . InterpreterBreakCall
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites . DebuggerCallback
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites . CallbackGuardCode
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites . CallbackTargetSlot
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites . PrivateBreakpointMethod0
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites . PrivateBreakpointMethod1
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites . LegacyBreakpointCallback
2026-03-24 18:14:20 +01:00
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
2026-03-25 17:36:16 +01:00
Set-ConfiguredCandidateProfile -ProfileKey $profileKeyForWrite
$activeProfile = Get-ConfiguredCandidateProfile
$ctrlQFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites . CtrlQDebuggerInit
$interpreterBreakCallFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites . InterpreterBreakCall
$debuggerCallbackFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites . DebuggerCallback
$callbackTargetFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites . CallbackTargetSlot
$privateMethod0FixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites . PrivateBreakpointMethod0
$privateMethod1FixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites . PrivateBreakpointMethod1
$legacyCallbackFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites . LegacyBreakpointCallback
2026-03-24 18:14:20 +01:00
$hookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites . Hook
$legacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites . LegacyDeferredHook
2026-03-25 17:36:16 +01:00
$ctrlQBytes = if ( $CtrlQPatched ) { $sites . CtrlQDebuggerInit . Patched } else { $sites . CtrlQDebuggerInit . Original }
$ctrlQTargetSeg = if ( $CtrlQPatched ) { [ int ] $sites . CtrlQDebuggerInit . Fixup . PatchedTargetSeg } else { [ int ] $sites . CtrlQDebuggerInit . Fixup . OriginalTargetSeg }
$ctrlQTargetOffset = if ( $CtrlQPatched ) { [ int ] $sites . CtrlQDebuggerInit . Fixup . PatchedTargetOffset } else { [ int ] $sites . CtrlQDebuggerInit . Fixup . OriginalTargetOffset }
$interpreterBreakCallBytes = $sites . InterpreterBreakCall . Original
$interpreterBreakCallSeg = [ int ] $sites . InterpreterBreakCall . Fixup . OriginalTargetSeg
$interpreterBreakCallOffset = [ int ] $sites . InterpreterBreakCall . Fixup . OriginalTargetOffset
if ( $CtrlQPatched ) {
$interpreterBreakCallBytes = $sites . InterpreterBreakCall . Patched
$interpreterBreakCallSeg = [ int ] $sites . InterpreterBreakCall . Fixup . PatchedTargetSeg
$interpreterBreakCallOffset = [ int ] $sites . InterpreterBreakCall . Fixup . PatchedTargetOffset
2026-03-24 18:14:20 +01:00
}
2026-03-25 17:36:16 +01:00
$debuggerCallbackBytes = $sites . DebuggerCallback . Original
$debuggerCallbackSeg = [ int ] $sites . DebuggerCallback . Fixup . OriginalTargetSeg
$debuggerCallbackOffset = [ int ] $sites . DebuggerCallback . Fixup . OriginalTargetOffset
if ( $CtrlQPatched ) {
$debuggerCallbackBytes = $sites . DebuggerCallback . Patched
$debuggerCallbackSeg = [ int ] $sites . DebuggerCallback . Fixup . PatchedTargetSeg
$debuggerCallbackOffset = [ int ] $sites . DebuggerCallback . Fixup . PatchedTargetOffset
2026-03-24 18:14:20 +01:00
}
2026-03-25 17:36:16 +01:00
$callbackGuardBytes = $sites . CallbackGuardCode . Original
$callbackTargetBytes = $sites . CallbackTargetSlot . Original
$callbackTargetSeg = [ int ] $sites . CallbackTargetSlot . Fixup . OriginalTargetSeg
$callbackTargetOffset = [ int ] $sites . CallbackTargetSlot . Fixup . OriginalTargetOffset
$privateMethod0Bytes = $sites . PrivateBreakpointMethod0 . Original
$privateMethod0TargetSeg = [ int ] $sites . PrivateBreakpointMethod0 . Fixup . OriginalTargetSeg
$privateMethod0TargetOffset = [ int ] $sites . PrivateBreakpointMethod0 . Fixup . OriginalTargetOffset
$privateMethod1Bytes = $sites . PrivateBreakpointMethod1 . Original
$privateMethod1TargetSeg = [ int ] $sites . PrivateBreakpointMethod1 . Fixup . OriginalTargetSeg
$privateMethod1TargetOffset = [ int ] $sites . PrivateBreakpointMethod1 . Fixup . OriginalTargetOffset
$legacyCallbackBytes = $sites . LegacyBreakpointCallback . Original
$legacyCallbackTargetSeg = [ int ] $sites . LegacyBreakpointCallback . Fixup . OriginalTargetSeg
$legacyCallbackTargetOffset = [ int ] $sites . LegacyBreakpointCallback . Fixup . OriginalTargetOffset
$hookBytes = $sites . Hook . Original
$hookTargetSeg = [ int ] $sites . Hook . Fixup . OriginalTargetSeg
$hookTargetOffset = [ int ] $sites . Hook . Fixup . OriginalTargetOffset
$hookReserved = 0
$wrapperBytes = if ( $CtrlQPatched -and $activeProfile . PatchCurrentUnitWrapper ) { $sites . Wrapper . Patched } else { $sites . Wrapper . Original }
2026-03-24 18:14:20 +01:00
$legacyDeferredHookBytes = $sites . LegacyDeferredHook . Original
$legacyDeferredHookTargetSeg = [ int ] $sites . LegacyDeferredHook . Fixup . OriginalTargetSeg
$legacyDeferredHookTargetOffset = [ int ] $sites . LegacyDeferredHook . Fixup . OriginalTargetOffset
$legacyDeferredHookReserved = 0
2026-03-25 17:36:16 +01:00
$legacyDeferredWrapperBytes = if ( $CtrlQPatched -and $activeProfile . PatchModalWrapper ) { $sites . LegacyDeferredWrapper . Patched } else { $sites . LegacyDeferredWrapper . Original }
Set-ByteSlice -Bytes $fileBytes -Offset $sites . CtrlQDebuggerInit . Offset -Value $ctrlQBytes
$fileBytes [ $ctrlQFixupInfo . EntryOffset + 4 ] = [ byte ] $ctrlQTargetSeg
$fileBytes [ $ctrlQFixupInfo . EntryOffset + 5 ] = 0
Set-U16Le -Bytes $fileBytes -Offset ( $ctrlQFixupInfo . EntryOffset + 6 ) -Value $ctrlQTargetOffset
Set-ByteSlice -Bytes $fileBytes -Offset $sites . InterpreterBreakCall . Offset -Value $interpreterBreakCallBytes
$fileBytes [ $interpreterBreakCallFixupInfo . EntryOffset + 4 ] = [ byte ] $interpreterBreakCallSeg
$fileBytes [ $interpreterBreakCallFixupInfo . EntryOffset + 5 ] = 0
Set-U16Le -Bytes $fileBytes -Offset ( $interpreterBreakCallFixupInfo . EntryOffset + 6 ) -Value $interpreterBreakCallOffset
Set-ByteSlice -Bytes $fileBytes -Offset $sites . DebuggerCallback . Offset -Value $debuggerCallbackBytes
$fileBytes [ $debuggerCallbackFixupInfo . EntryOffset + 4 ] = [ byte ] $debuggerCallbackSeg
$fileBytes [ $debuggerCallbackFixupInfo . EntryOffset + 5 ] = 0
Set-U16Le -Bytes $fileBytes -Offset ( $debuggerCallbackFixupInfo . EntryOffset + 6 ) -Value $debuggerCallbackOffset
Set-ByteSlice -Bytes $fileBytes -Offset $sites . CallbackGuardCode . Offset -Value $callbackGuardBytes
Set-ByteSlice -Bytes $fileBytes -Offset $sites . CallbackTargetSlot . Offset -Value $callbackTargetBytes
$fileBytes [ $callbackTargetFixupInfo . EntryOffset + 4 ] = [ byte ] $callbackTargetSeg
$fileBytes [ $callbackTargetFixupInfo . EntryOffset + 5 ] = 0
Set-U16Le -Bytes $fileBytes -Offset ( $callbackTargetFixupInfo . EntryOffset + 6 ) -Value $callbackTargetOffset
Set-ByteSlice -Bytes $fileBytes -Offset $sites . PrivateBreakpointMethod0 . Offset -Value $privateMethod0Bytes
$fileBytes [ $privateMethod0FixupInfo . EntryOffset + 4 ] = [ byte ] $privateMethod0TargetSeg
$fileBytes [ $privateMethod0FixupInfo . EntryOffset + 5 ] = 0
Set-U16Le -Bytes $fileBytes -Offset ( $privateMethod0FixupInfo . EntryOffset + 6 ) -Value $privateMethod0TargetOffset
Set-ByteSlice -Bytes $fileBytes -Offset $sites . PrivateBreakpointMethod1 . Offset -Value $privateMethod1Bytes
$fileBytes [ $privateMethod1FixupInfo . EntryOffset + 4 ] = [ byte ] $privateMethod1TargetSeg
$fileBytes [ $privateMethod1FixupInfo . EntryOffset + 5 ] = 0
Set-U16Le -Bytes $fileBytes -Offset ( $privateMethod1FixupInfo . EntryOffset + 6 ) -Value $privateMethod1TargetOffset
Set-ByteSlice -Bytes $fileBytes -Offset $sites . LegacyBreakpointCallback . Offset -Value $legacyCallbackBytes
$fileBytes [ $legacyCallbackFixupInfo . EntryOffset + 4 ] = [ byte ] $legacyCallbackTargetSeg
$fileBytes [ $legacyCallbackFixupInfo . EntryOffset + 5 ] = 0
Set-U16Le -Bytes $fileBytes -Offset ( $legacyCallbackFixupInfo . EntryOffset + 6 ) -Value $legacyCallbackTargetOffset
2026-03-24 18:14:20 +01:00
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 )
2026-03-25 17:36:16 +01:00
$ctrlQState = Get-SiteState -FileBytes $verifyBytes -Site $sites . CtrlQDebuggerInit
$interpreterCallState = Get-SiteState -FileBytes $verifyBytes -Site $sites . InterpreterBreakCall
$privateDispatchState = Get-SiteState -FileBytes $verifyBytes -Site $sites . DebuggerCallback
$callbackGuardState = Get-SiteState -FileBytes $verifyBytes -Site $sites . CallbackGuardCode
$callbackTargetState = Get-SiteState -FileBytes $verifyBytes -Site $sites . CallbackTargetSlot
2026-03-24 18:14:20 +01:00
$hookState = Get-SiteState -FileBytes $verifyBytes -Site $sites . Hook
$wrapperState = Get-SiteState -FileBytes $verifyBytes -Site $sites . Wrapper
2026-03-25 17:36:16 +01:00
$deferredHookState = Get-SiteState -FileBytes $verifyBytes -Site $sites . LegacyDeferredHook
$deferredWrapperState = Get-SiteState -FileBytes $verifyBytes -Site $sites . LegacyDeferredWrapper
$verifiedCtrlQFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites . CtrlQDebuggerInit
$verifiedInterpreterBreakCallFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites . InterpreterBreakCall
$verifiedDebuggerCallbackFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites . DebuggerCallback
$verifiedCallbackTargetFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites . CallbackTargetSlot
$verifiedPrivateMethod0FixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites . PrivateBreakpointMethod0
$verifiedPrivateMethod1FixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites . PrivateBreakpointMethod1
$verifiedLegacyCallbackFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites . LegacyBreakpointCallback
2026-03-24 18:14:20 +01:00
$verifiedHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites . Hook
$verifiedLegacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites . LegacyDeferredHook
2026-03-25 17:36:16 +01:00
$verifiedCtrlQBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites . CtrlQDebuggerInit . Offset -Count $ctrlQBytes . Length
$verifiedInterpreterBreakCallBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites . InterpreterBreakCall . Offset -Count $interpreterBreakCallBytes . Length
$verifiedDebuggerCallbackBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites . DebuggerCallback . Offset -Count $debuggerCallbackBytes . Length
$verifiedCallbackGuardBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites . CallbackGuardCode . Offset -Count $callbackGuardBytes . Length
$verifiedCallbackTargetBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites . CallbackTargetSlot . Offset -Count $callbackTargetBytes . Length
$verifiedPrivateMethod0Bytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites . PrivateBreakpointMethod0 . Offset -Count $privateMethod0Bytes . Length
$verifiedPrivateMethod1Bytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites . PrivateBreakpointMethod1 . Offset -Count $privateMethod1Bytes . Length
$verifiedLegacyCallbackBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites . LegacyBreakpointCallback . Offset -Count $legacyCallbackBytes . Length
2026-03-24 18:14:20 +01:00
$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
2026-03-25 17:36:16 +01:00
if ( -not ( Test-ByteArrayEqual -Left $verifiedCtrlQBytes -Right $ctrlQBytes ) ) {
throw 'Ctrl+Q debugger-init body verification failed after write.'
}
if ( $verifiedCtrlQFixupInfo . TargetSeg -ne $ctrlQTargetSeg -or $verifiedCtrlQFixupInfo . TargetOffset -ne $ctrlQTargetOffset ) {
throw 'Ctrl+Q debugger-init relocation verification failed after write.'
}
if ( -not ( Test-ByteArrayEqual -Left $verifiedInterpreterBreakCallBytes -Right $interpreterBreakCallBytes ) ) {
throw 'Interpreter debugger callsite verification failed after write.'
}
if ( $verifiedInterpreterBreakCallFixupInfo . TargetSeg -ne $interpreterBreakCallSeg -or $verifiedInterpreterBreakCallFixupInfo . TargetOffset -ne $interpreterBreakCallOffset ) {
throw 'Interpreter debugger callsite relocation verification failed after write.'
}
if ( -not ( Test-ByteArrayEqual -Left $verifiedDebuggerCallbackBytes -Right $debuggerCallbackBytes ) ) {
throw 'Private debugger UI call verification failed after write.'
}
if ( $verifiedDebuggerCallbackFixupInfo . TargetSeg -ne $debuggerCallbackSeg -or $verifiedDebuggerCallbackFixupInfo . TargetOffset -ne $debuggerCallbackOffset ) {
throw 'Private debugger UI call relocation verification failed after write.'
}
if ( -not ( Test-ByteArrayEqual -Left $verifiedCallbackGuardBytes -Right $callbackGuardBytes ) ) {
throw 'Break-next dispatch patch verification failed after write.'
}
if ( -not ( Test-ByteArrayEqual -Left $verifiedCallbackTargetBytes -Right $callbackTargetBytes ) ) {
throw 'Guarded callback target slot bytes verification failed after write.'
}
if ( $verifiedCallbackTargetFixupInfo . TargetSeg -ne $callbackTargetSeg -or $verifiedCallbackTargetFixupInfo . TargetOffset -ne $callbackTargetOffset ) {
throw 'Guarded callback target relocation verification failed after write.'
}
if ( -not ( Test-ByteArrayEqual -Left $verifiedPrivateMethod0Bytes -Right $privateMethod0Bytes ) ) {
throw 'Private vtable method-0 bytes verification failed after write.'
}
if ( $verifiedPrivateMethod0FixupInfo . TargetSeg -ne $privateMethod0TargetSeg -or $verifiedPrivateMethod0FixupInfo . TargetOffset -ne $privateMethod0TargetOffset ) {
throw 'Private vtable method-0 relocation verification failed after write.'
}
if ( -not ( Test-ByteArrayEqual -Left $verifiedPrivateMethod1Bytes -Right $privateMethod1Bytes ) ) {
throw 'Private vtable method-1 bytes verification failed after write.'
}
if ( $verifiedPrivateMethod1FixupInfo . TargetSeg -ne $privateMethod1TargetSeg -or $verifiedPrivateMethod1FixupInfo . TargetOffset -ne $privateMethod1TargetOffset ) {
throw 'Private vtable method-1 relocation verification failed after write.'
}
if ( -not ( Test-ByteArrayEqual -Left $verifiedLegacyCallbackBytes -Right $legacyCallbackBytes ) ) {
throw 'Legacy callback cleanup bytes verification failed after write.'
}
if ( $verifiedLegacyCallbackFixupInfo . TargetSeg -ne $legacyCallbackTargetSeg -or $verifiedLegacyCallbackFixupInfo . TargetOffset -ne $legacyCallbackTargetOffset ) {
throw 'Legacy callback cleanup relocation verification failed after write.'
}
2026-03-24 18:14:20 +01:00
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 )
2026-03-25 17:36:16 +01:00
Write-Host ( " 0x410 init body @ 0x{0:X}: {1} " -f $sites . CtrlQDebuggerInit . Offset , $ctrlQState )
Write-Host ( " Interpreter call @ 0x{0:X}: {1} " -f $sites . InterpreterBreakCall . Offset , $interpreterCallState )
Write-Host ( " Private UI call @ 0x{0:X}: {1} " -f $sites . DebuggerCallback . Offset , $privateDispatchState )
Write-Host ( " Legacy break hook @ 0x{0:X}: {1} " -f $sites . CallbackGuardCode . Offset , $callbackGuardState )
Write-Host ( " Legacy target slot @ 0x{0:X}: {1} " -f $sites . CallbackTargetSlot . Offset , $callbackTargetState )
Write-Host ( " Current-unit args @ 0x{0:X}: {1} " -f $sites . Wrapper . Offset , $wrapperState )
Write-Host ( " Modal wrapper args @ 0x{0:X}: {1} " -f $sites . LegacyDeferredWrapper . Offset , $deferredWrapperState )
Write-Host ( " Deferred hook cleanup @ 0x{0:X}: {1} " -f $sites . LegacyDeferredHook . Offset , $deferredHookState )
Write-Host ( " Direct hook cleanup @ 0x{0:X}: {1} " -f $sites . Hook . Offset , $hookState )
2026-03-24 18:14:20 +01:00
Write-Host ''
Write-Host 'What this means:'
2026-03-25 17:36:16 +01:00
Write-Host '- Ctrl+Q still goes through the real 0x410 keyboard lane and keeps the original 0x844 cheat gate.'
if ( $CtrlQPatched ) {
Write-Host ( '- The 0x410 body now correctly handles both cases: it arms an existing seg1408 debugger-state object in place, or lazily creates one and stores it at 0x659c/0x659e before arming break-next mode (+0x74=1, +0x75=0).' )
Write-Host ( " - {0} stops using the unsafe 1478:6597 data slot entirely and retargets the existing interpreter call at 1418:04b5 straight into the corrected private 13e8:232d stub. " -f $activeProfile . Label )
if ( $activeProfile . PatchCurrentUnitWrapper ) {
Write-Host '- The current-unit debugger wrapper at 13a0:0086 has its inherited caller-word pushes zeroed so the callback does not forward the debugger-object pointer as UI arguments.'
}
if ( $activeProfile . PatchModalWrapper ) {
Write-Host '- The modal debugger wrapper at 13a0:020d has its inherited caller-word pushes zeroed for the same reason.'
}
}
else {
Write-Host '- All debugger patch changes are restored to the retail byte pattern, including the 0x410 body and the interpreter break call at 1418:04b5.'
Write-Host '- Older private-vtable and direct/deferred experiment sites are also written back to retail bytes during restore.'
}
2026-03-24 18:14:20 +01:00
Write-Host ''
}
function Invoke-MenuChoice {
param ( [ string ] $SelectedChoice )
2026-03-25 17:36:16 +01:00
switch ( $SelectedChoice . Trim ( ) . ToLowerInvariant ( ) ) {
2026-03-24 18:14:20 +01:00
'1' {
2026-03-25 17:36:16 +01:00
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
2026-03-24 18:14:20 +01:00
}
'2' {
2026-03-25 17:36:16 +01:00
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
}
'candidate-i' {
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
}
'candidate-j' {
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
}
'candidate-m' {
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
}
'candidate-n' {
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
}
'candidate-o' {
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
}
'candidate-p' {
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-p' -Label 'Ctrl+Q -> interpreter callsite current-unit stub patch (Candidate P)'
2026-03-24 18:14:20 +01:00
}
'3' {
2026-03-25 17:36:16 +01:00
Set-DesiredState -CtrlQPatched $false -ProfileKey $null -Label 'Restore original bytes'
}
'restore' {
Set-DesiredState -CtrlQPatched $false -ProfileKey $null -Label 'Restore original bytes'
2026-03-24 18:14:20 +01:00
}
'4' {
return $false
}
2026-03-25 17:36:16 +01:00
'exit' {
return $false
}
2026-03-24 18:14:20 +01:00
default {
Write-Warning 'Invalid selection.'
}
}
return $true
}
if ( -not ( Test-Path -LiteralPath $exePath ) ) {
2026-03-25 17:36:16 +01:00
throw " CRUSADER.EXE was not found at ' $exePath '. Pass -ExePath to point at the retail install or place the EXE next to this script. "
2026-03-24 18:14:20 +01:00
}
if ( $PSBoundParameters . ContainsKey ( 'Choice' ) ) {
[ void ] ( Invoke-MenuChoice -SelectedChoice $Choice )
return
}
: mainloop while ( $true ) {
$currentBytes = [ System.IO.File ] :: ReadAllBytes ( $exePath )
Show-Status -FileBytes $currentBytes
2026-03-25 17:36:16 +01:00
$choice = Read-Host 'Select 1-4'
2026-03-24 18:14:20 +01:00
if ( [ string ] :: IsNullOrEmpty ( $choice ) ) { break mainloop }
if ( -not ( Invoke-MenuChoice -SelectedChoice $choice ) ) {
break mainloop
}
}