Documentation improvements

This commit is contained in:
MaddoScientisto 2026-03-29 01:14:09 +01:00
commit c34f481c3a
34 changed files with 2800 additions and 20 deletions

View file

@ -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)) {