VPX_PF_WATCH (fork, cpu/paging.cpp): on a guest page fault at the watched
linear address, dump guest registers, the last 64 serial RX deliveries with
guest cs:eip at each, the code bytes at the faulting EIP and the stack top.
serialnamedpipe's doReceive feeds the ring. Armed in podrun.sh and
launch_pod.ps1.
The first catch decoded the residual fault completely: CS:EIP 00FF:000066D4
in the DPMI host, EBX = F000CA60 -- an IVT entry read as a dword, segment
F000 offset CA60, a BIOS default interrupt handler -- and the faulting access
is [EBX+0x3004], whose 0x80000000 segment-base wrap gives exactly cr2
7000FA64. The host probes a word 0x3004 bytes past a real-mode vector value
treated as a flat pointer: harmless for its own low-memory handlers, a fault
for BIOS F000:xxxx defaults. The serial ring shows a steady 1-byte/1-3ms
vRIO stream with nothing special at the fault -- the stream determines which
vectors get walked, not the crash itself. The 0x3004 appears nowhere in
DPMI32VM.OVL or 32RTM.EXE as an immediate, so the probe now also dumps code
bytes at EIP; faulthunt.sh loops runs until the next catch.
Shipped baseline streak: 4/4 clean -- consistent with exposure, not yet
discriminating.
The inside view now loads the COCKPIT skeleton: the fleet-wide X-variant
naming convention (MAD->MAX etc., all 64 skeletons present) selects the same
25-joint chain with a single object -- max_cop.bgf, the MAX_COP canopy shell
with the PUNCH-texel windows from the capture forensics. The donor names the
same mechanism from the decomp side (inside = SkeletonType_A with '_cop'
selection). Fallback to the body skeleton when no X file exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The canopy-eye confirming run (5th post-Torso-fix run with vRIO live) faulted
with the old signature during the load drain -- four clean runs then one
fault. The two-runs rule keeps earning its keep.
The BT_MAP_LOG audit now prints the whole resolved contract, not just NULLs.
The L4 list carries 13 direct mappings: mapper stick/throttle/reverse/looks
(ids 3/4/6/10-12), Torso torsoCenter (id 14, matching the donor), and
TriggerState (id 19 = the known 0x13 binding) on six weapons. Every
destination resolves and every size is right -- the 8-byte joystick write
lands in an 8-byte ControlsJoystick, buttons are ints into ints.
One type wart noted, not fixed: MechWeapon binds TriggerState onto
fireImpulse, a Scalar -- a ButtonGroup direct writes an int bit-pattern, so a
real trigger press stores 1.4e-45f. Size-safe, but the fire FSM will never
read a real button press as a pull; fire worked in tests only via the
BT_FORCE_FIRE hook, which writes a proper float. The RIO trigger needs an
int home for TriggerState.
Surviving fault theories, in current order: an emulator/DPMI-host serial-IRQ
interaction whose probability guest timing merely modulates (the fault's
constants -- fixed host EIP, fixed cr2, always during the load drain -- fit
this better than the corruption story ever did, since the smash point never
moved across builds where our BSS moved); a second corruption source outside
the direct mappings; sampling noise. riostreak.sh is measuring the post-fix
rate over four consecutive runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RecurseSKLFile now walks "site=" children. Sites get no draw component --
which is why the walk's 26 nodes matched the real capture all along: sites
were never DCS nodes -- and siteeyepoint spawns the cockpit camera with the
donor's exact construction (bt411 btl4vid.cpp:462, decomp FUN_004579a8):
offset = the site's own local rest transform, parent = the site's PARENT
joint's DCS, not the hull root. World orientation, torso twist and gait all
arrive through chain composition. The root-DCS eye remains only as the
zero-construction fallback for skeletons with no siteeyepoint.
Measured on the live pod with the RIO streaming: [eye] cockpit eye on
'jointeye', 26 nodes / 19 objects / 1 eye, and the bridge camera sits at
Y = hull + 7.7 -- the exact chain sum jointlocal 5.29 + hip 0.37 + torso 0.31
+ jointeye 1.69. canopy_eye1.png is the pilot's view, the mech's own nose
wedge visible below the sightline. Own-mech cage handling is the next layer.
Also: eye_count threading through ReadSKLFile/RecurseSKLFile, and the [skl]
summary now reports it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The [map] audit named it in one run: subsystem 17 = Torso, attribute ids 12/13
unresolved. The donor's decompiled torso.hpp carries the authentic enum with
binary offsets -- ids 3..15, including StickPosition(9), TorsoUp/Down/
Left/Right(10-13), TorsoCenter(14), MotionState(15). Our table stopped at
seven entries with MotionState at id 9, on the strength of a comment claiming
the rest were messages.
The streamed control mappings bind BY ID as direct WRITE destinations, so the
truncation produced two different crashes from one cause: in TM mode the
button ids 12/13 resolved NULL (the boot write-fault the moment the joystick
polled), and in RIO mode the analog id 9 resolved to our motionState
StateIndicator -- every analog packet from a live vRIO wrote raw floats over a
watcher-socketed object, and the corrupted chains walked into unmapped memory
~30-40s later. That was the 'intermittent' pod fault at the fixed DPMI-host
address. vRIO down = no analog = no corruption, which is why the norio conf
launched: every observation from the whole hunt drops out of this mechanism.
Fix: the full 13-entry donor table; five int command members carved from the
dynamicsState reserve (class size unchanged); ctor zeros them.
Verified, two agreeing runs each way: TM mode launches with a clean audit and
the Thrustmaster joystick driving the torso (stickY=0.907 -> torsoElev 0.349);
RIO mode with vRIO STREAMING launches, zero faults, weapons cycling.
pod_render_rec is no longer poisoned and the norio workaround is obsolete.
The audit-before-the-archive-call pattern (BT_MAP_LOG walks the same streamed
table CreateStreamedMappings consumes, naming what will not resolve) turned a
two-day intermittent-corruption hunt into a one-run lookup. The streamed RES
tables are binding contracts on our attribute enums.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pose_chase.png: the MadCat assembled from its 19 parts, standing in the arena
at its live articulated position. Feet, legs, torso, weapon pods, canopy
plate. Not a collapsed stack at the origin, which is what every prior frame
actually showed.
The convention is no longer inferred -- it is read from the engine.
RootRenderable's ctor writes an entity pose into a DCS with
*(Matrix4x4*)dpl_GetDCSMatrix(myDCS) = localToWorld, so
Matrix4x4::operator=(const AffineMatrix&) IS the wire layout: row-major,
rotation in rows 0-2, zeros in column 3, translation in ROW 3 (entries
12/13/14).
The old 3/7/11 choice rested on two errors, both corrected in the code
comments: AffineMatrix does keep translation at 3/7/11, but because it is
COLUMN-major -- citing it as precedent for a row-major layout read the storage
and ignored the indexer (AFFNMTRX.HPP:99). And 'the last row made the maths
blow up' came from the contaminated bisect; those crashes were the RIO fault.
Rotation now composes through the engine's own Matrix4x4::operator=(const
EulerAngles&) from the page's pitch/yaw/roll, and the node write uses the
RootRenderable idiom (dpl_GetDCSMatrix + assign).
Also banked: the chase-frame fifobridge variant -- camera framed on the
articulated root instead of the arena bounds -- as the offline proof harness
for any future pose question.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
deadlock it exposed
The camera gap root-caused and fixed through three layers:
LAYER 1 (ours): "chain the engine after the skeleton" was a non-fix. The
engine's MakeEntityRenderables only accepts Object/Rubble resources, so
chaining it with a Skeleton printed "wrong video resource type" and built
NOTHING -- the fifodump proved it: zero vr_flush_dcs_artic records. The real
fix mirrors the engine's Mover composition (L4VIDEO.CPP:4795-4860) inside our
mech case: a Dynamic RootRenderable (whose ctor seeds its DCS from
localToWorld and whose Execute re-flushes on change -- the ONLY source of
per-frame wire articulation), the skeleton hung UNDER its DCS via ReadSKLFile's
new parent_dcs parameter, and for the inside view a DPLEyeRenderable on that
root with the published EyepointRotation. Also explains the mystery monolith
in the first frame: the skeleton was parked at the world origin with the
camera inside its shins.
LAYER 2 (1995 library, read from our own linked symbols): dpl_DrawSceneComplete
= velocirender_frameack(0), and frameack's unsolicited-reply path prints
"dpl error - unsolicited input during frame ack", adds the 1995 authors' own
puzzled "flush artic??" when the stray action is 0x1f, and calls exit(9).
Documented before it ever fires.
LAYER 3 (emulator, the actual deadlock): with per-frame articulation flowing
for the first time, the game stopped drawing at frame 232 -- one 0x1f burst
per frame forever, no receives, no error. The VPX device feeds the frame ack
only after 6 CONSECUTIVE empty polls and reset that counter on EVERY
outputData write; per-frame articulation writes made the count unreachable, so
dpl_DrawSceneComplete never went true. Fix: the reset is gated on
!frame_outstanding -- once a draw is outstanding the only receive the game
will do next is the frame ack. The iserver-drain concern the reset guarded is
boot-time only, when no frame is outstanding.
Verified: two agreeing runs of ours on the fixed emulator (611 draws / 222
artic batches in run 2 -- the 232 wall is gone), camera travelling with the
mech (cam -329.7,0,60.5 -> -362.3,0,54.5 across 8s), anim_abs 0 -> 1 on the
bridge, view now INSIDE the cockpit cage. Shipped binary re-run on the fixed
emulator: launches and runs, no regression.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MakeEntityRenderables no longer stops after the skeleton. It builds ours AND
chains DPLRenderer, which is what creates the RootRenderable and the
DPLEyeRenderable the camera follows. Verified over three runs:
[mer] 208 class=3001 view=1 res=1
[vid] type=1 file='mad.skl'
[skl] video\mad.skl -> 26 nodes, 19 objects
[chain] into DPLRenderer, skeleton=yes, stack at 0x12dde7
No fault, mission launches, sim runs. Worth noting separately: the engine's
eyepoint construction -- prime suspect for the page fault through most of a
session -- runs perfectly here, with the stack at 0x12dde7, right beside the
main loop's frame. Both are consistent with the corrected finding that the
fault is a live-RIO artifact at a fixed DPMI host address.
The camera still does not move: two frames six seconds apart differ by zero
pixels while [sim] shows the mech travelling from (487,20,288) to (493,20,301),
and the bridge reads cam (0.0, 10.0, 0.0) -- world origin plus eye height. So
the eye renderable was necessary but not sufficient.
Eliminated by reading our own code rather than guessing: the viewpoint entity
IS the mech (BTL4APP.CPP:354), the video renderer IS linked to it
(APP.CPP:1337), and SetupCull would Fail() loudly if the mech lacked
"siteeyepoint" or EyepointRotation -- neither Fail appears. That leaves the
path from SetupCull's worldToEyeMatrix to what the bridge reads off the wire,
which is bridge territory rather than reconstruction territory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every conclusion that named EulerAngles::operator=(const LinearMatrix&) as the
fault site was wrong, including the disassembly built on top of it. Three
independent proofs:
1. ROTATION.CPP is ours, so the function was instrumented directly -- print
this, &matrix and a local's address on the first twelve calls plus any wild
pointer. The run faulted with ZERO [euler] lines: never called.
2. After the probe was added, 0x66D9 disassembles to a two-byte conditional
jump (jnl) that touches no memory. The dump reports ErrCode 0004, a data
READ.
3. EIP 0x66D9 and cr2 0x7000FA64 are identical to the byte across every build
this session, including builds where the code at that offset changed
completely. A fault in our CODE segment moves when the code moves. This
one does not -- it lives in the DPMI host, which loads low and never gets
relinked.
The map lookup was a coincidence: our _TEXT is section-relative from 0, so its
offsets overlap the host's low addresses, and the map will name a function for
any small number. Before resolving a fault address against the map, confirm it
belongs to our segment -- cheapest check is whether it moves on relink.
What is actually true, by same-binary A/B:
vRIO DOWN, serial1 still a namedpipe -> mission LAUNCHES
vRIO UP, same conf, same binary -> FAULT
The trigger is a LIVE RIO stream, not an unplugged cable. The emulator reports
continuous serial1 RX overruns, the fault lands wherever the app happened to be
(terrain one run, the mech's inside view another), and the shipped binary
survives the identical conditions. A fault at a fixed host address, at
arbitrary points in the app, requiring an interrupt-driven stream, is an
interrupt re-entrancy signature rather than a wild pointer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pod_render_norio.conf is pod_render_rec.conf with one line changed,
serial1=disabled instead of the vrio named pipe. Same binary, same egg. With
it, our build launches and the mech walks:
[launch] state=2 minPriorityEmpty=1 ticks=85316
[queues] p0=- p1=- p2=BUSY p3=- p4=-
BTL4Application::RunMissionMessageHandler
Turning Plasma Score Display On
[sim] pos=(180.466,10,-358.703) yaw=-0.275605 spd=14.4
[sim] pos=(251.959,10,-250.498) yaw=-0.89103 spd=14.4
No fault, 290 log lines and counting. This is the first time the
reconstruction has reached a running mission on the pod.
The hang was never starvation, a deadlock, or a refill loop -- it was VOLUME,
the first candidate I listed and then talked myself out of. 530 renderer
events queue at priority 0 during load; BackgroundTasks::Execute runs exactly
one task per call round-robin over seven tasks, and the 1ms frame budget is
always blown here so no extra background passes happen. That is ~9 events per
real second, so a load legitimately takes ~60s. The fault arrived at ~40s,
before the backlog could clear. Every 'the queue never drains' reading was
really 'the process dies before it can'.
The disproof was already in hand: the post tally climbed to 530 and went FLAT,
which means nothing was refilling. p0=BUSY with nextReady=1 is equally
consistent with a large finite backlog still draining, and I read it as refill.
Why the RIO port: the pipe has no server attached, which should behave as an
unplugged cable. The emulator log shows steady serial1 RX overruns, our build
prints 'RIO never came back from test mode!', and the SHIPPED binary prints
'lost RIO analog request' and launches anyway on this identical rig. So an
unplugged RIO is survivable and our handling of it is not -- a real defect, not
a rig artifact, since a pod with an unplugged RIO cable should still boot.
The wild reference at EulerAngles::operator=(const LinearMatrix&)+0x19 is still
unexplained, but it is now reproducible on demand by enabling serial1 rather
than being a coin flip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Added BT_NO_SKL, which skips the skeleton build so ONE executable can be run
with and without it. That is the only clean way to test an intermittent
fault, and it settles attribution:
walk enabled ~50% of runs die (page fault at 66D9) at DIFFERENT points
walk disabled 0 crashes in 2 runs
shipped exe 0 crashes in 2 runs, same rig and conf
So it is our new code, not the rig, and the varying fault point points at
memory corruption surfacing later rather than a bad instruction in the walk.
Hypotheses killed by reading the private library headers: the matrix array is
the right size (dpl_MATRIX is float32[4][4]); and neither s_dplobject nor the
common dpl_node header retains an object name, weakening the dangling-string
theory (a private name cache inside the library is still possible and is the
best surviving suspect).
Also checked something that would have been much worse than a crash:
~NotationFile REWRITES its file when dirty, and these are the game's shipped
.SKL data files. MAD.SKL is untouched, and ReadSKLFile now carries the
engine's own Verify(!IsDirty()) guard.
Next tests are listed cheapest-first in the roadmap: keep the NotationFile
alive, copy the Object= names, count board allocations, and re-check whether
MakeEntityRenderables runs twice per mech.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BTL4VideoRenderer answers MechClassID: walks the video-object chain the way
the engine does, hands every L4VideoObject::Skeleton entry to ReadSKLFile, and
recurses the .SKL into a dpl_DCS tree with geometry instanced onto it.
Verified repeatedly on the live pod: '[skl] video\mad.skl -> 26 nodes, 19
objects', with no 'wrong video resource type' complaint and no load failures.
Those counts are exactly what the file declares (25 joint= entries + root, 19
Object= entries).
Corrects the earlier success criterion in this file, which said 22 instances
by reading the reference capture's 'instance x22' against DZoneCount=22.
Damage zones are not geometry -- 28 dzone= tags spread across 19 objects.
Translations are written to matrix[3]/[7]/[11], MUNGA's own AffineMatrix
layout. It walks cleanly but no frame has been seen WITH the mech yet, so
the slot choice is recorded as unconfirmed. Rotation stays identity by
design: every base-pose angle in MAD.SKL is 0 or ~1e-3, so translation alone
assembles the model and isolates one convention at a time.
AND A CORRECTION I have to flag loudly: I earlier concluded from single runs
that non-identity translations crashed the pod, 'isolated' it, and 'confirmed'
the alternative also crashed. That was all noise. The same binary re-run
gives walk / crash / walk / crash -- the known intermittent plane-write defect
is now firing on ~half of pod runs and lands at different points each time,
which is precisely what made it look deterministic. Never accept a single
pod run as evidence on this rig; require two agreeing runs.
That defect is now the top of the list: a run must survive both the skeleton
build and the launch to render anything, which at ~50% is a coin flip on a
four-minute cycle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DPLRenderer::LoadMissionImplementation (L4VIDEO.CPP:6007) is not empty: it
calls DPLReadEnvironment (which opens L4DPLCFG/btdpl.ini and sets the dPL
object/material/texmap paths from the objectpath= entries) and then
LoadNameBitmaps. The 'bring-up no-op' installed earlier therefore did not do
nothing -- it REPLACED that, so the loader never learned where the art lives
and every dpl_LoadObject returned NULL.
I justified that no-op by pointing at VideoRenderer's bare Tell and
GaugeRenderer's identical one; neither is our base. Check the ACTUAL base
before overriding in this engine and chain it unless there is a reason not
to. DPLReadEnvironment is private to DPLRenderer, so chaining is the only
way a game renderer can reach it at all.
before: 40x 'couldn't load object', 'NULL instance', run ends,
wire ~24 bytes/sec
after: 0 failures, run continues, wire ~12 KB/s (1.17MB climbing),
bridge live at 88fps
Proved it was ours and not the rig by running the SHIPPED exe under the same
conf: it loaded every object. That A/B is cheap and is the right first move
whenever the pod misbehaves.
Still black: the bridge camera sits at its default (0,10,0), so the wire
carries state and object loads rather than a populated scene. The mech and
arena entities still need MakeEntityRenderables bodies.
Rig improvements, both from the user: nosound is fine on the SLOW clock (it
is the FAST SOS clock that needs the AWE32), which saves ~4 min and ~300MB of
audio taps per run; and serial3=file + '> COM3' gives a live unbuffered log,
so a run that does NOT crash is finally readable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BTL4VideoRenderer now overrides MakeEntityRenderables. Class 3035 resolves
to BTPlayerClassID (the BT enum block starts at 3000 in VDATA.HPP), and a
player carries no graphics -- exactly what the engine already does for its
own PlayerClassID with an empty case. Everything else chains to DPLRenderer,
so the override can only add answers.
On the pod: the 'couldn't figure out how to MakeEntityRenderables' complaint
is gone and the run no longer exits, it keeps running. It still renders
nothing -- the wire fifodump grows at ~24 bytes/sec, keep-alive rather than a
frame stream -- which is expected, since only the player has been answered
and the mech and arena still have no renderables.
Banked a practical problem worth fixing before the next session: a pod run
that does NOT crash produces no readable log, because the conf redirects the
game's stdout and DOS buffers it. Every readable log this session came from
a run that crashed and had its buffer flushed by the fault handler. The
RC.TXT marker (a separate command) survives that, and the com3/serial3=file
trick from the emulator notes would give live unbuffered output -- worth
wiring in, otherwise progress is invisible precisely when things are going
well.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RUNG 1: BTL4VideoRenderer::LoadMissionImplementation, Fail -> bring-up no-op.
Legal, not a cheat: the engine's own VideoRenderer version (VIDREND.CPP:259)
is a bare Tell and our working GaugeRenderer ships the same, so the renderer
comes up and runs its frame loop with an empty scene -- exactly what we want
to measure before writing content. The authentic shape for the real body is
pinned by the surviving sibling header CODE/RP/RP_L4/RPL4VID.HPP (walk the
mission entities -> MakeEntityRenderables -> ReadSKLFile/RecurseSKLFile).
RUNG 2: Mech::EyepointRotation published. The engine then Failed at
SetupCull, which fetches that attribute BY NAME off the viewpoint entity and
composes it with the siteeyepoint segment to build worldToEyeMatrix. The
mech already HAD EulerAngles eyepointRotation with a getter -- only the
publication was missing. One enum id, one table row.
The run now reaches the full mech build, constructs the renderer, boots the
board (~907K VPX wire transactions), loads the mission, runs the per-frame
cull and enters geometry loading with ZERO Fails and zero exceptions. It
stops at 'couldn't load object buttee.bgf' / 'NULL instance' -- and that is
not a defect: the models are present and BTDPL.INI points at them, but .BGF
loading goes through the board to the external GL render bridge that the VPX
HLE tees over VPX_FIFOSOCK, and that bridge was not running. 'NULL instance'
comes from the prebuilt LIBDPL.LIB refusing to instance an object that never
loaded.
Next rung is the documented full rig (render-bridge/launch_pod.ps1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PlayerStatus (dirty-gated, blits an 8-bit pixmap -- but the widget map for a
DIRTY run contains no playerStatus at all; it belongs to cameraInit and never
exists in a mech cockpit) and heat-driven redraw (a BT_FORCE_FIRE run came
back clean). Seven hypotheses killed by measurement so far.
It is a heisenbug: dirty on ~3 runs in 25, and every run since instrumenting
has been clean. The pattern tracks STARTUP LOG VOLUME -- the three dirty
runs all had the lighter conf, and a chatty trace slows init enough to
suppress it. The blit trace is therefore narrowed to fire only for pixmaps
landing on a non-palette port, which is near-silent when nothing is wrong.
That narrowed trace caught real traffic: 17x17 PixelMap8 blits onto
Mfd1/2/3 -- 8-bit pixmaps on single-bit planes. They render correctly, so
the library thresholds them; not the culprit but the right bug class.
Also banked a hunting recipe, because two detector traps cost runs: Eng1 == 0
means the cockpit is not up (not dirty), and an early capture during mission
start caught a full 3-D cockpit frame with the gauges at pod positions
(Eng1 62927) that did not recur over the next minute. BTDPL.INI is in the
mount and DPLRenderer is linked, so the dPL path may emit a frame during
launch even with L4VIDEO=OFF -- first evidence the 3-D path produces
anything, and explicitly NOT a claim that btl4vid works.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four hypotheses tried and killed by measurement, recorded so the next session
does not repeat them: misrouted eng port (traced -- resolves correctly),
OneOfSeveral's DrawPixelMap8 branch (only OneOfSeveralPixInt uses it, and all
four instances are on the 6-bit colour port where it is correct), an 8-bit
value overflowing a 6-bit field (the bits are far too high), and a displaced
copy from elsewhere in the buffer (the rows appear nowhere else).
Observed structure: constant low byte, varying high byte -- one field written
correctly while another is garbage.
The next move is an instrument, not a fifth hypothesis: trace at the VIEW
layer and log every draw whose absolute position lands in the artifact rect,
with port, colour and call site. The rect is tiny so the log is short.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same binary (md5 identical), different run: Eng1/Eng2/Comm read
17981/17981/15656 on some runs and 19011/19011/16909 on others. The shipped
binary is STABLE across three runs, so this is ours. Eleven consecutive
clean runs is what let it hide, and the dirty numbers are exactly the
pre-gate numbers from 5.3.32 -- the same artifact recurring.
Read off the raw words rather than inferred: in the artifact rect the shipped
binary writes ONE masked plane bit per pixel (0x4000 = Heat, 0x002D = sec),
while we write arbitrary 16-bit words (0x8BD6 / 0x8AD6 / 0x8AC0) that light
sec+overlay+Mfd1+Eng1+Eng2+Comm at once. Raw values going into a
plane-packed buffer -- which is why one bad draw appears as extra pixels on
six heads and as 483 MISSING on Heat at the same time.
Intermittent because these widgets are change-driven: on runs where the
coolant value never moves after the first paint, the bad draw never happens.
The six fixes of 5.3.33-39 stand (each moved specific attributable pixels).
What must be restated is the claim that four heads are exact -- true only
when this defect does not fire. The rig now needs a run-variance check
before any per-head number is believed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
I had it recorded as 'the 3-D scene head, this is btl4vid'. It is not. Port
0 carries map / headingPointer / digitalClock / numericSpeed / messageBoard /
the armour colour-mappers / the four GeneratorClusters, and its plate shows
the radar scope, the SPEED / HEADING / MISSION TIME dials and the ARMOR
DAMAGE diagram -- the pod's colour screen, the one the VDB splits out
alongside the five mono MFDs.
So sec is ordinary gauge work and is now the largest single head delta.
btl4vid is a separate deliverable this rig cannot measure at all: the 3-D
main view never enters the gauge framebuffer -- it goes down the dPL path to
the render board, and these confs run L4VIDEO=OFF, so it is not even being
produced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CFG parameter order is fine (Make already maps third/frames by name,
with a comment saying so), and we do not paint nothing: cropping the plane
shows both binaries draw the hatch, ours about three columns narrower. So
it is a level/frame-width detail, and most likely the same root as the
coolant numerals -- our coolant value differing by a step. Folded the two
into one thread and pointed it at the value rather than the widget.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Final per-head state, the binding audit (31 CFG bindings resolve, zero NULL),
and the open list in priority order: btl4vid for the sec head, then the
leak-gauge hatch (with the CFG parameter-order question that probably
explains it), the coolant numerals, and the MFD remainders.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two things, both read off the shipped framebuffer.
SLOT 0 IS THE OWN ROW. Its layout entry is the odd one out -- {180,225} at
layout mode 0, KILLS/DEATHS at +0x95/+0xe8 instead of the strip boxes'
+0x48 -- and the shipped Comm page fills it with the LOCAL pilot while the
seven surrounding boxes stay empty in a solo mission. So slot 0 takes the
mission player directly and only slots 1..7 come from the pod roster. That
also explains the artifact this session opened with: the ungated walk was
feeding the mission player into the STRIP slots, which is why gating the
roster removed wrong pixels but left the centre row missing.
THE OWN ROW TAKES THE LARGE NAME RASTER. Mission carries both; we asked for
the small one, so 'Aeolus' drew visibly undersized and low in the box. The
strip slots keep the small raster -- their boxes are half the height.
Comm missing 2099 -> 733 -> 0 extra 0 -> 152 -> 0
The Comm head is now EXACT: 15656 pixels against the shipped binary's 15656.
Four heads exact (Eng1/2/3, Comm), the three MFDs within ~150px, and the
whole cockpit at 98.9% identical / 99% coverage -- from 89.7% / 92% when the
session started.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Where the cockpit stands per head, the five defects the rig found and the
order they fell, and the measurement lesson: a large overlapping error makes
a small one measure as already-optimal, which is exactly what I recorded for
the title banner before the strip art was fixed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The class computed segmentSpan = 0.75 in its ctor and nothing ever read it:
the engine SegmentArc base has no span member, and SegmentArc270 declared no
Execute, so the recharge dial lit all 20 segments at PercentDone 1.0 where
the shipped cockpit lights 15 and leaves the quarter gap. That was the last
MFD delta -- four extra spokes at the upper-left of every weapon disc.
A derived Execute is the only place the 0.75 can reach the draw, which is the
same role SegmentArcRatio's Execute plays with its own copy. It scales the
raw fraction, draws, then restores: the connection only rewrites currentValue
when its source changes, so scaling in place would compound frame after frame
and walk the dial to zero.
head missing extra
Mfd1 140 (=) 679 -> 7
Mfd2 97 (=) 462 -> 14
Mfd3 31 (=) 461 -> 13
Cockpit: 98.2% identical / 97% coverage. The three MFD heads and the three
engineering heads are now within a few dozen pixels of the shipped binary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
y+0xb3 -> y+0xb4. Earlier this session I measured 0xb3 as the optimum and
wrote that the banner was at most 1-2px off -- that reading was confounded:
the strip-art misplacement (5.3.35) was ten times larger and overlapped the
same bands, so it dominated the metric and hid the one-row error. With the
strip art correct the title fringe isolated cleanly at 553/553 missing/extra
per head, and +1 collapsed it.
head missing extra
Mfd1 1107 -> 140 1646 -> 679
Mfd2 1616 -> 97 1981 -> 462
Mfd3 584 -> 31 1014 -> 461
Cockpit: 97.9% identical / 97% coverage, from 89.7%/92% at the start of the
session. Method note for the worksheet: fix the largest error on a head
FIRST, then re-measure the small ones -- a big overlapping defect makes a
small one measure as noise, or as already-optimal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CoolingLoopConnection::Update wrote a constant 0 -- the documented inert stub
-- because HeatSink::coolantAvailable, HeatSink::linkedSinks and
Condenser::condenserNumber are protected with no accessor, so every MFD panel
showed OFF where the shipped cockpit shows the loop number.
heat.hpp now publishes GetCoolantAvailable(), ResolveCoolingMaster() and
GetCondenserNumber(), and the connection follows the binary: a sink with
coolant reports the loop number of the Condenser it is plumbed to, otherwise
frame 0. Same shape as PowerSourceConnection beside it, which was already
live and already matched.
The box now reads 4 / A against the shipped binary's 4 / A, pixel for pixel.
head missing extra
Mfd1 1331 -> 1107 1776 -> 1646
Mfd2 1908 -> 1616 2196 -> 1981
Mfd3 730 -> 584 1119 -> 1014
Cockpit: 96.6% identical / 96% coverage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
image_names[auxScreenPlacement] (qblh0..qblh6) is the mech diagram with THIS
subsystem's segment lit, and SubsystemCluster drew it at the panel origin.
Shipped puts it in the quadrant's middle.
Solved from the framebuffer rather than guessed: our extra pixels clustered
at panel x 0..70 against shipped's missing at x 140..230, and a
cross-correlation of the two sets peaked at dx=+144 dy=-54 independently on
all three panels of the head. +144 is 0x90 exactly; y is bottom-up in this
space, so up the screen is +0x36.
head missing extra
Mfd1 4908 -> 1331 3960 -> 1776
Mfd2 6870 -> 1908 5280 -> 2196
Mfd3 3091 -> 730 2565 -> 1119
~10.9K missing and ~6.7K extra pixels recovered. The whole cockpit is now
96.4% pixel-identical / 95% coverage against the shipped binary, from
89.7%/92% at the start of the session -- and the MFD weapon panels are
visually indistinguishable apart from the state box, which still reads OFF
where shipped shows a digit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BallisticWeaponCluster's two round counters pointed at UnboundIntegerSource
because ProjectileWeapon::ammoBinLink is protected -- the documented inert
gap in the ctor ledger. ProjectileWeapon now exposes GetAmmoCount() (the
resolved bin's rounds, -1 with no bin) and the cluster copies it into a live
cell each Execute, the same pattern it already uses for jammed and
reloadSeconds. The binary pointed its NumericDisplayInteger straight at the
resolved bin's count.
The STREAK 6 panel now reads 0024 against the shipped binary's 0024, in the
same font, stencilled into the fire-ready disc the same way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MissileLauncher::DefaultData passed Subsystem::MessageHandlers and
Subsystem::AttributeIndex where every sibling weapon passes MechWeapon's --
and a launcher publishes no tables of its own, so it must chain MechWeapon's
exactly as ProjectileWeapon (its own base) does. Wired to Subsystem's, EVERY
weapon attribute on a missile launcher resolved to nothing and the gauge read
the unbound zero cell: PercentDone, TriggerState, WeaponState,
DistanceToTarget, RearFiring.
The cockpit is what found it. The shipped binary lights a fire-ready disc on
the STREAK 6 panel (WeaponCluster gates it on PercentDone >= 0.99) and ours
never did -- while the launcher itself reported state=Loaded recoil=0 level=1
bin=24. A healthy weapon behind a dead binding.
head missing before -> after extra
Mfd1 8807 -> 4991 4063 -> 4003
Mfd3 6935 -> 3174 2659 -> 2608
Mfd2 6882 -> 6870 (unchanged -- it hosts no missile panel)
~7.6K missing pixels recovered. The disc, its rays and the stencilled ammo
cells now draw; the digits themselves stay blank pending the ammo feed, which
is the documented inert gap in the BallisticWeaponCluster ledger.
Traces added, all env-gated on BT_VIS_LOG and kept as tooling: [warn]
(cluster percentDone/warn state) and [proj-state] (launcher state, recoil,
recharge rate, bin count) -- the pair that separated "broken weapon" from
"broken binding" in one run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the engineering heads exact, the entire remaining cockpit difference is
the three MFD quadrant heads -- the SubsystemCluster panels, which the widget
map shows are built by vehicleSubSystems rather than by the CFG, so they are
our code end to end.
Banked with evidence: the ammo weapon's disc+rays are absent while both
energy weapons draw theirs; the ammo digits render as empty cells at exactly
the right position; the mech silhouette is misplaced low-left; the state box
reads OFF where shipped shows a digit.
Also settled by experiment that the title banner is NOT misplaced: 0xb3 is
the measured optimum (0xb6 and 0xb0 both score worse), and the equal
missing/extra counts are overlap fringes, not an offset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gauge framebuffer is PLANE-PACKED: L4GAUGE.CFG gives each port a bit
mask, not a rectangle, so all six heads share the same x/y and are separated
only by bit -- the packing the VDB splits into the pod's physical screens.
Comparing RGB was therefore measuring nothing useful; the "magenta artifact"
just meant the wrong heads were lit at that pixel.
New instruments (emulator/render-bridge/gauge-ab/, with a README):
planes.py per-head scoreboard -- shipped vs ours, missing vs extra, per
port mask, recovering the 16-bit word from DOSBox's RGB565
BT_VIS_LOG a complete cockpit widget map from the single dispatch every
gauge widget is built through (MethodDescription::Execute),
printing name + port + authored position
ab.sh stages the fresh build over BTL4REC.EXE and kills any running
DOSBox before launching -- see below
The fix: the Comm page's pilot roster is the POD roster (the viewpoint mech's
ControlsMapper pilot array, mapper attributes 15/16), not the mission's
player list. It is zero in a solo mission, so the shipped page draws empty;
ours resolved the local player and painted its icon in colour 0xff. The gate
belongs in the row source, not PilotList::Execute -- Execute's empty branch
still has to run, because erasing is what the shipped page draws.
head shipped ours missing extra (was)
Eng1 17981 17981 0 0 (0 / 1030)
Eng2 17981 17981 0 0
Eng3 17981 17981 0 0
Comm 15656 13557 2099 0 (2099 / 1253)
Heat 21374 20913 566 105 (566 / 990)
Title-band magenta 404 -> 0. The 2099 Comm pixels we are missing were
missing before the gate, so it is a strict improvement.
Method note, which cost more than the bug: the rig's confs run BTL4REC.EXE
out of the mount while the build writes build410/btl4opt.exe, and nothing
connected the two. Three measurements ran an hour-old binary, so a change
that "did nothing" three times had never been tested -- and the correct first
hypothesis was discarded on that evidence. ab.sh now re-stages every launch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three rounds of source inspection produced three wrong suspects; one
instrumented run settled it:
[pl] PilotList slot=0 at (180,225) <- the only drawer
(no [ps] lines at all)
So the magenta blocks over the top-left title are PilotList's slot-0 entry
(the local pilot's name art + mech icon) on the Comm port -- content that
SHOULD draw; what is wrong is the entry's coordinate convention inside that
port. The port geometry itself is proven right: the Comm background
(btcomm.pcx, the KILLS/DEATHS columns) lands pixel-identical in both exes.
Second, separate finding: PlayerStatus never draws at all. The cfg builds
8 of them (sec port, 4x2, players 1..8) but our roster resolve is 0-based
against 1-based cfg player numbers, so every panel stays unbound and its
Execute returns early. Fixing that will add a whole scoreboard page, so it
belongs in an A/B session against the shipped reference rather than a blind
edit -- both leads are written up in GAUGE-BLOCK.NOTES.md.
Also recorded: the capture rig's occlusion failure mode (CopyFromScreen
grabs whatever is in front -- one run scored 77%/47% purely because an
editor covered half the window) and the PrintWindow fix for unattended runs.
Verified build, zero faults; last valid A/B measurement stands at 94.7%
identical / 92% coverage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The name art was never missing. The egg carries it as notation pages of hex
rows ([BitMap::Small::Aeolus]); the engine BitMap has a ctor that reads
exactly that format; and the AUTHENTIC Mission ctor already loads them into
smallNameBitmapChain (MISSION.CPP:525 -- implemented, called, working). The
break was ours: btl4gau3's LookupPlayerNameBitmap was a bring-up stub
returning NULL. It now uses the authentic API (GetCurrentMission()->
GetSmallNameBitmap(playerBitmapIndex)) and resolves live -- traced index=1
to a real BitMap, with the fallback-box branch provably never firing.
Coverage 91% -> 92%.
DIAGNOSIS CORRECTED for the stray magenta blocks over the top-left title:
they are NOT a missing-name placeholder. They are the REAL name bitmap
drawn at the wrong slot coordinates -- PilotList lays its 8 roster slots
out from the PE-recovered DAT_0051af88 (x,y,mode) table, and ours puts them
top-left instead of the shipped exe's centre row. Recovering that table is
the next cockpit thread; the egg-hex -> live-BitMap pipeline is verified end
to end, so what remains there is layout, not art or lookup.
(A duplicate loader written before finding the authentic one was removed --
CODE/RP/MUNGA/MISSION.CPP already does this job.)
Gauge fight 11/11, smoke and novice clean, zero faults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reading the art settled where the titles live: qsensors/qmyomers/qermlas/
qppc/qstrk6.pcc are 299x26 TITLE BANNERS, while the qblh0..7.pcc strings
the cfg hands vehicleSubSystems are 77x120 MECH ICONS (already lit by the
5.3.28 placement fix). So the authored auxScreenLabel IS the panel title
-- it just belongs on the MFD panel, not only the ENG page.
SubsystemCluster now builds a titleBanner child from the cached label.
Placement was MEASURED against the shipped framebuffer through the A/B rig
(BackgroundBitmap places by left/BOTTOM): a bring-up BT_TITLE_DY knob, then
a green-pixel correlation over the title band, then baked at x+0x09,
y+0xb3 from the panel origin. "SENSOR CLUSTER", "MYOMERS", "TYPE II 65
TONS" and both "ERMED LASER RANGE 500M" banners now render where the
shipped binary puts them.
Score progression vs the shipped cockpit on identical GAUGE content:
5.3.26 birth 90.1% identical / 82% coverage
5.3.28 authored screens 93.2% / 85%
5.3.29 titles tuned 94.8% / 91%
Also: grab.ps1 now focuses the target window before capturing -- the
screen-copy grabbed whatever was in front, which cost two junk captures
(a white frame and an unrelated 3-D app window).
Gauge fight 11/11 rounds and smoke both clean, zero faults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ran the reconstructed exe and the shipped ALPHA_1 binary against the SAME
GAUGE content, same mount, gauges on, and diffed the 640x480x16
framebuffers. OUR EXE DRAWS THE COCKPIT: 90.1% pixel-identical on the
first run, and every difference traced to a single root cause.
The authored aux-screen block (screen number / background placement /
label) lives in the subsystem resource -- which is freed with the Mech
ctor's stream buffer (the 5.3.24 use-after-free landmine). Both this tree
and the BT411 port had stubbed it, which is why BT411's displays show the
same artifacting. PoweredSubsystem now caches the block at ctor time with
accessors, and btl4gau2 uses the AUTHORED screen instead of a roster-order
stand-in and builds the placement strip art. Panels snapped onto their
authored screens -- weapon dials now land on the same panels as the
shipped exe, mech icons appear on the sensor/myomer panels, generator
letters correct -- taking the match to 93.2% identical / 85% coverage.
Remaining differences are donor-inherited stubs: the panel title art (now
proven to come from the q-strip statusImage path, not auxScreenLabel --
the label blit is wired and guarded regardless), the pilot name bitmap,
and some lamp frames.
The A/B rig is banked at emulator/render-bridge/gauge-ab/ (both confs +
the window-grab script) so the comparison is repeatable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gauges now read the real simulation: heat/power/sensor/mech attribute
tables published under the 1995 L4GAUGE.CFG spellings the widgets already
bind by name. Temperatures, coolant mass/capacity/leak rate, condenser
valve settings, generator voltages and numbers, radar percent, and the
mech's radar/speed rows all resolve to live members.
THE NUMBERING IS PINNED: the chain publishes 2..0x0E so
PoweredSubsystem::NextAttributeID lands on the authentic 0x0F -- the
surviving SENSOR.HPP numbers RadarPercent off it and MechWeapon's
binary-pinned table starts at 0x12, so its pads shrank 16 -> 3
(0x0F..0x11) exactly as the 5.3.x header comment predicted. Sensor's
authentic enum finally has its table defined; MechWeapon/Sensor rechain to
PoweredSubsystem and Reservoir/AggregateHeatSink to HeatSink so the whole
family is visible where the cfg expects it.
Verified with BT_GAUGE_ATTR_LOG: 48 OK / 2 NULL (was 33/17, and 0/50 at
the block's birth). The two remaining have no member to bind --
HeatSink/AmbientTemperature (a sim constant) and Searchlight/LightOn (a
memberless subclass) -- and fall to the documented zero cell. Gauge
fight (15/15 rounds, 127 zone hits), smoke and novice all clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six TUs born in one wave (4-agent re-hosting workflow from the BT411 donors
+ inline integration): BTL4GAUG (16 widget primitives + 5 connections, the
TU anchor @004c3f6c), BTL4GAU2 (13 composite panel clusters), BTL4GAU3
(PilotList/PlayerStatus/SectorDisplay/MessageBoard/mapping group), BTL4RDR
(the MapDisplay radar vs the authentic surviving header), BTL4GRND (the
real renderer: L4LampManager + L4Warehouse + BuildConfigurationFile with
the 19-entry BT registry chained onto the engine table). btl4.lib grows
7 -> 11 members; all 50 game TUs compile; link clean.
VERIFIED LIVE: with L4GAUGE=640x480x16 the interpreter parses the full
authentic L4GAUGE.CFG, Bhk1Init runs, ColorMapperArmor binds the live
mech's damage zones by name (cross-chassis names take the authentic inert
path), and the complete mutual missile fight runs with the cockpit stack
updating from real combat -- 16/16 rounds, 138 zone hits, zero faults.
Gauges-off regressions untouched.
Engine bring-up deviation (documented): ParseAttribute normalizes
unpublished attribute names to a shared writable zero cell -- the 1995
GaugeConnectionDirectOf ctor hard-derefs its source (first boot crashed
there); widgets on unpublished names draw static zeros and light up
automatically as the attribute waves publish the 1995 CFG spellings the
bindings already carry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three-dossier workflow output (65KB, verbatim): every class across the six
missing gauge TUs audited -- engine base, the L4GAUGE.CFG keyword + the
attribute each widget binds (and which reconstructed subsystem publishes
it), the port-shim -> 1995-native mapping, BC++4.52 hazards, and the
binary @ADDR anchors. This is the implementing session's hot-start
worksheet for the first rendered milestone (see RENDER-ROADMAP.NOTES.md).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconstructed the MechControlsMapper attribute table (StickPosition..PilotArray,
IDs 3-0x16 chained from Subsystem::NextAttributeID==2) + the real members
(stickPosition ControlsJoystick, throttle/pedals/speed/turn Scalars, the look/
torso ControlsButtons, control/display/pilotArray ints) + AttributePointers[] +
AttributeIndex (chains Subsystem::AttributeIndex). Fixed the mapper hierarchy
statics in BTL4MPPR.CPP: L4/RIO/Thrustmaster ClassDerivations now chain correctly
(L4 defined first for static-init order) and all use MechControlsMapper::
AttributeIndex so the mapper instance publishes the control attributes the
streamed mappings bind to.
Mech still constructs (33 subsystems). The post-construction crash is UNCHANGED
(GetAttributePointer, attribute=0x13) -> so it's NOT the mapper; a streamed
AttributeWatcher (numeric ID 19) resolves on another object via GetSimulation.
Next diagnostic: which object/subsystemID the watcher targets. BT: 42 ok.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reconstructed tree now produces a runnable binary with the authentic
1995 toolchain (BC4.52 / tlink32 / DPMI32):
- BTL4.CPP main TU reconstructed from the 4.11 Ghidra decomp (FUN_0040109c)
+ the 4.10 binary's own string pool + surviving RPL4TOOL.CPP house style;
probe_main.cpp scaffold retired (BTL4.NOTES.md documents every decoded call)
- L4NET.CPP staged: L4NetworkManager ctor/dtor + 10 vtable-pulled virtuals
(standalone-benign ones no-op, network ones Fail loudly) + NetNub client
globals (Net_Common_Ptr=NULL routes L4File to its plain-DOS path)
- build410.sh: libs now built in the AUTHENTIC makefile member order
(MUNGA.MAK / mungal4.mak / BT.MAK / BTL4.MAK). Order is load-bearing:
tlink emits static-init records in module pull order, and alphabetical
order booted into a null-vptr crash (IcomManager::ClassDerivations
constructing before parent NetworkClient::ClassDerivations). Also fixed
stage_link to the proven 32-bit lib set (SOSDBXC+SOSMBXC, no WATTCPLG)
- BOXTREE.HPP MemoryBlock unify, BTL4GRND notify stubs, remaining engine
backfills (audio/gauge/resource/stream TUs) that closed the deep ledger
Smoke test (DOSBox-X + 32RTM, copy of the pod BT tree, our exe swapped in):
BattleTech v4.10
BTL4Application::BTL4Application
l4net.cpp(22): L4NetworkManager -- l4net.cpp not yet reconstructed
Static init, main, -egg parse, BTL4.RES load (version 1.0.6 check passes),
ApplicationManager and the BTL4Application ctor chain all execute real
reconstructed code; boot halts at the first staged Fail() as designed.
Next brick: the real l4net.cpp body.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tlink32 campaign after the checkpoint: eleven engine bodies back-dated
from BT412 (rotation player team explode dropzone terrain cultural receiver
subsystm app l4gauge - the Application core included), the WinTesla-ectomy
handled by backdate.py's new unwrap rules (accessor-fn statics -> 1995 static
objects, decl rewrites, pointer->reference Derivation ctor, 2007 windowed-
gauge/console-marshal/idle-pump excisions), staged statics TUs opened for the
game classes (MECH/BTPLAYER/BTDIRECT/PROJTILE/MISSILE/BTL4MPPR .CPP + L4APP/
NETWORK engine statics).
State: every compiled symbol short of the shallow graph RESOLVED (the 52- and
17-symbol ledgers burned to zero); with 32stub.exe in place the linker reaches
full vtable closure and emits the deep ledger (~210 symbols, UNRESOLVED-
LEDGER.txt): remaining engine bodies (objstrm cstr gauge/gaugrend graphics
pixelmap palette resfile ray scnrole explosion-table), the DOS driver extern
layer (_SVGA*/_PCSerial*/_PCSPAK*/joystick/netnub/sosMIDI), the full L4App
body, and the real game-TU frontier (Mech::Make et al.). Recipe proven for
every category.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All 10 surviving original TUs plus the reconstructed BTL4APP.CPP pilot build
clean with the authentic OPT.MAK flags (compile410.sh --sweep). First time
the 4.10 BattleTech source has compiled since 1996.
- layout_probe.cpp: the period compiler measures 1995 layouts from the
surviving headers; validated 3/3 against binary alloc sizes (BTMission
0xFC, BTL4ModeManager 0xC, BTRegistry 0x10). Base boundary: Simulation
0xD0 / Entity 0x1C4 / Mover 0x300 / JointedMover 0x328 => Mech-own region
0x328-0x854 (MECH-LAYOUT.md staging worksheet).
- 13 staged headers close the family ([T3] reserved[]-parked layouts;
interfaces PROVEN by the surviving consumers): MECH, MECHSUB, HEAT,
POWERSUB, MECHWEAP, EMITTER, MECHMPPR, BTPLAYER (BTPlayer__MakeMessage =
Player's 8 args + roleName/teamName [T1 via BTREG]), PROJTILE, MISSILE,
BTL4MPPR, BTL4VID + the round-2 BTCNSL/BTSCNRL.
- compile410.sh: PCH gate retired (bt.hpp/mungal4.hpp full include sets),
DPL SDK roots on the include line.
- BTL4APP.CPP: appmgr include + GetApplicationManager()->GetFrameRate() +
ResourceDescription:: scoping; Fail() held at its recorded line 400.
- Evidence ledgers: STAGED-HEADERS.NOTES.md, MECH-LAYOUT.md, BACKFILLS,
BTCNSL (binary-recovered console wire IDs), BTSCNRL.
Remaining for a linkable BTL4OPT: the ~40 missing TU bodies (917-function
manifest) - the header skeleton they grow into now exists and compiles.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- BORLAND/: Borland C++ 4.52 (chosen over 4.5 by byte-match: CODE/RP/CW32.LIB
is identical to 4.52's install lib). BCC32/TLINK32/TLIB/MAKE run natively on
Win11; CODE/BT/OPT.MAK is the shipped BTL4OPT.EXE's exact flag recipe
(extender = Borland PowerPack DPMI32, not Phar Lap TNT).
- restoration/source410/: the literal 1995-form reconstruction of the missing
BT game source (never mixed into CODE/). Round 1-3 state:
* 6 of 10 surviving original TUs COMPILE CLEAN under the period toolchain
(BTMSSN, BTCNSL, BTSCNRL, BTTEAM, BTL4MODE, BTL4ARND) - first builds
since 1996.
* BT_L4/BTL4APP.CPP pilot reconstruction: 12/12 functions, Fail() lands on
its binary-recorded line 400 exactly.
* BT/BTCNSL.HPP: console wire IDs recovered from the binary's ctors
(Killed=9, Damaged=10, ScoreUpdate=13, DeathWithoutHonor=15 [T1];
TeamScore=12 flagged [T4]).
* MUNGA/: 8 engine-header backfills back-dated from the BT412 WinTesla tree
(VDATA numbering decomp-verified; AUDREND's OpenAL-era virtual removed -
the period compiler is the drift detector).
* Tooling: backdate.py (WinTesla->1995 header transform), compile410.sh
(per-TU verification sweep under authentic OPT.MAK flags).
* README: corrected roadmap - MECH.HPP is the capstone grown with the mech
TU reconstructions; BTREG.CPP green = the header-family milestone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>