Documentation improvements
This commit is contained in:
parent
d78808d6b5
commit
c34f481c3a
34 changed files with 2800 additions and 20 deletions
|
|
@ -1,7 +1,13 @@
|
|||
param(
|
||||
[ValidateSet('1', '2', '3', '4', 'candidate-i', 'candidate-j', 'candidate-m', 'candidate-n', 'candidate-o', 'candidate-p', 'restore', 'exit')]
|
||||
[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'
|
||||
|
|
@ -258,6 +264,13 @@ $sites = @{
|
|||
@('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]@{
|
||||
|
|
@ -346,10 +359,10 @@ function Set-ConfiguredCandidateProfile {
|
|||
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
|
||||
$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
|
||||
}
|
||||
|
||||
|
|
@ -511,6 +524,147 @@ function Convert-StoredHexToBytes {
|
|||
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,
|
||||
|
|
@ -861,6 +1015,7 @@ function Show-Status {
|
|||
$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'
|
||||
|
|
@ -885,13 +1040,16 @@ function Show-Status {
|
|||
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) {
|
||||
$profile = $candidateProfiles[$profileKey]
|
||||
$candidateProfile = $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)
|
||||
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 ''
|
||||
}
|
||||
|
|
@ -1206,6 +1364,55 @@ function Invoke-MenuChoice {
|
|||
'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
|
||||
}
|
||||
|
|
@ -1232,7 +1439,7 @@ if ($PSBoundParameters.ContainsKey('Choice')) {
|
|||
:mainloop while ($true) {
|
||||
$currentBytes = [System.IO.File]::ReadAllBytes($exePath)
|
||||
Show-Status -FileBytes $currentBytes
|
||||
$choice = Read-Host 'Select 1-4'
|
||||
$choice = Read-Host 'Select 1-6 (4 exits)'
|
||||
if ([string]::IsNullOrEmpty($choice)) { break mainloop }
|
||||
|
||||
if (-not (Invoke-MenuChoice -SelectedChoice $choice)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue