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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -3,6 +3,8 @@
<PROJECT_DATA_XML_NAME NAME="DISPLAY_DATA"> <PROJECT_DATA_XML_NAME NAME="DISPLAY_DATA">
<SAVE_STATE> <SAVE_STATE>
<ARRAY NAME="EXPANDED_PATHS" TYPE="string"> <ARRAY NAME="EXPANDED_PATHS" TYPE="string">
<A VALUE="Crusader:regret:" />
<A VALUE="Crusader:ja:" />
<A VALUE="Crusader:" /> <A VALUE="Crusader:" />
</ARRAY> </ARRAY>
<STATE NAME="SHOW_TABLE" TYPE="boolean" VALUE="false" /> <STATE NAME="SHOW_TABLE" TYPE="boolean" VALUE="false" />

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<FILE_INFO>
<BASIC_INFO>
<STATE NAME="CONTENT_TYPE" TYPE="string" VALUE="ProgramUserData" />
<STATE NAME="PARENT" TYPE="string" VALUE="/" />
<STATE NAME="FILE_ID" TYPE="string" VALUE="c0a86451fcd2547353220145000" />
<STATE NAME="FILE_TYPE" TYPE="int" VALUE="0" />
<STATE NAME="READ_ONLY" TYPE="boolean" VALUE="false" />
<STATE NAME="NAME" TYPE="string" VALUE="udf_c0a86451c285202638031072100" />
</BASIC_INFO>
</FILE_INFO>

Binary file not shown.

View file

@ -1,6 +1,7 @@
VERSION=1 VERSION=1
/ /
0000000a:udf_c0a86451c281202637836837200:c0a86451c652220429919955700 0000000a:udf_c0a86451c281202637836837200:c0a86451c652220429919955700
0000000c:udf_c0a86451c285202638031072100:c0a86451fcd2547353220145000
00000008:udf_c0a86451c28c202638381579400:c0a86451f608205075819887000 00000008:udf_c0a86451c28c202638381579400:c0a86451f608205075819887000
0000000b:udf_c0a86451c28e202638509414500:ac18b01ab332438409229485800 0000000b:udf_c0a86451c28e202638509414500:ac18b01ab332438409229485800
00000006:udf_c0a86451f2583322595358500:c0a86451c1883616844258300 00000006:udf_c0a86451f2583322595358500:c0a86451c1883616844258300
@ -12,5 +13,5 @@ VERSION=1
00000000:udf_c0a8647bf0178892741854800:c0a8647bd36236342207469100 00000000:udf_c0a8647bf0178892741854800:c0a8647bd36236342207469100
00000001:udf_c0a8647bf4b212984786819600:c0a8647bd36336342224113900 00000001:udf_c0a8647bf4b212984786819600:c0a8647bd36336342224113900
00000003:udf_c0a8647bfe7615910786193500:c0a8647bd36536342248279100 00000003:udf_c0a8647bfe7615910786193500:c0a8647bd36536342248279100
NEXT-ID:c NEXT-ID:d
MD5:d41d8cd98f00b204e9800998ecf8427e MD5:d41d8cd98f00b204e9800998ecf8427e

View file

