Add headless runtime tooling and Campaign.win analysis

This commit is contained in:
Jan Petykiewicz 2026-04-10 01:22:47 -07:00
commit 27172e3786
37 changed files with 11867 additions and 302 deletions

View file

@ -142,17 +142,69 @@ transition.
by the Multiplayer preview dataset object at `0x006cd8d8`, and only falls back to the normal
reference-bundle path when that dataset object is absent. The shell-side mode owner above those
file coordinators is clearer now too. `shell_transition_mode` no longer reads like a generic mode
switch: its constructor jump table now resolves mode `1` to `Game.win`, mode `2` to `Setup.win`,
mode `3` to `Video.win`, mode `4` to `LoadScreen.win`, mode `5` to `Multiplayer.win`, mode `6`
to `Credits.win`, and mode `7` to `Campaign.win`. The strongest current load-side owner inside
that table remains the mode-`4` branch around `0x4830ca`, which publishes the new active mode
object into `0x006cec78` and then calls `shell_active_mode_run_profile_startup_and_load_dispatch`
as `thiscall(active_mode, 1, 0)`. The caller split above that owner is tighter now too:
switch: its ABI is now grounded as a `thiscall` with two stack arguments because the body reads
the requested mode from `[esp+0x0c]` and returns with `ret 8`. The grounded world-entry
load-screen call shape is `(4, 0)`, not a one-arg mode switch. The second stack argument is now
tighter too: current local evidence reads it as an old-active-mode teardown flag, because the
`0x482fc6..0x482fff` branch only runs when it is nonzero and then releases the prior active-mode
world through `0x434300`, `0x433730`, and the common free path before clearing `0x006cec78`.
The later world-entry reactivation branch correspondingly uses `(1, esi)` rather than `(1, 0)`.
The current live hook probes now push the remaining auto-load gap much later too: on the
hook-driven path `shell_transition_mode(4, 0)` returns cleanly, and the full old-mode teardown
stack under `0x5389c0` now returns too, including `0x5400c0`, the `0x53fe00 -> 0x53f860`
remove-node sweep over `[object+0x74]`, and the nearby mode-`2` teardown helper `0x00502720`.
The same live run now also reaches and returns from `shell_load_screen_window_construct`
`0x004ea620` and the immediate shell publish through `0x00538e50`.
Its constructor jump table is tighter now too: mode `1` is not the direct `Game.win`
constructor, but the startup-dispatch arm rooted at `0x483012`; mode `2` enters `Setup.win`,
mode `3` enters `Video.win`, mode `4` enters the plain `LoadScreen.win` branch at `0x4832e5`,
mode `5` enters `Multiplayer.win`, mode `6` enters `Credits.win`, and mode `7` enters
`Campaign.win`. The important static correction is that the startup-runtime slice
`0x004ea710 -> 0x0053b070(0x46c40) -> 0x004336d0 -> 0x00438890` belongs to mode `1`, not mode
`4`. Mode `4` only constructs and publishes the `LoadScreen.win` object through `0x004ea620`
and `0x00538e50`. The older hook-driven `(4, 0)` path therefore was not mysteriously skipping
the startup-runtime object; it was entering the wrong jump-table arm for that work. The caller
split above that owner is tighter now too:
`world_entry_transition_and_runtime_bringup` reaches the same owner at `0x443b57` with `(0, 0)`
after dismissing the current shell detail panel and servicing `0x4834e0(0, 0)`, while the
saved-runtime path at `0x446d7f` does the same before immediately building the `.smp` bundle
payloads through `0x530c80/0x531150/0x531360`. That makes the `LoadScreen.win` startup lane the
only currently grounded caller that enters `0x438890` with `(1, 0)` instead of `(0, 0)`. The
remaining runtime uncertainty is narrower now too: the plain-run logs still show the plain
`LoadScreen.win` state under `(4, 0)`, and the corrected allocator-window run now reinforces the
same read because the post-construct allocator stream stays empty instead of showing the expected
`0x46c40` startup-runtime allocation. The first lower allocator probe on `0x005a125d` was not
trustworthy because that shared cdecl body sits behind the thunk and the initial hook used the
wrong entry shape, and the first direct thunk hook was also not trustworthy because a copied
relative-`jmp` thunk cannot be replayed through an ordinary trampoline. But the later corrected
thunk run plus the jump-table decode now agree: the next meaningful hook-driven test is mode
`1`, not mode `4`.
`mode_id = 2`, and it never advanced into `ready gate passed`, staging, or transition. So that
run did not actually exercise the `0x0053b070 -> 0x004336d0 -> 0x00438890` subchain at all.
The next runtime pass now lowers the ready-poll defaults to `1` and `0` and adds an explicit
ready-count log so the mode-`4` startup lane either stages immediately or shows exactly how far
the gate gets. That adjustment worked on the next run: the hook now stages and completes the
`shell_transition_mode` path again, with `LoadScreen.win` construction and publish returning
cleanly. But the post-publish startup subchain is still unresolved: there is still no trusted
`0x46c40` allocator hit, no direct `0x004336d0` entry, and no direct `0x00438890` entry. So
the next clean runtime boundary is the tiny `LoadScreen.win` scalar setter at `0x004ea710`,
which sits immediately before the `0x0053b070` allocation in the static mode-`4` branch. The
immediate next runtime check is even more concrete than the helper hook, though: inspect the
state that `0x004ea710` should leave behind. The hook now logs the post-transition
`LoadScreen.win` singleton, its field `[+0x78]`, `0x006cec78`, the shell state's `[+0x0c]`
active-mode object field, and the startup selector. If `0x004ea710` really ran on the mode-`4`
branch, `[LoadScreen.win+0x78]` should no longer be zero after `shell_transition_mode` returns.
The latest run answered that directly: after transition return, `field_active_mode_object` is
still the `LoadScreen.win` singleton, `0x006cec78` is still null, `[LoadScreen.win+0x78]` is
still zero, and the startup selector is still `3`. So the current best read is that RT3 is
still parked in the plain `LoadScreen.win` state at transition return rather than having entered
the separate runtime-object path yet. That shifts the best next runtime boundary from “deeper
inside `shell_transition_mode`” to “what later active-mode service tick, if any, promotes the
load-screen object into the startup-dispatch path.” The next run now logs the first few
shell-state service ticks after auto-load is attempted with that same state tuple
(`0x006cec78`, `[shell_state+0x0c]`, `0x006d10b0`, `[LoadScreen.win+0x78]`, selector), so the
next question is very narrow: does one later service tick finally promote the plain
`LoadScreen.win` state into the startup-runtime object path, or does it stay frozen as-is? The
internal selector split in `0x438890` is tighter now too: `[0x006cec7c+0x01]` is a separate
seven-way startup selector, not the shell mode id. Values `1` and `7` load `Tutorial_2.gmp` and
`Tutorial_1.gmp`, values `3/5/6` collapse into the same profile-seeded file-load lane through
@ -222,6 +274,37 @@ transition.
sandbox-backed `new` list while mode `12` is its `load` sibling. Combined with the builder at
`0x4333f0`, that shows only submodes `4` and `10` using the alternate localized-stem list-label
path; `6`, `9`, `12`, and `14` keep the direct filename-normalization lane.
The `rt3_2WIN.PK4` extraction path is grounded a bit more cleanly now too: current `pack4`
samples use a shared `0x03eb` header, a fixed `0x4a`-byte directory entry stride, and a payload
base at `8 + entry_count * 0x4a`. In the checked UI bundle the entry table is contiguous and
`Campaign.win` extracts cleanly at directory index `3` with payload length `0x306a`.
The extracted `.win` payload family now has one narrow shared shape too: `Campaign.win`,
`CompanyDetail.win`, and `setup.win` all share the same first `0x50` bytes at offsets
`0x00`, `0x0c`, `0x10`, `0x14`, `0x34`, `0x38`, `0x40`, and `0x48`, while
`CompanyDetail.win` and `setup.win` also carry an inline root `.imb` name immediately at
`0x51`. Current resource scans show `Campaign.win` embedding the `litCamp*/Ribbon*` family,
`CompanyDetail.win` embedding mainly `CompanyDetail.imb`, `GameWindow.imb`, and `Portrait.imb`,
and `setup.win` embedding the broader `Setup_Background/Buttons/New_Game/Load/Sandbox` art
families. The control records between those strings are still undecoded, but the resource-record
shell itself is tighter now: all checked `.win` samples use the same three-word prelude prefix
`0x0bb8, 0x0, 0x0bb9`, and the fourth prelude word matches `resource_name_len + 1`. The next
word after the terminating NUL then behaves like a per-record selector lane. In `setup.win`
the dominant `Setup_Buttons.imb` family alternates between `0x00040000` and the incrementing
`0x00010c1c..0x00010c86` series with dominant inter-record strides `0xb7/0xdb`; in
`Campaign.win` the `litCamp*/Ribbon*` family carries the incrementing `0x0004c372..0x0004c38e`
selector block with dominant `0x158/0x159` and `0xb2/0xb3` strides. That campaign lane is now
directly aligned to the executable-side control ids too: the low 16 bits of
`litCamp1..16` map exactly to `0xc372..0xc381`, and the low 16 bits of `Ribbon1..16` map
exactly to `0xc382..0xc391`, matching the `Campaign.win` progress and selector control bases
already grounded from `RT3.exe`. The fuller selector export now tightens the auxiliary families
too: `litArrows.imb` covers `0xc36c..0xc371` exactly and `litExits.imb` covers
`0xc397..0xc39a` exactly, matching the constructor-side six-control arrow strip and four-control
exit strip. The second post-name dword is tighter now too: its middle 16-bit lane groups those
same `Campaign.win` records under `0xc368`, `0xc369`, `0xc36a`, and `0xc36b`, which matches the
four page-strip controls and cleanly buckets the first five campaign entries, the next five, the
next three, and the final three under the same page families. There are still no `.imb`
selector records for `0xc393..0xc396` or `0xc39b`, which makes those message-side page-write
controls look more like structural buttons than art-backed repeated resource records.
The grouped `3/4/5` family is narrower now too: `0x0ce5` is no longer part of it, because that
control writes selector `3` and then selects submode `9` as the `Load Map` sibling. The nearby
`0x0dcb` branch instead conditionally selects submode `5` or `3`, which keeps `3` as the
@ -265,7 +348,33 @@ transition.
shape. Only after that does `0x502220` copy payload fields `+0x14/+0x3b2/+0x3ba/+0x20` into the
staged runtime profile through `0x47be50`, which in turn normalizes the payload category bytes at
`[payload+0x31a + row*9]` and the local marker-slot bytes at `[payload+0x2c9..]` through
`0x47bc80`. The adjacent region-side worker family is tighter in a negative way too: the setup
`0x47bc80`. The copied-field consumer split is tighter now too: payload `+0x14` becomes staged
profile `[0x006cec7c+0x77]`, which is the visible setup scalar later formatted into controls
`0x0e84` and `0x0d4f` and adjusted by the `0x0d49/0x0d4a` pair; payload `+0x3b2` becomes
`[0x006cec7c+0x79]`, the live-row threshold that the payload-row draw callback uses to choose
style slot `0x18` versus `0x19`; and payload `+0x3ba` becomes `[0x006cec7c+0x7b]`, the current
scroll or row-index lane adjusted by the `0x0ce6/0x0ce7` pair. So the known setup-side payload
consumers still stop well before the later candidate table, but the early copied lanes are no
longer anonymous. The adjacent region-side worker family is tighter in a negative way too: the setup
payload path now gives one useful upper bound on the newer candidate-availability source block
too. The map-style setup loader is definitely pulling chunk families `0x0004/0x2ee0/0x2ee1`
into one large `0x100f2`-byte payload record, and the fixed `0x6a70..0x73c0` candidate table
clearly lives inside the same broad file family; but the grounded setup-side consumers we can
actually name after that load still only touch earlier payload offsets such as `+0x14`,
`+0x20`, `+0x2c9`, `+0x31a`, `+0x3ae`, `+0x3b2`, and `+0x3ba`. So the current evidence is
strong enough to say the candidate table is probably housed inside the setup payload family, but
not yet strong enough to name one setup-side consumer that reads the `0x6a70` header or the
fixed-width entries directly. The newer fixed-offset compare pass tightens that lower setup slice
too: across the checked map/save pairs `Alternate USA.gmp -> Autosave.gms`,
`Southern Pacific.gmp -> p.gms`, and `Spanish Mainline.gmp -> g.gms`, the known setup payload
lanes `+0x14` and `+0x3b2` are preserved map-to-save on the same scenario-family split as the
later candidate-table headers (`0x0771/0x0001`, `0x0746/0x0006`, `0x0754/0x0001`), while
`+0x3ae` stays fixed at `0x0186` and `+0x3ba` stays fixed at `1` across all six files. By
contrast, `+0x20` does not survive as one shared source value (`0xd3 -> 0xb4`,
`0x6f -> 0x65`, `0xe3 -> 0x78` across those same pairs). So the current best read is that the
setup payload mixes preserved scenario metadata and later save-variant state well before the
`0x6a70` candidate table, rather than acting as one uniformly copied prelude.
The adjacent region-side worker family is tighter in a negative way too: the setup
payload loader is now clearly separate from the broader region-building and placement cluster
around `0x422320..0x423d30`, whose current grounded helpers now include `0x422320`, `0x4228b0`,
`0x422900`, `0x422a70`, `0x422be0`, `0x422ee0`, `0x4234e0`, `0x4235c0`, and
@ -295,8 +404,95 @@ transition.
selector-`5` siblings, while the neighboring selector-`3` validated lane is at least shared by
`0x0de9` and `0x0df3`; the shared bridge at `0x4425d0` then forces `[0x006cec7c+0xc5] = 1`,
mirrors payload bytes into `[profile+0xc4]`, `[profile+0x7d]`, and `[profile+0xc6..+0xd5]`, and
only then issues shell request `0x0cc`. Only `0x0e81` also sets `[0x006cec7c+0x82] = 1`, which
is currently the strongest sandbox-side anchor beneath the later `.gmx` load family.
only then issues shell request `0x0cc`. The file-side staging bytes in that bridge are now
tighter too: across the checked `Alternate USA`, `Southern Pacific`, and `Spanish Mainline`
map/save pairs, payload `+0x33` stays `0`, but payload `+0x22` and token block `+0x23..+0x32`
do not preserve the earlier map-to-save pairing seen at `+0x14/+0x3b2`. Instead they split by
the finer file family, with examples `Alternate USA.gmp = 0x53 / 0131115401...` versus
`Autosave.gms = 0xae / 01439aae01...`, `Southern Pacific.gmp = 0xeb / 00edeeeb...` versus
`p.gms = 0x21 / 0100892101...`, and `Spanish Mainline.gmp = 0x5b / 0044f05b...` versus
`g.gms = 0x7a / 0022907a...`. So the validated launch bridge now looks more like a file-family
staging lane than a simple copy-forward of the earlier setup summary fields. The destination-side
consumers are tighter now too: `[profile+0xc4]` is no longer just an unnamed staged byte, but the
campaign-progress slot later consumed by the numbered `%s%02d.gmc` save helper `0x00517c70`;
`[profile+0x7d]` is the same compact option byte mirrored back into the `Setup.win` option
controls `0x0ce9..0x0ced`; and the campaign-side selector block is no longer only a one-byte
anchor. Direct `RT3.exe` disassembly of the campaign-side dispatcher at `0x004b89c0` now shows
the exact mirror shape: when the local campaign-page selector `[window+0x78] <= 4`, the helper
writes one highlighted progress control `0x0c372 + [profile+0xc4]` and then mirrors the full
sixteen-byte band `[profile+0xc6..+0xd5]` into controls `0x0c382..0x0c391` through repeated
`0x540120` calls. The same body also treats `[profile+0xc4]` as an index into the fixed
sixteen-entry scenario-name table at `0x00621cf0`, whose observed entries include `Go West!`,
`Germantown`, `Central Pacific`, `Texas Tea`, `War Effort`, `State of Germany`, `Britain`,
`Crossing the Alps`, `Third Republic`, `Orient Express`, `Argentina Opens Up`,
`Rhodes Unfinished`, `Japan Trembles`, `Greenland Growing`, `Dutchlantis`, and
`California Island`. On the launch-side branch the same helper writes startup selector `6`,
copies the resolved campaign filename into `[profile+0x11]`, forces `[profile+0xc5] = 1`, and
then issues shell request `0x0cc`. The lower refresh tail at `0x004b8d49..0x004b8d69` also
shows the observed page-band split over the same progress byte: values `< 5` pair with campaign
page `1`, values `5..9` with page `2`, values `10..12` with page `3`, and values `>= 13` with
page `4`. So `[profile+0xc6..+0xd5]` is no longer a generic opaque span at all, but the staged
sixteen-byte per-scenario campaign selector or unlock band consumed directly by `Campaign.win`.
The message-dispatch side is tighter now too. The local classifier at `0x004b91d8` routes the
control range `0x0c352..0x0c39b` through five concrete case classes: `0x0c352..0x0c361` enter
the shared selector or launch branch, `0x0c362..0x0c367` force local page `1`,
`0x0c368..0x0c392` force local page `2`, `0x0c393..0x0c396` force local page `0`, and
`0x0c39b` alone forces local page `3`. That matches the constructor and refresh shape: the
sixteen scenario selector controls live at `0x0c352..0x0c361`, the four page-strip controls
live at `0x0c368..0x0c36b`, the six-arrow auxiliary strip lives at `0x0c36c..0x0c371`, the
progress band lives at `0x0c372..0x0c381`, the ribbon or selector band lives at
`0x0c382..0x0c391`, the string or voice target lane lives at `0x0c392`, the four exit controls
live at `0x0c397..0x0c39a`, and `0x0c39b` remains the one-off special action control that
spawns or dismisses the campaign-side companion object. The `Campaign.win` blob now exposes that
structural split directly too: alongside the art-backed `0x0004c3xx` records it carries
anonymous zero-name records with the same `0x0bb8 / 0 / 0x0bb9` prelude but selector words in
the `0x0010c3xx` family. Current local extraction shows exactly
`0x0010c352..0x0010c361`, `0x0010c362..0x0010c367`, `0x0010c393..0x0010c396`, and one
`0x0010c39b` record, which is the same non-`.imb` band the message dispatcher treats as the
structural selector and page-write family. Their second selector word then buckets those records
under the same page-strip ids as the art-backed records: `0xc368` for
`0xc352..0xc356`, `0xc362`, `0xc393`, and `0xc39b`; `0xc369` for `0xc357..0xc35b`,
`0xc363`, and `0xc394`; `0xc36a` for `0xc35c..0xc35e`, `0xc365..0xc366`, and `0xc395`; and
`0xc36b` for `0xc367`, `0xc35f..0xc361`, and `0xc396`. One final anonymous record at
`0x0002c392` has no page bucket at all, which fits the already-grounded read of `0xc392` as the
one-off string or voice target lane rather than a normal page-cell control. The anonymous body
layout is only partially named, but one file-side distinction is already stable: the ordinary
structural selector/page records all carry footer words `0xd3000000` and `0xd2000007` at
relative `+0x98/+0x9c`, while the lone `0xc392` record carries `0x00000000/0x00000000` there.
So `0xc392` is no longer just “outside the page buckets”; it is also the only current
anonymous-record outlier in that trailing structural footer pair. The structural selector side is
tighter now too: `0xc352..0xc361` partition exactly as `5 + 5 + 3 + 3` across page buckets
`0xc368..0xc36b`, matching the observed campaign page bands for scenario progress values `< 5`,
`5..9`, `10..12`, and `>= 13`. That makes the anonymous selector records the file-side mirror of
the same campaign page split, not a separate unrelated control family. The file-order adjacency
is tighter in the same way: `0xc352..0xc361` each sit immediately after one `RibbonN.imb`
record and before the next `litCamp` record, which makes them the strongest current file-side fit
for the structural selector or click-target siblings of the visible campaign ribbon controls.
The auxiliary anonymous bands line up the same way: `0xc362..0xc367` sit inside the local
`litArrows.imb` clusters, `0xc393..0xc396` sit inside the `litExits.imb` clusters, and
`0xc39b` sits once between the first arrow and first exit group. So the current best read is
that those anonymous records are the structural hitbox or action siblings of the visible arrow,
exit, and selector art families, not an unrelated hidden control layer. The generic record
cadence is tighter now too: in the current `Campaign.win` dump the ordinary anonymous structural
records all span `0x0a7` bytes, while the named art-backed records cluster at `0x0b1`, `0x0b2`,
and `0x0b3`. The only two visible outliers are the leading `0x0080c351` record at `0x0041`
with span `0x0a3`, and the trailing `0x0002c392` record at `0x2fb9` with span `0x0b1`. That
reinforces the current read that `0xc351` and `0xc392` are one-off structural controls flanking
the main selector or page family rather than ordinary repeated art-backed records.
The setup-launch corpus now tightens one corrective caveat there too: when the checked
`Alternate USA`, `Southern Pacific`, and `Spanish Mainline` `.gmp/.gms` files are decoded with
that same campaign-side interpretation, payload byte `+0x22` is always outside the known
campaign progress range `0..15` (`0x53`, `0xeb`, `0x5b`, `0xae`, `0x21`, `0x7a`), so the
ordinary validated setup lane is not simply staging a normal campaign-screen state. The paired
token block `+0x23..+0x32` is still structurally compatible with the campaign selector band, but
in the checked files it only populates a small recurring subset of the sixteen scenario lanes,
chiefly `Go West!`, `Germantown`, `Central Pacific`, `Texas Tea`, and `War Effort`, with
scenario-family-specific byte values rather than a simple `0/1` unlock bitmap. That makes the
setup-side staging bridge look more like one shared destination layout being reused for a broader
launch token family, not a proof that ordinary setup-launched `.gmp/.gms` content is already in
campaign-screen form.
Only `0x0e81` also sets `[0x006cec7c+0x82] = 1`, which is currently the strongest sandbox-side
anchor beneath the later `.gmx` load family.
That launch band is slightly tighter now too: `0x0dca` is the grayscale-map picker branch above
the `TGA Files (*.tga)` / `.\Data\GrayscaleMaps` helper at `0x004eb0b0`, not another ordinary
save-file validator. That launch band is tighter in a second way too: `0x0ddf` is not a lobby
@ -793,6 +989,15 @@ transition.
earlier hidden prepass is tighter now too: `0x00437743` is the scenario-side named
candidate-availability seeding pass over the live candidate pool `0x0062b268`, feeding the
override collection at `[state+0x66b2]` through `0x00434f20` before any visible banner is posted.
That grounded write path is narrower than the static file evidence, though: the startup reset
helper `world_runtime_reset_startup_dispatch_state_bands` `0x004336d0` explicitly clears
`[state+0x66b2]` before the dispatch path begins, and the current exported write-side callers
we can name after that reset are still just the live-pool preseed `0x00437743`, the sibling
startup lane `0x00436ad7`, and the editor-side `Industry (Overall)` toggle handler
`0x004cf430` through `0x00434f20`. So while the fixed `0x6a70..0x73c0` candidate-availability
block is now well grounded as bundled map/save source data, a direct bulk-import path from that
block into the live runtime collection `[state+0x66b2]` is still not grounded by the current
disassembly notes.
That candidate-side table now has a grounded fixed record layout too: each entry is a `0x22`-byte
blob with a zero-terminated candidate-name slot at `[entry+0x00..+0x1d]` and one trailing
availability dword at `[entry+0x1e]`, read through `0x00434ea0` and mirrored later into
@ -1952,6 +2157,106 @@ transition.
bytes `[world+0x66de]` and `[world+0x66f2]`, restores the selected year/profile lane through
`[profile+0x77]` into `[world+0x05/+0x09/+0x15]` through
`world_set_selected_year_and_refresh_calendar_presentation_state` `0x00409e80`; that restore now
also has one concrete file-side correlation in the classic `.gms` family: local save inspection
now consistently finds `0x32dc` at `0x76e8`, `0x3714` at `0x76ec`, and `0x3715` at `0x77f8` in
`Autosave.gms`, `kk.gms`, and `hh.gms`, leaving one exact `0x108`-byte span from `0x76f0` to
`0x77f8` between `0x3714` and `0x3715`. That span already carries staged-profile-looking payload
text such as `British Isles.gmp`, so the current static-file evidence now supports the atlas-side
`0x108` packed-profile note for the classic save family even though the exact field layout inside
that block is still unresolved. The same classic corpus is tighter now too: inside that
`0x108` span the map-path C string begins at relative offset `0x13`, the display-name C string
begins at `0x46`, the block is otherwise almost entirely zeroed, and the three local samples are
byte-identical except for the leading dword at `+0x00` (`3` in `Autosave.gms` and `hh.gms`,
`5` in `kk.gms`). The currently atlas-tracked bytes `[profile+0x77]`, `[profile+0x82]`,
`[profile+0x97]`, and `[profile+0xc5]` are all `0` in that classic sample set, so the current
file-side evidence grounds the block boundaries and the embedded strings but does not yet show
live examples of those branch-driving latches being set. One 1.05-era file-side analogue is now
visible too, but only as an inference from repeated save structure rather than a disassembly-side
field map: local `.gms` files in `rt3_105/Saved Games` carry one compact string-bearing block at
`0x73c0` with the same broad shape as the classic profile slab, including a leading dword at
`+0x00`, one map-path string at `+0x10`, one display-name string at `+0x43`, and a small
nonzero tail around `+0x76..+0x88`. In that 1.05 corpus the analogue bytes at relative `+0x77`
and `+0x82` are now nonzero in every checked sample (`Autosave.gms`/`nom.gms` show `0x07` and
`0x4d`; `p.gms`/`q.gms` show `0x07` and `0x90`; `g.gms` shows `0x07` and `0xa3`), while
relative `+0x97` and `+0xc5` remain `0`. The compared 1.05 save set is tighter now too:
`Autosave.gms` and `nom.gms` cluster together on `Alternate USA.gmp` with `+0x82 = 0x4d`,
`g.gms` carries `Spanish Mainline.gmp` with `+0x82 = 0xa3`, and `p.gms`/`q.gms` cluster on
`Southern Pacific.gmp` with `+0x82 = 0x90`; across all five files the same inferred analogue
lane at `+0x77` stays fixed at `0x07`, while the same map- or scenario-sensitive tail word at
`+0x80` tracks those `0x4d/0xa3/0x90` byte lanes (`0x364d0000`, `0x29a30000`, `0x1b900000`).
The leading dword at `+0x00` also splits the same corpus, with `Autosave.gms` alone at `3` and
the other four checked 1.05 saves at `5`. That is enough to say the wider save corpus does
contain nonzero candidates for two of the atlas-tracked profile lanes, and that one of them
varies coherently with the loaded scenario family, but not yet enough to claim that the 1.05
block reuses the exact same semantic field assignments as the classic one. The loader-side
family split is tighter now too: `p.gms` and `q.gms` no longer live under a generic fallback;
their save headers now classify as one explicit `rt3-105-scenario-save` branch with preamble
words `0x00040001/0x00018000/0x00000746` and the early secondary window
`0x00130000/0x86a00100/0x21000001/0xa0000100`, while `g.gms` now classifies as a second
explicit `rt3-105-alt-save` branch with the different preamble lane
`0x0001c001/.../0x00000754` and early window
`0x00010000/0x49f00100/0x00000002/0xa0000000`. That branch now carries the same bootstrap,
anchor-cycle, named 1.05 trailer, and narrow profile-block extraction path as the other 1.05
saves. The bridge just below that trailer is now explicit too: the common 1.05 save branch
carries selector/descriptor `0x7110 -> 0x7801` in `Autosave.gms` and `0x7110 -> 0x7401` in
`nom.gms`, and both still reach the same first later candidate at
`span_target + 0x189c`, well before the packed profile at `span_target + 0x3d48`; the
`rt3-105-alt-save` branch instead carries `0x54cd -> 0x5901` and its first later candidate
lands at `packed_profile + 0x104`, essentially on the profile tail; the scenario-save branch
still diverges locally with `0x0001 -> 0x0186` and never enters that later `0x32c8`-spanned
bridge at all. The common-branch bridge payload is narrower now too: both checked base saves
expose the same 0x20-byte primary block at `0x4f14` followed by the same denser secondary block
at `0x671c`, `0x1808` bytes later, and that secondary block now appears to run intact up to the
packed-profile start at `0x73c0` for a total observed span of `0xca4` bytes. The trailing slice
of that secondary block is now typed one level further: a small header at `secondary+0x354`
carries the observed stride `0x22`, capacity `0x44`, and count `0x43`, followed by a fixed-width
67-entry name table starting at `secondary+0x3b5` and running through names like
`AluminumMill`, `AutoPlant`, `Bakery`, `Port00..11`, and `Warehouse00..11`, with a short footer
(`dc3200001437000000`) after the last entry. The trailing per-entry word is now surfaced too:
most entries carry `0x00000001`, while the currently observed zero-trailer subset is
`Nuclear Power Plant`, `Recycling Plant`, and `Uranium Mine`. That footer is tighter now too:
it parses directly as `0x32dc`, `0x3714`, and one trailing zero byte, so the shared
map/save catalog currently ends on the same two grounded late-rehydrate progress ids that the
classic staged-profile band already exposed. The strongest structural read is therefore that the
entire `0x6a70..0x73c0` catalog region is shared verbatim between `Alternate USA.gmp` and the
derived `Autosave.gms`, not rebuilt independently during save. Combined with the earlier
grounded record-layout work under `0x00437743`, `0x00434ea0`, and `0x00434f20`, the current
safest semantic read is that this shared catalog is the bundled source form of the scenario-side
named candidate-availability table later mirrored into `[state+0x66b2]`, with each entry's
trailing dword now reading as the same availability override bit later copied into
`[candidate+0x7ac]`. The loader-side coverage is tighter now too: the same table parser now
attaches both to the common-save bridge payload and directly to the fixed source range in
`.gmp` files and the non-common `rt3-105-scenario-save` / `rt3-105-alt-save` branches. That
makes the scenario variation explicit instead of anecdotal. `Alternate USA` keeps only three
zero-availability names in this table (`Nuclear Power Plant`, `Recycling Plant`, `Uranium
Mine`), `Southern Pacific` widens the zero set to twelve (`AutoPlant`, `Chemical Plant`,
`Electric Plant`, `Farm Rubber`, `FarmRice`, `FarmSugar`, `Nuclear Power Plant`, `Plastics
Factory`, `Recycling Plant`, `Tire Factory`, `Toy Factory`, `Uranium Mine`), and `Spanish
Mainline` widens it again to forty-two, including `Bauxite Mine`, `Logging Camp`, `Oil Well`,
`Port00`, and the `Warehouse00..11` run while also flipping `Recycling Plant` back to
available. The header lanes just ahead of the table vary coherently with those scenario
branches too: `Alternate USA` carries `header_word_0 = 0x10000000`, `Southern Pacific`
carries `0x00000000`, and `Spanish Mainline` carries `0xcdcdcdcd`, while the structural
fields from `header_word_2` onward remain stable (`0x332e`, `0x1`, `0x22`, `0x44`, `0x43`)
and the 9-byte footer still decodes as `0x32dc`, `0x3714`, `0x00` in all three checked maps.
A wider corpus scan over the visible `.gmp`/`.gms` files makes those two anonymous header
lanes less mysterious too: the parser currently sees only three stable
`(header_word_0, header_word_1)` pairs across 79 files with this table shape, namely
`(0x00000000, 0x00000000)`, `(0x10000000, 0x00009000)`, and
`(0xcdcdcdcd, 0xcdcdcdcd)`. The zero-availability count varies widely underneath the first and
third pairs (`0..56` under the zero pair, `14..67` under the `0xcdcdcdcd` pair), so those two
lanes no longer look like counts or direct availability payload; the safest current read is
that they are coarse scenario-family or source-template markers above the stable
`0x332e/0x22/0x44/0x43` table header, with `0xcdcdcdcd` still plausibly acting as one reused
filler or sentinel lane rather than a meaningful numeric threshold. Current exported
disassembly notes still do not ground one direct loader-side or editor-side consumer of
`header_word_0` or `header_word_1` themselves, so that family-marker read remains an
inference from corpus structure rather than a named field assignment.
The new loader-side compare command makes the save-copy claim sharper too: for the checked
pairs `Alternate USA.gmp -> Autosave.gms`, `Southern Pacific.gmp -> p.gms`, and
`Spanish Mainline.gmp -> g.gms`, the parsed candidate-availability table contents now match
exactly entry-for-entry, with the only reported differences being the outer container family
(`map` vs `save`) and source-kind path (`map-fixed-catalog-range` vs the save-side branch).
has the explicit companion `world_refresh_selected_year_bucket_scalar_band` `0x00433bd0`, which
rebuilds the dependent selected-year bucket floats after the packed year changes; and then
rehydrates the named locomotive availability collection at `[world+0x66b6]` through
@ -2407,7 +2712,57 @@ transition.
loop; inside sandbox it raises localized id `3899` `The ledger is not available in sandbox mode.`
instead. That narrows the remaining `LoadScreen.win` gap again: `TRAIN DETAIL` and
`STATION DETAIL` now read as dormant title-table entries unless some still-unrecovered nonstandard
selector reaches them.
selector reaches them. The live auto-load boundary is tighter now too: hook-driven
`shell_transition_mode(4, 0)` now completes old-mode teardown, reconstructs and republishes
`LoadScreen.win`, and returns cleanly, but the later post-transition service ticks still keep
`[0x006cec78] = 0`, `[shell_state+0x0c]` on the same `LoadScreen.win` singleton, and
`[LoadScreen.win+0x78] = 0` through at least counts `2..8`. So the next runtime edge is no
longer the old mode-`4` teardown or publish band; it is the `LoadScreen.win` message owner
`0x004e3a80` itself, because later service alone is not promoting the plain load screen into the
separate startup-runtime object path. One later runtime probe did not actually reach that edge:
the `0x004e3a80` hook installed, but the run never produced any ready-count, staging,
transition, post-transition, or load-screen-message lines, only ordinary shell node-vcall
traffic. So that result is now treated as a gate-or-cadence miss rather than as evidence against
the `LoadScreen.win` message path itself. The newer shell-state service trace tightens it again:
on a successful staged run the later service ticks do execute in `mode_id = 4`, but the
`0x004e3a80` message hook still stays silent while the state remains frozen in the plain
`LoadScreen.win` shape. So the next runtime boundary is now the shell-runtime prime call
`0x00538b60` beneath `shell_state_service_active_mode_frame` `0x00482160`, not the message owner
alone. The first direct `0x00538b60` probe run is not trustworthy yet, though: it stopped
immediately after the first shell-state service-entry line, before any ready-count or
runtime-prime entry logs. So that result is currently treated as probe validation work, not as a
real boundary move inside RT3. The static service branch is conditional too: `0x00482160` only
enters `0x00538b60` when `[shell_state+0xa0] == 0`, so silence from the runtime-prime hook does
not yet prove the shell stops before that call unless the service-entry logs also show the `+0xa0`
gate open. The newer run now closes that condition: `[shell_state+0xa0]` is `0`, and the
`0x00538b60` runtime-prime hook enters and returns cleanly. The newer run closes the next owner
too: `0x00520620` `shell_service_frame_cycle` also enters and returns cleanly on the same frozen
mode-`4` path, and the logged fields match its generic branch rather than a startup-promotion
lane (`[+0x1c] = 0`, `[+0x28] = 0`, `flag_56 = 0`, `[+0x58]` pulsed then cleared, and
`0x006cec78` stayed `0`). So the next runtime boundary under the same shell-state service pass is
now one level deeper: the per-object service walker `0x0053fda0` beneath `0x00538b60`. The newer
run closes that owner too: it enters and returns cleanly while servicing the `LoadScreen.win`
object itself, with `field_1d = 1`, `field_5c = 1`, and a stable child list under
`[obj+0x70/+0x74]`, and its first child-service vcall target at slot `+0x18` stays
`0x005595d0`. Since `0x006cec78` still stays `0` through those clean object-service passes, the
next runtime boundary is now the child-service target `0x005595d0`, not the higher object
walker. The newer child-service run narrows that again: the first sixteen `0x005595d0` calls are
stable child lanes under the same `LoadScreen.win` parent, with `[child+0x86]` pointing back to
the load-screen object, `field_b0 = 0`, and a split where earlier children carry
`flag_68 = 0x03` and return `4` while later siblings carry `flag_68 = 0x00` and return `0`. The
static body matches that read too: `0x005595d0` is gated by `0x00558670` and then spends most of
its work in draw or overlay helpers `0x54f710`, `0x54f9f0`, `0x54fdd0`, `0x53de00`, and
`0x552560`, so it now reads as another presentation-side service lane rather than the missing
startup-runtime promotion. The widened allocator-window trace then reconciled the runtime with
the static mode-`4` branch one step further: the first transition-window allocation is `0x7c`,
which matches the static pre-construct `0x48302a -> 0x53b070` alloc exactly, and the later
`0x111/0x84/0x3a/0x25...` allocations all occur before `LoadScreen.win` construct returns, so
they now read as constructor-side child or control setup. That means the allocator probe did not
disprove the still-silent startup-runtime slice; it simply exhausted its log budget inside the
constructor before the post-construct block. The later reset-at-return run is now the decisive
one: after `LoadScreen.win` construct returns there are still no further allocator hits before
publish and transition return, which matches the corrected jump-table decode because mode `4`
does not own the `0x46c40 -> 0x4336d0 -> 0x438890` startup-runtime path.
- Editor breadth: the broader map-editor page owner is now bounded through
`map_editor_panel_select_active_section` `0x004ce070` and
`map_editor_panel_dispatch_active_section_message` `0x004cf700`, which switch among the grounded