1448 lines
No EOL
64 KiB
PowerShell
1448 lines
No EOL
64 KiB
PowerShell
param(
|
|
[ValidateSet('1', '2', '3', '4', '5', '6', 'candidate-i', 'candidate-j', 'candidate-m', 'candidate-n', 'candidate-o', 'candidate-p', 'restore', 'exit', 'map-redirect', 'map-default')]
|
|
[string]$Choice,
|
|
|
|
[ValidateRange(0, 65535)]
|
|
[int]$FirstMissionMap,
|
|
|
|
[ValidateRange(0, 65535)]
|
|
[Nullable[int]]$FirstMissionEgg,
|
|
|
|
[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'
|
|
}
|
|
)
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$exePath = $ExePath
|
|
$statePath = Join-Path $PSScriptRoot 'patch_crusader_cheat_menu.state.json'
|
|
|
|
$sites = @{
|
|
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
|
|
}
|
|
}
|
|
Hook = @{
|
|
Label = 'Rejected direct cheat 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 = 'Rejected 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')
|
|
)
|
|
}
|
|
FirstMissionStart = @{
|
|
Label = 'Fresh-game startup map/egg selector'
|
|
Offset = 0x42041
|
|
Original = [byte[]](0x6A, 0x01, 0x66, 0x68, 0x01, 0x00, 0x1E, 0x00)
|
|
DefaultMap = 1
|
|
DefaultEgg = 0x1E
|
|
}
|
|
}
|
|
|
|
$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'."
|
|
}
|
|
|
|
$candidateProfile = $candidateProfiles[$ProfileKey]
|
|
$sites.CtrlQDebuggerInit.Patched = New-CtrlQPatchedBytes -ArmMode ([string]$candidateProfile.ArmMode)
|
|
$sites.DebuggerCallback.Fixup.PatchedTargetSeg = [int]$candidateProfile.UiTargetSeg
|
|
$sites.DebuggerCallback.Fixup.PatchedTargetOffset = [int]$candidateProfile.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
|
|
|
|
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 Convert-ToUInt16Value {
|
|
param(
|
|
[string]$Text,
|
|
[string]$Label
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Text)) {
|
|
throw "$Label is required."
|
|
}
|
|
|
|
$trimmed = $Text.Trim()
|
|
if ($trimmed.StartsWith('0x', [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
return [Convert]::ToUInt16($trimmed.Substring(2), 16)
|
|
}
|
|
|
|
return [Convert]::ToUInt16($trimmed, 10)
|
|
}
|
|
|
|
function New-FirstMissionStartBytes {
|
|
param(
|
|
[int]$Map,
|
|
[int]$Egg
|
|
)
|
|
|
|
$bytes = [byte[]](0x6A, 0x01, 0x66, 0x68, 0x00, 0x00, 0x00, 0x00)
|
|
Set-U16Le -Bytes $bytes -Offset 4 -Value $Map
|
|
Set-U16Le -Bytes $bytes -Offset 6 -Value $Egg
|
|
return $bytes
|
|
}
|
|
|
|
function Get-FirstMissionStartConfig {
|
|
param([byte[]]$FileBytes)
|
|
|
|
$site = $sites.FirstMissionStart
|
|
$current = Get-ByteSlice -Bytes $FileBytes -Offset $site.Offset -Count $site.Original.Length
|
|
if ($current[0] -ne 0x6A -or $current[1] -ne 0x01 -or $current[2] -ne 0x66 -or $current[3] -ne 0x68) {
|
|
return @{
|
|
State = 'Unknown'
|
|
Bytes = $current
|
|
}
|
|
}
|
|
|
|
$map = Get-U16Le -Bytes $current -Offset 4
|
|
$egg = Get-U16Le -Bytes $current -Offset 6
|
|
$state = if ($map -eq $site.DefaultMap -and $egg -eq $site.DefaultEgg) { 'Original' } else { 'Custom' }
|
|
return @{
|
|
State = $state
|
|
Map = $map
|
|
Egg = $egg
|
|
Bytes = $current
|
|
}
|
|
}
|
|
|
|
function Format-FirstMissionStartStatus {
|
|
param([hashtable]$Config)
|
|
|
|
if ($Config.State -eq 'Unknown') {
|
|
return 'Unknown'
|
|
}
|
|
|
|
return ('{0} (map {1}, egg 0x{2:X4})' -f $Config.State, $Config.Map, $Config.Egg)
|
|
}
|
|
|
|
function Assert-FirstMissionStartKnown {
|
|
param([byte[]]$FileBytes)
|
|
|
|
$site = $sites.FirstMissionStart
|
|
$config = Get-FirstMissionStartConfig -FileBytes $FileBytes
|
|
if ($config.State -eq 'Unknown') {
|
|
throw (
|
|
"{0} at file offset 0x{1:X} does not match the expected startup push pattern.`nCurrent : {2}`nOriginal: {3}`n`nRefusing to modify an unknown executable state." -f
|
|
$site.Label,
|
|
$site.Offset,
|
|
(Format-HexBytes -Bytes $config.Bytes),
|
|
(Format-HexBytes -Bytes $site.Original)
|
|
)
|
|
}
|
|
}
|
|
|
|
function Set-FirstMissionStartConfig {
|
|
param(
|
|
[int]$Map,
|
|
[int]$Egg,
|
|
[string]$Label
|
|
)
|
|
|
|
$fileBytes = [System.IO.File]::ReadAllBytes($exePath)
|
|
Assert-FirstMissionStartKnown -FileBytes $fileBytes
|
|
|
|
$targetBytes = New-FirstMissionStartBytes -Map $Map -Egg $Egg
|
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.FirstMissionStart.Offset -Value $targetBytes
|
|
[System.IO.File]::WriteAllBytes($exePath, $fileBytes)
|
|
|
|
$verifyBytes = [System.IO.File]::ReadAllBytes($exePath)
|
|
$verified = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.FirstMissionStart.Offset -Count $targetBytes.Length
|
|
if (-not (Test-ByteArrayEqual -Left $verified -Right $targetBytes)) {
|
|
throw 'Fresh-game startup map/egg verification failed after write.'
|
|
}
|
|
|
|
$config = Get-FirstMissionStartConfig -FileBytes $verifyBytes
|
|
Write-Host ''
|
|
Write-Host ("Applied: {0}" -f $Label)
|
|
Write-Host ("Fresh-game start @ 0x{0:X}: {1}" -f $sites.FirstMissionStart.Offset, (Format-FirstMissionStartStatus -Config $config))
|
|
if ($config.State -eq 'Custom') {
|
|
Write-Host '- This only redirects the normal fresh-game startup call in Game_Start.'
|
|
Write-Host '- The debug -warp mission table and the rest of the cheat-menu patch logic are unchanged.'
|
|
}
|
|
else {
|
|
Write-Host '- The normal fresh-game startup selector is back at retail defaults: map 1, egg 0x001E.'
|
|
}
|
|
Write-Host ''
|
|
}
|
|
|
|
function Read-FirstMissionRedirectInput {
|
|
param([hashtable]$CurrentConfig)
|
|
|
|
$mapText = Read-Host 'Enter fresh-game startup map number (decimal or 0x-prefixed hex)'
|
|
$map = Convert-ToUInt16Value -Text $mapText -Label 'First mission map'
|
|
|
|
$defaultEgg = if ($CurrentConfig.State -eq 'Unknown') {
|
|
[int]$sites.FirstMissionStart.DefaultEgg
|
|
}
|
|
else {
|
|
[int]$CurrentConfig.Egg
|
|
}
|
|
|
|
$eggPrompt = 'Enter teleport egg id (blank keeps 0x{0:X4})' -f $defaultEgg
|
|
$eggText = Read-Host $eggPrompt
|
|
$egg = if ([string]::IsNullOrWhiteSpace($eggText)) {
|
|
$defaultEgg
|
|
}
|
|
else {
|
|
Convert-ToUInt16Value -Text $eggText -Label 'First mission egg'
|
|
}
|
|
|
|
return @{
|
|
Map = $map
|
|
Egg = $egg
|
|
}
|
|
}
|
|
|
|
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'
|
|
}
|
|
}
|
|
|
|
$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'
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
$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
|
|
$hookState = Get-SiteState -FileBytes $FileBytes -Site $sites.Hook
|
|
$wrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.Wrapper
|
|
$deferredHookState = Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredHook
|
|
$deferredWrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredWrapper
|
|
$firstMissionConfig = Get-FirstMissionStartConfig -FileBytes $FileBytes
|
|
|
|
Write-Host ''
|
|
Write-Host 'CRUSADER.EXE patch status'
|
|
Write-Host '------------------------'
|
|
Write-Host ("EXE: {0}" -f $exePath)
|
|
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)
|
|
Write-Host ("Fresh-game start @ 0x{0:X}: {1}" -f $sites.FirstMissionStart.Offset, (Format-FirstMissionStartStatus -Config $firstMissionConfig))
|
|
Write-Host ''
|
|
foreach ($profileKey in $candidateProfiles.Keys) {
|
|
$candidateProfile = $candidateProfiles[$profileKey]
|
|
$marker = if ($appliedProfileKey -eq $profileKey) { 'applied' } else { 'ready' }
|
|
Write-Host ("{0}. Apply {1} [{2}] {3}" -f $candidateProfile.MenuKey, $candidateProfile.Label, $marker, $candidateProfile.Summary)
|
|
}
|
|
Write-Host '3. Restore original bytes'
|
|
Write-Host '5. Redirect fresh-game startup map/egg'
|
|
Write-Host '6. Restore fresh-game startup default'
|
|
Write-Host '4. Exit'
|
|
Write-Host ''
|
|
}
|
|
|
|
function Set-DesiredState {
|
|
param(
|
|
[bool]$CtrlQPatched,
|
|
[string]$Label,
|
|
[string]$ProfileKey
|
|
)
|
|
|
|
$fileBytes = [System.IO.File]::ReadAllBytes($exePath)
|
|
|
|
$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
|
|
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
|
|
|
|
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
|
|
$hookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.Hook
|
|
$legacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.LegacyDeferredHook
|
|
$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
|
|
}
|
|
$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
|
|
}
|
|
$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 }
|
|
$legacyDeferredHookBytes = $sites.LegacyDeferredHook.Original
|
|
$legacyDeferredHookTargetSeg = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetSeg
|
|
$legacyDeferredHookTargetOffset = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetOffset
|
|
$legacyDeferredHookReserved = 0
|
|
$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
|
|
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)
|
|
$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
|
|
$hookState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Hook
|
|
$wrapperState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Wrapper
|
|
$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
|
|
$verifiedHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.Hook
|
|
$verifiedLegacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.LegacyDeferredHook
|
|
$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
|
|
$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 $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.'
|
|
}
|
|
|
|
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 ("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)
|
|
Write-Host ''
|
|
Write-Host 'What this means:'
|
|
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.'
|
|
}
|
|
Write-Host ''
|
|
}
|
|
|
|
function Invoke-MenuChoice {
|
|
param([string]$SelectedChoice)
|
|
|
|
switch ($SelectedChoice.Trim().ToLowerInvariant()) {
|
|
'1' {
|
|
Set-DesiredState -CtrlQPatched $true -ProfileKey 'candidate-o' -Label 'Ctrl+Q -> interpreter callsite modal stub patch (Candidate O)'
|
|
}
|
|
'2' {
|
|
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)'
|
|
}
|
|
'3' {
|
|
Set-DesiredState -CtrlQPatched $false -ProfileKey $null -Label 'Restore original bytes'
|
|
}
|
|
'restore' {
|
|
Set-DesiredState -CtrlQPatched $false -ProfileKey $null -Label 'Restore original bytes'
|
|
}
|
|
'5' {
|
|
$fileBytes = [System.IO.File]::ReadAllBytes($exePath)
|
|
Assert-FirstMissionStartKnown -FileBytes $fileBytes
|
|
$currentConfig = Get-FirstMissionStartConfig -FileBytes $fileBytes
|
|
$egg = if ($PSBoundParameters.ContainsKey('FirstMissionEgg')) {
|
|
[int]$FirstMissionEgg.Value
|
|
}
|
|
else {
|
|
$null
|
|
}
|
|
|
|
if ($PSBoundParameters.ContainsKey('FirstMissionMap')) {
|
|
$map = [int]$FirstMissionMap
|
|
if ($null -eq $egg) {
|
|
$egg = [int]$currentConfig.Egg
|
|
}
|
|
}
|
|
else {
|
|
$inputValues = Read-FirstMissionRedirectInput -CurrentConfig $currentConfig
|
|
$map = [int]$inputValues.Map
|
|
$egg = [int]$inputValues.Egg
|
|
}
|
|
|
|
Set-FirstMissionStartConfig -Map $map -Egg $egg -Label ('Redirect fresh-game startup to map {0}, egg 0x{1:X4}' -f $map, $egg)
|
|
}
|
|
'map-redirect' {
|
|
$fileBytes = [System.IO.File]::ReadAllBytes($exePath)
|
|
Assert-FirstMissionStartKnown -FileBytes $fileBytes
|
|
$currentConfig = Get-FirstMissionStartConfig -FileBytes $fileBytes
|
|
if (-not $PSBoundParameters.ContainsKey('FirstMissionMap')) {
|
|
throw 'Choice map-redirect requires -FirstMissionMap, or use interactive menu option 5.'
|
|
}
|
|
|
|
$map = [int]$FirstMissionMap
|
|
$egg = if ($PSBoundParameters.ContainsKey('FirstMissionEgg')) {
|
|
[int]$FirstMissionEgg.Value
|
|
}
|
|
else {
|
|
[int]$currentConfig.Egg
|
|
}
|
|
|
|
Set-FirstMissionStartConfig -Map $map -Egg $egg -Label ('Redirect fresh-game startup to map {0}, egg 0x{1:X4}' -f $map, $egg)
|
|
}
|
|
'6' {
|
|
Set-FirstMissionStartConfig -Map ([int]$sites.FirstMissionStart.DefaultMap) -Egg ([int]$sites.FirstMissionStart.DefaultEgg) -Label 'Restore fresh-game startup default'
|
|
}
|
|
'map-default' {
|
|
Set-FirstMissionStartConfig -Map ([int]$sites.FirstMissionStart.DefaultMap) -Egg ([int]$sites.FirstMissionStart.DefaultEgg) -Label 'Restore fresh-game startup default'
|
|
}
|
|
'4' {
|
|
return $false
|
|
}
|
|
'exit' {
|
|
return $false
|
|
}
|
|
default {
|
|
Write-Warning 'Invalid selection.'
|
|
}
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $exePath)) {
|
|
throw "CRUSADER.EXE was not found at '$exePath'. Pass -ExePath to point at the retail install or place the EXE next to this script."
|
|
}
|
|
|
|
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-6 (4 exits)'
|
|
if ([string]::IsNullOrEmpty($choice)) { break mainloop }
|
|
|
|
if (-not (Invoke-MenuChoice -SelectedChoice $choice)) {
|
|
break mainloop
|
|
}
|
|
} |