@ -14,6 +14,9 @@
}, },
{ {
"path": "../crusader-disasm" "path": "../crusader-disasm"
},
{
"path": "../Crusader_Decomp_Public"
} }
], ],
"settings": { "settings": {

BIN
REGRET.EXE Normal file

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,805 @@
<#
.SYNOPSIS
Patch the fresh-game startup selector and editor-object visibility in a local Crusader executable copy.
.DESCRIPTION
This script patches two retail behavior families:
- the hardcoded new-game startup call inside Game_Start
- the `SI_EDITOR` gates that suppress editor objects in the world-item renderer
It auto-detects whether the local executable is Crusader: No Remorse (`CRUSADER.EXE`) or
Crusader: No Regret (`REGRET.EXE`) based on the executable found in the same folder as this
script.
In both supported games, the retail startup selector payload is:
6A 01 66 68 01 00 1E 00
Decoded as the startup selector payload used by this patcher:
6A 01 PUSH 1 ; dostasis = 1
66 68 01 00 PUSH map 0x0001
66 68 1E 00 PUSH egg 0x001E
For the startup selector patch, this script preserves the fixed instruction prefix
6A 01 66 68 and rewrites only the trailing 16-bit little-endian map and egg values.
That means:
- bytes 4-5 are the startup map number
- bytes 6-7 are the startup egg id
- only the normal fresh-game startup path is changed
- the separate debug -warp mission table is untouched
For the editor-object visibility patch, this script resolves both recovered `SI_EDITOR`
gates and flips their short conditional branches:
1180:0951..095c ; upstream world-item builder skip, before draw-node allocation
1198:0332..033d ; downstream sprite paint skip, inside Item_PaintSprite
74 03 ; retail: jump over the early-out only when SI_EDITOR is clear
EB 03 ; patched: always jump over the early-out, forcing editor shapes visible
That means:
- each site remains a 1-byte behavior change inside the existing control flow
- the surrounding `JMP` to the skip/epilogue path is left intact
- restore writes the retail `74 03` bytes back at both sites
The script resolves the exact patch site at runtime by scanning the detected executable for the
fresh-game startup selector signature.
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File .\patch_crusader_map_load.ps1 -Choice status
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File .\patch_crusader_map_load.ps1 -Choice map-redirect -FirstMissionMap 248
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File .\patch_crusader_map_load.ps1 -Choice map-redirect -FirstMissionMap 248 -FirstMissionEgg 0x1E
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File .\patch_crusader_map_load.ps1 -Choice map-default
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File .\patch_crusader_map_load.ps1 -Choice editor-visible
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File .\patch_crusader_map_load.ps1 -Choice editor-default
#>
param(
[ValidateSet('1', '2', '3', '4', '5', '6', 'status', 'map-redirect', 'map-default', 'editor-visible', 'editor-default', 'exit')]
[string]$Choice,
[ValidateRange(0, 65535)]
[int]$FirstMissionMap,
[ValidateRange(0, 65535)]
[Nullable[int]]$FirstMissionEgg
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$script:CliParameterState = @{
HasFirstMissionMap = $PSBoundParameters.ContainsKey('FirstMissionMap')
HasFirstMissionEgg = $PSBoundParameters.ContainsKey('FirstMissionEgg')
}
$gameProfiles = @(
@{
GameTitle = 'Crusader: No Remorse'
Mode = 'remorse'
ExeName = 'CRUSADER.EXE'
SiteDefinitions = @(
@{
Label = 'Fresh-game startup map/egg selector'
StatusLabel = 'Fresh-game startup'
GhidraSelectorPush = '1020:0243'
GhidraSelectorCall = '1020:0249'
Signature = @{
Bytes = [byte[]](0x6A, 0x01, 0x66, 0x68, 0x01, 0x00, 0x1E, 0x00, 0x9A, 0x00, 0x00, 0x00, 0x00, 0x83, 0xC4, 0x06)
Mask = [bool[]]($true, $true, $true, $true, $false, $false, $false, $false, $true, $false, $false, $false, $false, $true, $true, $true)
}
}
)
EditorVisibilityDefinitions = @(
@{
Label = 'Editor world-item visibility gate'
StatusLabel = 'Editor world-item gate'
Signature = @{
Bytes = [byte[]](0x26, 0x8A, 0x47, 0x06, 0x25, 0x01, 0x00, 0x0B, 0xC0, 0x74, 0x03, 0xE9, 0x8D, 0x02)
Mask = [bool[]]($true, $true, $true, $true, $true, $true, $true, $true, $true, $false, $true, $true, $true, $true)
}
Original = [byte[]](0x26, 0x8A, 0x47, 0x06, 0x25, 0x01, 0x00, 0x0B, 0xC0, 0x74, 0x03, 0xE9, 0x8D, 0x02)
Enabled = [byte[]](0x26, 0x8A, 0x47, 0x06, 0x25, 0x01, 0x00, 0x0B, 0xC0, 0xEB, 0x03, 0xE9, 0x8D, 0x02)
},
@{
Label = 'Editor sprite-paint visibility gate'
StatusLabel = 'Editor sprite-paint gate'
Signature = @{
Bytes = [byte[]](0x26, 0x8A, 0x47, 0x06, 0x25, 0x01, 0x00, 0x0B, 0xC0, 0x74, 0x03, 0xE9, 0x19, 0x06)
Mask = [bool[]]($true, $true, $true, $true, $true, $true, $true, $true, $true, $false, $true, $true, $true, $true)
}
Original = [byte[]](0x26, 0x8A, 0x47, 0x06, 0x25, 0x01, 0x00, 0x0B, 0xC0, 0x74, 0x03, 0xE9, 0x19, 0x06)
Enabled = [byte[]](0x26, 0x8A, 0x47, 0x06, 0x25, 0x01, 0x00, 0x0B, 0xC0, 0xEB, 0x03, 0xE9, 0x19, 0x06)
}
)
},
@{
GameTitle = 'Crusader: No Regret'
Mode = 'regret'
ExeName = 'REGRET.EXE'
SiteDefinitions = @(
@{
Label = 'Menu-start startup map/egg selector'
StatusLabel = 'Menu-start startup'
GhidraSelectorPush = '1008:1448'
GhidraSelectorCall = '1008:144e'
Signature = @{
Bytes = [byte[]](0x6A, 0x01, 0x66, 0x68, 0x01, 0x00, 0x1E, 0x00, 0x9A, 0x00, 0x00, 0x00, 0x00, 0x83, 0xC4, 0x06)
Mask = [bool[]]($true, $true, $true, $true, $false, $false, $false, $false, $true, $false, $false, $false, $false, $true, $true, $true)
}
},
@{
Label = 'Mission-start map/egg selector'
StatusLabel = 'Mission-start selector'
GhidraSelectorPush = '1030:05c5'
GhidraSelectorCall = '1030:05cc'
Signature = @{
Bytes = [byte[]](0x6A, 0x01, 0x66, 0x68, 0x01, 0x00, 0x1E, 0x00, 0x0E, 0xE8, 0x00, 0x00, 0x83, 0xC4, 0x06)
Mask = [bool[]]($true, $true, $true, $true, $false, $false, $false, $false, $true, $true, $false, $false, $true, $true, $true)
}
}
)
EditorVisibilityDefinitions = @(
@{
Label = 'Editor world-item visibility gate'
StatusLabel = 'Editor world-item gate'
Signature = @{
Bytes = [byte[]](0x26, 0x8A, 0x47, 0x06, 0x25, 0x01, 0x00, 0x0B, 0xC0, 0x74, 0x03, 0xE9, 0xBB, 0x02)
Mask = [bool[]]($true, $true, $true, $true, $true, $true, $true, $true, $true, $false, $true, $true, $true, $true)
}
Original = [byte[]](0x26, 0x8A, 0x47, 0x06, 0x25, 0x01, 0x00, 0x0B, 0xC0, 0x74, 0x03, 0xE9, 0xBB, 0x02)
Enabled = [byte[]](0x26, 0x8A, 0x47, 0x06, 0x25, 0x01, 0x00, 0x0B, 0xC0, 0xEB, 0x03, 0xE9, 0xBB, 0x02)
},
@{
Label = 'Editor sprite-paint visibility gate'
StatusLabel = 'Editor sprite-paint gate'
Signature = @{
Bytes = [byte[]](0x26, 0x8A, 0x47, 0x06, 0x25, 0x01, 0x00, 0x0B, 0xC0, 0x74, 0x03, 0xE9, 0x19, 0x06)
Mask = [bool[]]($true, $true, $true, $true, $true, $true, $true, $true, $true, $false, $true, $true, $true, $true)
}
Original = [byte[]](0x26, 0x8A, 0x47, 0x06, 0x25, 0x01, 0x00, 0x0B, 0xC0, 0x74, 0x03, 0xE9, 0x19, 0x06)
Enabled = [byte[]](0x26, 0x8A, 0x47, 0x06, 0x25, 0x01, 0x00, 0x0B, 0xC0, 0xEB, 0x03, 0xE9, 0x19, 0x06)
}
)
}
)
function Format-OffsetList {
param([int[]]$Offsets)
if (-not $Offsets -or $Offsets.Count -eq 0) {
return '<none>'
}
return (($Offsets | ForEach-Object { '0x{0:X}' -f $_ }) -join ', ')
}
function Find-BytePatternOffsets {
param(
[byte[]]$Bytes,
[byte[]]$Pattern,
[bool[]]$Mask
)
if ($Pattern.Length -ne $Mask.Length) {
throw 'Pattern and mask lengths must match.'
}
$matches = New-Object System.Collections.Generic.List[int]
$lastOffset = $Bytes.Length - $Pattern.Length
for ($offset = 0; $offset -le $lastOffset; $offset++) {
$matched = $true
for ($index = 0; $index -lt $Pattern.Length; $index++) {
if ($Mask[$index] -and $Bytes[$offset + $index] -ne $Pattern[$index]) {
$matched = $false
break
}
}
if ($matched) {
[void]$matches.Add($offset)
}
}
return [int[]]$matches.ToArray()
}
function Find-ResolvedSingleOffset {
param(
[byte[]]$Bytes,
[hashtable]$Signature,
[string]$GameTitle,
[string]$Label
)
$offsets = @(Find-BytePatternOffsets -Bytes $Bytes -Pattern $Signature.Bytes -Mask $Signature.Mask)
if ($offsets.Count -eq 0) {
throw (
"{0} did not contain the expected signature for '{1}'. Refusing to patch an unknown executable layout." -f
$GameTitle,
$Label
)
}
if ($offsets.Count -gt 1) {
throw (
"{0} matched the signature for '{1}' at multiple offsets ({2}). Refusing to patch an ambiguous executable layout." -f
$GameTitle,
$Label,
(Format-OffsetList -Offsets $offsets)
)
}
return $offsets[0]
}
function Get-ResolvedTarget {
$matches = @(
foreach ($profile in $gameProfiles) {
$candidatePath = Join-Path $PSScriptRoot $profile.ExeName
if (Test-Path -LiteralPath $candidatePath) {
@{
GameTitle = $profile.GameTitle
Mode = $profile.Mode
ExeName = $profile.ExeName
ExePath = $candidatePath
SiteDefinitions = $profile.SiteDefinitions
EditorVisibilityDefinitions = $profile.EditorVisibilityDefinitions
}
}
}
)
if ($matches.Count -eq 0) {
throw (
"No supported Crusader executable was found next to this script. Expected one of: {0}" -f
(($gameProfiles | ForEach-Object { $_.ExeName }) -join ', ')
)
}
if ($matches.Count -gt 1) {
throw (
"Multiple supported executables were found next to this script: {0}. Leave only one supported executable in the folder so the mode is unambiguous." -f
(($matches | ForEach-Object { $_.ExeName }) -join ', ')
)
}
$target = $matches[0]
$fileBytes = [System.IO.File]::ReadAllBytes($target.ExePath)
$resolvedSites = @()
foreach ($siteDefinition in $target.SiteDefinitions) {
$resolvedSites += @{
Label = $siteDefinition.Label
StatusLabel = $siteDefinition.StatusLabel
Offset = Find-ResolvedSingleOffset -Bytes $fileBytes -Signature $siteDefinition.Signature -GameTitle $target.GameTitle -Label $siteDefinition.Label
Prefix = [byte[]](0x6A, 0x01, 0x66, 0x68)
Original = [byte[]](0x6A, 0x01, 0x66, 0x68, 0x01, 0x00, 0x1E, 0x00)
DefaultMap = 1
DefaultEgg = 0x1E
GhidraSelectorPush = $siteDefinition.GhidraSelectorPush
GhidraSelectorCall = $siteDefinition.GhidraSelectorCall
}
}
$resolvedEditorVisibilitySites = @(
foreach ($definition in $target.EditorVisibilityDefinitions) {
@{
Label = $definition.Label
StatusLabel = $definition.StatusLabel
Offset = Find-ResolvedSingleOffset -Bytes $fileBytes -Signature $definition.Signature -GameTitle $target.GameTitle -Label $definition.Label
Original = $definition.Original
Enabled = $definition.Enabled
}
}
)
$target.Sites = $resolvedSites
$target.EditorVisibilitySites = $resolvedEditorVisibilitySites
return $target
}
$target = Get-ResolvedTarget
$exePath = $target.ExePath
$sites = $target.Sites
$primarySite = $sites[0]
$editorVisibilitySites = $target.EditorVisibilitySites
$site = @{
GameTitle = $target.GameTitle
Mode = $target.Mode
ExeName = $target.ExeName
Sites = $sites
}
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 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 Get-U16Le {
param(
[byte[]]$Bytes,
[int]$Offset
)
return [BitConverter]::ToUInt16($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 Set-ByteSlice {
param(
[byte[]]$Bytes,
[int]$Offset,
[byte[]]$Value
)
[Array]::Copy($Value, 0, $Bytes, $Offset, $Value.Length)
}
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[]]$primarySite.Original.Clone()
Set-U16Le -Bytes $bytes -Offset 4 -Value $Map
Set-U16Le -Bytes $bytes -Offset 6 -Value $Egg
return $bytes
}
function Get-FirstMissionStartConfig {
param(
[byte[]]$FileBytes,
[hashtable]$SiteDefinition = $primarySite
)
$current = Get-ByteSlice -Bytes $FileBytes -Offset $SiteDefinition.Offset -Count $SiteDefinition.Original.Length
$prefix = Get-ByteSlice -Bytes $current -Offset 0 -Count $SiteDefinition.Prefix.Length
if (-not (Test-ByteArrayEqual -Left $prefix -Right $SiteDefinition.Prefix)) {
return @{
Label = $SiteDefinition.Label
StatusLabel = $SiteDefinition.StatusLabel
State = 'Unknown'
Bytes = $current
}
}
$map = Get-U16Le -Bytes $current -Offset 4
$egg = Get-U16Le -Bytes $current -Offset 6
$state = if ($map -eq $SiteDefinition.DefaultMap -and $egg -eq $SiteDefinition.DefaultEgg) { 'Original' } else { 'Custom' }
return @{
Label = $SiteDefinition.Label
StatusLabel = $SiteDefinition.StatusLabel
State = $state
Map = $map
Egg = $egg
Bytes = $current
}
}
function Format-FirstMissionStartStatus {
param([hashtable]$Config)
if ($Config.State -eq 'Unknown') {
return ('Unknown ({0})' -f (Format-HexBytes -Bytes $Config.Bytes))
}
return ('{0} (map {1}, egg 0x{2:X4})' -f $Config.State, $Config.Map, $Config.Egg)
}
function Assert-FirstMissionStartKnown {
param([byte[]]$FileBytes)
foreach ($siteDefinition in $sites) {
$config = Get-FirstMissionStartConfig -FileBytes $FileBytes -SiteDefinition $siteDefinition
if ($config.State -eq 'Unknown') {
throw (
"{0} at file offset 0x{1:X} does not match the expected startup push pattern.`nCurrent : {2}`nExpected prefix: {3}`n`nRefusing to modify an unknown executable state." -f
$siteDefinition.Label,
$siteDefinition.Offset,
(Format-HexBytes -Bytes $config.Bytes),
(Format-HexBytes -Bytes $siteDefinition.Prefix)
)
}
}
}
function Get-EditorVisibilityConfig {
param([byte[]]$FileBytes)
$siteStates = @(
foreach ($siteDefinition in $editorVisibilitySites) {
$current = Get-ByteSlice -Bytes $FileBytes -Offset $siteDefinition.Offset -Count $siteDefinition.Original.Length
$state = 'Unknown'
if (Test-ByteArrayEqual -Left $current -Right $siteDefinition.Original) {
$state = 'RetailHidden'
}
elseif (Test-ByteArrayEqual -Left $current -Right $siteDefinition.Enabled) {
$state = 'ForcedVisible'
}
@{
Label = $siteDefinition.Label
StatusLabel = $siteDefinition.StatusLabel
Offset = $siteDefinition.Offset
State = $state
Bytes = $current
}
}
)
$unknownStates = @($siteStates | Where-Object { $_.State -eq 'Unknown' })
$retailHiddenStates = @($siteStates | Where-Object { $_.State -eq 'RetailHidden' })
$forcedVisibleStates = @($siteStates | Where-Object { $_.State -eq 'ForcedVisible' })
$overallState = 'Mixed'
if ($unknownStates.Count -gt 0) {
$overallState = 'Unknown'
}
elseif ($retailHiddenStates.Count -eq $siteStates.Count) {
$overallState = 'RetailHidden'
}
elseif ($forcedVisibleStates.Count -eq $siteStates.Count) {
$overallState = 'ForcedVisible'
}
return @{
Label = 'Editor-object visibility toggle'
StatusLabel = 'Editor-object visibility'
State = $overallState
Sites = $siteStates
}
}
function Format-EditorVisibilityStatus {
param([hashtable]$Config)
switch ($Config.State) {
'RetailHidden' { return 'Retail hidden' }
'ForcedVisible' { return 'Forced visible' }
'Mixed' { return 'Mixed' }
default { return 'Unknown' }
}
}
function Assert-EditorVisibilityKnown {
param([byte[]]$FileBytes)
$config = Get-EditorVisibilityConfig -FileBytes $FileBytes
if ($config.State -eq 'Unknown') {
$details = ($config.Sites | ForEach-Object {
"{0} @ 0x{1:X}: {2}" -f $_.StatusLabel, $_.Offset, (Format-HexBytes -Bytes $_.Bytes)
}) -join "`n"
throw (
"Editor visibility sites do not match the expected retail or forced-visible bytes.`n{0}`n`nRefusing to modify an unknown executable state." -f
$details
)
}
}
function Show-Status {
param([byte[]]$FileBytes)
$configs = @(
foreach ($siteDefinition in $sites) {
Get-FirstMissionStartConfig -FileBytes $FileBytes -SiteDefinition $siteDefinition
}
)
Write-Host ''
Write-Host 'Crusader map-load patch status'
Write-Host '-----------------------------'
Write-Host ("Game: {0}" -f $site.GameTitle)
Write-Host ("Mode: {0}" -f $site.Mode)
Write-Host ("EXE: {0}" -f $exePath)
foreach ($config in $configs) {
$siteDefinition = $sites | Where-Object { $_.Label -eq $config.Label } | Select-Object -First 1
Write-Host ("{0} @ 0x{1:X}: {2}" -f $config.StatusLabel, $siteDefinition.Offset, (Format-FirstMissionStartStatus -Config $config))
Write-Host ("{0} Ghidra push: {1}" -f $config.StatusLabel, $siteDefinition.GhidraSelectorPush)
Write-Host ("{0} Ghidra call: {1}" -f $config.StatusLabel, $siteDefinition.GhidraSelectorCall)
}
$editorVisibilityConfig = Get-EditorVisibilityConfig -FileBytes $FileBytes
Write-Host ("{0}: {1}" -f $editorVisibilityConfig.StatusLabel, (Format-EditorVisibilityStatus -Config $editorVisibilityConfig))
foreach ($siteConfig in $editorVisibilityConfig.Sites) {
Write-Host ("{0} @ 0x{1:X}: {2}" -f $siteConfig.StatusLabel, $siteConfig.Offset, (Format-EditorVisibilityStatus -Config $siteConfig))
}
Write-Host ''
Write-Host 'How it works:'
Write-Host '- The script auto-detects CRUSADER.EXE or REGRET.EXE in this folder.'
Write-Host '- It resolves each patch site by scanning for the hardcoded selector signature used by that game.'
Write-Host '- Startup selector resolution now wildcards the current map/egg bytes so status and restore still work after a prior redirect.'
Write-Host '- In No Regret, the script patches both the menu-start selector and the later mission-start selector used for an actual new game.'
Write-Host '- The script reads 8 bytes at each matched selector call site.'
Write-Host '- Bytes 0-3 must remain 6A 01 66 68, which is the instruction prefix this patch expects.'
Write-Host '- Bytes 4-5 are the little-endian startup map number.'
Write-Host '- Bytes 6-7 are the little-endian startup egg id.'
Write-Host '- Redirect changes only those two 16-bit values at each matched selector and leaves the rest of the executable alone.'
Write-Host '- Restore writes the retail values back everywhere: map 1, egg 0x001E.'
Write-Host '- The editor visibility patch flips both recovered SI_EDITOR branches from 74 03 to EB 03: the upstream world-item node-allocation skip and the downstream sprite-paint skip.'
Write-Host ''
Write-Host '1. Show status'
Write-Host '2. Redirect fresh-game startup map/egg'
Write-Host '3. Restore retail default'
Write-Host '4. Force editor objects visible'
Write-Host '5. Restore retail editor-object hide'
Write-Host '6. Exit'
Write-Host ''
}
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
foreach ($siteDefinition in $sites) {
Set-ByteSlice -Bytes $fileBytes -Offset $siteDefinition.Offset -Value $targetBytes
}
[System.IO.File]::WriteAllBytes($exePath, $fileBytes)
$verifyBytes = [System.IO.File]::ReadAllBytes($exePath)
foreach ($siteDefinition in $sites) {
$verified = Get-ByteSlice -Bytes $verifyBytes -Offset $siteDefinition.Offset -Count $targetBytes.Length
if (-not (Test-ByteArrayEqual -Left $verified -Right $targetBytes)) {
throw ("{0} verification failed after write." -f $siteDefinition.Label)
}
}
$configs = @(
foreach ($siteDefinition in $sites) {
Get-FirstMissionStartConfig -FileBytes $verifyBytes -SiteDefinition $siteDefinition
}
)
Write-Host ''
Write-Host ("Applied: {0}" -f $Label)
foreach ($config in $configs) {
$siteDefinition = $sites | Where-Object { $_.Label -eq $config.Label } | Select-Object -First 1
Write-Host ("{0} @ 0x{1:X}: {2}" -f $config.StatusLabel, $siteDefinition.Offset, (Format-FirstMissionStartStatus -Config $config))
}
Write-Host ''
}
function Set-EditorVisibilityEnabled {
param(
[bool]$Enabled,
[string]$Label
)
$fileBytes = [System.IO.File]::ReadAllBytes($exePath)
Assert-EditorVisibilityKnown -FileBytes $fileBytes
foreach ($siteDefinition in $editorVisibilitySites) {
$targetBytes = if ($Enabled) { $siteDefinition.Enabled } else { $siteDefinition.Original }
Set-ByteSlice -Bytes $fileBytes -Offset $siteDefinition.Offset -Value $targetBytes
}
[System.IO.File]::WriteAllBytes($exePath, $fileBytes)
$verifyBytes = [System.IO.File]::ReadAllBytes($exePath)
foreach ($siteDefinition in $editorVisibilitySites) {
$targetBytes = if ($Enabled) { $siteDefinition.Enabled } else { $siteDefinition.Original }
$verified = Get-ByteSlice -Bytes $verifyBytes -Offset $siteDefinition.Offset -Count $targetBytes.Length
if (-not (Test-ByteArrayEqual -Left $verified -Right $targetBytes)) {
throw ("{0} verification failed after write." -f $siteDefinition.Label)
}
}
$config = Get-EditorVisibilityConfig -FileBytes $verifyBytes
Write-Host ''
Write-Host ("Applied: {0}" -f $Label)
Write-Host ("{0} @ {1}" -f $config.StatusLabel, (Format-EditorVisibilityStatus -Config $config))
foreach ($siteConfig in $config.Sites) {
Write-Host ("{0} @ 0x{1:X}: {2}" -f $siteConfig.StatusLabel, $siteConfig.Offset, (Format-EditorVisibilityStatus -Config $siteConfig))
}
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 = [int]$CurrentConfig.Egg
$eggText = Read-Host ('Enter teleport egg id (blank keeps 0x{0:X4})' -f $defaultEgg)
$egg = if ([string]::IsNullOrWhiteSpace($eggText)) {
$defaultEgg
}
else {
Convert-ToUInt16Value -Text $eggText -Label 'First mission egg'
}
return @{
Map = $map
Egg = $egg
}
}
function Invoke-MenuChoice {
param([string]$SelectedChoice)
switch ($SelectedChoice.Trim().ToLowerInvariant()) {
'1' {
$currentBytes = [System.IO.File]::ReadAllBytes($exePath)
Show-Status -FileBytes $currentBytes
}
'status' {
$currentBytes = [System.IO.File]::ReadAllBytes($exePath)
Show-Status -FileBytes $currentBytes
}
'2' {
$fileBytes = [System.IO.File]::ReadAllBytes($exePath)
Assert-FirstMissionStartKnown -FileBytes $fileBytes
$currentConfig = Get-FirstMissionStartConfig -FileBytes $fileBytes
if ($script:CliParameterState.HasFirstMissionMap) {
$map = [int]$FirstMissionMap
$egg = if ($script:CliParameterState.HasFirstMissionEgg) {
[int]$FirstMissionEgg.Value
}
else {
[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 $script:CliParameterState.HasFirstMissionMap) {
throw 'Choice map-redirect requires -FirstMissionMap, or use interactive option 2.'
}
$map = [int]$FirstMissionMap
$egg = if ($script:CliParameterState.HasFirstMissionEgg) {
[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)
}
'3' {
Set-FirstMissionStartConfig -Map ([int]$primarySite.DefaultMap) -Egg ([int]$primarySite.DefaultEgg) -Label 'Restore retail default startup selector'
}
'map-default' {
Set-FirstMissionStartConfig -Map ([int]$primarySite.DefaultMap) -Egg ([int]$primarySite.DefaultEgg) -Label 'Restore retail default startup selector'
}
'4' {
Set-EditorVisibilityEnabled -Enabled $true -Label 'Force editor-object rendering visible'
}
'editor-visible' {
Set-EditorVisibilityEnabled -Enabled $true -Label 'Force editor-object rendering visible'
}
'5' {
Set-EditorVisibilityEnabled -Enabled $false -Label 'Restore retail editor-object hide'
}
'editor-default' {
Set-EditorVisibilityEnabled -Enabled $false -Label 'Restore retail editor-object hide'
}
'6' {
return $false
}
'exit' {
return $false
}
default {
Write-Warning 'Invalid selection.'
}
}
return $true
}
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-4'
if ([string]::IsNullOrWhiteSpace($choice)) {
break mainloop
}
if (-not (Invoke-MenuChoice -SelectedChoice $choice)) {
break mainloop
}
}

View file

@ -4,12 +4,34 @@ This file is an index. Detailed notes have been split into the `docs/` folder by
Active live analysis target is now `CRUSADER.EXE`. Existing `CRUSADER-RAW.EXE` notes remain in scope as cross-reference evidence and should be cited alongside live NE addresses when they support a rename, variable role, or behavior claim. Active live analysis target is now `CRUSADER.EXE`. Existing `CRUSADER-RAW.EXE` notes remain in scope as cross-reference evidence and should be cited alongside live NE addresses when they support a rename, variable role, or behavior claim.
Recent verified Japanese-build batch: [docs/jp-remorse-windows9x-investigation.md](docs/jp-remorse-windows9x-investigation.md) now records a focused live-Ghidra investigation of `/ja/CRUSADER.EXE` around the claim that the Japanese release runs natively on Windows 95 / Windows 9x instead of requiring a DOS boot path. Current best static-analysis read is strongly in favor: the JP executable is a flat Win32 image with PE-style sections, a Windows import table, native window creation, DirectDraw/DirectSound initialization, registry-backed config under `Software\Electronic Arts\Crusader: No Remorse\J1.21`, and a meaningful `GetVersion`-based Win9x compatibility branch that changes TLS allocation behavior when the classic Win9x high bit is set. The only remaining uncertainty is practical deployment rather than architecture: this pass did not runtime-test on real Win95 or prove which DirectX/runtime prerequisites are required.
Recent verified Japanese-build follow-up: [docs/jp-remorse-cheats-and-launch-params.md](docs/jp-remorse-cheats-and-launch-params.md) now records a focused pass on the surviving cheat/debug and startup-argument lanes in `/ja/CRUSADER.EXE`. Current best read is that the JP Win32 build kept real executable cheat/debug machinery, not just leftover strings: `-laurie` is still a special parser case, the hidden `JASSICA16` sequence matcher still toggles the cheat-active state with live `Cheats are now active/inactive.` messages, the option-key handler still contains the immortality toggle path, and the command-line parser still executes live handlers for `-debug`, `-u <arg>`, `-warp <mission>`, `-skill <n>`, `-mapoff <delta>`, `-egg <id>`, and `-demo`. The same pass also narrows one important difference from older DOS-side notes: the JP Win32 parser has not yet been proven to support positional `-warp <mission> <x> <y> <z>` consumption, so that form should not currently be assumed for this build.
Recent verified localized-build batch: [docs/spanish-cheat-differences.md](docs/spanish-cheat-differences.md) now records a tighter live-Ghidra comparison against `/es/CRUSADER.EXE` for the known cheat/debug control areas. Current best read is now narrower than the earlier "moved matcher" theory: the Spanish executable still preserves the same broad cheat/debug framework as the English build with relocated addresses rather than different behavior, but it does **not** preserve the English `jassica16` table as the same static data object and this pass also failed to recover any replacement compiled matcher or any translated `~` cheat-latch toggle. The `-laurie` parser still sets the broad cheat/debug gate (`1478:0910`), the gameplay-input gate still exists at `1478:0927`, and Hack Mover still toggles through `13e8:24a5`; but the old English-side slot at `1478:2833` now contains pointer-like words, the old English immortality-string slots at `1478:2850/2866` are also repurposed as non-string data in Spanish, `1478:0910` has only the `-laurie` write at `1050:0985`, `1478:5fb3` only has the Laurie-hint helper writes at `13e8:0071/0077`, `World_HandleKeyboardInput` does not expose a recovered `0x7e` / tilde branch, and `1478:8ad6` still has no recovered writer even though Hack Mover checks it. The new keyboard-side conclusion is stronger too: `1478:5fb3` does not act like a live positive enable latch in Spanish, because every recovered consumer requires it to be zero and the Laurie-hint helper pulses it back to zero immediately, while the nearby `8ad7/8ad8/8ad9` runtime-state writes still do not explain `8ad6`. The Hack Mover runtime chain is also tighter now: `1478:5fb2` is the actual on/off toggle, `13e8:0ef9` / `13e8:0f77` clear it, `13e8:282f` is the adjacent runtime helper using `1478:8ad9`, and `13e8:2f0e` / `13e8:3009` bracket the active drag state via `1478:8ac0`, `1478:8acc`, and `1478:8ace`. Current safest localized-build read is therefore `-laurie is the only recovered positive enabler for the surviving broad Spanish cheat/debug family; no replacement hidden matcher, no runtime keyboard-latch bootstrap, and no direct Spanish F10 cheat branch have been recovered`, with the remaining open question narrowed to whether `1478:8ad6` is written through an analysis-dark path or is just a dead leftover gate. Recent verified localized-build batch: [docs/spanish-cheat-differences.md](docs/spanish-cheat-differences.md) now records a tighter live-Ghidra comparison against `/es/CRUSADER.EXE` for the known cheat/debug control areas. Current best read is now narrower than the earlier "moved matcher" theory: the Spanish executable still preserves the same broad cheat/debug framework as the English build with relocated addresses rather than different behavior, but it does **not** preserve the English `jassica16` table as the same static data object and this pass also failed to recover any replacement compiled matcher or any translated `~` cheat-latch toggle. The `-laurie` parser still sets the broad cheat/debug gate (`1478:0910`), the gameplay-input gate still exists at `1478:0927`, and Hack Mover still toggles through `13e8:24a5`; but the old English-side slot at `1478:2833` now contains pointer-like words, the old English immortality-string slots at `1478:2850/2866` are also repurposed as non-string data in Spanish, `1478:0910` has only the `-laurie` write at `1050:0985`, `1478:5fb3` only has the Laurie-hint helper writes at `13e8:0071/0077`, `World_HandleKeyboardInput` does not expose a recovered `0x7e` / tilde branch, and `1478:8ad6` still has no recovered writer even though Hack Mover checks it. The new keyboard-side conclusion is stronger too: `1478:5fb3` does not act like a live positive enable latch in Spanish, because every recovered consumer requires it to be zero and the Laurie-hint helper pulses it back to zero immediately, while the nearby `8ad7/8ad8/8ad9` runtime-state writes still do not explain `8ad6`. The Hack Mover runtime chain is also tighter now: `1478:5fb2` is the actual on/off toggle, `13e8:0ef9` / `13e8:0f77` clear it, `13e8:282f` is the adjacent runtime helper using `1478:8ad9`, and `13e8:2f0e` / `13e8:3009` bracket the active drag state via `1478:8ac0`, `1478:8acc`, and `1478:8ace`. Current safest localized-build read is therefore `-laurie is the only recovered positive enabler for the surviving broad Spanish cheat/debug family; no replacement hidden matcher, no runtime keyboard-latch bootstrap, and no direct Spanish F10 cheat branch have been recovered`, with the remaining open question narrowed to whether `1478:8ad6` is written through an analysis-dark path or is just a dead leftover gate.
Recent verified batch: [docs/retail-debug-arg.md](docs/retail-debug-arg.md) now records the live NE proof that retail `CRUSADER.EXE` still recognizes and executes a real `-debug` command-line branch. That branch prints `Debugging mode ON.`, sets `g_debugMsgLevel` at `1478:87e0`, and toggles two debug globals at `1478:0845/0859`. The later sink pass also closes the text-output target more tightly: `ProbablyPrintDebugMessage` formats through the static stdio-style table at `1478:6c32..6c81` and writes to the handle-`1` entry at `1478:6c46`, so the non-video side is ordinary DOS `stdout` gated by the debug threshold, plus the already-confirmed AVI timing overlay. Current best read remains `surviving debug-output / instrumentation switch`, not `the missing bootstrap for the hidden seg109/seg1408 usecode debugger`. The same batch also leaves the earlier `-laurie` and `0x659c/659e` debugger-state conclusions intact: `-debug` is a separate switch and is not currently evidenced as constructing the hidden usecode-debugger break-state object. Recent verified batch: [docs/retail-debug-arg.md](docs/retail-debug-arg.md) now records the live NE proof that retail `CRUSADER.EXE` still recognizes and executes a real `-debug` command-line branch. That branch prints `Debugging mode ON.`, sets `g_debugMsgLevel` at `1478:87e0`, and toggles two debug globals at `1478:0845/0859`. The later sink pass also closes the text-output target more tightly: `ProbablyPrintDebugMessage` formats through the static stdio-style table at `1478:6c32..6c81` and writes to the handle-`1` entry at `1478:6c46`, so the non-video side is ordinary DOS `stdout` gated by the debug threshold, plus the already-confirmed AVI timing overlay. Current best read remains `surviving debug-output / instrumentation switch`, not `the missing bootstrap for the hidden seg109/seg1408 usecode debugger`. The same batch also leaves the earlier `-laurie` and `0x659c/659e` debugger-state conclusions intact: `-debug` is a separate switch and is not currently evidenced as constructing the hidden usecode-debugger break-state object.
Recent tooling batch: [docs/map-rendering.md](docs/map-rendering.md) now starts a dedicated offline map-rendering lane. `tools/render_crusader_map.py` can load `FIXED.DAT`, expand `GLOB.FLX`, decode the required `SHAPES.FLX` entries with Crusader frame headers, apply `GAMEPAL.PAL`, and write a first-pass PNG, with a `--fixed-dat` override so the same pipeline can be pointed at either game's map file. The current renderer is intentionally limited to fixed-map content and a simple deterministic painter rather than the full Pentagram/ScummVM dependency sorter, and the current workspace caveat is that `STATIC_REGRET` still lacks a copied `FIXED.DAT`, so No Regret rendering needs that file supplied explicitly. Recent tooling batch: [docs/map-rendering.md](docs/map-rendering.md) now starts a dedicated offline map-rendering lane. `tools/render_crusader_map.py` can load `FIXED.DAT`, expand `GLOB.FLX`, decode the required `SHAPES.FLX` entries with Crusader frame headers, apply `GAMEPAL.PAL`, and write a first-pass PNG, with a `--fixed-dat` override so the same pipeline can be pointed at either game's map file. The current renderer is intentionally limited to fixed-map content and a simple deterministic painter rather than the full Pentagram/ScummVM dependency sorter, and the current workspace caveat is that `STATIC_REGRET` still lacks a copied `FIXED.DAT`, so No Regret rendering needs that file supplied explicitly.
Recent map/editor visibility batch: [docs/editor-object-visibility.md](docs/editor-object-visibility.md) now records a focused live-Ghidra pass on whether retail `CRUSADER.EXE` explicitly hides editor-only map objects and whether any built-in switch re-enables them. The current best read is now tighter than the first pass: `Item_PaintSprite` at `1198:02e4` does contain a real downstream `flags2 & 1` (`SI_EDITOR`) early-out, but the active world-item renderer also has an upstream controlling skip at `1180:0951..095c` that filters editor-tagged shapes before draw-node allocation. That corrected render-path model explains why the first executable patch attempt, which only flipped the downstream draw-time branch, produced no visible change in-game. The same note still closes the negative side of the question more tightly: no recovered retail `-debug`, cheat/debug hotkey, Laurie/usecode-debugger path, or `0x410` event behavior currently reaches either gate or exposes a `show editor items` state. The closest confirmed toggle remains ScummVM's own `_showEditorItems` debugger command, which is an engine-added reimplementation feature rather than evidence of a retail built-in toggle.
Recent startup/map-selection batch: [docs/first-mission-map-selection.md](docs/first-mission-map-selection.md) now records the live proof that fresh-game map choice is code-selected rather than read from `CRUSADER.CFG` or another external mission-mapping file. For `CRUSADER.EXE`, the normal fresh-game path still hardcodes map `1`, egg `0x1e` inside `Game_Start`. For `REGRET.EXE`, the same values are hardcoded twice: once in an early `Game_Start` selector and again in the later `FUN_1030_032d` mission-start path that actually controls a real new game. The same note also captures the separate debug `-warp mission` path: it indexes a small executable-embedded mission-to-map word table at `1478:0488` (`0,1,3,5,...,0x1d,0x28`) and then applies `-mapoff`, while the actual map contents remain external in `FIXED.DAT`.
New REGRET startup-flow batch: [docs/regret-game-start.md](docs/regret-game-start.md) now documents the live `REGRET.EXE` `Game_Start` neighborhood more thoroughly. That note promotes `HandleCommandlineArgs`, `Game_RunNewGameFlow`, `Game_DrawCenteredStartupSplash`, `Game_EnterFrontendMenuViewport`, and `Game_RestoreGameplayViewport` in the live database, records the startup-state globals used by the new-game and `-warp` lanes, and explains the current best reason map `1` is set twice in No Regret: two separate live startup entry paths still own their own teleporter literals instead of sharing one final startup-map source.
New command-line argument batch: [docs/command-line-parameters.md](docs/command-line-parameters.md) now consolidates the currently recovered startup/debug argument set across the retail Crusader executables. The key new closure is the actual direct-warp syntax in `REGRET.EXE`: `-warp <mission> [x y z]` rather than separate `-x/-y/-z` switches. The same note also records the now-proven precedence rule that nonnegative `-egg` overrides beat the X/Y/Z teleport path, the practical parameter-only route into eggless maps (`-warp <mission> <x> <y> <z>` plus `-mapoff`, with `-egg` omitted), and the current best read of `-setver` as a displayed version/build-string override rather than a gameplay compatibility switch.
Follow-up No Remorse cross-check: the same command-line note and [docs/first-mission-map-selection.md](docs/first-mission-map-selection.md) now record the matching live `CRUSADER.EXE` proof. `HandleCommandlineArgs` at `1048:0adc` uses the same positional `-warp <mission> [x y z]` syntax as Regret, and `Game_Start` at `1020:029e` / `1020:02d0` applies the same precedence rule where nonnegative `-egg` overrides beat the direct-coordinate `NPC_Teleport` path.
Latest command-line follow-up: that same [docs/command-line-parameters.md](docs/command-line-parameters.md) note now closes the retail non-Japanese `-u` lane as well. In live `CRUSADER.EXE`, the parser case at `1048:0a46` copies the following token into `1478:065a`, and the newly named `startup_apply_u_override_if_present` at `1420:0cdf` consumes that buffer to load an alternate usecode/EUSECODE source into `1478:6611/6613` before rebuilding the cumulative slot-base words. Current best read is therefore `real startup usecode override`, not `JP-only feature` and not `dead parser-table residue`. The same follow-up also means the older consolidated `-setver` note is now weaker on the CRUSADER side and should be treated as needing a direct retail re-close.
Latest `-u` deep dive: new note [docs/usecode-startup-override.md](docs/usecode-startup-override.md) now follows that retail override into the live usecode runtime itself. Current best read is that `-u` replaces the single live usecode root at `1478:6611/6613` rather than adding a parallel overlay. The same root is later consumed by `Usecode_ItemCallEvent`, `UsecodeProcess_CreateProcess`, `Interpreter_NextUsecodeOp`, and `Item_GetDamaged`, so the override reaches ordinary scripted gameplay behavior, not just a startup-only side lane. Current safest tooling implication is `runtime replacement for the existing Crusader usecode VM`, not `arbitrary native plug-in system`.
Latest `-u` token-shape follow-up: the same [docs/usecode-startup-override.md](docs/usecode-startup-override.md) note now tightens the argument semantics materially. In live retail `CRUSADER.EXE`, `startup_apply_u_override_if_present` does not pass the copied argv token through as an arbitrary final filename. It loads the mutable filename template `eusecode.flx` from `1478:07a0` via the far pointer at `1478:06d6/06d8`, forces the first byte to `'e'`, and calls `Filespec_GetFullPath(0, s_usecode, "eusecode.flx", 0)`. Current safest read is therefore `path/root override for the standard EUSECODE archive family`, not `free-form arbitrary filename switch`. The same note now also separates the stock-path status more cleanly: the raw-side VM bootstrap is strongly cross-referenced, but the exact live-NE writer that seeds `1478:6611/6613` without `-u` is still not directly closed.
Latest `-u` loader-layout follow-up: the same [docs/usecode-startup-override.md](docs/usecode-startup-override.md) note now records the direct constructor/loader pair behind the override in the live NE session. `1420:1499` is now renamed `entity_vm_runtime_create` and currently reads as a `0x1319`-byte runtime-object constructor with a `0x1300`-byte front region that behaves like `0x80` stride-`0x26` slot/runtime records plus tail metadata at `0x1300..0x1318`. `1430:0000` is now renamed `entity_vm_runtime_owner_resource_create` and currently reads as the compact `0x14`-byte file-backed helper allocated from the resolved `eusecode.flx` path and attached to the runtime object at `+0x1315/+0x1317`.
Latest doc-reconciliation batch: [docs/ne-segment1.md](docs/ne-segment1.md) now has a combined hidden-debugger component table that explicitly separates the seg109/raw-reference UI wrappers (`000b:9a86`, `000b:9c0d`, `000b:b3b1`, `000b:b62c`, `000b:2882`) from the live seg1408 breakpoint-state helpers (`1408:0000`, `1408:0053`, `1408:00dd`, `1408:029e`, `1408:03b0`, `1408:03f7`, `1408:0419`, `1408:0432`, `1408:0444`) and the interpreter hook at `1418:04aa..04b5`. Current best read remains `two connected layers of one hidden usecode debugger`, not `conflicting address claims for the same function family`. Latest doc-reconciliation batch: [docs/ne-segment1.md](docs/ne-segment1.md) now has a combined hidden-debugger component table that explicitly separates the seg109/raw-reference UI wrappers (`000b:9a86`, `000b:9c0d`, `000b:b3b1`, `000b:b62c`, `000b:2882`) from the live seg1408 breakpoint-state helpers (`1408:0000`, `1408:0053`, `1408:00dd`, `1408:029e`, `1408:03b0`, `1408:03f7`, `1408:0419`, `1408:0432`, `1408:0444`) and the interpreter hook at `1418:04aa..04b5`. Current best read remains `two connected layers of one hidden usecode debugger`, not `conflicting address claims for the same function family`.
Follow-up cheat-key correction pass: [docs/ne-segment1.md](docs/ne-segment1.md) now also records a live NE cleanup of several folklore keyboard-cheat claims. `~` is a real runtime cheat-latch toggle at `13e8:203d`, `Ctrl+C` is wrong for this build and should be `Ctrl+L` for the coordinate popup at `13e8:255e`, and the third F7-family overlay really does exist as a separate `Ctrl+F7` path at `13e8:1a20` alongside the other two cheat-gated F7 overlay toggles. Follow-up cheat-key correction pass: [docs/ne-segment1.md](docs/ne-segment1.md) now also records a live NE cleanup of several folklore keyboard-cheat claims. `~` is a real runtime cheat-latch toggle at `13e8:203d`, `Ctrl+C` is wrong for this build and should be `Ctrl+L` for the coordinate popup at `13e8:255e`, and the third F7-family overlay really does exist as a separate `Ctrl+F7` path at `13e8:1a20` alongside the other two cheat-gated F7 overlay toggles.
@ -25,6 +47,8 @@ The same `docs/ne-segment1.md` note now also has the first consolidated cheat/de
| [docs/overview.md](docs/overview.md) | Binary overview, installed copy findings, address space layout, NE fixup placeholder, segment map, NE import details, next steps | | [docs/overview.md](docs/overview.md) | Binary overview, installed copy findings, address space layout, NE fixup placeholder, segment map, NE import details, next steps |
| [docs/phar-lap-extender.md](docs/phar-lap-extender.md) | DOS extender architecture, named functions (entry, loading, memory, I/O, interrupts), key string references | | [docs/phar-lap-extender.md](docs/phar-lap-extender.md) | DOS extender architecture, named functions (entry, loading, memory, I/O, interrupts), key string references |
| [docs/ne-segment1.md](docs/ne-segment1.md) | NE Segment 1 full analysis: cursor, input, entity system, shot lifecycle, combat, weapons, AI, player/HUD, destruction, entity constants, vtable index, cheat system | | [docs/ne-segment1.md](docs/ne-segment1.md) | NE Segment 1 full analysis: cursor, input, entity system, shot lifecycle, combat, weapons, AI, player/HUD, destruction, entity constants, vtable index, cheat system |
| [docs/jp-remorse-windows9x-investigation.md](docs/jp-remorse-windows9x-investigation.md) | Focused note on the Japanese `/ja/CRUSADER.EXE` Windows-native claim: PE/Win32 image evidence, Win32 windowing, DirectDraw/DirectSound, registry config under `J1.21`, IME/DBCS clues, and the GetVersion-driven Win9x compatibility branch |
| [docs/jp-remorse-cheats-and-launch-params.md](docs/jp-remorse-cheats-and-launch-params.md) | Focused note on surviving JP `/ja/CRUSADER.EXE` cheat/debug and startup-argument lanes: `-laurie`, `JASSICA16`, immortality, the recovered Win32 parser table, the live `-u` usecode override, and the current caution that JP `-warp` is only directly proven in mission-only form |
| [docs/spanish-cheat-differences.md](docs/spanish-cheat-differences.md) | Focused comparison note for `/es/CRUSADER.EXE` versus the English build's known cheat/debug lanes: `-laurie`, broad cheat gate, gameplay-input gate, low-level keyboard latch, `Ctrl+Q`, Hack Mover, and the current status of the unresolved secret sequence | | [docs/spanish-cheat-differences.md](docs/spanish-cheat-differences.md) | Focused comparison note for `/es/CRUSADER.EXE` versus the English build's known cheat/debug lanes: `-laurie`, broad cheat gate, gameplay-input gate, low-level keyboard latch, `Ctrl+Q`, Hack Mover, and the current status of the unresolved secret sequence |
| [docs/raw-porting-progress.md](docs/raw-porting-progress.md) | seg091 RNG, 0x4588 callback lifecycle batches 1-6, 0007 gameplay helper batches, snap_entity_to_ground, AI sweep, animation/range/command globals, seg043 boundary recovery | | [docs/raw-porting-progress.md](docs/raw-porting-progress.md) | seg091 RNG, 0x4588 callback lifecycle batches 1-6, 0007 gameplay helper batches, snap_entity_to_ground, AI sweep, animation/range/command globals, seg043 boundary recovery |
| [docs/raw-000e.md](docs/raw-000e.md) | 000e parser helper cluster (record table init/parse/dispatch), 000e RIFF/animation cluster (animation object field map, RIFF format, constructor variants) | | [docs/raw-000e.md](docs/raw-000e.md) | 000e parser helper cluster (record table init/parse/dispatch), 000e RIFF/animation cluster (animation object field map, RIFF format, constructor variants) |
@ -39,6 +63,11 @@ The same `docs/ne-segment1.md` note now also has the first consolidated cheat/de
| [docs/scummvm-crusader-reference.md](docs/scummvm-crusader-reference.md) | ScummVM Ultima8/Pentagram Crusader integration survey: USECODE/event tables, FLEX/resource formats, world/map loaders, HUD/media, and RE follow-up priorities | | [docs/scummvm-crusader-reference.md](docs/scummvm-crusader-reference.md) | ScummVM Ultima8/Pentagram Crusader integration survey: USECODE/event tables, FLEX/resource formats, world/map loaders, HUD/media, and RE follow-up priorities |
| [docs/pentagram-crusader-reference.md](docs/pentagram-crusader-reference.md) | Pentagram-source Crusader/U8 reference: direct Crusader USECODE parser and VM evidence, U8 usecode docs, runtime-confidence limits, and cross-checks against the ScummVM note | | [docs/pentagram-crusader-reference.md](docs/pentagram-crusader-reference.md) | Pentagram-source Crusader/U8 reference: direct Crusader USECODE parser and VM evidence, U8 usecode docs, runtime-confidence limits, and cross-checks against the ScummVM note |
| [docs/map-rendering.md](docs/map-rendering.md) | Offline map-rendering lane: `FIXED.DAT`/`GLOB.FLX`/`SHAPES.FLX`/`GAMEPAL.PAL` format notes, current Python renderer, supported inputs, and fidelity gaps | | [docs/map-rendering.md](docs/map-rendering.md) | Offline map-rendering lane: `FIXED.DAT`/`GLOB.FLX`/`SHAPES.FLX`/`GAMEPAL.PAL` format notes, current Python renderer, supported inputs, and fidelity gaps |
| [docs/editor-object-visibility.md](docs/editor-object-visibility.md) | Focused note on retail editor-only map object hiding: the live `1198:02e4` `SI_EDITOR` early-out in the normal item paint path, the lack of a recovered retail visibility toggle, and the ScummVM/Pentagram cross-check that treats `show editor items` as an engine-side debug feature |
| [docs/first-mission-map-selection.md](docs/first-mission-map-selection.md) | Focused note on fresh-game startup map selection: No Remorse `Game_Start`, No Regret's early and later mission-start selectors, the separate embedded `-warp mission` table, and the split between code-selected startup and external `FIXED.DAT` map content |
| [docs/regret-game-start.md](docs/regret-game-start.md) | Detailed `REGRET.EXE` startup-flow note: `Game_Start`, `Game_RunNewGameFlow`, newly named helpers, startup override globals, and the current best explanation for the duplicated map-1 selector |
| [docs/command-line-parameters.md](docs/command-line-parameters.md) | Consolidated startup/debug argument reference for the retail Crusader executables: live retail `-u` usecode override, the current `-setver` caution, `-debug`, `-asylum`, `-warp`, `-skill`, `-mapoff`, `-egg`, `-demo`, the `-laurie` cross-reference, and the evidence-backed direct-coordinate warp syntax/limits |
| [docs/usecode-startup-override.md](docs/usecode-startup-override.md) | Focused retail `-u` deep dive: startup call order, why the override looks like full live-root replacement rather than addition, which event/process/interpreter consumers use that root, and what that implies for future custom usecode experiments |
| [docs/usecode-roundtrip-ir.md](docs/usecode-roundtrip-ir.md) | ScummVM-to-binary USECODE cross-walk, owner-loaded class-layout and header/event-count reconciliation, conservative IR v0 plan, and the generated class-event/body-window outputs that now ground reversible `_BOOT`, `SURCAM*`, and environmental family decompile artifacts plus repeated-family regression checks | | [docs/usecode-roundtrip-ir.md](docs/usecode-roundtrip-ir.md) | ScummVM-to-binary USECODE cross-walk, owner-loaded class-layout and header/event-count reconciliation, conservative IR v0 plan, and the generated class-event/body-window outputs that now ground reversible `_BOOT`, `SURCAM*`, and environmental family decompile artifacts plus repeated-family regression checks |
| [docs/usecode-pentagram-ghidra-path.md](docs/usecode-pentagram-ghidra-path.md) | Pentagram-derived Crusader USECODE parser plan, proof-of-concept workflow, canonical IR v1 goals, and the Ghidra-side annotation import path | | [docs/usecode-pentagram-ghidra-path.md](docs/usecode-pentagram-ghidra-path.md) | Pentagram-derived Crusader USECODE parser plan, proof-of-concept workflow, canonical IR v1 goals, and the Ghidra-side annotation import path |
| [docs/usecode-tooling-comparison.md](docs/usecode-tooling-comparison.md) | Comparison of Pentagram's converter/disassembler, the local `crusader-disasm` corpus/scripts, and the current workspace parser/pseudocode exporter, with emphasis on assumptions, strengths, and repo-specific differences | | [docs/usecode-tooling-comparison.md](docs/usecode-tooling-comparison.md) | Comparison of Pentagram's converter/disassembler, the local `crusader-disasm` corpus/scripts, and the current workspace parser/pseudocode exporter, with emphasis on assumptions, strengths, and repo-specific differences |

View file

@ -0,0 +1,210 @@
# Crusader Command-Line Parameters
## Scope
This note consolidates the currently recovered startup/debug argument set across the retail Crusader executables.
The current strongest parser/control-flow evidence now comes from both live targets:
- `REGRET.EXE`
- `CRUSADER.EXE`
Two conclusions matter most for the recent startup-map work:
- the executable patch was still required to change the stock fresh-game start map
- the built-in manual/debug warp lane is real, and its direct-coordinate form is `-warp <mission> [x y z]`
That direct-coordinate syntax is now confirmed in both retail games, not just Regret.
## Short Answer
If the question is `could the normal first-mission executable hack have been replaced by parameters all along?`, the answer is still no.
The stock new-game path hardcodes map `1`, egg `0x1e` in code. The parameterized warp path is a separate startup/debug branch that only runs when `-warp` is present.
If the question is `can the executable already warp into another map without patching files?`, the answer is yes.
The current best-proven forms are:
- `-warp <mission>`
- `-warp <mission> <x> <y> <z>`
- optional `-mapoff <delta>` and `-egg <id>` modifiers
The important restriction is that `-egg` beats X/Y/Z. If you supply a nonnegative egg override, the game routes through the teleporter-egg path and does not use the direct-coordinate teleport.
That precedence rule is also now confirmed in both retail games:
- `REGRET.EXE` routes through `Game_RunNewGameFlow`
- `CRUSADER.EXE` routes through `Game_Start`
For the specific `-u` question, the current live No Remorse answer is now yes.
The regular non-Japanese `CRUSADER.EXE` still contains a real `-u` lane, and the current best read is no longer `unresolved leftover token`. In this build it is a live startup override for the usecode/EUSECODE load source.
## Parameter Table
| Argument | Current best syntax | Confidence | Current best effect |
|------|------|------|------|
| `-?` | `-?` | Medium | Help/usage-style startup path. The option is present in the parser table, and the nearby startup strings include `You DO need help!`. The exact REGRET-side case-to-string tie was not re-closed in this pass. |
| `-u` | `-u <arg>` | High | Live in retail `CRUSADER.EXE`. The parser copies the following token into `1478:065a`, and `startup_apply_u_override_if_present` later uses that buffer as a startup usecode/EUSECODE load-source override. The JP build matches the same interpretation. |
| `-debug` | `-debug` | High | Live parser branch. Raises the debug-print threshold, prints `Debugging mode ON.`, and enables the known movie-player timing overlay / debug-print lane. |
| `-setver` | `-setver <text>` | Medium | Still present in the broader Crusader parser-family tables, but the earlier CRUSADER-side assignment of the string-copy branch now belongs to `-u`. Treat the exact retail `CRUSADER.EXE` `-setver` behavior as still unresolved here; the older version/banner read is now best treated as REGRET-weighted rather than closed for both games. |
| `-asylum` | `-asylum` | High | Joke/no-op style switch. Current best recovered effect is printing `Enabling ENHANCED mode. (NOT!)` with no stronger gameplay-side state change recovered in this pass. |
| `-warp` | `-warp <mission>` | High | Enables the manual/debug warp lane by setting `g_warpToLevelNoArg`. The runtime then computes `target_map = mission_table[mission] + mapoff`. |
| `-warp` with coordinates | `-warp <mission> <x> <y> <z>` | High | Direct coordinate warp. After parsing the mission number, the parser checks the next argv token. If it is present and does not begin with `-`, the next three argv tokens are parsed as X, Y, and Z. |
| `-skill` | `-skill <n>` | High | Parses a difficulty override into `1480:0ace`. The parser clamps `0` up to `1`, then `Game_RunNewGameFlow` copies the value into the difficulty level before the mission-start hop. |
| `-mapoff` | `-mapoff <delta>` | High | Adds an offset to the mission-table result inside the manual/debug warp path. It does not affect the stock fresh-game selector when `-warp` is absent. |
| `-egg` | `-egg <id>` | High | Sets the destination egg override used by the warp path. When the value is nonnegative, it takes precedence over the direct-coordinate teleport branch. |
| `-demo` | `-demo` | High | Sets `1480:0ad4`, prints `Demo mode.`, and changes the ORIGIN/ANIM01 startup-video behavior in `Game_RunNewGameFlow`. |
| `-laurie` | `-laurie` | High | Separate from the main `HandleCommandlineArgs` switch table. This is the broader cheat/debug-enablement argument already documented in the live No Remorse notes. |
## Warp Syntax And Precedence
The current best evidence-backed parser behavior is now confirmed in both `REGRET.EXE` and `CRUSADER.EXE`.
In the No Remorse live database, the exact matching parser case is `HandleCommandlineArgs` at `1048:0adc`, and the matching consumer branch is in `Game_Start` at `1020:029e` / `1020:02d0`.
In the Regret live database, the matching parser case is `HandleCommandlineArgs` at `1058:0cd2`, and the matching consumer branch is in `Game_RunNewGameFlow` at `1030:0628` / `1030:069a`.
The shared behavior is:
1. `-warp` always consumes one numeric mission argument.
2. The parser then peeks at the next argv token.
3. If that token is missing or begins with `-`, the game takes the mission-only path and prints `Warping to mission %d.`.
4. Otherwise the parser consumes the next three argv tokens as X, Y, and Z, stores them in `1480:0ac8`, `1480:0aca`, and `1480:0acc`, and prints `Warping to mission %d @ x:%d y:%d z:%d.`.
For No Remorse the corresponding X/Y/Z globals are the earlier siblings:
- `1478:084c` = X override
- `1478:084e` = Y override
- `1478:0850` = Z override
That means the recovered syntax is positional:
- `-warp <mission>`
- `-warp <mission> <x> <y> <z>`
This pass did not recover any separate literal `-x`, `-y`, or `-z` switches in the startup parser.
At runtime, both games apply the same precedence:
1. Compute the target map from the embedded mission-to-map table plus `-mapoff`.
2. If X is unset (`0xffff` / `-1`), use the egg teleporter path.
3. If X is set but `-egg` is also nonnegative, still use the egg teleporter path.
4. Only when X/Y/Z are present and the egg override is still negative does the game bypass egg lookup and call `NPC_Teleport` directly.
No Remorses direct consumer branch is especially clear:
- `1020:029e` checks whether X is still `-1`
- `1020:02a5` / `1020:02d0` check whether `-egg` is nonnegative
- `1020:02d7..02eb` call `NPC_Teleport` only when X/Y/Z are present and egg override is still negative
## Eggless Maps And The Non-Patching Workaround
This directly answers the recent map-254 style question.
There is a parameter-only route into maps with no usable destination egg, but it is not `-egg`.
The working shape is:
- `-warp <mission> <x> <y> <z> -mapoff <delta>`
with `-egg` omitted.
Why that matters:
- `-egg` keeps the code on the teleporter/egg path
- the direct-coordinate form falls into `NPC_Teleport`
- the inspected `NPC_Teleport` lane writes the exact map/X/Y/Z values and moves the camera there
- the currently inspected teleport path does not show any automatic `snap-to-ground` or `find a safe spawn` rescue
So the non-patching workaround for an eggless map is `known-good coordinates`, not `some hidden eggless-map flag`.
If the supplied X/Y/Z values are bad, the game can still strand the avatar in empty space even though the warp itself succeeded.
## What `-u` Does In Retail `CRUSADER.EXE`
The live non-Japanese No Remorse database now closes `-u` much more tightly than the older note.
In `HandleCommandlineArgs`, the retail parser case at `1048:0a46` consumes the next argv token and copies it into the fixed startup buffer at `1478:065a`.
The only recovered consumer of that buffer in the same retail executable is `startup_apply_u_override_if_present` at `1420:0cdf`.
Recovered behavior in that helper:
- check whether `1478:065a` is non-empty
- if empty, leave the default startup path alone and return
- if non-empty, route startup through an alternate load/resolve path using the copied string
- store the resulting far pointer in `1478:6611/6613`
- set the loaded-state byte at `1478:6615`
- rebuild the cumulative slot-base words at `1478:8c7c..8c82`
That is strong retail No Remorse evidence for this meaning:
> `-u <arg>` is a live startup override for the usecode/EUSECODE load source.
What is still not fully closed from this static pass is the exact Filespec path syntax for the copied token.
What is now materially tighter:
- the helper does **not** treat the argv token as the final archive filename
- it uses the token as the `path` component to `Filespec_GetFullPath`
- it uses the mutable filename template at `1478:07a0`, which is `eusecode.flx`, as the fixed `filename` component
- it forces that template's first byte to `'e'` before the existence probe and final load call
So the safest current retail read is:
- `-u <arg>` expects a directory/resource-root style path argument for the standard `eusecode.flx` archive family
- it does **not** currently look like a free-form arbitrary filename override
But the important uncertainty is now only `exact naming rules`, not `whether the switch is real`. In the regular non-Japanese `CRUSADER.EXE`, the switch is clearly still live.
This also aligns with the already-stronger JP Win32 result, where the matching `-u` lane was recovered as the same kind of usecode override.
For the deeper runtime-side investigation of whether `-u` replaces or augments the stock usecode root, and what game systems that replacement feeds, see [docs/usecode-startup-override.md](docs/usecode-startup-override.md).
Current best answer from that follow-up is:
- `-u` behaves like a replacement of the live usecode runtime root, not an additive overlay
- the replaced root is then used by normal item-event dispatch, usecode process creation, interpreter bytecode stepping, and gameplay-side scripted capability checks
The current best replacement-vs-addition answer is also now stronger:
- there is one live root at `1478:6611/6613`
- `startup_apply_u_override_if_present` overwrites that root directly when `-u` is present
- later event/process/interpreter consumers read the replacement root through the same global pair
So the safest current retail read is `full runtime root replacement for the session`, not `load one extra add-on script beside stock usecode`.
## What `-setver` Most Likely Does
`-setver` should now be treated more cautiously in this consolidated note.
For retail `CRUSADER.EXE`, the string-copy branch that had previously been grouped under `-setver` is now better closed as the live `-u` handler described above.
That means the older `displayed version/build string override` reading is no longer closed for the non-Japanese No Remorse executable itself.
Current evidence bundle:
- the option name is still present in the broader Crusader parser-family tables
- older REGRET-side evidence still points toward a startup/UI presentation role rather than gameplay control
- but this pass did not directly re-close the exact retail No Remorse `-setver` consumer after reassigning the resolved string-copy branch to `-u`
Current best read:
- `-setver <text>` still looks more like a presentation/version banner switch than a gameplay/startup-map selector
- but that statement is now provisional for consolidated retail notes until the retail `CRUSADER.EXE` handler is isolated directly
The exact retail No Remorse `-setver` branch is therefore an open cleanup item again, while `-u` is now the closed string-consuming retail startup override.
## No Remorse Cross-Check Summary
The main recent uncertainty was whether Regret had gained an extra direct-coordinate warp feature that No Remorse lacked. The live `CRUSADER.EXE` pass now closes that question.
Current best answer:
- No Remorse supports the same positional `-warp <mission> [x y z]` syntax.
- No Remorse uses the same `-egg` precedence rule over X/Y/Z.
- No Remorse reaches the same direct `NPC_Teleport` style fallback when coordinates are present and the egg override is still negative.
So the parameter-only workaround for eggless maps is not Regret-specific. The same approach should work in No Remorse too, as long as the supplied coordinates are valid for the chosen target map.

View file

@ -0,0 +1,225 @@
# Editor-Object Visibility In Retail Crusader
## Question
Investigate whether the retail game explicitly hides editor-only map objects during gameplay, and whether any built-in debug, cheat, or command-line switch can enable those objects in the normal in-game renderer.
Active analysis target for the binary side was live `CRUSADER.EXE` in the Ghidra MCP session.
## Result
Current best answer:
- Yes, the retail executable explicitly hides editor-tagged shapes in the normal world-item render path, with at least two recovered `SI_EDITOR` gates.
- No built-in retail flag, cheat, or command-line switch has been recovered so far that re-enables those editor objects in the normal in-game renderer.
- The closest confirmed "show editor items" toggle is in ScummVM/Pentagram, not in the original retail executable.
## Binary Findings In `CRUSADER.EXE`
### 1. The downstream sprite painter explicitly skips `SI_EDITOR`
The strongest direct proof is live function `Item_PaintSprite` at `1198:02e4`.
Verified decompile behavior:
- It loads the current item's shape number from `g_itemShapeNos`.
- It resolves the matching `ShapeData` entry from `g_shapeData`.
- It immediately checks `local_8->flags2 & 1`.
- When that bit is set, the function returns before caching or drawing the shape.
Relevant instruction window at `1198:0332..033d`:
```text
1198:0332 MOV AL,byte ptr ES:[BX + 0x6]
1198:0336 AND AX,0x1
1198:0339 OR AX,AX
1198:033b JZ 1198:0340
1198:033d JMP 1198:0959
```
`1198:0959` is the function epilogue, so the branch is an unconditional early-out for bit `0` of shape byte `6`.
This aligns with the existing `SI_EDITOR` comment at `1198:0339` and with the open-source Crusader typeflag decoders, where Crusader `TYPEFLAG.DAT` byte `6`, bit `0` maps to `SI_EDITOR`.
### 2. The paired setup path still processes those items
The neighboring helper `Item_SetScreenBoxAndWorldCoords` at `1198:095d` still computes:
- world coordinates,
- projected screen coordinates,
- bounding boxes,
- and weapon-overlay extents.
That function does **not** perform an `SI_EDITOR` check.
Current safest read after the first pass was:
- editor objects remain present in the live item/cache/display-list setup state,
- but the normal sprite paint stage refuses to draw them.
That read turned out to be incomplete rather than wrong.
### 3. The active world-item render-list builder also skips `SI_EDITOR` earlier
Follow-up tracing from the live camera redraw path into segment `1180` found a more important upstream gate in the world-item render-list builder.
Relevant instruction window at `1180:0951..095c`:
```text
1180:0951 MOV AL,byte ptr ES:[BX + 0x6]
1180:0955 AND AX,0x1
1180:0958 OR AX,AX
1180:095a JZ 1180:095f
1180:095c JMP 1180:0bec
```
This is the same Crusader `TYPEFLAG.DAT` byte `6`, bit `0` test, but here the consequence is stronger than in `Item_PaintSprite`: the builder skips the world-item draw-node allocation path entirely before that item can ever reach the normal draw dispatch.
That closes the runtime discrepancy from the first patch attempt. Patching only the later `1198:033b` branch changed a real live draw method, but it did **not** make editor objects visible in-game because the active renderer had already filtered them out earlier at `1180:0951..095c`.
### 4. Why the first executable patch appeared to do nothing
The first public patch only flipped the downstream `Item_PaintSprite` branch:
```text
1198:033b JZ 1198:0340 -> EB 03
```
That patch was not dead code. Export relocation evidence still shows the draw-node method slot at `1478:2c1f` dispatching to `1198:02e4`, so the downstream painter is a real live node callback.
But the active world-item renderer never reached that callback for editor-tagged shapes, because the upstream builder branch at `1180:095a..095c` had already skipped node creation.
Current patching implication:
- patching `1198:033b` alone is insufficient,
- the upstream `1180:095a` branch is the controlling visibility gate for normal world-item rendering,
- and a practical visibility patch should flip both recovered `SI_EDITOR` branches together.
### 5. The recovered item-flag helpers match the same shape-bit model
`Item_GetTypeFlagCrusader` reads arbitrary shape/typeflag bits from the per-shape `ShapeData` records.
Additional direct flag helpers nearby include:
- `Item_IsTargetable`, which reads `flags2 & 0x10`
- `Item_IsShapeFlagOccl`, which reads `flags0 & 0x10`
That supports the interpretation that `Item_PaintSprite` is not testing some temporary per-item state. It is reading a stable per-shape editor flag.
## Negative Search For A Retail "Show Editor Items" Toggle
I checked the known retail debug/cheat/control lanes already documented in the live database and repo notes.
### `-debug`
Retail `-debug` is live at `1048:0a93`, but the current closed behavior is:
- raise `g_debugMsgLevel` to `10`
- print `Debugging mode ON.`
- set `1478:0845`
- enable the seg1468 video-player timing overlay via `1478:0859`
No recovered link from `-debug` reaches the `SI_EDITOR` skip in `Item_PaintSprite` or enables a parallel item-paint path.
### Cheat/debug hotkeys
Recovered cheat/debug hotkeys do toggle several overlays, but the confirmed ones are unrelated to editor-shape rendering:
- `Ctrl+F7` at `13e8:1a20` toggles `1478:0ee0`, the egg-hatcher trigger-range overlay.
- `Alt+F7` at `13e8:1a50` toggles `1478:2bc9`, another cheat-gated overlay lane.
- The remaining F7-family toggle uses `1478:2bca` for the coarse grid overlay.
Those paths force camera redraws, but the recovered behavior is overlay drawing, not "draw editor-tagged world items."
### Hidden usecode debugger / Laurie lane
The hidden seg109/seg1408 usecode-debugger lane remains real, and `-laurie` plus the cheat/debug latches still enable related hidden behavior. But no recovered debugger-side state or menu action currently bypasses the `Item_PaintSprite` editor skip, and no candidate global has been found that matches ScummVM's later `showEditorItems` concept.
### Event `0x410`
This lane remains unrelated.
Current live conclusion is unchanged:
- retail `0x410` toggles the CD transfer display state,
- it is not the immortality toggle,
- and it is not an editor-object visibility switch.
## Cross-Check Against Open-Source Engine Behavior
ScummVM and Pentagram both independently preserve the same semantic meaning for the Crusader editor flag.
### Flag mapping
In both codebases, Crusader `TYPEFLAG.DAT` byte `6`, bit `0` maps to `SI_EDITOR`.
ScummVM `type_flags.cpp`:
```cpp
if (data[6] & 0x01) si._flags |= ShapeInfo::SI_EDITOR;
```
Pentagram `TypeFlags.cpp`:
```cpp
if (data[6] & 0x01) si.flags |= ShapeInfo::SI_EDITOR;
```
### Runtime treatment
ScummVM `game_map_gump.cpp` shows the exact higher-level behavior one would expect from the retail binary finding:
```cpp
if (!showEditorItems && item->getShapeInfo()->is_editor())
continue;
```
Pentagram documents the same meaning directly in `docs/u8typeflag.txt`:
- `bit 4 : editor shape (don't render in-game)`
For Crusader, the bit location differs in the later 9-byte Crusader format, but the semantic meaning is the same.
### Important limit of this cross-check
ScummVM exposes its own engine variable and debugger command:
- engine field: `_showEditorItems`
- setter: `setShowEditorItems(bool flag)`
- debugger command: `showEditorItems [on|off]`
That is useful as behavioral confirmation, but it is **not** evidence that retail `CRUSADER.EXE` ships with an equivalent built-in toggle.
## Conclusion
The retail executable does contain explicit logic to hide editor-only objects during ordinary gameplay rendering.
Current best model:
1. map/editor objects exist in the same broad world-item/runtime data flow as ordinary items,
2. the active world-item builder in segment `1180` checks `SI_EDITOR` and skips draw-node allocation when that bit is set,
3. if such an item still reached the standard sprite painter, `Item_PaintSprite` would also early-out on the same bit,
4. and no recovered retail debug/cheat/argument lane currently bypasses either gate.
I did **not** recover any retail equivalent of:
- a `showEditorItems` global,
- a command-line switch that enables editor-object rendering,
- a cheat/debug hotkey that enables editor-object rendering,
- or a hidden debugger command that obviously bypasses the `SI_EDITOR` skip.
So the present evidence supports:
- explicit retail hiding behavior exists,
- but no official or currently recovered internal toggle exists to reveal those objects in the shipped game.
If the goal is to see them in the original executable, the most direct current path is no longer searching cheats or args first. It is either:
- finding a second, debug-only display-list builder that bypasses the `1180:0951` gate, or
- patching both recovered `SI_EDITOR` branches in a writable copy.
## Next RE Follow-Up If Revisited
- Recover the exact containing function and wider caller chain for the `1180:0951` world-item builder skip and check whether any alternate debug-only builder exists.
- Inspect the segment `1180` redraw/display-list builders for a second path that draws world items outside the normal world-item allocation branch.
- If a runtime proof is wanted, patch both recovered `SI_EDITOR` branches only on a writable executable copy and test whether editor shapes become visible without destabilizing collision or selection.

View file

@ -0,0 +1,369 @@
# First Mission Map Selection
## Question
What determines which map a fresh game starts on, and would changing the first mission from map `1` to map `248` require a code patch or an external data-file change?
For a deeper REGRET-side control-flow breakdown, including the now-named helper functions and startup globals around `Game_Start`, see `docs/regret-game-start.md`.
## Short Answer
For a normal new game, the start map is chosen in code, not from `CRUSADER.CFG` or another external mission-mapping file.
The live `CRUSADER.EXE` fresh-game path hardcodes:
- map array `1`
- teleport egg `0x1e`
The relevant Remorse call is in `Game_Start`:
- `1020:0243 PUSH 0x1e0001`
- `1020:0249 CALLF 0x1090:04ce`
No Regret also hardcodes the same values, but there are two relevant selectors:
- an early menu-start selector in `Game_Start` at `1008:1448`
- the actual mission-start selector later in `FUN_1030_032d` at `1030:05c5`
Those sites all encode map `1`, egg `0x1e` in code. For No Regret specifically, patching only the earlier `Game_Start` site is not enough to change where a real new game starts, because the later `FUN_1030_032d` mission-start path repeats the same hardcoded selector.
Changing fresh-game startup from map `1` to map `248` would therefore be a program-code modification in the startup path, not a config-file tweak.
## Evidence Chain
### 1. Command-line parsing does not pick the default fresh-game map
`HandleCommandlineArgs` recognizes these mission/map-related switches:
- `-warp` -> stores mission number in `g_warpToLevelNoArg`
- `-mapoff` -> stores an additive map offset in `g_mapoffArgValue`
- `-egg` -> stores an egg override in `g_eggArgValue`
The same startup lane also clearly supports optional warp coordinates at runtime. The currently recovered evidence for that is the startup status string:
- `Warping to mission %d @ x:%d y:%d z:%d.`
and the paired startup globals used by the REGRET-side flow:
- `1480:0ac8` = warp X override
- `1480:0aca` = warp Y override
- `1480:0acc` = warp Z override
The parser/control-flow work is now tight enough to promote the actual syntax: `-warp <mission> [x y z]`.
That syntax is now confirmed directly in both retail games. In `CRUSADER.EXE`, the parser case at `1048:0adc` mirrors the Regret-side branch: it defaults X to `0xffff`, parses the mission into `1478:084a`, then either logs the mission-only path or consumes the next three argv tokens into `1478:084c`, `1478:084e`, and `1478:0850`.
The parser does not expose separate recovered `-x`, `-y`, or `-z` switches. Instead, after it parses the mission number, it looks at the next argv token. If the next token is missing or begins with `-`, the code takes the mission-only path. Otherwise it consumes the next three argv tokens as X, Y, and Z.
One important runtime detail also matters for later map-warp experiments: a nonnegative `-egg` override beats the coordinate path. This is now confirmed in both games. In No Remorse, `Game_Start` checks `1478:084c` at `1020:029e` and `1478:0856` at `1020:02a5` / `1020:02d0`; if `-egg` is nonnegative, the code still routes through `Teleporter_CreateProcessDirect` and ignores the direct-coordinate `NPC_Teleport` path.
This function only records override values. It does not set the normal fresh-game start map.
Relevant globals:
- `1478:084a` = `g_warpToLevelNoArg`
- `1478:0854` = `g_mapoffArgValue`
- `1478:0856` = `g_eggArgValue`
### 2. Normal new-game startup is hardcoded in `Game_Start`
`Game_Start` has two distinct startup branches:
- normal new game: no `-warp` argument
- debug/manual warp: `g_warpToLevelNoArg != -1`
In the normal new-game branch, the game does this directly:
```c
Camera_MoveTo(0,0,0,0);
g_globsDone = 0;
Camera_SetCentreOn(1);
Teleporter_CreateProcessDirect(1,0x1e,1);
```
The decompiler comment already matches the runtime meaning:
- `start on level 1, starting egg (0x1e)`
Disassembly of the key site:
- `1020:0241 PUSH 0x1`
- `1020:0243 PUSH 0x1e0001`
- `1020:0249 CALLF 0x1090:04ce`
This is the strongest direct proof that fresh-game mission start is code-selected.
### 2a. No Regret cross-check: `Game_Start` still contains an early hardcoded selector
The currently opened `REGRET.EXE` session shows the same hardcoded startup arguments in its own `Game_Start` body.
Relevant decompiler lane:
```c
Cameara_SetCameraOn(0);
Camera_MoveTo(0,0,0,0);
DAT_1480_1453 = 0;
Cameara_SetCameraOn(1);
Teleporter_ChangeMap(1,0x1e,1);
```
The `DAT_1480_1453 = 0` write at `1008:1437` is a useful local anchor, because the startup selector call follows immediately after it.
Disassembly of the key site:
- `1008:1437 MOV byte ptr [0x1453],0x0`
- `1008:1446 PUSH 0x1`
- `1008:1448 PUSH 0x1e0001`
- `1008:144e CALLF 0x1030:0000`
This is a real hardcoded selector, but it is not the only No Regret control point.
### 2b. No Regret actual mission-1 start: the later new-game path repeats the same selector in `FUN_1030_032d`
The actual No Regret new-game path later reinitializes the world, runs the new-game videos/modal flow, and then performs the mission-start hop inside `FUN_1030_032d`.
Relevant decompiler lane:
```c
if (g_warpToLevelNoArg == -1) {
Camera_MoveTo(0,0,0,0);
DAT_1480_1453 = 0;
Cameara_SetCameraOn(1);
FUN_1030_022e();
Teleporter_ChangeMap(1,0x1e,1);
Fade_SetPaletteToAllBlack();
}
```
Disassembly of the key selector site:
- `1030:05c3 PUSH 0x1`
- `1030:05c5 PUSH 0x1e0001`
- `1030:05cb PUSH CS`
- `1030:05cc CALL 0x1030:0000`
This is the selector that matters for an actual No Regret mission-1 start after the early startup/menu flow. That is why changing only the earlier `1008:1448` site did not redirect a real new game.
So No Regret still does not move fresh-game map selection into external config, but the effective mission-start path is split across two hardcoded selectors instead of one.
### 3. The debug `-warp mission` path uses an executable-embedded mission-to-map table
If `g_warpToLevelNoArg != -1`, `Game_Start` does not use the hardcoded `1`. Instead it computes:
```c
iVar5 = *(int *)(g_warpToLevelNoArg * 2 + 0x488) + g_mapoffArgValue;
```
Disassembly:
- `1020:0254 MOV BX,word ptr [0x84a]`
- `1020:0258 SHL BX,0x1`
- `1020:025a MOV AX,word ptr [BX + 0x488]`
- `1020:025e ADD AX,word ptr [0x854]`
Static bytes at `1478:0488`:
```text
1478:0488: 00 00 01 00 03 00 05 00 07 00 09 00 0b 00 0d 00
1478:0498: 0f 00 11 00 13 00 15 00 17 00 19 00 1b 00 1d 00
1478:04a8: 28 00
```
Interpreted as little-endian words, that table is:
- `0, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 40`
Expanded as mission-to-map entries, the current recovered No Remorse table is:
| `-warp` mission | Base map from table |
|------|------|
| `0` | `0` |
| `1` | `1` |
| `2` | `3` |
| `3` | `5` |
| `4` | `7` |
| `5` | `9` |
| `6` | `11` |
| `7` | `13` |
| `8` | `15` |
| `9` | `17` |
| `10` | `19` |
| `11` | `21` |
| `12` | `23` |
| `13` | `25` |
| `14` | `27` |
| `15` | `29` |
| `16` | `40` |
So the `-warp mission` path also uses code/data embedded in the executable, not a separate mission-mapping file. The first real mission entry maps to `1`, and `-mapoff` can then shift it.
`-mapoff` therefore matters only inside the manual/debug warp path. It does not affect the ordinary fresh-game selector when no `-warp` argument is present.
Practical implication for the earlier patch question:
- if the goal was `start a normal new game on a different first map`, the executable patch was still required
- if the goal was `enter a chosen mission/map through the debug/manual warp path`, `-warp` plus `-mapoff` already provided a built-in route without patching the startup selector itself
- if the goal was `reach a map with no usable egg`, the built-in non-patching route is `-warp <mission> <x> <y> <z>` plus `-mapoff`, with `-egg` omitted so the code uses the direct `NPC_Teleport` path
The No Remorse cross-check now makes that last point stronger: this eggless-map workaround is not just a Regret quirk. `CRUSADER.EXE` uses the same parser shape and the same consumer-side precedence.
### 3a. Why `-warp 0 28670 30718 0 -mapoff 246` likely lands in a bad spot in No Remorse
The current cached scene data from `Crusader_Decomp_Public/map_renderer/.cache/scene-cache/remorse/map-246/bb7e36195d39ac72/scene.json` confirms that map `246` is real and nonempty:
- `rawItemCount = 504`
- `itemCount = 1080`
- `terrain = 902`
- `egg = 149`
- `editor = 10`
- `npcLinkedItems = 16`
- `invalidItemCount = 0`
So the black-room result is not explained by `map 246 does not exist` or `the cache is effectively empty`.
The more likely problem is the coordinate pair itself.
`-warp 0 -mapoff 246` does resolve to map `246`, because mission `0` maps to base map `0` and the runtime then adds the `246` offset.
But the cached scene data does **not** show `x = 28670, y = 30718` as a recovered world-coordinate pair on map `246`.
What the cache does show is that those numbers belong to different active coordinate bands on that map:
- `x = 28670` appears repeatedly with y-values such as `19454`, `20478`, `21502`, `22526`, `23550`, `24574`, and `25598`
- `x = 30718` appears repeatedly with the same lower/mid y-bands such as `19454`, `20478`, `21502`, `22526`, `23550`, `24574`, and `25598`
- `y = 30718` is also real on map `246`, but in a different band; one cached pair there is `x = 22526, y = 30718, z = 0`
That means the failing command is very likely mixing a valid X lane from one populated region with a valid Y lane from a different populated region.
Current best explanation for the black-room result:
- the direct `NPC_Teleport` path is working
- map `246` itself is present and populated
- but `28670,30718,0` is not a recovered occupied/world-backed placement from the cached scene, so the avatar is probably landing in an unhelpful void or dark staging cell rather than on the visible floor grid
The cache suggests more defensible test candidates would be real paired coordinates already present in the scene, for example:
- `28670,25598,0`
- `30718,25598,0`
- `22526,30718,0`
Those are not yet promoted here as guaranteed safe spawn points, but they are materially stronger than mixing one X value and one Y value that the cache never shows together as a real item location.
## How The Map Number Is Consumed
`Teleporter_CreateProcessDirect` stores the passed map number into the teleporter process:
- `struct TeleporterProcess::mapno` at offset `0x32`
`TeleporterProcess_Run` then compares that `mapno` against `g_currentMapArray`.
If the target map differs from the current map, it moves the avatar/camera onto that map array before searching for the destination teleport egg.
Relevant logic:
```c
if (p_proc->mapno != g_currentMapArray) {
if (DAT_1478_085f == '\0') {
Camera_MoveTo(0,0,0,p_proc->mapno);
}
else {
NPC_Teleport(&g_avatarItemNo,0,0,0,(byte)p_proc->mapno);
}
}
```
Then it searches for a teleport egg with the requested id and completes the move on that same target map.
This means the startup value is a real map-array selector, not just a mission label.
## Where The External Data Starts
The selected map contents are external data, even though the startup choice is hardcoded.
The clearest loader anchor is `ItemCache_InitAndLoadFixedDat`, which:
- initializes item data
- calls `FixedDat_LoadData(...)`
- checks for `static\fixed.dat`
- loads the optional patch-layer `static\fixed.dat`
- resets `g_currentMapArray = 0xffff`
`FixedDat_LoadData` builds a full path using `g_fixedDatFilenamePtr`, which is the game-side `fixed.dat` asset name.
Cross-check from the existing map-rendering note:
- `FIXED.DAT` contains a map table
- map count is stored at file offset `0x54`
- map entries live in a table at file offset `0x80`
So the world geometry/object placement for map `1`, map `248`, and the rest lives in external map resources, but the decision to start a fresh game on map `1` is made in code.
## What Is Not Controlling Fresh-Game Map Selection
### `CRUSADER.CFG`
The local `CRUSADER.CFG` only contains sound/video/install settings such as:
- `irq`
- `dma`
- `port`
- `sound`
- `music`
- `fullinstall`
- `bigvideo`
- `subtitles`
- `cdletter`
- `flicpath`
No mission-start or map-start key is present.
### `LoadConfigFile`
`LoadConfigFile` parses `crusader.cfg`, but the recovered keys are audio/video/control/install options and custom music-tune redirects. This function does not provide a fresh-game mission-to-map selector.
## Practical Conclusion For A Map-248 Hack
If the goal is specifically:
- normal first mission
- fresh game
- load map `248` instead of map `1`
then the controlling point is the hardcoded new-game startup call inside `Game_Start`, not an external config file.
The cleanest future patch-design targets are therefore:
- the immediate startup call that currently passes map `1`, egg `0x1e`
- or a nearby wrapper rewrite that substitutes the startup map before `Teleporter_CreateProcessDirect` runs
The external data side still matters, because map `248` must exist as a valid entry in the shipped map resources and must contain a compatible teleport egg if the startup egg remains `0x1e`.
## Script Patcher Update
The public PowerShell patcher now supports both supported retail executables:
- `CRUSADER.EXE` for No Remorse
- `REGRET.EXE` for No Regret
It now chooses the game mode from whichever supported executable is present next to the script, then resolves the on-disk patch site by scanning for selector signatures instead of relying on one fixed file offset.
The runtime signatures used by the patcher are the selector payload plus the nearby call/stack cleanup shape:
```text
No Remorse, and No Regret menu-start:
6A 01 66 68 01 00 1E 00 9A ?? ?? ?? ?? 83 C4 06
No Regret mission-start:
6A 01 66 68 01 00 1E 00 0E E8 ?? ?? 83 C4 06
```
For No Regret, the patcher now updates both hardcoded selectors so the later mission-start path cannot silently override the earlier menu-start one.
## Current Best Answer
- Fresh-game first-mission start map is determined in code.
- No Remorse hardcodes map `1`, egg `0x1e` in `Game_Start`.
- No Regret hardcodes the same values twice: once in `Game_Start`, and again in the later `FUN_1030_032d` mission-start path that actually controls a real new game.
- The debug `-warp mission` path uses an executable-embedded mission-to-map word table at `1478:0488`, plus `-mapoff`.
- External files such as `FIXED.DAT` hold the actual map contents.
- `CRUSADER.CFG` does not control which map a new game starts on.

View file

@ -0,0 +1,169 @@
# Japanese No Remorse Cheats And Launch Parameters
## Scope
This note records the current live-Ghidra read of `/ja/CRUSADER.EXE` for two questions:
1. did the hidden cheat/debug features survive in the Japanese Windows build?
2. which launch parameters still survive in executable code?
Active Ghidra target for this note: `/ja/CRUSADER.EXE`.
## Short Answer
Yes, important cheat/debug features clearly survived in the JP Win32 build.
Current strongest proof:
- `-laurie` is still parsed explicitly and still enables the broader cheat/debug gates
- the hidden `JASSICA16` sequence matcher still exists as live code
- the JP option-key cheat handler still contains the immortality on/off path with live UI messages
- the JP parser still recognizes a broad launch-option table including `-debug`, `-u`, `-setver`, `-vesatest`, `-asylum`, `-warp`, `-skill`, `-mapoff`, `-egg`, and `-demo`
The main caution from this pass is that the JP Win32 parser is not yet proven to support every old DOS-side syntax exactly as previously documented. In particular, this pass did **not** recover a JP parser branch that consumes positional X/Y/Z arguments after `-warp`; the live JP parser block currently closes only the `-warp <mission>` form.
## Main Findings
### 1. `-laurie` definitely survived
`00466774`, now named `handle_commandline_args_jp`, still special-cases `-laurie` outside the normal option table.
Recovered behavior:
- exact literal compare against `-laurie`
- sets the two broad cheat/debug gate bytes at `0x4957d8` and `0x495830`
- prints the message at `0046ea5a`: `FART ...TRY... -laurie (Have fun, Jely)`
This is strong evidence that the JP Windows build still preserves the Laurie/debug enable lane rather than removing it.
### 2. The `JASSICA16` hidden-sequence lane definitely survived
`00418454`, now named `key_check_jassica16_toggle_jp`, is a live hidden-sequence matcher.
Recovered behavior:
- consumes incoming key bytes
- compares them against a byte sequence table starting at `0x47b1c9`
- on full match, toggles the global cheat-active state
- emits the live messages:
- `Cheats are now active.`
- `Cheats are now inactive.`
Adjacent JP data still contains the literal string `JASSICA16` at `0047b1cc`.
The safest current read is:
- the hidden `JASSICA16` cheat-enable concept survived
- the JP executable still has executable code that toggles cheat state from a secret key sequence
### 3. The option-key cheat handler still contains immortality
`00415eec`, now named `key_handle_option_cheats_jp`, still contains the immortality toggle path.
Recovered behavior inside that function:
- checks cheat/debug gating before entering the relevant branch
- still reaches both live strings:
- `Immortality enabled.`
- `Immortality disabled.`
- still pushes those strings into the normal in-game message path
This is direct proof that at least one high-value keyboard cheat survived beyond the sequence matcher itself.
The same function is larger than just the immortality branch and still appears to host a broader cheat/debug action menu. This pass did not fully close every other branch in that handler.
### 4. `-u` survived, and the JP build gives it a clearer meaning than the older notes
The JP parser block at `0046688f` consumes the next argv token and copies it into the fixed buffer at `0x47be40`.
That buffer is later consumed by `00466ebc`, now named `startup_apply_u_override_if_present`.
Recovered behavior in `startup_apply_u_override_if_present`:
- checks whether `s_usecode_0047be40` is non-empty
- if so, routes startup into a replacement usecode/EUSECODE load path instead of the default one
This is the strongest current JP-side closure for `-u`:
> `-u <path-or-name>` is a live startup override for the usecode/EUSECODE load source.
That is materially better than the older unresolved `-u` note.
## Launch Parameter Table
### Definitely recovered as live JP parser behavior
| Argument | Current best JP syntax | Confidence | JP evidence |
|------|------|------|------|
| `-laurie` | `-laurie` | High | Explicit special-case in `handle_commandline_args_jp`; sets cheat/debug gates and prints the Laurie joke string. |
| `-debug` | `-debug` | High | Dedicated parser block sets debug state, writes `0x0a` to the debug level word at `0x482620`, and prints `Debugging mode ON.`. |
| `-u` | `-u <arg>` | High | Parser consumes the next token into `0x47be40`; later startup code uses that as a live usecode override path. |
| `-warp` | `-warp <mission>` | High | JP parser consumes one following numeric token, stores it, and prints `Warping to mission %d.`. |
| `-skill` | `-skill <n>` | High | JP parser consumes one following numeric token and prints `Defaulting to skill level %d`. |
| `-mapoff` | `-mapoff <delta>` | High | JP parser consumes one following numeric token and prints `Map offset = %d`. |
| `-egg` | `-egg <id>` | High | JP parser consumes one following numeric token and prints `Destination Egg = %d`. |
| `-demo` | `-demo` | High | JP parser sets the demo flag and prints `Demo mode.`. |
### Present in the JP parser table, but not fully re-closed in this pass
| Argument | Current best JP syntax | Confidence | Current best JP read |
|------|------|------|------|
| `-?` | `-?` | Medium | Still present in the JP option table. Likely some help/usage-style path, but the exact handler was not isolated in this pass. |
| `-setver` | `-setver <text>` | Medium | Still present in the JP option table. Existing non-JP notes still make `displayed version/build string override` the best read, but this exact JP handler was not re-closed here. |
| `-vesatest` | `-vesatest` | Low | Still present in the JP option table. The exact JP-side effect remains unresolved. |
| `-asylum` | `-asylum` | Low | Still present in the JP option table. The exact JP-side effect remains unresolved in this pass. |
## Important JP-Specific Caution About `-warp`
The older DOS-side command-line note documents a positional coordinate form:
- `-warp <mission> <x> <y> <z>`
This pass did **not** re-close that form in the JP Win32 parser.
What the live JP parser block at `004668e2` actually does:
- call the tokenizer once more
- parse a single numeric token
- store that value as the mission id
- print `Warping to mission %d.`
- return to normal token scanning
What this pass did **not** recover:
- a branch that consumes three extra positional numeric tokens after `-warp`
- a JP-side format string of the older `Warping to mission %d @ x:%d y:%d z:%d.` style
So the safest current JP statement is:
> `-warp <mission>` is directly proven. The positional JP Win32 coordinate form is not yet proven by this pass and should not be assumed from the older DOS-side note.
## Cheat-State Model After This Pass
The current strongest JP cheat/debug model is:
1. `-laurie` still raises the broad cheat/debug enable gates.
2. The hidden `JASSICA16` matcher still exists and can toggle cheat-active state independently.
3. The option-key handler still contains at least the immortality toggle and a wider cheat/debug action family.
That means the Japanese Windows build did not merely keep some orphaned strings. It still preserves executable cheat/debug machinery.
## Ghidra Changes Made During This Pass
Renamed and commented in the active `/ja/CRUSADER.EXE` database:
- `00466774` -> `handle_commandline_args_jp`
- `00415eec` -> `key_handle_option_cheats_jp`
- `00418454` -> `key_check_jassica16_toggle_jp`
- `00466ebc` -> `startup_apply_u_override_if_present`
Decompiler comments were added to each of those entry points to preserve the evidence in the live database.
## Open Follow-Up
1. Recover the remaining JP option-table handlers for `-?`, `-setver`, `-vesatest`, and `-asylum` explicitly rather than by table presence only.
2. Re-open the JP `-warp` lane and prove whether any positional coordinate form still exists in some later startup consumer.
3. Identify the exact key bytes in the JP `0x47b1c9` matcher table and compare them byte-for-byte against the ASCII-adjacent `JASSICA16` data.
4. Walk the rest of `key_handle_option_cheats_jp` so the non-immortality branches are named and classified.
5. Compare the JP cheat/debug global addresses directly against the English DOS build to see which lanes were ported verbatim versus re-laid for Win32.
6. Runtime-test the JP build with `-laurie`, `-debug`, `-demo`, `-warp`, and `-u` to confirm the static conclusions from the Windows binary.

View file

@ -0,0 +1,200 @@
# Japanese No Remorse Windows 9x Investigation
## Question
Investigate the claim that the Japanese build of Crusader: No Remorse supports Windows 9x and can run natively on Windows 95 instead of requiring a DOS boot path.
Active Ghidra target for this note: `/ja/CRUSADER.EXE`.
## Verdict
Current static-analysis verdict: **strongly supports the claim**.
The loaded Japanese executable is not just a DOS executable with a small helper around it. It is a native flat Win32 image with Windows API startup, Windows registry configuration, DirectDraw/DirectSound initialization, and one explicit Win9x-aware compatibility branch.
What is proven from static analysis:
- the Japanese binary is a PE-style Win32 program loaded at `00400000`, with sections such as `BEGTEXT`, `DGROUP`, `.idata`, `.reloc`, and `.rsrc`
- it creates a real top-level Windows game window and runs through Win32 startup code
- it initializes DirectDraw and DirectSound directly
- it reads and writes settings in the Windows registry
- it contains Japanese/Win9x-facing support clues, including IME handling and a `GetVersion`-based compatibility branch
What is **not** proven by this pass:
- an actual runtime launch on real Windows 95 hardware or emulation
- whether extra runtime prerequisites such as the expected DirectX version are always present on a bare Windows 95 install
- whether every shipped JP build variant behaves the same way
So the safest wording is:
> The claim that the Japanese release has a native Windows 9x execution path is strongly supported by the code. The stricter claim that it will always run on a stock Windows 95 machine without additional runtime requirements is not fully closed by static analysis alone.
## Main Evidence
### 1. The loaded JP executable is a Win32 image, not the old DOS/NE game binary
Ghidra reports the active program as `/ja/CRUSADER.EXE` with entry at `004729e0`.
Relevant image layout recovered from Ghidra:
- `Headers: 00400000 - 004003ff`
- `BEGTEXT: 00401000 - 0047afff`
- `DGROUP: 0047b000 - 00481bff`
- `.bss: 00482000 - 00495fff`
- `.idata: 00496000 - 00496dff`
- `.reloc: 00497000 - 0049e3ff`
- `.rsrc: 0049f000 - 004a03ff`
That is a normal PE/Win32 layout, not the segmented NE or DOS-extender layout used by the original DOS executable.
### 2. The import table is decisively Windows-native
Recovered imports include all the major Windows subsystems expected of a native Win9x game executable:
- process and OS: `GetVersion`, `CreateThread`, `ExitProcess`, `TlsAlloc`, `VirtualAlloc`, `GetCommandLineA`, `GetModuleHandleA`, `GetModuleFileNameA`
- window/UI: `CreateWindowExA`, `RegisterClassA`, `DefWindowProcA`, `DispatchMessageA`, `PeekMessageA`, `ShowWindow`, `RegisterWindowMessageA`, `MessageBoxA`
- graphics: `DirectDrawCreate`, `CreateBitmap`, `CreateCompatibleDC`, `CreatePalette`, `TextOutA`
- audio/timing/input: `DirectSoundCreate`, `DirectSoundEnumerateA/W`, `timeGetTime`, `joyGetDevCapsA`, `joyGetPosEx`
- configuration/storage: `RegCreateKeyExA`, `RegOpenKeyExA`, `RegQueryValueExA`, `RegSetValueExA`, `CreateFileA`, `ReadFile`, `WriteFile`
- Japanese text/input support: `IsDBCSLeadByte`, `GetCPInfo`, `WINNLSEnableIME`
This is not a DOS launcher plus one or two helper calls. It is a full Windows application stack.
### 3. The main window path is explicit and native
`00445f40` is now named `win32_create_main_window`.
Key behavior recovered from decompilation:
- registers a window class with `RegisterClassA`
- creates a fullscreen popup window with `CreateWindowExA`
- uses the title string `"Crusader: No Remorse"`
- calls `ShowWindow(..., 10)`
- calls `WINNLSEnableIME(0,0)` immediately after successful creation
- registers `"MSWHEEL_ROLLMSG"`
This is direct evidence of a native Windows UI path. The IME call is especially relevant in a Japanese build because it shows the executable is consciously managing Windows-side JP text/input behavior.
### 4. DirectDraw and DirectSound are initialized directly by the game
`004459b0` is now named `video_init_directdraw_and_directsound`.
Recovered behavior:
- calls `DirectDrawCreate()`
- sets cooperative level and display mode on the DirectDraw object
- creates the palette and primary surface
- calls `DirectSoundCreate()`
- hides the cursor with `ShowCursor(0)`
This is a classic Windows 9x era DirectX setup path. It strongly supports the idea that this build was intended to run as a native Windows game executable.
### 5. Registry-backed configuration is built in
String at `0047b178`:
- `Software\Electronic Arts\Crusader: No Remorse\J1.21`
`004138e8` is now named `config_load_registry_and_cfg`.
Recovered behavior:
- reads `installpath`, `cdpath`, and `flicpath` from `HKEY_LOCAL_MACHINE`
- reads user preferences such as `video`, `subtitles`, `limitblasts`, `animation`, `frameskip`, `musicvolume`, `soundvolume`, and `mousespeed` from `HKEY_CURRENT_USER`
- falls back to parsing `crusader.cfg`
`00413760` is now named `config_write_registry_string` and writes string values back into the same registry branch.
This is strong evidence of a normal Windows-installed configuration model rather than a DOS-only setup flow.
### 6. There is a real Win9x-specific compatibility branch
`00472c41` is now named `win32_runtime_capture_process_context`.
Recovered behavior:
- captures environment strings
- captures module filename and command line
- calls `GetVersion()`
- stores split version fields into globals at `0x47c09f`, `0x47c0a0`, and `0x47c0a1`
The more important follow-up is `00476616`, now named `win32_tls_alloc_with_win9x_guard`.
Recovered behavior:
- calls `TlsAlloc()`
- checks the saved `GetVersion()` word
- if the version word has the high `0x8000` bit set, it retries while the TLS slot is less than `3`
That is not generic Windows code. It is exactly the kind of branch you expect when a program needs to behave differently on the Win9x line, where `GetVersion()` reports the platform using the high bit and some low TLS slots are treated specially.
This is the strongest single code-level clue that the executable was designed with Win9x behavior in mind, not just NT-family Windows.
## Secondary Evidence
### Japanese-specific Windows integration
The import set and main-window path together show several JP-specific Windows integration points:
- `WINNLSEnableIME`
- `IsDBCSLeadByte`
- `GetCPInfo`
That does not by itself prove Windows 95 compatibility, but it does reinforce that this is a localized Windows build, not a DOS build being loosely wrapped.
### WinMM and joystick APIs
The import table also includes:
- `joyGetDevCapsA`
- `joyGetPosEx`
- `timeGetTime`
These are ordinary Windows multimedia/input APIs and fit the same native-executable picture.
## Interpretation
The claim can be split into two parts.
### Part A: "The Japanese version supports Windows 9x"
This is strongly supported.
The binary:
- is a Win32 image
- uses Win32 startup and window management
- uses DirectX-era Windows multimedia APIs
- uses Windows registry storage
- has a `GetVersion` branch that specifically distinguishes Win9x-style version reporting
### Part B: "It can run on Windows 95 natively without switching to DOS first"
This is also strongly supported, but with one caveat.
The executable is clearly built to run as a native Windows application, and the version/TLS branch is the best evidence that Win9x behavior was considered explicitly. The remaining uncertainty is practical, not architectural: an actual Windows 95 runtime may still need the expected DirectX/runtime environment installed.
So the architectural answer is yes; the deployment answer is very likely yes, but not fully closed until runtime-tested on a Win95 environment.
## Ghidra Changes Made During This Pass
Renamed and commented in the active `/ja/CRUSADER.EXE` database:
- `00472c41` -> `win32_runtime_capture_process_context`
- `00476616` -> `win32_tls_alloc_with_win9x_guard`
- `00445f40` -> `win32_create_main_window`
- `004459b0` -> `video_init_directdraw_and_directsound`
- `004138e8` -> `config_load_registry_and_cfg`
- `00413760` -> `config_write_registry_string`
Each of those now has a decompiler comment explaining why it matters to the Win9x/native-Windows investigation.
## Remaining Follow-Up
1. Run the JP executable under an actual Windows 95 target or a faithful Win95 VM to confirm runtime behavior.
2. Identify the expected DirectX version from installer files or runtime error strings.
3. Trace the message pump and main game loop entry after window creation for a fuller WinMain reconstruction.
4. Locate the import thunks or callsites for `GetCPInfo` and `IsDBCSLeadByte` to document the JP text path more precisely.
5. Check whether the JP installer writes the same `J1.21` registry branch or whether the executable can self-heal missing keys.
6. Compare the JP Windows binary against the English DOS binary to isolate which subsystems were ported versus shared.

171
docs/regret-game-start.md Normal file
View file

@ -0,0 +1,171 @@
# No Regret Game_Start Flow
## Scope
This note documents the currently opened live `REGRET.EXE` startup lane around `Game_Start` and the later helper that performs the actual new-game mission hop.
The immediate goals of this pass were:
- name the directly involved startup helpers in Ghidra
- annotate the startup-state globals that feed the new-game path
- explain why No Regret appears to set the start map twice
## Named Functions
The following previously-unnamed functions are now promoted in the live `REGRET.EXE` database.
| Address | New name | Why the name is justified |
|------|------|------|
| `1058:0bbe` | `HandleCommandlineArgs` | Matches the already-recovered Remorse-side parser role. It compares argv entries against a startup-option table and writes `g_warpToLevelNoArg`, the difficulty override, map offset, egg override, and the extra startup-video flag. |
| `1030:032d` | `Game_RunNewGameFlow` | This is the full No Regret new-game helper. It plays the ORIGIN/ANIM01 intro lane, opens the difficulty modal when not warping, rebuilds the world/avatar state, and then performs the actual mission-start teleporter hop or the debug `-warp` path. |
| `1030:022e` | `Game_DrawCenteredStartupSplash` | Called immediately before the teleporter hop in both the normal and `-warp` startup lanes. It fades black, clears the active gameplay state, centers one startup screen object, and redraws the viewport. |
| `11c8:0d96` | `Game_EnterFrontendMenuViewport` | Used before modal-gump dispatch. It clears the gameplay-style viewport flag, expands the mouse Y range to the taller menu height, and refreshes the screen object. |
| `11c8:0d39` | `Game_RestoreGameplayViewport` | Used after the modal/front-end phase. It restores the gameplay-style viewport state, notifies NPC update lanes, restores the shorter mouse Y range, and refreshes the screen object again. |
## Game_Start Structure
`Game_Start` in No Regret still looks like the top-level frontend entry point.
High-level shape:
1. Freeze the current gameplay/process state enough to enter a frontend/modal lane.
2. Enter the taller frontend viewport mode and dispatch the startup modal gump.
3. Handle non-new-game modal results such as exit/other menu actions.
4. For the new-game path, clear world/gameplay state, rebuild the avatar and starter items, and do an early teleporter hop to map `1`, egg `0x1e`.
5. Fade/reset again and hand back into the wider startup framework.
The key early selector is still:
- `1008:1446 PUSH 0x1`
- `1008:1448 PUSH 0x1e0001`
- `1008:144e CALLF 0x1030:0000`
That early site is real, but it is not the only startup teleporter call that matters.
## Actual New-Game Mission Start
The function now named `Game_RunNewGameFlow` is the important second lane.
It is called from at least two concrete places:
- `CreditsProcess_Run` at `1008:0056`
- another startup/control lane at `1058:0a2a`
That already shows it is not dead duplicate code. It is a standalone startup helper reused by the frontend.
Its normal no-warp branch does all of the following in one place:
1. Play the ORIGIN/ANIM01 intro-video lane.
2. Open the difficulty-selection modal.
3. Reinitialize NPC/world/tracker state.
4. Apply any command-line difficulty override.
5. Recreate starter inventory items.
6. Draw the centered startup splash.
7. Perform the actual mission-start teleporter hop.
The effective mission-start selector is:
- `1030:05c3 PUSH 0x1`
- `1030:05c5 PUSH 0x1e0001`
- `1030:05cb PUSH CS`
- `1030:05cc CALL 0x1030:0000`
This is the selector that redirected real new games only after the script was updated to patch it together with the earlier `Game_Start` site.
## Startup Globals
These data roles are now annotated in the live Ghidra session.
| Address | Current best role |
|------|------|
| `1480:1453` | Startup-world transition flag. `Game_Start` clears it before the early menu-start teleporter hop. `Game_RunNewGameFlow` sets it during world/NPC rebuild and clears it again immediately before the actual mission-start teleporter hop. |
| `1480:0ac8` | Optional command-line warp X override. `0xffff` means no direct coordinate warp is requested. |
| `1480:0aca` | Optional command-line warp Y override. |
| `1480:0acc` | Optional command-line warp Z override. |
| `1480:0ace` | Command-line difficulty override parsed by `HandleCommandlineArgs`. If greater than zero, the new-game flow copies it into `g_difficultyLevel` before the teleporter hop. |
| `1480:0ad0` | Command-line `-mapoff` additive map offset used by the debug `-warp` path. |
| `1480:0ad2` | Command-line egg override used by the `-warp` path. When negative or `0x001e`, the code falls back to the default startup egg. |
| `1480:0ad4` | Extra startup-video behavior flag parsed by `HandleCommandlineArgs`. Clear means the ORIGIN/ANIM01 lane is played once; set means the same lane is repeated until `1480:95f0` becomes nonzero. |
| `1480:95f0` | Startup-video loop exit latch used by `Game_RunNewGameFlow` and `CreditsProcess_Run`. |
## Command-Line Warp Controls
The startup parser exposes more real gameplay-side command-line controls than just `-warp`.
Currently proven from the recovered parser strings and control flow:
- `-warp`
- `-skill`
- `-mapoff`
- `-egg`
- `-debug`
- `-demo`
- `-setver`
- `-asylum`
The same executables also contain the runtime status strings:
- `Warping to mission %d.`
- `Warping to mission %d @ x:%d y:%d z:%d.`
- `Defaulting to skill level %d`
- `Destination Egg = %d`
That parser behavior is now tight enough to promote the actual syntax:
- `-warp <mission>` = mission-only manual warp
- `-warp <mission> <x> <y> <z>` = direct coordinate warp
The key parser detail is that there are no separately recovered `-x`, `-y`, or `-z` switches in this lane. After `HandleCommandlineArgs` parses the mission number, it checks the next argv token. If the next token is missing or begins with `-`, the parser takes the mission-only path and prints `Warping to mission %d.`. Otherwise it consumes the next three argv tokens as X, Y, and Z and prints `Warping to mission %d @ x:%d y:%d z:%d.`.
There is one important precedence rule in `Game_RunNewGameFlow`: `-egg` wins over the coordinate override path. When X is present but the egg override is nonnegative, the code still routes back into the egg-based teleporter lane. The direct `NPC_Teleport` path only runs when X/Y/Z are present and the egg override is still negative.
## Could Parameters Replace The Executable Hack?
For a normal fresh game, no.
The executable patch was needed because the stock new-game path still hardcodes the startup selector payload `(map = 1, egg = 0x1e)` in code. No Regret does that twice, once in `Game_Start` and once again in `Game_RunNewGameFlow`.
The command-line warp lane is different:
- it only runs when `g_warpToLevelNoArg != -1`
- it uses a debug/manual warp flow rather than the ordinary fresh-game path
- it computes the target map from an embedded mission-to-map table plus `-mapoff`
- it can also change the destination egg
- it can apply explicit X/Y/Z destination overrides through `-warp <mission> <x> <y> <z>`
So a command line could likely have reproduced the practical destination change for a manual debug warp session, but not the ordinary fresh-game mission-1 behavior that the patch script changed.
For maps with no usable teleport egg, the parameter-only workaround is now clearer too: use `-warp <mission> <x> <y> <z>` together with `-mapoff`, and do not pass `-egg`. That path bypasses the egg lookup and calls `NPC_Teleport` directly. The current inspected teleport lane does not show any automatic `snap-to-ground` rescue, so this only works if the supplied coordinate triple is already valid for the target map.
Current best distinction:
- executable patch = changes the stock fresh-game startup behavior
- command-line warp args = invoke a separate manual/debug startup path that can land on a different mission/map/egg and likely a different coordinate triple
## Likely Reason Map 1 Is Set Twice
The most likely explanation is that No Regret preserved two valid entry paths into an in-engine gameplay state instead of refactoring them into one shared startup selector.
Evidence for that read:
- `Game_Start` contains an early teleporter hop immediately after rebuilding the avatar and starter items.
- `Game_RunNewGameFlow` is a separate, live helper called from `CreditsProcess_Run` and another startup/control lane.
- `Game_RunNewGameFlow` owns the full intro-video, difficulty-modal, world-reset, and real mission-start sequence.
- Both paths call the same local teleporter wrapper with the same literal `(1, 0x1e, 1)` payload.
Current best model:
- the early `Game_Start` hop is a frontend/bootstrap entry into a playable engine state
- the later `Game_RunNewGameFlow` hop is the authoritative mission-start step for a real new game after the intro/difficulty flow
- both kept their own literal selector instead of sharing one startup-map global or helper constant
So the duplication looks more like split control-flow ownership and startup code reuse than a deliberate “set it twice just in case” pattern.
## Practical Patch Implication
For No Regret, changing the fresh-game destination map must update both hardcoded selectors:
- early `Game_Start` selector at `1008:1448`
- actual mission-start selector at `1030:05c5`
Patching only the early site is insufficient because the later new-game helper can silently overwrite that choice during the real mission-start sequence.

View file

@ -104,6 +104,32 @@ That does not prove every shape-id use of `0x04D0` is literally a monster actor
This is the strongest reason not to read `ALARMHAT` as just a decorative siren hat sprite. The script is actively scanning for nearby `0x04D0` objects and equipping them. This is the strongest reason not to read `ALARMHAT` as just a decorative siren hat sprite. The script is actively scanning for nearby `0x04D0` objects and equipping them.
## Follow-up: what controls immediate spawn versus waiting
The nearby `ALARMHAT` logic is only one half of the `0x04D0` story. The matching `MONSTER` class now adds one verified activation rule:
- `MONSTER::enterFastArea` only checks `0x04D0` objects when `Item.getFrame(arg_06) == 0`
- inside that frame-0 lane it reads `Item.getMapNum(arg_06)` and only auto-runs `MONSTER.equip(pid, 0, arg_06)` when `(mapNum & 0x08) == 0`
Current safest read:
- `frame 0` = the only state that participates in the automatic `enterFastArea` spawn lane
- `mapNum bit 0x08` = suppresses that automatic lane without changing DTABLE row selection
- `frame 1` = skips `enterFastArea` entirely and is therefore more likely to be used in paired or externally signaled setups
That fits the recurring authored pairs already visible in exported map data:
- Remorse map 246: frame-0 `item:162` uses `mapNum = 8`, `npcNum = 0`, while colocated frame-1 `item:163` uses `mapNum = 1`, `npcNum = 8`
- Remorse map 9: frame-0 `item:338` uses `mapNum = 0`, `npcNum = 0`, while colocated frame-1 `item:339` uses `mapNum = 7`, `npcNum = 2`
This follow-up also narrows one tempting overread: `quality` low byte is not the primary `spawn now vs wait` control in `MONSTER::enterFastArea`. The current Regret `ALARMHAT` body does still compare nearby `0x04D0` `Item.getQLo(...)` values against difficulty lanes `0/1/2`, so low quality remains relevant as a secondary filter, just not as the automatic-enter-area gate.
The wider exported corpus now supports keeping that claim local but real. Additional `0x04D0`-adjacent bodies in `ITEM.slot_2D`, `FUSPAC.slot_01`, and `MISS8.slot_20` also scan nearby `0x04D0` objects and branch on frame and/or `Item.getQLo(...)`. Current safest synthesis is therefore:
- `frame` plus `mapNum bit 0x08` controls the automatic `MONSTER.enterFastArea` lane
- `quality` low byte is still a real local signal key used by several authored trigger/alarm/helper families
- but that low byte is not yet proven to be a single universal direct-link field across every `0x04D0` interaction
## Comparison to the rest of the alarm family ## Comparison to the rest of the alarm family
`ALARMHAT` fits the broader alarm family, but it is not identical to the other alarm classes. `ALARMHAT` fits the broader alarm family, but it is not identical to the other alarm classes.

View file

@ -0,0 +1,325 @@
# Retail `-u` USECODE Startup Override
## Question
If retail non-Japanese `CRUSADER.EXE` is started with `-u <arg>`, does that add extra usecode on top of the stock runtime, or does it replace the stock usecode root? And if it really is a replacement path, what parts of the game does that replacement actually control?
Active analysis target for the binary side was live `CRUSADER.EXE` in the Ghidra session.
## Short Answer
Current best answer:
- `-u` is a real retail startup override, not dead parser residue.
- In retail `CRUSADER.EXE`, it behaves like a **replacement** of the live usecode runtime root, not an additive overlay.
- The replacement root is then used by the normal usecode event/process/interpreter machinery.
So the practical meaning is:
> `-u <arg>` points startup at an alternate usecode/EUSECODE source, and subsequent scripted behavior runs against that replacement runtime instead of the stock one.
The strongest remaining open questions are now the precise Filespec path rules for `<arg>` and the still-unclosed live-NE stock bootstrap path that seeds the same runtime when `-u` is absent.
## What `<arg>` Currently Looks Like
The token-shape question is no longer completely open.
Current best retail read from the live `1420:0cdf` helper is:
- the parser still copies the raw argv token into `1478:065a`
- the helper does **not** pass that token straight to the loader as the final filename
- instead it loads a far pointer from `1478:06d6/06d8` to the mutable string at `1478:07a0`, which is `eusecode.flx`
- it forces the first byte of that filename template to `'e'`
- it then calls `Filespec_GetFullPath(0, s_usecode, "eusecode.flx", 0)`
- it probes that constructed full path with `File_Exists`
- if the path exists, it rebuilds the same full path again and passes that result into the runtime loader/constructor path
That makes the safest current interpretation:
- `<arg>` is a Filespec **path-like** argument that supplies the directory/resource-root component
- the filename component is still the fixed retail archive name `eusecode.flx`
So the current evidence argues **against** these stronger interpretations:
- arbitrary full filename override
- free-form alternate archive basename
- direct one-off script/body filename
If the user passes a literal filename as `<arg>`, the recovered helper shape suggests the game would still try to append the fixed filename template rather than use the supplied token as the final filename.
The remaining uncertainty is narrower now:
- whether the path component can be only a filesystem directory
- whether it can also be a loader alias/resource root that `Filespec_GetFullPath` understands
- whether relative, absolute, and CD-backed forms are all accepted equally
External engine code reinforces the fixed-filename part of this read. Both Pentagram and ScummVM build the default Crusader main usecode path as `usecode/` + language letter + `usecode.flx`, and for Remorse/Regret the language-usecode letter is `'e'`, which matches the retail helper's forced `eusecode.flx` template.
## Why This Looks Like Replacement, Not Addition
### 1. There is one live runtime root, not a list of roots
The key global pair is:
- `1478:6611/6613`
That far pointer is read directly by all of the main recovered usecode-side consumers, including:
- `Usecode_ItemCallEvent` at `1420:0e3a`
- `UsecodeProcess_CreateProcess` at `1420:0f20`
- `Interpreter_NextUsecodeOp` at `1418:332c`
- `Item_GetDamaged` at `10a0:277b`
No parallel second root pointer, linked list, or merge walk was recovered in this pass.
That by itself already makes additive-overlay behavior unlikely.
### 2. The `-u` startup helper overwrites that single root pointer
The retail parser case at `1048:0a46` copies the following argv token into `1478:065a`.
Startup later calls `startup_apply_u_override_if_present` at `1420:0cdf` from `1048:05d3`.
Inside `startup_apply_u_override_if_present`:
- if `1478:065a` is empty, it returns without changing anything
- if `1478:065a` is non-empty, it resolves/loads an alternate source and then writes the resulting far pointer to `1478:6611/6613`
- it sets `1478:6615 = 1`
- it rebuilds the cumulative slot-base words at `1478:8c7c..8c82`
The important behavior is the direct assignment:
- `1420:0d49` writes `DX -> 1478:6613`
- `1420:0d4d` writes `AX -> 1478:6611`
That is a swap of the live root pointer, not registration of a side table.
### 3. The helper rebuilds the slot-base state for the new root
After the root overwrite, `startup_apply_u_override_if_present` immediately recomputes the cumulative slot-base words:
- `1478:8c7c`
- `1478:8c7e`
- `1478:8c80`
- `1478:8c82`
Those are the same runtime bases later used by the slot-index/category mapping and owner-row lookups.
That makes the replacement behavior even stronger:
- the code is not merely keeping an alternate archive pointer around
- it is re-deriving the runtime indexing state that downstream usecode dispatch relies on
### 4. No merge step was recovered
This pass did **not** recover any logic that:
- appends records from the `-u` source onto the stock root
- patches individual classes/events into an existing live root
- or checks two roots in order during normal dispatch
Instead, the recovered shape is:
1. default startup path establishes the ordinary runtime state
2. `startup_apply_u_override_if_present` runs once during startup
3. if `-u` was supplied, it replaces the root pointer and rebuilds the slot bases
4. later gameplay/usecode consumers all read the same replacement root
The stock runtime may still remain allocated in memory if it was created earlier, but the current evidence says it is no longer the one normal dispatch uses. That is replacement semantics, not additive semantics.
## Best Current Read Of The Stock Path
The default non-`-u` bootstrap is only partially closed in the live NE database.
What is directly closed in live `CRUSADER.EXE`:
- `Init_Everything` calls `startup_apply_u_override_if_present` at `1048:05d3`
- `1478:6611/6613` starts as zero in the loaded image
- the only currently recovered **explicit** write to `1478:6611/6613` in the live NE session is the `-u` override helper itself
That means the stock bootstrap still has an unresolved step in the live NE target.
The best current cross-reference evidence for that missing default path comes from the verified raw-side VM note in [docs/raw-000a-000d.md](docs/raw-000a-000d.md):
- `entity_vm_runtime_init_from_path_if_configured` (`000d:44df`) builds the external path and creates the runtime
- `entity_vm_runtime_create` (`000d:4c99`) creates the runtime object
- `entity_vm_runtime_owner_resource_create` (`000d:7000`) allocates and fills the owner/resource table used later by slot/context creation
- that same raw-side init path also seeds the cumulative category bases at `0x8c7c/0x8c7e/0x8c80/0x8c82` from the four counts at `0x6608..0x660e`
That raw-side bootstrap is important because it matches the same state that the live retail `-u` helper rebuilds after replacement:
- same four category counts at `1478:6608..660e`
- same cumulative bases at `1478:8c7c..8c82`
- same downstream owner-row and slot-index consumers already recovered in the live NE session
So the safest current combined read is:
- the normal game also has a stock runtime bootstrap in the same VM family
- the raw-side path is strongly evidenced as that bootstrap
- but the exact live-NE caller that seeds `1478:6611/6613` before gameplay without `-u` is still not directly closed
That distinction matters for documentation quality. We can now say the stock path is strongly cross-referenced, but we should not yet claim a fully recovered live-NE writer when the current explicit xrefs do not show it.
## Replacement Root Layout So Far
The direct constructor/loader pair behind the retail `-u` path is now materially clearer in the live NE session.
### 1. `entity_vm_runtime_create` at `1420:1499`
This function is the direct constructor called by `startup_apply_u_override_if_present` after the alternate `eusecode.flx` path is resolved.
Current best read:
- allocates `0x1319` bytes when no existing object is supplied
- clears the first `0x1300` bytes through `1420:1536`
- stores several tail defaults in the `0x1300..0x1314` range
- calls `entity_vm_runtime_owner_resource_create` with the resolved path
- stores the returned far pointer at `+0x1315/+0x1317`
The zeroed first `0x1300` bytes are especially suggestive:
- `1420:1575` later walks that area as `0x80` entries of stride `0x26`
- for each entry it frees two far-pointer pairs at offsets `+0x1e/+0x20` and `+0x22/+0x24`
So the safest current shape is:
- runtime object header/tail metadata at `0x1300..0x1318`
- leading table of `0x80` slot/runtime records occupying `0x0000..0x12ff`
### 2. `entity_vm_runtime_owner_resource_create` at `1430:0000`
This helper is now named in the live NE session because its behavior matches the already-verified raw-side owner/resource loader closely.
Current best read:
- allocates a compact `0x14`-byte helper object
- initializes it as a file-backed loader object
- opens the resolved external `eusecode.flx` path through its vtable-backed file path
- queries an entry count through vtable `+0x04`
- allocates a backing buffer and stores its far pointer at `+0x10/+0x12`
- materializes indexed owner/resource records through vtable `+0x0c`
That aligns well with the raw-side note that the owner/resource loader manages indexed file-set materialization rather than directly exposing simple class-name lookups.
### 3. Why This Matters For `-u`
This constructor/loader pair gives the override note a more concrete partial object model:
- `-u` is not just swapping in a raw file handle or archive pointer
- it is constructing a full VM runtime object
- that object owns both a slot/runtime table region and an attached owner/resource helper
- the attached helper is what materializes the indexed owner-loaded data later consumed by event/process/interpreter paths
That makes the replacement semantics stronger in practice. The override is swapping the live VM runtime object graph, not merely redirecting one filename global.
## What The Replacement Root Actually Controls
### 1. Item/event dispatch
`Usecode_ItemCallEvent` at `1420:0e3a` reads `1478:6611`, follows the live runtime tables, checks the owner-row capability bits, and creates the event/process context from that data.
That means the `-u` override reaches normal item-event lanes such as the surviving Crusader event families already documented elsewhere:
- `use`
- `look`
- `hit`
- `gotHit`
- `equip`
- `unequip`
- `schedule`
- `anim`
- and the wider event-slot family behind class-specific behavior
### 2. Usecode process creation
`UsecodeProcess_CreateProcess` at `1420:0f20` also reads `1478:6611` and builds process/context state from the owner tables hanging off the live root.
That means the override is not limited to one-shot event callbacks. It reaches process-backed scripted behavior too.
### 3. Interpreter bytecode stepping
`Interpreter_NextUsecodeOp` at `1418:332c` pushes `1478:6611/6613` into its runtime helper path.
That is the strongest scope clue in the whole pass. The replacement is not just changing metadata or an event name table. It is feeding the same runtime used when the interpreter advances usecode bytecode.
### 4. Gameplay-side scripted capability checks
`Item_GetDamaged` at `10a0:277b` also reads the same live root and tests owner-row bit `0x40` before falling through to later behavior.
So the replacement root can affect gameplay-side scripted capability checks as well, not only the hidden/debugger-facing VM side.
## Practical Meaning For Custom USECODE
If we can construct a compatible replacement archive/source for the `-u` path, the current evidence says it should be able to influence the same kinds of scripted behavior the stock USECODE runtime already controls.
Current safest statement:
- a custom `-u` source should be able to replace class/event bodies that the retail interpreter and process/event creators consume
- that makes it relevant to experimentation with event handlers, scripted item behavior, process-backed behaviors, and other VM-driven logic already present in Crusader's usecode system
Current important limit:
- this does **not** imply arbitrary native-code injection
- the replacement still has to fit the existing Crusader usecode VM/runtime contracts
- it is constrained by the interpreter, intrinsic tables, class layout, event table layout, and slot/body decoding the retail game already knows how to execute
So the safe model is:
> `-u` gives us a path to run **custom data for the existing usecode VM**, not a generic plug-in system for arbitrary executable code.
## What A Replacement Probably Needs To Contain
Because the recovered runtime uses a single root plus one rebuilt slot-base family, the safest current expectation is that the `-u` target is a **complete replacement source** that the loader can resolve into a full owner/class runtime.
This pass did **not** recover evidence for a sparse overlay format such as:
- `only one class`
- `only one event body`
- `patch this one slot onto the stock archive`
That does not prove such a sparse mode is impossible, but it means we should not assume it.
For current tooling planning, the defensive assumption should be:
- prepare a structurally complete replacement source that satisfies the loader's normal expectations
- do not assume the game will merge one partial class file into the stock runtime for us
## Best Current Usefulness For Decompilation Work
This path is interesting for the current usecode-decompilation lane for two reasons.
### 1. It is a real in-engine execution route
If we can identify the accepted `-u` source format and feed it a compatible replacement archive, we get a retail-supported way to make the original game execute alternate usecode data.
That is far more valuable than a purely offline parser/export loop.
### 2. It can become a runtime validation path for reconstructed bodies
The current parser/export work is building toward editable and eventually round-trippable usecode.
If `-u` accepts a full replacement source, then a future recompiler/exporter could use it to answer questions like:
- does a reconstructed class/event body still execute in the retail engine?
- which opcodes/intrinsic signatures are mandatory for a valid replacement archive?
- which behavior changes are script-level and which still depend on native code?
That makes `-u` potentially useful as a **runtime validation hook** for the ongoing decompilation toolchain.
## Current Open Questions
1. Which exact Filespec forms does `<arg>` accept in practice: relative directory, absolute directory, resource alias, CD-rooted path, or more than one?
2. Which live-NE startup or game-start function establishes the stock `1478:6611/6613` root when `-u` is not used?
3. What exact source/container format does the `-u` loader expect on disk or in resources beyond the filename family `eusecode.flx`, and how does that map onto the `0x14`-byte owner-resource helper plus the `0x1319`-byte runtime object?
4. Does the replacement need to be a full archive, or can the loader tolerate a narrower-but-still-valid subset?
5. Are there any startup-visible sanity checks that reject malformed but syntactically valid usecode containers?
6. What is the smallest safe experimental replacement that can prove the runtime path end-to-end without destabilizing unrelated startup systems?
## Recommended Next Steps
1. Close the remaining Filespec semantics for `s_usecode`: confirm which directory/alias forms `Filespec_GetFullPath` accepts in this call shape.
2. Identify the live-NE stock-runtime initialization site that seeds `1478:6611/6613` when `-u` is absent.
3. Continue walking `entity_vm_runtime_create` / `entity_vm_runtime_owner_resource_create` so more of the `0x26`-stride slot records and the owner/helper object fields are named concretely.
4. Compare the resolved runtime root against known EUSECODE/USECODE container structure to decide whether `-u` expects a full FLEX-style archive or another packaging layer.
5. Design the smallest conservative replacement experiment, ideally one structurally complete archive with one intentionally changed low-risk class/event body.
6. Tie that future experiment back to the current parser/export pipeline so reconstructed bodies can eventually be runtime-tested through `-u`.

View file

@ -322,7 +322,7 @@
]> ]>
<PROGRAM NAME="CRUSADER.EXE" EXE_PATH="/Users/stauff/dosbox/crusader/CRUSADER.EXE" EXE_FORMAT="New Executable (NE)" IMAGE_BASE="0000:0000"> <PROGRAM NAME="CRUSADER.EXE" EXE_PATH="/Users/stauff/dosbox/crusader/CRUSADER.EXE" EXE_FORMAT="New Executable (NE)" IMAGE_BASE="0000:0000">
<INFO_SOURCE USER="Maddo" TOOL="Ghidra 12.0.4" TIMESTAMP="Wed Mar 25 23:08:22 CET 2026" /> <INFO_SOURCE USER="Maddo" TOOL="Ghidra 12.0.4" TIMESTAMP="Sat Mar 28 14:17:48 CET 2026" />
<PROCESSOR NAME="x86" LANGUAGE_PROVIDER="x86:LE:16:Protected Mode:default" ENDIAN="little" /> <PROCESSOR NAME="x86" LANGUAGE_PROVIDER="x86:LE:16:Protected Mode:default" ENDIAN="little" />
<DATATYPES> <DATATYPES>
<FUNCTION_DEF NAME="dt_bbae6b42" NAMESPACE="/auto_proto"> <FUNCTION_DEF NAME="dt_bbae6b42" NAMESPACE="/auto_proto">
@ -7937,7 +7937,11 @@ Flags: 0d00
<COMMENT ADDRESS="1020:0055" TYPE="pre">play the intro movies</COMMENT> <COMMENT ADDRESS="1020:0055" TYPE="pre">play the intro movies</COMMENT>
<COMMENT ADDRESS="1020:0199" TYPE="pre">shape 0x4d4 = data link</COMMENT> <COMMENT ADDRESS="1020:0199" TYPE="pre">shape 0x4d4 = data link</COMMENT>
<COMMENT ADDRESS="1020:01f6" TYPE="pre">shape 0x598 = some weird smiley face thing?</COMMENT> <COMMENT ADDRESS="1020:01f6" TYPE="pre">shape 0x598 = some weird smiley face thing?</COMMENT>
<COMMENT ADDRESS="1020:0241" TYPE="end-of-line">Fresh-game startup path: pushes hardcoded map 1 and egg 0x1e before Teleporter_CreateProcessDirect. This is the normal new-game start selector, not a config-driven mission map lookup.</COMMENT>
<COMMENT ADDRESS="1020:0241" TYPE="pre">Fresh-game startup path hardcodes Teleporter_CreateProcessDirect(1, 0x1e, 1), so a normal new game starts on map 1 at teleport egg 0x1e.</COMMENT>
<COMMENT ADDRESS="1020:0249" TYPE="pre">start on level 1, starting egg (0x1e)</COMMENT> <COMMENT ADDRESS="1020:0249" TYPE="pre">start on level 1, starting egg (0x1e)</COMMENT>
<COMMENT ADDRESS="1020:0254" TYPE="end-of-line">Debug -warp path: mission number from g_warpToLevelNoArg indexes g_warpMissionToMapTable, then g_mapoffArgValue is added to produce the target map.</COMMENT>
<COMMENT ADDRESS="1020:0254" TYPE="pre">Manual warp path uses the embedded mission-to-map table at 1478:0488 (now g_warpMissionToMapTable) plus -mapoff; this table is separate from the normal fresh-game hardcoded start.</COMMENT>
<COMMENT ADDRESS="1020:02aa" TYPE="pre">warp x/y/z not set</COMMENT> <COMMENT ADDRESS="1020:02aa" TYPE="pre">warp x/y/z not set</COMMENT>
<COMMENT ADDRESS="1020:04da" TYPE="pre">script to find shape = 0x476 (1142) &quot;item&quot; editor item</COMMENT> <COMMENT ADDRESS="1020:04da" TYPE="pre">script to find shape = 0x476 (1142) &quot;item&quot; editor item</COMMENT>
<COMMENT ADDRESS="1020:0586" TYPE="pre">script to find shape = 0x476 (1142) &quot;item&quot; editor item</COMMENT> <COMMENT ADDRESS="1020:0586" TYPE="pre">script to find shape = 0x476 (1142) &quot;item&quot; editor item</COMMENT>
@ -8157,6 +8161,8 @@ Flags: 0d00
<COMMENT ADDRESS="1090:014c" TYPE="pre">fn ptr = 0x14780f3a == teleporter process functions</COMMENT> <COMMENT ADDRESS="1090:014c" TYPE="pre">fn ptr = 0x14780f3a == teleporter process functions</COMMENT>
<COMMENT ADDRESS="1090:01c9" TYPE="pre">fn ptr = 0x14780f3a == teleporterProcess</COMMENT> <COMMENT ADDRESS="1090:01c9" TYPE="pre">fn ptr = 0x14780f3a == teleporterProcess</COMMENT>
<COMMENT ADDRESS="1090:0223" TYPE="pre">have the new process wait for this one</COMMENT> <COMMENT ADDRESS="1090:0223" TYPE="pre">have the new process wait for this one</COMMENT>
<COMMENT ADDRESS="1090:022d" TYPE="end-of-line">TeleporterProcess_Run consumes TeleporterProcess.mapno as a real target map array: if it differs from g_currentMapArray, the avatar/camera is moved onto that map before the target teleport egg is searched.</COMMENT>
<COMMENT ADDRESS="1090:022d" TYPE="pre">mapno is a real map-array selector here, not just a mission label. Cross-map startup/warp logic uses it before egg search.</COMMENT>
<COMMENT ADDRESS="1090:0312" TYPE="pre">find teleport egg (family 8) with frame 1 and q 0xff and (PARAM).</COMMENT> <COMMENT ADDRESS="1090:0312" TYPE="pre">find teleport egg (family 8) with frame 1 and q 0xff and (PARAM).</COMMENT>
<COMMENT ADDRESS="1090:04ce" TYPE="plate">intrinsic 079</COMMENT> <COMMENT ADDRESS="1090:04ce" TYPE="plate">intrinsic 079</COMMENT>
<COMMENT ADDRESS="1090:04f7" TYPE="plate">intrinsic 096</COMMENT> <COMMENT ADDRESS="1090:04f7" TYPE="plate">intrinsic 096</COMMENT>
@ -8304,6 +8310,8 @@ Flags: 0d00
<COMMENT ADDRESS="10a8:07f3" TYPE="pre">item is contained</COMMENT> <COMMENT ADDRESS="10a8:07f3" TYPE="pre">item is contained</COMMENT>
<COMMENT ADDRESS="10a8:0842" TYPE="pre">check FLG_EQUIPPED</COMMENT> <COMMENT ADDRESS="10a8:0842" TYPE="pre">check FLG_EQUIPPED</COMMENT>
<COMMENT ADDRESS="10a8:0ba8" TYPE="pre">saves all the stuff into the file</COMMENT> <COMMENT ADDRESS="10a8:0ba8" TYPE="pre">saves all the stuff into the file</COMMENT>
<COMMENT ADDRESS="10a8:163a" TYPE="end-of-line">ItemCache_InitAndLoadFixedDat loads external fixed map content (FixedDat_LoadData plus optional static\\fixed.dat patch layer) and resets g_currentMapArray. Map contents are external even though the fresh-game start map is hardcoded in Game_Start.</COMMENT>
<COMMENT ADDRESS="10a8:163a" TYPE="pre">External map/resource loader anchor: fixed.dat/static\\fixed.dat provide map contents, while Game_Start chooses the fresh-game start map in code.</COMMENT>
<COMMENT ADDRESS="10a8:16e8" TYPE="pre">zero out the blocks</COMMENT> <COMMENT ADDRESS="10a8:16e8" TYPE="pre">zero out the blocks</COMMENT>
<COMMENT ADDRESS="10a8:1731" TYPE="pre">z = 0xf3 (free)</COMMENT> <COMMENT ADDRESS="10a8:1731" TYPE="pre">z = 0xf3 (free)</COMMENT>
<COMMENT ADDRESS="10a8:1743" TYPE="pre">510 is the &quot;bad newgumpid&quot; ?</COMMENT> <COMMENT ADDRESS="10a8:1743" TYPE="pre">510 is the &quot;bad newgumpid&quot; ?</COMMENT>
@ -8683,6 +8691,8 @@ Flags: 0d00
<COMMENT ADDRESS="1128:0ecf" TYPE="pre">male &quot;ugh!&quot;</COMMENT> <COMMENT ADDRESS="1128:0ecf" TYPE="pre">male &quot;ugh!&quot;</COMMENT>
<COMMENT ADDRESS="1128:0fab" TYPE="pre">0x259 == attack process</COMMENT> <COMMENT ADDRESS="1128:0fab" TYPE="pre">0x259 == attack process</COMMENT>
<COMMENT ADDRESS="1128:0fc5" TYPE="pre">probably &quot;sleep for random time&quot;</COMMENT> <COMMENT ADDRESS="1128:0fc5" TYPE="pre">probably &quot;sleep for random time&quot;</COMMENT>
<COMMENT ADDRESS="1128:0ff0" TYPE="end-of-line">Second direct 10a0:275f caller family: Item_ReceiveHit&apos;s non-NPC path passes hitter=0x4000 plus packed damage=(damagetype&lt;&lt;8)</COMMENT>
<COMMENT ADDRESS="1128:0ff0" TYPE="pre">Second direct 10a0:275f caller family: Item_ReceiveHit&apos;s non-NPC path passes hitter=0x4000 plus packed damage=(damagetype&lt;&lt;8)</COMMENT>
<COMMENT ADDRESS="1128:11da" TYPE="plate">Intrinsic 01E <COMMENT ADDRESS="1128:11da" TYPE="plate">Intrinsic 01E
This is either called from usecode or from an animation (via NPC_FireWeapon)</COMMENT> This is either called from usecode or from an animation (via NPC_FireWeapon)</COMMENT>
@ -8794,6 +8804,8 @@ Flags: 0d00
<COMMENT ADDRESS="1138:0c65" TYPE="pre">limit to 0x18</COMMENT> <COMMENT ADDRESS="1138:0c65" TYPE="pre">limit to 0x18</COMMENT>
<COMMENT ADDRESS="1138:0ee8" TYPE="plate">x,y,z inputs in Run are nowpt, pt3</COMMENT> <COMMENT ADDRESS="1138:0ee8" TYPE="plate">x,y,z inputs in Run are nowpt, pt3</COMMENT>
<COMMENT ADDRESS="1138:1074" TYPE="pre">sprite 0x426 == 1062, a little sparkly explosion</COMMENT> <COMMENT ADDRESS="1138:1074" TYPE="pre">sprite 0x426 == 1062, a little sparkly explosion</COMMENT>
<COMMENT ADDRESS="1138:1384" TYPE="end-of-line">Third direct 10a0:275f caller family: SuperSprite_HitAndFinish&apos;s non-NPC collision lane probes Item_GetDamaged with hitter=0x4000 and packed damage=(firetype&lt;&lt;8)</COMMENT>
<COMMENT ADDRESS="1138:1384" TYPE="pre">Third direct 10a0:275f caller family: SuperSprite_HitAndFinish&apos;s non-NPC collision lane probes Item_GetDamaged with hitter=0x4000 and packed damage=(firetype&lt;&lt;8)</COMMENT>
<COMMENT ADDRESS="1138:1565" TYPE="pre">terminate if we&apos;re not in the fast area</COMMENT> <COMMENT ADDRESS="1138:1565" TYPE="pre">terminate if we&apos;re not in the fast area</COMMENT>
<COMMENT ADDRESS="1138:17ba" TYPE="pre">new x value</COMMENT> <COMMENT ADDRESS="1138:17ba" TYPE="pre">new x value</COMMENT>
<COMMENT ADDRESS="1138:17d6" TYPE="pre">new y value</COMMENT> <COMMENT ADDRESS="1138:17d6" TYPE="pre">new y value</COMMENT>
@ -10288,6 +10300,8 @@ Flags: 0d01
LoadOnCall LoadOnCall
Impure (Non-shareable) Impure (Non-shareable)
</COMMENT> </COMMENT>
<COMMENT ADDRESS="1478:0488" TYPE="end-of-line">Embedded mission-to-map lookup table used by the debug -warp path in Game_Start. Current recovered word entries: 0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,40.</COMMENT>
<COMMENT ADDRESS="1478:0488" TYPE="pre">g_warpMissionToMapTable: executable-side mission-to-map word table used only by the -warp startup path, not by the normal fresh-game start.</COMMENT>
<COMMENT ADDRESS="1478:0542" TYPE="pre">g_fadeProcess3FnPtr</COMMENT> <COMMENT ADDRESS="1478:0542" TYPE="pre">g_fadeProcess3FnPtr</COMMENT>
<COMMENT ADDRESS="1478:06ce" TYPE="pre">g_gumpsFlxFilenamePtr</COMMENT> <COMMENT ADDRESS="1478:06ce" TYPE="pre">g_gumpsFlxFilenamePtr</COMMENT>
<COMMENT ADDRESS="1478:0782" TYPE="pre">s_music.flx</COMMENT> <COMMENT ADDRESS="1478:0782" TYPE="pre">s_music.flx</COMMENT>

View file

@ -1,7 +1,13 @@
param( 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, [string]$Choice,
[ValidateRange(0, 65535)]
[int]$FirstMissionMap,
[ValidateRange(0, 65535)]
[Nullable[int]]$FirstMissionEgg,
[string]$ExePath = $( [string]$ExePath = $(
if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'CRUSADER.EXE')) { if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'CRUSADER.EXE')) {
Join-Path $PSScriptRoot 'CRUSADER.EXE' Join-Path $PSScriptRoot 'CRUSADER.EXE'
@ -258,6 +264,13 @@ $sites = @{
@('6A', '00', 'FF', '76', '08', 'FF', '76', '06') @('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]@{ $candidateProfiles = [ordered]@{
@ -346,10 +359,10 @@ function Set-ConfiguredCandidateProfile {
throw "Unknown candidate profile '$ProfileKey'." throw "Unknown candidate profile '$ProfileKey'."
} }
$profile = $candidateProfiles[$ProfileKey] $candidateProfile = $candidateProfiles[$ProfileKey]
$sites.CtrlQDebuggerInit.Patched = New-CtrlQPatchedBytes -ArmMode ([string]$profile.ArmMode) $sites.CtrlQDebuggerInit.Patched = New-CtrlQPatchedBytes -ArmMode ([string]$candidateProfile.ArmMode)
$sites.DebuggerCallback.Fixup.PatchedTargetSeg = [int]$profile.UiTargetSeg $sites.DebuggerCallback.Fixup.PatchedTargetSeg = [int]$candidateProfile.UiTargetSeg
$sites.DebuggerCallback.Fixup.PatchedTargetOffset = [int]$profile.UiTargetOffset $sites.DebuggerCallback.Fixup.PatchedTargetOffset = [int]$candidateProfile.UiTargetOffset
$script:configuredProfileKey = $ProfileKey $script:configuredProfileKey = $ProfileKey
} }
@ -511,6 +524,147 @@ function Convert-StoredHexToBytes {
return $result 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 { function Get-HookFixupInfo {
param( param(
[byte[]]$FileBytes, [byte[]]$FileBytes,
@ -861,6 +1015,7 @@ function Show-Status {
$wrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.Wrapper $wrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.Wrapper
$deferredHookState = Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredHook $deferredHookState = Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredHook
$deferredWrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredWrapper $deferredWrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.LegacyDeferredWrapper
$firstMissionConfig = Get-FirstMissionStartConfig -FileBytes $FileBytes
Write-Host '' Write-Host ''
Write-Host 'CRUSADER.EXE patch status' 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 ("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 ("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 ("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 '' Write-Host ''
foreach ($profileKey in $candidateProfiles.Keys) { foreach ($profileKey in $candidateProfiles.Keys) {
$profile = $candidateProfiles[$profileKey] $candidateProfile = $candidateProfiles[$profileKey]
$marker = if ($appliedProfileKey -eq $profileKey) { 'applied' } else { 'ready' } $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 '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 '4. Exit'
Write-Host '' Write-Host ''
} }
@ -1206,6 +1364,55 @@ function Invoke-MenuChoice {
'restore' { 'restore' {
Set-DesiredState -CtrlQPatched $false -ProfileKey $null -Label 'Restore original bytes' 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' { '4' {
return $false return $false
} }
@ -1232,7 +1439,7 @@ if ($PSBoundParameters.ContainsKey('Choice')) {
:mainloop while ($true) { :mainloop while ($true) {
$currentBytes = [System.IO.File]::ReadAllBytes($exePath) $currentBytes = [System.IO.File]::ReadAllBytes($exePath)
Show-Status -FileBytes $currentBytes 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 ([string]::IsNullOrEmpty($choice)) { break mainloop }
if (-not (Invoke-MenuChoice -SelectedChoice $choice)) { if (-not (Invoke-MenuChoice -SelectedChoice $choice)) {

View file

@ -50,10 +50,22 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan
- 000e parser and animation subsystems have a real partial map. - 000e parser and animation subsystems have a real partial map.
- The auxiliary local disassembly corpus at `K:/ghidra/crusader-disasm` is now inventoried and integrated as a separate evidence source for shape metadata, static map/object dumps, opcode names, and older Remorse/Regret intrinsic-function vocabularies; its safe-reuse rules and porting implications are captured in `docs/crusader-disasm-reference.md`. - The auxiliary local disassembly corpus at `K:/ghidra/crusader-disasm` is now inventoried and integrated as a separate evidence source for shape metadata, static map/object dumps, opcode names, and older Remorse/Regret intrinsic-function vocabularies; its safe-reuse rules and porting implications are captured in `docs/crusader-disasm-reference.md`.
- The workspace now also has a first dedicated offline map-rendering/tooling lane: `tools/render_crusader_map.py` can load a chosen `FIXED.DAT`, expand `GLOB.FLX`, decode required `SHAPES.FLX` frames, apply `GAMEPAL.PAL`, and emit a first-pass PNG from either static set, while `docs/map-rendering.md` captures the current format contracts, the `--fixed-dat` override, and the intentionally limited compositor model. - The workspace now also has a first dedicated offline map-rendering/tooling lane: `tools/render_crusader_map.py` can load a chosen `FIXED.DAT`, expand `GLOB.FLX`, decode required `SHAPES.FLX` frames, apply `GAMEPAL.PAL`, and emit a first-pass PNG from either static set, while `docs/map-rendering.md` captures the current format contracts, the `--fixed-dat` override, and the intentionally limited compositor model.
- The map/editor-visibility lane is now tighter too. New note `docs/editor-object-visibility.md` records live `CRUSADER.EXE` proof that the downstream item draw helper `1198:02e4` (`Item_PaintSprite`) explicitly returns early on `ShapeData.flags2 & 1` (`SI_EDITOR`), but the follow-up render-path pass also found the controlling upstream skip at `1180:0951..095c` in the world-item builder. Current best read is therefore `editor-tagged shapes are filtered before draw-node allocation in the normal world-item renderer, with a second downstream paint-time guard still present`, which also explains why a first patch that only flipped `1198:033b` produced no visible change in-game. No recovered retail `-debug`, cheat/debug hotkey, Laurie/usecode-debugger path, or `0x410` lane currently re-enables those objects. The closest confirmed toggle remains ScummVM's own `_showEditorItems` debugger command, which is engine-added rather than retail.
- The localized-build comparison lane now covers the Japanese Windows-native executable too. New note `docs/jp-remorse-windows9x-investigation.md` records that `/ja/CRUSADER.EXE` is a PE-style Win32 image with native window creation, DirectDraw/DirectSound init, registry-backed config under `Software\Electronic Arts\Crusader: No Remorse\J1.21`, IME/DBCS-facing imports, and a `GetVersion`-driven Win9x compatibility branch that retries `TlsAlloc()` until the slot is above `2` when the classic Win9x version bit is set. Current best read is `real Windows 9x-native port with likely Win95 intent`, with runtime prerequisites still left to test.
- The Japanese localized-build lane now also covers surviving cheat/debug and startup-argument behavior. New note `docs/jp-remorse-cheats-and-launch-params.md` records that the JP Win32 build still has a live `-laurie` special-case, a live `JASSICA16` cheat-state matcher, a still-executable immortality toggle path, and a working Win32 parser for `-debug`, `-u`, `-warp`, `-skill`, `-mapoff`, `-egg`, and `-demo`. The same pass also adds one important caveat relative to the older DOS-side docs: the JP Win32 parser is only directly closed for mission-only `-warp <mission>` so far, not for positional `-warp <mission> <x> <y> <z>`.
- The startup map-selection lane is now tighter across both retail games too: No Remorse still hardcodes `Teleporter_CreateProcessDirect(1, 0x1e, 1)` inside `Game_Start`, while No Regret keeps the same literal selector in two live places, the early `Game_Start` site at `1008:1448` and the later authoritative new-game hop in `Game_RunNewGameFlow` at `1030:05c5`. The separate `-warp mission` path still uses an executable-embedded word table plus `-mapoff`, and the repo docs now include the dedicated REGRET-side note `docs/regret-game-start.md`. Current best read remains `startup map choice in code, map contents in external FIXED.DAT resources`, not `mission-start map configured in CRUSADER.CFG`.
- That same startup lane is now tighter at the argument level too. Current best parser/control-flow read in `REGRET.EXE` is `-warp <mission> [x y z]`, with X/Y/Z carried as positional argv tokens after the mission number rather than as separate recovered switches. The corresponding runtime branch in `Game_RunNewGameFlow` is also clearer: nonnegative `-egg` overrides beat the coordinate path, while the real eggless-map workaround is `-warp <mission> <x> <y> <z>` plus `-mapoff` with `-egg` omitted so the game falls into direct `NPC_Teleport` instead of the teleporter-egg lookup.
- The matching No Remorse cross-check is now closed too. Live `CRUSADER.EXE` `HandleCommandlineArgs` at `1048:0adc` uses the same positional `-warp <mission> [x y z]` parser shape, and `Game_Start` at `1020:029e` / `1020:02d0` uses the same runtime precedence: direct coordinates only win when the egg override is still negative, otherwise the code falls back to `Teleporter_CreateProcessDirect`. The parameter-only eggless-map workaround is therefore shared across both retail games, not Regret-specific.
- The command-line lane is tighter around `-u` now too. In live non-Japanese `CRUSADER.EXE`, the parser case at `1048:0a46` copies the following token into `1478:065a`, and the renamed `startup_apply_u_override_if_present` at `1420:0cdf` later consumes that buffer to resolve/load an alternate usecode/EUSECODE source into `1478:6611/6613`, mark `1478:6615`, and rebuild the cumulative slot-base words at `1478:8c7c..8c82`. Current best read is `real retail startup usecode override`, not `JP-only` and not `dead string-table residue`; the paired consequence is that the older CRUSADER-side `-setver` attribution should now be treated as reopened until its exact retail consumer is isolated directly.
- That same `-u` lane is now tighter at the runtime-scope level too. The follow-up note `docs/usecode-startup-override.md` now records that retail `-u` appears to replace the single live usecode root at `1478:6611/6613`, not add a sidecar table: `startup_apply_u_override_if_present` overwrites that root directly, rebuilds the cumulative slot-base words, and later consumers including `Usecode_ItemCallEvent`, `UsecodeProcess_CreateProcess`, `Interpreter_NextUsecodeOp`, and `Item_GetDamaged` all read the same replacement root. Current safest tooling implication is `runtime swap for the existing Crusader usecode VM`, which makes `-u` a potentially important future validation hook for round-tripped/custom usecode archives once the accepted source format is nailed down.
- The same `-u` lane is tighter at the token-shape level now too. Live `1420:0cdf` does not use the copied argv token as an arbitrary final filename; it treats `1478:065a` as the `Filespec_GetFullPath` path component while loading the fixed mutable filename template `eusecode.flx` from `1478:07a0` through `1478:06d6/06d8` and forcing the first byte to `'e'` before both the existence probe and the final load call. Current safest read is therefore `path/root override for standard EUSECODE archive naming`, not `free-form filename override`. The stock bootstrap side is also better scoped: `1478:6611/6613` starts zero in the live NE image and the only currently recovered explicit writer there is the `-u` helper, so the normal non-`-u` seed remains only cross-referenced through the verified raw-side VM bootstrap note rather than fully live-NE-closed.
- The same override lane now has a concrete live-NE constructor pair too. `1420:1499` is now renamed `entity_vm_runtime_create` and currently reads as a `0x1319`-byte runtime-object allocator that zeroes a `0x1300`-byte front region behaving like `0x80` stride-`0x26` slot/runtime records before storing an attached helper pointer at `+0x1315/+0x1317`. `1430:0000` is now renamed `entity_vm_runtime_owner_resource_create` and currently reads as the compact `0x14`-byte file-backed helper that opens the resolved `eusecode.flx` path, queries entry count through vtable `+0x04`, allocates a backing buffer at `+0x10/+0x12`, and materializes indexed owner/resource records through vtable `+0x0c`. Current safest implication is that `-u` swaps the live VM runtime object graph, not just a raw archive handle.
- The USECODE/VM owner/resource/runtime lane now has a workable partial model, a named sequencer entry, paired external file-family loader evidence, and supporting extraction/reporting tooling. - The USECODE/VM owner/resource/runtime lane now has a workable partial model, a named sequencer entry, paired external file-family loader evidence, and supporting extraction/reporting tooling.
- The USECODE/VM tooling lane now also has a concrete near-term implementation path: a Pentagram-derived proof-of-concept parser can reuse opcode decoding while swapping in the locally verified owner-loaded class and slot arithmetic, with a hybrid Ghidra comment/bookmark import path instead of a premature custom processor module. - The USECODE/VM tooling lane now also has a concrete near-term implementation path: a Pentagram-derived proof-of-concept parser can reuse opcode decoding while swapping in the locally verified owner-loaded class and slot arithmetic, with a hybrid Ghidra comment/bookmark import path instead of a premature custom processor module.
- The USECODE tooling lane now also has a first full readable corpus export: `tools/export_usecode_pseudocode.py` writes `977` current pseudocode bodies into `USECODE/EUSECODE_extracted/pseudocode`, and the first focused read of that corpus now shows `JELYHACK::use` / `JELYH2::use` as tiny shared `set_info(0x0207) -> process_exclude -> return` stubs rather than hidden active event cores. - The USECODE tooling lane now also has a first full readable corpus export: `tools/export_usecode_pseudocode.py` writes `977` current pseudocode bodies into `USECODE/EUSECODE_extracted/pseudocode`, and the first focused read of that corpus now shows `JELYHACK::use` / `JELYH2::use` as tiny shared `set_info(0x0207) -> process_exclude -> return` stubs rather than hidden active event cores.
- The USECODE tooling lane now also has two new follow-up notes grounded in the exported corpus: `docs/usecode-tool-improvement-plan.md` turns the Pentagram/`crusader-disasm` comparison into a concrete parser roadmap, and `docs/usecode-alarmhat-analysis.md` records the current best evidence-backed read of `ALARMHAT::equip` as a frame-driven local alarm-state controller that equips nearby `shape 0x04D0` helper objects in different modes. - The USECODE tooling lane now also has two new follow-up notes grounded in the exported corpus: `docs/usecode-tool-improvement-plan.md` turns the Pentagram/`crusader-disasm` comparison into a concrete parser roadmap, and `docs/usecode-alarmhat-analysis.md` records the current best evidence-backed read of `ALARMHAT::equip` as a frame-driven local alarm-state controller that equips nearby `shape 0x04D0` helper objects in different modes.
- That same `0x04D0` lane is tighter again after the next follow-up: `MONSTER::enterFastArea` is now verified as the immediate-spawn gate for `shape 0x04D0`, with the automatic lane only reached when `frame == 0` and `(mapNum & 0x08) == 0`, while frame `1` skips that hook entirely and remains the best current candidate for paired or externally signaled setups.
- The public renderer follow-up now has a better audit surface for that same lane: the `Monster Spawners` panel lists `0x04D0` records directly, the tooltip editor writes the verified `frame`/`mapNum bit 0x08` controls back into exportable FIXED records, and the new arrow overlay is intentionally scoped to evidence-backed link families only. Exported-usecode corroboration now reaches beyond `ALARMHAT` too: `ITEM.slot_2D`, `FUSPAC.slot_01`, and `MISS8.slot_20` all show nearby `0x04D0` scans keyed by frame and/or `Item.getQLo(...)`, which strengthens the current low-quality-byte-as-local-signal-key model without promoting it into a universal object pointer.
- The USECODE tooling lane now also has a broader equipment-event note: `docs/usecode-equipment-system.md` records live binary proof that `Item_Equip` / `Item_Unequip` are real generic usecode event dispatchers gated by owner-row capability masks (`0x400` / `0x800`), and that the exported corpus currently contains `77` `equip` bodies plus `50` `unequip` bodies spread across actor, turret, alarm, conveyor, camera, and hazard classes. Current best read is `surviving Ultima-style event vocabulary generalized into activation/setup/state-change semantics`, not yet `fully proven paper-doll RPG gear subsystem`. - The USECODE tooling lane now also has a broader equipment-event note: `docs/usecode-equipment-system.md` records live binary proof that `Item_Equip` / `Item_Unequip` are real generic usecode event dispatchers gated by owner-row capability masks (`0x400` / `0x800`), and that the exported corpus currently contains `77` `equip` bodies plus `50` `unequip` bodies spread across actor, turret, alarm, conveyor, camera, and hazard classes. Current best read is `surviving Ultima-style event vocabulary generalized into activation/setup/state-change semantics`, not yet `fully proven paper-doll RPG gear subsystem`.
- The USECODE tooling lane now also has its first implemented readability follow-through from that improvement list: `tools/poc_crusader_usecode_parser.py` and `tools/export_usecode_pseudocode.py` now regenerate the full `977`-body corpus with one verified wrapper alias seed (`FREE.waitNTimerTicks` for `0A0C:0032`), class-name-aware target rendering (`FREE.slot_21`, `BLASTPAC.slot_20`, `TRIGGER.slot_20`, etc.), first-pass selector decoding that upgrades the simpler alarm/trigger `loopscr` runs into `for ... in nearby_items(shape=..., origin=...)` / `for ... in nearby_items(family=..., origin=...)` loops, and a second readable selector-family fallback that collapses raw `loopscr 0x42` runs into `selector_0x42(arg0=..., arg1=..., arg2=..., origin=...)` annotations or `for ... in selector_0x42(...)` loops where the control flow is simple enough. - The USECODE tooling lane now also has its first implemented readability follow-through from that improvement list: `tools/poc_crusader_usecode_parser.py` and `tools/export_usecode_pseudocode.py` now regenerate the full `977`-body corpus with one verified wrapper alias seed (`FREE.waitNTimerTicks` for `0A0C:0032`), class-name-aware target rendering (`FREE.slot_21`, `BLASTPAC.slot_20`, `TRIGGER.slot_20`, etc.), first-pass selector decoding that upgrades the simpler alarm/trigger `loopscr` runs into `for ... in nearby_items(shape=..., origin=...)` / `for ... in nearby_items(family=..., origin=...)` loops, and a second readable selector-family fallback that collapses raw `loopscr 0x42` runs into `selector_0x42(arg0=..., arg1=..., arg2=..., origin=...)` annotations or `for ... in selector_0x42(...)` loops where the control flow is simple enough.
- The USECODE/VM lane now also has a verified generic masked-context creation hub (`000d:463a`) plus two concrete sequencer-internal consumer blocks (`000d:208b`, `000d:21ed`) built directly on `entity_vm_context_create_from_slot_index`. - The USECODE/VM lane now also has a verified generic masked-context creation hub (`000d:463a`) plus two concrete sequencer-internal consumer blocks (`000d:208b`, `000d:21ed`) built directly on `entity_vm_context_create_from_slot_index`.
@ -175,16 +187,17 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan
6. Use boundary repair only on active blockers with clear payoff, with `000c:db68` now downgraded to optional hygiene unless it blocks adjacent work again. 6. Use boundary repair only on active blockers with clear payoff, with `000c:db68` now downgraded to optional hygiene unless it blocks adjacent work again.
7. Revisit the `0x4588` callback object only when caller-side evidence is strong enough to support behavioral naming. 7. Revisit the `0x4588` callback object only when caller-side evidence is strong enough to support behavioral naming.
8. Exercise `tools/render_crusader_map.py` on a few representative No Remorse and No Regret maps, then tighten the paint order using `TYPEFLAG.DAT` footpads and any mismatches visible against in-game screenshots or `crusader-disasm` map evidence. 8. Exercise `tools/render_crusader_map.py` on a few representative No Remorse and No Regret maps, then tighten the paint order using `TYPEFLAG.DAT` footpads and any mismatches visible against in-game screenshots or `crusader-disasm` map evidence.
9. If the map/editor-visibility lane is revisited, start from `docs/editor-object-visibility.md` and the upstream `1180:0951..095c` world-item builder gate first; rule in or rule out a second debug-only world-item builder before spending more time on cheat or command-line searches.
9. Recover the real upstream caller/selector path into `entity_vm_opcode_sequence_run`, most likely by finding the first non-recursive `0x6714` context-method caller or vtable dispatch site rather than by repeating raw xref queries that still return no direct edges. 10. Recover the real upstream caller/selector path into `entity_vm_opcode_sequence_run`, most likely by finding the first non-recursive `0x6714` context-method caller or vtable dispatch site rather than by repeating raw xref queries that still return no direct edges.
10. Recover real caller roles for `entity_vm_context_try_create_mask_0400_slot0a_with_offset` and `entity_vm_context_try_create_mask_0800_slot0b_with_offset` by treating them as the remaining dark members of the now-verified signed-additive masked-materializer subfamily and comparing them against the newly anchored slot-`0x12` caller pattern. 11. Recover real caller roles for `entity_vm_context_try_create_mask_0400_slot0a_with_offset` and `entity_vm_context_try_create_mask_0800_slot0b_with_offset` by treating them as the remaining dark members of the now-verified signed-additive masked-materializer subfamily and comparing them against the newly anchored slot-`0x12` caller pattern.
11. Tighten the newly surfaced higher-slot wrapper ladder around `0005:3115..31da`, especially the two slot-`0x12` caller sites at `0005:1776` / `0005:1945` and the slot-`0x10` guarded callsite, so any future promotion to `leaveFastArea` / `func11|cast` / `justMoved` / `AvatarStoleSomething` / `animGetHit` is driven by binary caller behavior rather than by external tables alone. 12. Tighten the newly surfaced higher-slot wrapper ladder around `0005:3115..31da`, especially the two slot-`0x12` caller sites at `0005:1776` / `0005:1945` and the slot-`0x10` guarded callsite, so any future promotion to `leaveFastArea` / `func11|cast` / `justMoved` / `AvatarStoleSomething` / `animGetHit` is driven by binary caller behavior rather than by external tables alone.
12. Tighten the outward caller chains around the renamed seg006 masked helpers `entity_vm_context_try_create_mask_0008_slot30_with_offset` (`0006:0ba4`) and `entity_vm_context_try_create_mask_0010_slot08_with_offset_if_ready` (`0006:108c`) so the local state-selector lane and the adjacent class-linked value family can be tied back to concrete gameplay subsystems rather than only to class-detail fields. 13. Tighten the outward caller chains around the renamed seg006 masked helpers `entity_vm_context_try_create_mask_0008_slot30_with_offset` (`0006:0ba4`) and `entity_vm_context_try_create_mask_0010_slot08_with_offset_if_ready` (`0006:108c`) so the local state-selector lane and the adjacent class-linked value family can be tied back to concrete gameplay subsystems rather than only to class-detail fields.
13. Tighten the paired-file-family reading of the seg070 twin loops at `0009:67b6` and `0009:6916` by recovering which temporary buffer and record schema each family populates behind `entity_vm_runtime_owner_resource_create`. 14. Tighten the paired-file-family reading of the seg070 twin loops at `0009:67b6` and `0009:6916` by recovering which temporary buffer and record schema each family populates behind `entity_vm_runtime_owner_resource_create`.
14. Promote additional ledger rows where the current docs already justify `Foothold`, `Partial`, or `Deep`. 15. Promote additional ledger rows where the current docs already justify `Foothold`, `Partial`, or `Deep`.
15. If the VM lane stalls again, revisit `000e:ffb0` from the now-verified `00db/00dc` caller windows and try to recover an adjacent non-overlapped helper before attempting any boundary repair. 16. If the VM lane stalls again, revisit `000e:ffb0` from the now-verified `00db/00dc` caller windows and try to recover an adjacent non-overlapped helper before attempting any boundary repair.
16. If the immortality lane is revisited, stay focused on `NPCTRIG` slot `0x0a` first, with slot `0x20` still treated as the typed/setup companion and `EVENT` only as the generic hub baseline; the three currently recovered direct `0005:295f` caller families are now all closed and comment-backed in the live NE program at `10f0:02d9`, `10f0:0379`, `10f0:03c3`, `10f0:03e5`, `1128:0ff0`, and `1138:1384`, so the next defensible step is an earlier producer that assigns subtype `0x20b/0x20c` into field `+0x3c` or otherwise chooses the owner-loaded class family before these generic damage consumers run. 17. If the immortality lane is revisited, stay focused on `NPCTRIG` slot `0x0a` first, with slot `0x20` still treated as the typed/setup companion and `EVENT` only as the generic hub baseline; the three currently recovered direct `0005:295f` caller families are now all closed and comment-backed in the live NE program at `10f0:02d9`, `10f0:0379`, `10f0:03c3`, `10f0:03e5`, `1128:0ff0`, and `1138:1384`, so the next defensible step is an earlier producer that assigns subtype `0x20b/0x20c` into field `+0x3c` or otherwise chooses the owner-loaded class family before these generic damage consumers run.
17. Use the new Pentagram-derived parser proof of concept as the first tooling bridge for raw class/slot bodies: extend opcode coverage conservatively, emit IR v1 artifacts, and only then prototype a Ghidra-side annotation importer against compiled anchors like `000d:51fd`, `000d:5572`, `000d:46ec`, `000d:22bc`, and `000d:ebe3`. 18. Use the new Pentagram-derived parser proof of concept as the first tooling bridge for raw class/slot bodies: extend opcode coverage conservatively, emit IR v1 artifacts, and only then prototype a Ghidra-side annotation importer against compiled anchors like `000d:51fd`, `000d:5572`, `000d:46ec`, `000d:22bc`, and `000d:ebe3`.
## Remaining Work To Reach A Reasonably Complete Decompilation State ## Remaining Work To Reach A Reasonably Complete Decompilation State