epilectrik: "the issue seems to be related to a tick rate that advances the
heat accumulation... i need to trace out what the clock origin was." Traced.
THE CLOCK ORIGIN [T1]: the DOS binary keeps its engine frame rate in the
global DAT_0052140c, set once at startup:
0x401ace: mov [0x52140c], 0x41E00000 = 28.0f (nominal)
0x401ada: mov [0x52140c], 0x4191A6E0 = 18.2065f (BIOS-tick fallback)
The DOS main pushes it straight into the ApplicationManager ctor (0x401189:
push [0x52140c]), and ~90 sites across the image fmul/fdiv by it -- it is THE
per-frame<->per-second conversion scalar of the whole 1995 engine. The engine
executes every interesting master every frame and every executable subsystem
every entity tick (UPDATE.cpp / ENTITY.cpp, T0 -- no throttle at any layer),
so subsystem Performance cadence == this rate: the pod's myomer tick was
NOMINALLY 28 Hz. My previous 30 Hz reference (the i860 BOARD frame) was
wrong by 7%, not the 2x the veterans hear.
THE RESIDUAL IS EFFECTIVE RATE, NOT NOMINAL: Oracle, on the halved build,
still asks "If you could please halve the myomer heat rate again. We will
bracket to something reasonable." A 486 host missing beats halves the real
Performance cadence without changing any constant -- and the 18.2 fallback
existing at all says slow paths were expected. Statics cannot settle the
in-pod effective rate, so his bracket is the right instrument:
effective default = 28 [T1 nominal] x 0.5 [T3 testimony calib] = 14 Hz
BT_MYO_HZ=<hz> still overrides absolutely for bracketing
the resolved rate now logs under BT_MYO_LOG, tier tags inline
Measured (Owens, seek 4, flat out): peak T 1007 -> ~640, degradation never
reached -- the halving Oracle requested, delivered as one auditable knob
instead of a silent constant edit.
KB: decomp-reference.md gains the DAT_0052140c section (~90 consumer sites --
whenever the decomp shows an unexplained mul/div by _DAT_0052140c it is
per-second<->per-frame at 28); btl4main.cpp's "30 = pod authentic" pacer note
corrected (30 kept deliberately for display smoothness, the divergence now
documented). Candidate follow-up recorded: the frame-rate-dependent particle
trail density (rendering.md) should normalize against the same 28, which may
bear on Ronin's smoke-density report (#114).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
51 KiB
id, title, status, source_sections, related_topics, key_terms, open_questions
| id | title | status | source_sections | related_topics | key_terms | open_questions | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| decomp-reference | Decomp Reference — resource types, ClassIDs, offsets, addresses, env gates | established | CLAUDE.md §10c; game/reconstructed/CLASSMAP.md; reference/decomp/all/part_*.c |
|
|
|
Decomp Reference
The quantitative hub for the reconstruction: resource-type enum, ClassID map, mech struct
offsets, key FUN_ addresses, and env gates. Cite a section here rather than re-deriving.
Everything is [T1] (decompiled + byte-verified) unless tagged otherwise. The living
source-of-truth is reference/decomp/all/part_*.c; the class map is
game/reconstructed/CLASSMAP.md.
1. Resource lookup
ResourceFile::FindResourceDescription(name, type, index=-1) (FUN_00406ff8 = find-by-type-and-name;
FUN_00407064 = SearchList(resourceID, type)). Lookup is by NAME or by ID. Mech::ResolveAnimationClip(prefix,suffix) builds a name and calls it.
⚠ A clip_id from ResolveAnimationClip is a DIRECT resource ID → FindResourceDescription(clip_id),
NOT SearchList(clip_id, 16) (which treats the arg as a LIST → walks garbage → AV). [T1]
Resource type enum
| Type | Value | Holds |
|---|---|---|
| SkeletonStream | — | .SKL skeletons |
| DamageZoneStream | 0x14 | damage-zone records (Mech ctor reads count + array) |
| GaugeImageStream | 18 (0x12) | radar PIP vector shapes (.GIM linelist; keyed by the entity model's resource id -- bhk1=1025, madcat=401, …; 110 ship in BTL4.RES). task #17 CORRECTION: the old "ModelList = model/mech names" label was wrong -- the names matched because pips are stored under their model's name/id. |
| AnimationResourceType | 16 (0x10) | animation clips (lowercase: blhrrl=904, blhrrr=905, blhwwli=910, blhrrli=916) |
| damage-zone segment list | 0x14 | per-zone segment list |
| critical-subsystem segment | 0x1e | crit subsystem segments |
| CameraStream | 27 (0x1b) | per-map spectator camera network, resource named after the map (all 8 maps ship; arena1 = 62 cams). Loaded by the CameraShip system when no cameras\<map> dev-override FILE exists (FUN_0042be3c → FUN_0042c078; records {count; per-cam {size; type 1|2; pos+quat…}} @0042ad54/@0042b9f8). open-questions §camera seat. [T1] |
Animation clip names in BTL4.RES are lowercase. [T1]
2. ClassID map (subsystem factory)
Base 3000 = 0xBB8. The factory switches on the resource ClassID; the case <Name>ClassID
LABELS are systematically mislabeled — trust the // FUN_004xxxxx ctor-address comment
reconciled via CLASSMAP.md, not the case label. [T1]
| ClassID | Real class | ctor @ | Notes |
|---|---|---|---|
| 0xBB9 | Mech | @004a2d48 (Mech::Make) |
sizeof 0x854 (binary); reconstructed 0x638 |
| 0xBBD | Condenser | @4ae568 | valve; case="?" |
| 0xBBE | AggregateHeatSink (heat-sink BANK) | @4ae8d0 | GUID 0x50e590, sizeof 0x1E4; case=SensorClassID |
| 0xBC0 | Reservoir | @4af408 | case=Condenser |
| 0xBC1 | Generator | @4b… | power bus |
| 0xBC2 | PoweredSubsystem | @004b0f74 | sizeof 0x31C |
| 0xBC3 | Sensor | — | case=MyomersClassID |
| 0xBC4 | Gyroscope | @004b3778 | LIVE (task #56: byte-exact ctor/integrators/writers; hit-bounce fan-out wired) |
| 0xBC5 | Torso | @004b6b0c | sizeof 0x280; case=SinkSource |
| 0xBC6 | Myomers | @4b8fec | case=Actuator; un-stubbed (8b36440, full recon; per-tick Sim SetPerformance call-site still best-effort) |
| 0xBC8 | Emitter | @004b1d18 | sizeof 0x478; energy weapon |
| 0xBCB | AmmoBin | — | case=JumpJet; sizeof 0x22C |
| 0xBCD | ProjectileWeapon | @4bc3fc | sizeof 0x448 (case≠ this — see VDATA.h) |
| 0xBCE | GaussRifle | @4bdcb4 | : Emitter; FireWeapon is a no-op in this build |
| 0xBD0 | MissileLauncher | @4bcff0 | sizeof 0x44C |
| 0xBD3 | SubsystemMessageManager | @0049bca4 | damage/explosion consolidation hub (ConsolidateAndSendDamage, weaponExplosions); cached to Mech[0x10d]=0x434 (✅ task #7 afefaee: factory builds the real class + mech.hpp names it messageManager; the controlsMapper mislabel is swept — the real mapper is roster slot 0). NOT the valve/Myomers gate (that's the owning BTPlayer @mech+0x190). |
| 0xBD4 | PPC (: Emitter) | — | same FireWeapon as Emitter; factory calls it GaussRifle |
| 0xBD6 | HUD | — | alloc 0x2a4; case=MechDisplay-ish |
| 0xBD8 | Searchlight | — | case=LegSubsystem |
| 0xBDC | MechTech | — | alloc 0x104 |
| 0xBDE | ThermalSight | — | case=MechDisplay |
Identify a class at runtime with sub->GetClassID() (== sub+4). [T1]
Class hierarchy (the two branches that share only MechSubsystem)
- Heat leaf:
PoweredSubsystem : HeatSink : HeatableSubsystem : MechSubsystem(Emitter/PPC/Sensor/Myomers/weapons chain here). [T1] - Watcher branch:
Torso/Gyroscope/Searchlight/ThermalSight/HUD : PowerWatcher : HeatWatcher : MechSubsystem;AmmoBin : HeatWatcher. Uses a 0xC connection + 0x54 alarm (NOT the 4/8-byte heat-leaf types). [T1]
Derivation-tag addresses (FUN_0041a1a4(obj, tag) = IsDerivedFrom)
| Tag | Class | Evidence |
|---|---|---|
| 0x511830 | MechWeapon::ClassDerivations | The reticle AddWeapon loop (part_014.c:5386) hard-aborts on missing WeaponRange/PipPosition/… attrs for every member; part_012.c:9344 counts ready weapons; the Mech ctor roster at this[0x1ef] (weaponRoster, ex-mislabel "poweredSubsystems") ORs weapon capabilityFlags@+0x334. [T1] |
| 0x50f4bc | PoweredSubsystem | btl4gau2 gauge wave. [T1] |
| 0x50e3ec | HeatableSubsystem | btl4gaug widget gate. [T1] |
| 0x50e604 | HeatWatcher (task #57 CORRECTION — was "HeatSink") | HeatWatcher::TestInstance @4aec38 tests its OWN class vs this tag; the factory watcher-CONNECT loop (slot +0x38) selects on it; the btl4gaug widget gate ORs it after HeatableSubsystem (a HeatSink test there would be redundant — HeatSink : HeatableSubsystem). 0x50e590 = the HeatSinkBank (0xBBE) tag. [T1] |
| 0x51155c | roster at Mech this[0x1eb] (heatableSubsystems label) |
tag ≠ 0x50e3ec — the label is UNVERIFIED [T4]; arbitrate before relying on it. |
| 0x50e4fc | roster at Mech this[499] (damageableSubsystems label) |
[T3] |
| 0x50bdb4 | Mech | emitter.cpp destroyed-check. [T1] |
| 0x5121a8 | ProjectileWeapon-family (checked after 0x511830; reads ammo state @+0x364) | [T3] |
Watcher electrical chain (task #57 — all [T1], several from RAW-BYTE recovery)
| Address | What |
|---|---|
| @004aec54 | HeatWatcher::CreateStreamedSubsystem — parses WatchedSubsystem=<name> → segment index +2 at resource+0xE4 |
| @004aee2c / @004b1a40 | vtable slot 14 (+0x38) CONNECT bodies (HeatWatcher / PowerWatcher+Torso, byte-identical): master-gated watchedLink(+0x114).Add(owner->roster[watchedSubsystem(+0x128)]). Ghidra missed both starts — recovered from raw exe bytes (vtables.tsv rows have GAPS at skipped slots; dump vtable+slot*4 bytes when a slot looks absent) |
| @004b181c | PowerWatcher::UpdateWatch — THE registered Performance (PTR @0050f5fc); watchdogAlarm(+0x184) MIRRORS watched subsystem's electrical level (+0x278); brownout→1 when gen outputVoltage(+0x1DC) <= minVoltage(+0x180) × ratedVoltage(+0x1D8). Torso sims call it first-line |
| @004b1804 | PowerWatcher::ResetToInitialState (slot 10) — old recon MISLABELED it "Simulation" |
| 0x4b1924 | MinVoltageScale — a 10-byte x87 literal (0a d7 a3 70 3d 0a d7 a3 f8 3f = 0.01); the port had 1.0f → permanent brownout |
| @004b6a78 | Torso::ReadUpdateRecord (slot 6; base 41bd34) — REPLICANT apply of twist extras +0x10/14/18. Old recon mislabeled it Write |
| @004b6a1c | Torso::WriteUpdateRecord (slot 7; base 41c500) — MASTER serialize; recordLength=0x1C; Ghidra missed the start |
| 41bd34 / 41c500 | base Subsystem Read (apply) / Write (produce) update-record fns — engine [T0]: Write=produce, Read=apply; don't flip |
3. Mech struct offsets (Mech*, binary layout)
Reconstructed access MUST use named members/accessors — these raw offsets are the BINARY's, valid for reading the decomp, NOT for our compiled layout (see reconstruction-gotchas). [T1]
| Offset | Field | Notes |
|---|---|---|
| +0x11c / +0x120 | damageZoneCount / damageZones[] |
inherited Entity; Mech ctor populates the array |
| +0x124 / +0x128 | subsystemCount / subsystemArray[] |
the subsystem-roster (NOT the segment table) |
| +0x190 | owning BTPlayer (GetPlayerLink()) |
set by FUN_0049f624 (mech↔player bind; also player->playerVehicle(+0x1fc)=mech). The valve guard (FUN_004ac9c8 →player+0x274 = novice lockout) + heat-model gate (FUN_004ad7d4 →player+0x260 = veteran/expert) read the owning player's EXPERIENCE flags — see experience-levels + the BTPlayer flag block below. MECH_OWNING_PLAYER. |
| +0x434 (word 0x10d) | SubsystemMessageManager cache |
the 0xBD3 damage/explosion hub (factory @10142). ✅ task #7 (afefaee): mech.hpp names this messageManager; the controlsMapper mislabel is swept — the real mapper is roster slot 0. |
| +0x260 / +0x26c | localOrigin = { Point3D linearPosition; Quaternion angularPosition } |
ORIGIN.h:15; position@+0x100 in some subsystem views |
| +0x300 | segment table | skeleton EntitySegments (muzzle sites, joints) |
| +0x37c | target world Point3D |
aim source |
| +0x388 | target Entity* |
HasActiveTarget() gate |
| +0x38c | targeted sub-zone (int) | -1 = whole |
| +0x42c | shadowJointNode (Joint*) |
ShadowJointName @model+0xB4 |
| +0x438 | torso subsystem cache | HUD/mapper read it |
| +0x5b4 | HUD subsystem cache | (factory case 0xBD6 writes param_1[0x16d]) |
| +0x650 | deathAnimationLatched / crashed flag |
|
| +0x7cc | damageableSubsystems chain |
the condenser chain (GUID 0x50e4fc) RecomputeCondenserValves iterates |
mech+0x100 (a subsystem-view of localOrigin.linearPosition): .x@+0x100, .z@+0x108. [T1]
BTPlayer (0x294) experience/game-mode flag block (ctor @4c0bc8; see experience-levels) [T1]
Seeded on the master branch from btMission(+0x1f8)->experienceLevel(+0xe4) (egg experience,
parse @4d2f3e → BTMission+0xe4) and advancedDamageOn(+0xf0). The +0x1f8 mission pointer is
the inherited Player::playerMission. Port members (issue #2): simLive/heatModelOn/
advancedDamageOn(+2)/levelFlag26c/levelFlag270/experienceLevel; bridges
BTPlayerExperienceSimLive/…HeatModelOn/BTPlayerRoleLocksAdvanced (btplayer.cpp):
| Offset | Value by level (nov/std/vet/exp) | Verified readers |
|---|---|---|
| +0x25c | 0/1/1/1 — "sim live" (novice lockout) | jam roll @4bbfcc, searchlight ToggleLamp @4b860c, powersub handler @4b0efc, mech-ineffective eval part_012.c:9364 |
| +0x260 | 0/0/1/1 — heat-model master switch | FUN_004ad7d4 → Myomers @4b8d18, Emitter, heat.cpp, mislanch, projweap heat |
| +0x264/+0x268 | = mission advancedDamageOn (both) |
heat-family sim part_013.c:8757 |
| +0x26c | 0/1/1/0 | none located (supercharge-clamp hypothesis [T4]) |
| +0x270 | 0/1/1/1 | none located |
| +0x274 | raw level 0..3 | FUN_004ac9c8 = (==0) novice predicate (valve guard), powersub @4b21d0 |
Binary ATTRIBUTE TABLES (2026-07-16, walked from the image) [T1]
Row format: 16-byte {id, namePtr, memberOffset+1, 0} (MIND THE ALIGNMENT — an 8-off walk
invents a fake one-slot shift; verified against the live-tested ConfigureActivePress row
@0x50de54). Walker: find the attr NAME string in DATA, find the pointer to it, decode ±16-byte
rows until sanity fails (python + struct, see the session commits cc2b109/21378ec).
- Mech entity table @0x50c0xx, ids 21–56: MaxAcceleration@0x344 (=forwardCycleRate),
CurrentSpeed@0x348 (=legCycleSpeed), MaxRunSpeed@0x34c (=run-cycle avg = the mapper's demand
multiplier), Collision{State,Normal,Speed,MaterialType}@0x450/0x4a8/0x4b4/0x4a4,
EyepointRotation@0x360 (the eye renderable's view rotation — the old
mechName@0x360label is a MISREAD), TargetReticle@0x36c, FootStep@0x394, AnimationState@0x39c, ReplicantAnimationState @0x714, LinearSpeed/AngularSpeed/ClimbRate/AccelerationLastFrame@0x81c/0x824/0x828/0x82c (the 15-sample-ring velocity means + the accel snapshot), damage levels @0x354-0x35c, TestButton1-6 @0x328-0x33c, ReduceButton@0x340, Radar{Range,LinearPosition,AngularPosition}@0x404-0x40c, RearFiring@0x410 (OR of the weapons' rear flags; oldstateFlagslabel was wrong), RequestDuckAnimation@0x398, UnstablePercentage@0x3f0, SuperStop@0x3f4, DuckState@0x3f8, IncomingLock@0x3fc, DistanceToMissile@0x400 (FLT_MAX far default; oldmaxSpeedlabel wrong). - HeatSink table @0x50e438, ids 3–12: HeatSink@0x164, CurrentTemperature@0x114, Degradation/FailureTemperature@0x118/0x11c, Normalized/DegradationPressure@0x120/0x124, CoolantCapacity@0x128, CoolantMass@0x12c, CoolantMassLeakRate@0x130, ReportLeak@0x138 (= coolantActive, the leak hysteresis flag). Confirms every port heat binding.
- Torso table, ids 3–15: RotationOfTorsoVertical/Horizontal@0x1e4/0x1d8, HorizontalLimit R/L@0x1dc/0x1e0, SpeedOfTorsoVertical/Horizontal@0x1ec/0x1e8 (|rate|, abs'd), StickPosition@0x1f0, TorsoUp/Down/Left/Right/Center@0x1f8-0x208, MotionState@0x20c (statusFlags; ==2 = the limit-hit frame).
- MechWeapon rows: RearFiring id 0x1B @0x334 = "the mount segment page name contains 'b'"
(marker @0x511aa2 — the back gun ports
sitelbgunport/siterbgunport; ctor part_013.c:6913-30).
4. Damage delivery
Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)→target->Dispatch(&msg).- Base
Entity::TakeDamageMessageHandlerIGNORES zone==-1 — the Mech STEP-6 cylinder-lookup override (✅ done,d07ac7d;dmgtable.cppResolveHit+Mech::damageLookupTable) resolves an unaimed (zone==-1) hit to a zone before base-routing. [T1/T2] class Damage { damageType(enum Collision/Ballistic/Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint, burstCount }. [T1]- Weapon effect id: "explode" = 13 (
Explosion::Make). [T2] DestroyEntityMessage(id,size)removes an entity — but a killed mech STAYS (a WRECK); death = a STATE transition (SetGraphicState(DestroyedGraphicState)), NOT removal. Issuing removal-on-death is the P5 teardown bug (do not). [T2]
5. Weapon fire-rate constants (recovered)
From the weapon .SUB records + the charge-curve .data constants (PE-parsed as x87 float10). [T1]
DischargeTime= beam-on time. ER-medium laser (BLH) = 0.2 s.RechargeRate= recharge time in seconds exactly (the two ln5 factors cancel): ER-S 1.0 / ER-M 2.0 / ER-L 4.0 / PPC 5.0.- One BLH ER-M cycle = 0.2 + 2.0 = 2.2 s; ~12 lasers staggered → aggregate ≈ 5/s.
WeaponRange500 (ER laser),DamageAmount3.5,PipColor1.0 0.0 0.0 (RED).- Charge constants:
_DAT_004bb3c4=1.0,_DAT_004bb3b8=1e-4,_DAT_004bb3b4=0.5; RatedVoltage (GENRATOR.SUB)=1e4; fire thresholdseekVoltage[rec]=0.8*RatedVoltage. - The electrical chain (task #10, LANDED) [T1]: ctor @004bb120 calibrates
voltageScale@0x310 = (RechargeRate / -ln(1 - 1e-4*seekV[rec])) / EC;ChargeTimeScale@004b0d50 reads it back every tick x(1 + thermalResistivityCoefficient (res+0x100, emitters 0.0024) x max(0, srcT-srcT0))-- hot generators charge slower;TrackSeekVoltage@004ba838 puts the charging I^2R (seekRate^2 x dtScale x dt) into the GENERATOR'spendingHeat@0x1c8(~3.5e8/full PPC charge).ComputeOutputVoltage@004ba738 snaps rechargeLevel@0x320 to 1.0 within 0.01. FailureHeat consumers:this+0x184(own heatAlarm level) == 2 -> emitter @004baa88 resets firing + holds charge 0; ballistic @004bbd36 pinsrecoil=rechargeRate+ alarm 7. - @004bbd04 ProjectileWeaponSimulation FULLY RECOVERED (capstone disasm, Gitea #12,
2026-07-19) [T1] — PORT NOW IMPLEMENTS IT (landed 2026-07-19, projweap.cpp; solo-verified:
missiles fly, denial blip stays Loaded, recoil clamped, slot-17 dial animates; MP re-verify
pending) — the old RivetGun-modeled body diverged; the authentic machine:
PoweredSubsystemSimulation@4b0bd0; trigger=CheckFireEdge@4b9608; UpdateEject@4bbb50;
gate 1 @4bbd36:
simulationState@0x40==1(destroyed — NOT simulationFlags@0x28!) || heatAlarm@0x184==2 || FUN_49fb54(owner disabled) → recoil=rechargeRate + alarm 7; gate 2 @4bbd71: bin=Resolve(this+0x43c)@417ab4; bin alarm LEVEL@+0x1A8 ∈{2,3} || bin simulationState@0x40==1 → alarm 7 (re-pinned per frame while dry). switch(alarm@0x364): Loaded(2) @4bbec2: on trigger —viewFireEnable@0x3E0==0 || ownerTarget@[owner+0x388]==0 → SetLevel(4);SetLevel(2)(denial BLIP, stays Loaded, NO ammo pull); elsebin->FeedAmmo@4bd4f4(fail → stays Loaded silently) → SetLevel(0 Firing) →updateModel@0x18 |= 1→ vtbl+0x48 FireWeapon → (bin level==2 → SetLevel(7)) else (CheckForJam@4bbfcc → SetLevel(5)) else SetLevel(3) →updateModel|=1→ recoil=rechargeRate. Loading(3) @4bbdd2: trigger → blip 4→3; if electricalState@0x278==4 { DrawFiringCharge@4bbc78; recoil<0 → clamp 0 + (bin level==1 → SetLevel(2)) }; always vtbl+0x44. Jammed(5) @4bbe8e: trigger → 6→5; recoil=rechargeRate. 7 (unavailable/NoAmmo) @4bbe4d: trigger → blip 1; re-SetLevel(7) unconditionally (roach motel — permanent once entered); recoil=rechargeRate; vtbl+0x44. States 0/1/4/6 = transient audio blips (no case body). vtbl+0x44 (slot 17) @004b9c9c =rechargeLevel@0x320 = (rechargeRate@0x3DC − recoil@0x3E8)/rechargeRate— the projectile recharge-dial writer (Emitter overrides this slot with ComputeOutputVoltage@4ba738). MissileLauncher::FireWeapon @004bcc60 carries NO view/target/ammo gating and NO recoil set (part_013.c:8741+: heat add + spawn descriptor only) — ALL gating lives in the caller's Loaded case. ⚠ The DIRTY-for-replication bit isSimulation::updateModel@0x18 |= 1(== engineForceUpdate()), NOTsimulationFlags@0x28 |= 1— bit 0 of simulationFlags is the engine DelayWatchersFlag (SIMULATE.h): setting it permanently skips the subsystem's ExecuteWatchers (audio watchers). Cockpit power-routing handlers @004b099c/@004b09e4/@004b0a2c/@004b0a74 (bind generator by authored id 1-4 via @004b0b18) + @004b0abc (auto/manual, modeAlarm@0x2CC); attach/detach @004b0dd8/@004b0e30 (generator client capacity @0x1e4/count @0x1e8).
The CONFIG-MODE session chain (task #6) [T1]
- MechWeapon handler table @0x511860: {9,"ConfigureMappables"→@004b9550}, {10,"ChooseButton"→@004b95b8} (strings @0x511994/@0x5119A7). PoweredSubsystem table @0x50F4EC: ids 4-8 = SelectGeneratorA-D / ToggleGeneratorMode → @004b099c/@004b09e4/@004b0a2c/@004b0a74/@004b0abc.
Binary message tables decoded 2026-07-20 (glass input audit) [T1]
16-byte HandlerEntry {id, namePtr, fnPtr, 0} rows in section_dump. Per-receiver-class id spaces.
- Mech (entity) @0x50BDF8..: 0x12 TakeDamage@004a0230, 0x14 PlayerLink@0049f624, 0x15 RealMaxSpeed@0049f604, 0x16 BalanceCoolant@0049f728 (WIRED 2026-07-21, issue #20: press-only, NO novice guard; sets every condenser valveState@0x1D0=1 then the shared redistribute @0049f788 == BTRecomputeCondenserValves; verify BT_BALTEST=1 + BT_VALVE_LOG=1**)**, 0x17 SetBurningState@0049f674, 0x18 ClearBurningState@0049f700, 0x19 EjectPilot@0049f854, 0x1a DuckRequest@0049fa00 (the manual's CROUCH; streamed button 0x13 sends it — unreconstructed).
- HeatableSubsystem @0x50E41C: {3, "ToggleCooling"→@004ad6f8}. Disassembled 2026-07-20
(
tools/disas2.py 0x4ad6f8): a per-subsystem coolant on/off TOGGLE (NOT a multi-level "cycle priority" — that's the emergent effect, not the mechanism):if (FUN_004ac9c8(this)) return;(==BTPlayerRoleLocksAdvancedNOVICE lockout, owner→ BTPlayer→+0x274==0) →if (msg->dataContents(+0xc) <= 0) return;(press only) → togglecoolantAvailable(+0x134)0↔1 andcoolantFlowScale(+0x15C)0.0f↔1.0f. So a pilot can cut a subsystem's coolant flow (raising its heat) to free the shared loop for others. UNWIRED cause found: the reconstruction'sPoweredSubsystem::GetMessageHandlerschains straight onto theReceiverroot (powersub.cpp:144), SKIPPINGHeatSink→HeatableSubsystem— so a weapon's handler chain never reaches id 3. Faithful wiring = defineHeatableSubsystem::GetMessageHandlers{3, ToggleCooling} + restore the chainPoweredSubsystem→HeatSink→HeatableSubsystem. Sibling template:Condenser::MoveValveMessageHandler(id 4, same novice guard). See experience-levels. - Generator @0x50FB90: {4, "ToggleGeneratorOnOff"→@004b1ed0} (streamed buttons 0x1A-0x1D → GeneratorA-D — unreconstructed).
- Searchlight @0x51117C: {3, "ToggleLamp"→@004b838c}; ThermalSight @0x51120C: {3,
"ToggleLamp"→@004b860c}. ⚠ CORRECTED 2026-07-25 (#61) — these were swapped here (and in
searchlight.cpp:51,hud.cpp:282), which propagated a false "1995 latent bug" across four context files; see the WAVE-4 note in subsystems. The two TUs emit PARALLEL shared-data blocks with identical stride — msg entry, +0x5C attribute entries, +0x84 Performance triple:TU msg entry handler attributes Performance Searchlight @0x51117C {3,"ToggleLamp"@0x511440} @004b838c LightOn→0x1D8, LightState→0x1E4 @004b841c (reads 0x1E0) ThermalSight @0x51120C {3,"ToggleLamp"@0x51146A} @004b860c LightOn→0x1D8, LightState→0x1E0 @004b8648 (reads 0x1DC) What pins each entry to its TU: "ToggleLamp" is NOT pooled across the two (two copies exist) and each sits immediately before its own class-name string — "Searchlight"@0x51144Band"ThermalSight"@0x511475. [T1:reference/decomp/section_dump.txt:69661-69711]@004b838c is a Ghidra EXPORT GAP (#60 list) — functions_index.tsvjumps straight to 4b83b8.RECOVERED 2026-07-25 by raw disasm of content/BTL4OPT.EXE(scratchpad/dis838c.py, theEjectAmmo technique) [T1]: if (msg+0xC > 0) [this+0x1E0] = ([this+0x1E0]==0);thenor word ptr [this+0x18],1unconditionally (==ForceUpdate(), per #59). Note it has NOnovice/+0x25C gate — that is ThermalSight-only; see experience-levels. Both sets WIRED 2026-07-25 (#61); both were default-constructed blackholes before. - Myomers-class @0x51158C: {9, "ToggleSeekVoltage"→@004b8a48}; energy-weapon variant
@0x511DB8: {0xb, "ToggleSeekVoltage"→@004ba478}; ammo-weapon @0x512210: {0xb,
"EjectAmmo"→@004bb9b8}. Both ToggleSeekVoltage handlers WIRED 2026-07-21 (issue #19):
Myomers' was an empty stub (body recovered by raw disasm of the untagged gap: guards @4ac9c8
novice + @4ad7d4 heat-model + press-only, then idx@0x320=(idx+1)%(max@0x32C+1), NO
ResetFiringState); Emitter's faithful body existed as the orphaned "AdvanceSeekVoltage" (zero
callers) -- both registered via MESSAGE_ENTRY (ids 9/0xb). Verify: BT_SEEKTEST=1 +
BT_SEEK_LOG=1 →
[seek] <name> -> gear Ncycling. EjectAmmo WIRED 2026-07-23 (issue #31): MESSAGE_ENTRY id 0xb onProjectileWeapon::GetMessageHandlers(projweap.cpp) +AmmoBin::SetAmmoStateaccessor; rig-verified both paths (BT_EJECTTEST=1tap = one round out/return to Loading,=hold= bay DUMPED;[weap] EJECT tap/[ammo] bay DUMPEDlogs). Awaiting live cockpit-button verification. EjectAmmo @004bb9b8 DECODED 2026-07-23 (raw disasm; the fn sits in a Ghidra export gap) [T1] — old-timer testimony ("tap ejects a round, hold ejects the bay, eject clears a jam") matched the bytes exactly: novice guard @4ac9c8 → PRESS (msg+0xc > 0): bin alarm (bin+0x194) → 3,timeToEject=total,percentOfEject=0,ejectState=0(UpdateEject @004bbb50 runs the ~3 s countdown; completion = DumpAmmo @004bd588 whole-bay → weaponAlarm 7). RELEASE with the countdown still live (TAP): bin alarm 4→1,EjectOneRound@004bd4f4 (count--, feedTimer reset, count<1 → bin state 2); bin empty → weaponAlarm 7, else weaponAlarm 3 (Loading) + reloadTimer restart — the weapon RETURNS TO SERVICE. A TAP is the in-mission UNJAM (state 5 has no exit inside the Execute machine, but this handler overwrites it). The earlier "jams are mission-permanent / no unjam" claim was WRONG (read only the hold path) — corrected + swept 2026-07-23 (projweap.cpp case-5/jam-log comments, players/README.txt). LATENT ARCADE BUG found + diverged (issue #21, 2026-07-21) [T1 byte-verified + T2 repro]: the Emitter charge integrates toward the GENERATOR voltage (@004ba838fld [src+0x1dc]), full is detected only inside the +-1% snap window aroundseekVoltage[idx](ComputeOutputVoltage @004ba738:|rl-1|<=0.01 -> 1.0), and OVERCHARGE reads ZERO (rl>1.01 -> 0,_DAT_004ba830 = 0.0byte-verified). Toggle the seek gear while a charge is in flight and the level lands ABOVE the new gear's window -> rechargeLevel pinned 0, the==1.0Loaded test (_DAT_004bac04) never fires, charging continues to the generator ceiling where EVERY gear reads overcharged -> the weapon is PERMANENTLY bricked (arc dark, ready-dot dark, no fire; gen re-select cannot help). The arcade shipped this (locked 60 fps + rare seek use hid it); the port's clickable seek button triggers it in seconds. Deliberate divergence (emitter.cpp Loading tick): overcharge (currentLevel > seekVoltage[idx]) counts as fully charged -> Loaded ("full == the gear's seek voltage" is the arcade's own discharge algebra). Repro + verify: BT_SEEKTEST seek-abuse -- pre-fix deadlocks at pct=0/alarm=3; post-fix 38 rescues, ends Loaded/pct=1.[seek]log prints the per-gear table + rescue events. - AMMO COOK-OFF cluster DECODED 2026-07-25 (issue #46, raw disasm
scratchpad/disammo.py) [T1]:@004bd394AmmoBinSimulation (arm on heatAlarm FAILURE / detonate / cancel-on-Empty);@004bdb94HandleMessage(1) = crit-induced arm; the FUSE =Now().ticks + round(10.0 × DAT_0052140c)— fixed 10 s (FUN_004dcd94is__ftol, the caller's x87 expression was dropped by the export — see reconstruction-gotchas §19;DAT_0052140c= SystemClock ticksPerSecond, set 28.0/18.206 at part_000.c:921/926;FUN_00414b60=Now()returning&Time, pause-aware);@004bd300CookOff = local Damage copy, amount = ammoCount ×bin+0x1F4(float —fmul, not fild), →@004ac274;@0041db7c=Damage::Damage()(== T0 DAMAGE.cpp);@004ac274=MechSubsystem::DistributeCriticalHit— statusAlarm pulse 2→1 (slot 13+0x34= @004ac8c0 theprintSimulationState@+0x104-gated state PRINT,__DefaultState/__Destroyed/__Exploding— NOT an explosion notify), own zone+0xE0→+0x158 = 1.0, then per-DamageZoneTakeDamageMessage(id 0x12, 100 B) to the owner — the collected plugs filterclassID@+4 == 0x4E=DamageZoneClassID(VDATA.h idx 78; idx 28 = AudioStateTrigger cross-checks the enum count),damageZone= zone+0x13C(damageZoneIndex),inflictingEntity= mech+0x184(SELF),inflictingSubsystemID= bin+0xD8(the explosion-bundling key);@004bc3fcProjectileWeapon ctor stampsbin+0x1F0..0x21C ← weapon->damageData@0x3A8viaowner->roster[0x128][res+0x1C0], unguarded, BEFORE MissileLauncher's ctor divides by missileCount. Env:BT_BAYTEST=<frame>(arm via message 1),[ammo] BAY FIRE / DETONATION / EXTINGUISHEDalways-on forensics,BAYBOOMmatchlog record. - GaugeAlarm / lamp-flash machinery MAPPED 2026-07-25 (issue #47, the eng-button flash) [T1]:
the shipped
BTL4.REScarries oneGaugeAlarmStreamresource (type 31) — the authored {condition, level, lampCode} annunciator data survives, baked. Chain: subsystem-alarmSetLevel→ StateIndicator gauge-watcher socket →Renderer::StartEntityAlarmMessage(id 7; Stop = 8) →GaugeAlarmManager::Activate @00448d00/Deactivate @00448e00(T0 GAUGALRM.cpp, manager at L4GaugeRenderer+0x1c0cc) → the BTL4 override bodies @004cc148.. @004cc2fc (@004cc2fcReadGaugeAlarmStreamItem: stream-read {level, lampCode}; lampCode<0x80fixed via @004cc148,≥0x80SUBSYSTEM-RELATIVE eng-button via @004cc1a0; Condenser (0x50e4fc) / power-family (0x50fb60) specials @004cc264/@004cc27c) →LampManager::FindLamp @00444c80(lamp map at renderer+0x1c0c8) →Lamp::SetAlertState @00444e64(the flash COUNTER at lamp+0x1C) → the L4 lamp flush@00474e94emits0x37/0x13=flashFastRIO states (== T0 L4LAMP.cpp:234-239) →RIO::SetLamp(binaryFUN_00476568; cached wrapper@00474d54). T0-compiled already: LAMP.cpp / L4LAMP.cpp / GAUGALRM.cpp / RENDERER messages. ✅ ALL THREE PIECES BUILT same day (see open-questions for the full closure note). Corrections pinned while building: the SENDER isMechTech::TechnicalAssistance@004ad33c (not a StateIndicator gauge watcher) — the 7-conditionGetStatusFlagsedge-scan; message ctors@00436688(Start, id 7 size 0x20) /@004366b8(Stop, id 8 size 0x1C) / broadcast@004364e4(app+0x38 manager, renderer chain +0x10);resourceID= MechTech+0x100alarmModel= the 'mechalrm' ModelList id 83 →SearchList(83, type 31)→ stream 331 (11 items, decoded: Destroyed→goto+cooling+busMode; CoolantLeaking→goto+cooling; AmmoBurning→goto+eject; Jammed→goto+eject; BadPower→goto+busMode).@004bbf88= ProjectileWeapon::GetStatusFlags (raw-disasm'd, export gap): base | 0x20 onweaponAlarm==5| 0x10 on linked-bincookOffArmed@0x18C. GUID identities: 0x50f4bc = PoweredSubsystem (via @004b1208 = its TestInstance; the old "Generator" comments were wrong — swept), 0x50fb60 = Generator, 0x50e4fc = Condenser. Lamp tables: names @0051cf1c (coolingLoop1-6, gotoEngineering 0x80, engGeneratorA-D 0x81-84, engEject 0x85, engBusMode 0x86, engCooling 0x87, engBackToQuad 0x88); quad lamps @0051cffc {0xF,0xD,0xB,0x9, 0x27,0x25,0x23,0x21, 0x7,0x5,0x3,0x1}[aux] with modes @0051cf9c; eng banks @0051d02c descend from {0xF/0x27/0x7} with modes @0051cfcc; condenser @0051d058[condenserNumber]; placement @0051d070[auxScreenPlacement] = {0x29,0x1A-0x1D}. Env:BT_LAMP_LOG=[techstat]/[galarm]/[lamp]— the full flash chain. - MechRIOMapper @0x51DD30: RE-REGISTERS the base ids 3..0x13 (Aux1Quad..ZoomOut) with the RIO override handlers @004d22fc..@004d24f8, + {0x19, Keypress→@004d2514} (RIO's OWN keypress, unreconstructed — we use the shared L4 0x17 @004d1bf0) + {0x1a, Hotbox→@004d2574} @0x51DE98. The reconstruction's enum chained these ids to 0x19-0x2a until 2026-07-20 — every streamed MFD-bank/zoom press hit the base FAIL trap; now pinned + static_assert-locked (btl4mppr.hpp).
- Unhandled ids are SILENT:
Receiver::ReceiveFind() returns NullHandler and does nothing. - MechWeapon ATTRIBUTE table @0x511890: ELEVEN entries — 0x12 PercentDone@0x320, 0x13 TriggerState@0x31C, 0x14 DistanceToTarget@0x324, 0x15 TargetWithinRange@0x34C, 0x16 WeaponRange@0x328, 0x17 PipPosition, 0x18 PipColor, 0x19 PipExtendedRange, 0x1A EstimatedReadyTime@0x330, 0x1B RearFiring@0x334, 0x1C WeaponState@0x350.
- Mapper vtables: base @0050F45C (+0x38 Enter Fail@004b0280 / +0x3C Exit Fail@004b029c / +0x40 AddOrErase-evt Fail@004b02b8 / +0x44 AddOrErase-dir Fail@004b02d4 / +0x48,+0x4C Notify @004b048c/@004b0494); L4 @0051E440 (+0x38=@004d1840, +0x3C=@004d18dc, +0x40/+0x44 = RET no-ops @004d195c/@004d1964, +0x48/+0x4C=@004d1acc/@004d1ae4); Thrustmaster @0051E3F0 (inherits the no-ops); RIO @0051E3A0 (+0x40=@004d25e8, +0x44=@004d262c). @004afbc4 = a pure Fail thunk ("Unhandled button mapping!", 0x7a).
- ControlsManager vtbl: +0x20 StartMappableButtonsConfigure / +0x24 Stop / +0x28 AddOrEraseMappableButton (T0 CONTROLS.cpp:291-351). Group vtbl: +0x0C Remove, +0x10 Update, +0x14 Add(evt), +0x18 Add(dir), +0x1C AddOrErase(evt), +0x20 AddOrErase(dir), +0x24 GetMapState (unmapped=0/others=1/mine=2).
- Mode masks (T0 BTL4MODE.HPP): ModeMapping=0x8000 (bit 15) / ModeNonMapping=0x10000 (bit 16), mutually exclusive. MechSubsystem +0xE8/+0xEC = controlDestination/controlMessageID (ctor pass-throughs; MechWeapon defaults destination=&fireImpulse@0x31C per @004b99a8). Subsystem+0x110: 0/-1 config-session flag written by handler 9 (reader unknown).
- ConfigMapGauge @004c6d80/@004c6f1c: table DAT_00518eb4 = {y,btn} {0x0d,0x45}{0x25,0x46}{0x3d,0x40} {0x55,0x47}, blit x=0xc; @004c6ee0 = the virtual GaugeBase::LinkToEntity override (vtbl @0051a1b8 slot 9 +0x24; @0x94 = linkedEntity, the Execute gate), broadcast at viewpoint bind (APP.cpp:1277 → GAUGREND.cpp:3011) — the gauge IS live in the shipped build (old "SetColor/no-caller/dormant" claim was wrong; corrected 2026-07-21).
6. Env-var gates (runtime; default OFF unless noted)
The authentic stack is DEFAULT-ON (BTEnvOn, mechrecon.hpp): BT_GAIT_CUTOVER, BT_GAIT_SM,
BT_COLLISION, BT_REAL_CONTROLS default ON — set =0 to fall back. ⚠ Most BOOLEAN gates check
getenv(name) != NULL or first-char != '0', so BT_X="" (empty) still ENABLES; use
Remove-Item Env:\BT_X. Value-parsing gates are NOT empty-enabled: BT_AUTOFIRE and
BT_KEY_NOFOCUS require '1', BT_GOTO requires a non-empty value, and BT_WARP_MIP is
default-ON ('0' disables).
| Var | Effect |
|---|---|
BT_FORCE_THROTTLE |
auto-walk forward (no key) |
BT_SPAWN_ENEMY |
spawn a target mech 120u ahead along the spawn facing |
BT_AUTOFIRE=1 |
hold the trigger (headless walk→fire→death harness; supersedes the dead BT_FORCE_FIRE — btl4main.cpp:310 set fireForced once at startup, but mech4.cpp:1567 unconditionally overwrites it every frame) |
BT_ASSERT_TO_DEBUGGER |
route CRT asserts to the debugger, not a modal box |
BT_HEAPCHECK |
whole-heap validation on every alloc/free (O(n²) at mission load — SLOW) |
BT_PROBE_BGF=<name|ALL> |
boot-time BGF loader probe, exits after |
BT_LOG=<file> |
per-instance log file (in content/) |
BT_GAUGE_ATTR_LOG |
[attr] <binding> OK/NULL per config binding |
BT_GAUGE_SKIP_LOG |
[gskip] <primitive> per unregistered gauge widget the parse skips |
BT_VALVE_LOG |
condenser valve flow distribution |
BT_BAYTEST=<frame> |
#46 rig: send message 1 (crit-induced cook-off arm) to the first AmmoBin at the given sim frame — the 10 s fuse then runs live (scratchpad/baytest.py / baypurge.py) |
BT_PLANE_AUDIT |
#48 forensics -- DEFAULT-ON (=0 opts out): logs any gauge draw whose color carries bits outside its port's plane mask ([plane] LEAK/PORT) AND any out-of-bounds draw start ([plane] OOB, clamped). First detection of each also writes a GLITCH matchlog record, so playtest evidence arrives with the round logs even unattended |
BT_LOOP_AUDIT |
[loop-audit] per-sound sample-flag × source-render-type -> AL_LOOPING (Gitea #51; rig scratchpad/loopaudit.py) |
BT_LOOP_LEGACY=1 |
restore the OLD always-loop rule (AL_LOOPING = sample != ForceStatic) for a field A/B — see the loop-flag note in wintesla-port |
BT_AUDIO_DUMP |
once-a-second [playing] dump of every playing AL source + gain/pitch/loop — catches a stuck looping source red-handed |
BT_AUD_TAIL |
[aud-tail] timestamped PLAY/STOP/fade tracing per source |
BT_SECTOR_LOG |
SectorDisplay Make/Execute |
BT_NET_TRACE |
[net-tx]/[net-rx]/[net-upd] MP tracing |
BT_DEV_GAUGES |
render the 6 pod MFD surfaces in a separate dev window |
BT_AIM="x y" |
pin the reticle crosshair (reticle coords) — headless aim harness |
BT_AIM_LOG |
[pick] ray/box/hit diagnostics (Mech::PickRayHit) |
BT_FIRE_ARC=<deg> |
OPT-IN external-camera fire-arc clamp (unset = authentic no-arc) |
BT_START_INSIDE |
begin in the cockpit view (V toggles) |
BT_FORCE_MODEL=<name> |
force the player mech (madcat/owens/…; btl4mssn.cpp — per-mech cockpit bring-up) |
BT_HIDE_COCKPIT |
hide the *_cop canopy shell (SHOWS by default — see cockpit-view) |
BT_COP_* / BT_EYE_FWD=<f> |
cockpit canopy + eye diagnostics (FRAME (unlit frame colour override, default 0.13,0.12,0.15) / SINGLE (single-sided diag; double-sided is default) / PLATES=1 (disable the punch stencil-cut kit) / FLIP / DEBUG / DUMP; eye forward-push probe) — cockpit-view |
BT_SHOT=<path.png> |
periodic backbuffer dump (every 90 frames) — non-disruptive visual verification |
BT_TLOC_LOG |
translocation-warp lifecycle log ([tloc] warp COLLAPSE/EXPAND/phase=…) |
BT_WARP_* |
translocation-warp visual tuning — all default to the verified values (see translocation-warp): EYE_UP=8.25 (on-axis), SPIN=4, TWIST=0, CONTRAST=1.0, LO_*/HI_* (lavender ramp), BLUR=0, TESS=3, MIP=1, CULL=cw, ANISO=0 |
BT_WARP_SELFTEST / BT_WARP_SELFSHOT=<prefix> |
DIAG (off by default): force a held warp in a solo game / dump backbuffer frames to disk (visual-verification harness) |
BT_GYRO_LOG |
gyro bring-up log (gyro.cpp) |
BT_GYRO_TRACE |
per-frame hit-bounce integrator trace |
BT_CRIT_PROBE=<zone> |
hammer one own-mech zone every 4s (crit-propagation diag, task #2; mech4.cpp) |
BT_REPL_LOG |
replicant/MP replication log (mech4.cpp) |
BT_GOTO="enemy"|"x z" / BT_GOTO_LOG |
self-driving beeline (to the enemy or a map coordinate) + its log |
BT_GOTO_STOP=<u> / BT_GOTO_THR=<0..1> |
beeline stop-radius / throttle overrides (Gitea #16 approach test: walk all the way in) |
BT_PARALLAX_LOG |
2 Hz [parallax]: the pick point back-projected through the RENDER view (BTProjectToRenderView, L4VIDEO.cpp) — a healthy boresight holds (rx,ry)≈(0,0) at every range (Gitea #16; combat-damage Targeting) |
BT_MP_LOG / BT_MP_NET / BT_MP_FORCE_DMG |
MP diagnostics / forced cross-pod damage |
BT_AUTODRIVE=<0..1> |
headless throttle |
BT_KEY_NOFOCUS=1 |
accept key input without window focus |
BT_KEY_STOP=1 |
enable the dev stop-mission keystroke '&' (Shift+7) — OFF by default since the tester crash report (APP.cpp; reconstruction-gotchas) |
BT_KEY_LOG |
[keych] keyboard-event delivery trace (btl4mppr.cpp) |
BT_RADAR_LOG |
radar ctor probe + 1 Hz [radar-wedge] torso-twist rot trace (gauges-hud) |
BT_SPAWN_AT="x z" |
override the spawn position (mech.cpp — MP test rigs) |
BT_RXJIT |
inbound update-record inter-arrival jitter histogram (mech4.cpp — the single-box affinity artifact) |
BT_SLIDE |
locomotion slide diag (mech.cpp) |
BT_RELAY=<host:port|auto> |
D1 relay mode — dial the relay/operator console instead of the P2P mesh; auto = LAN UDP-broadcast discovery on udp/15999 (multiplayer D1) |
BT_SELF=<[pilots] tag> |
claim a specific roster seat in relay mode; absent/auto = the relay assigns one (SEAT_REQUEST) |
BT_RELAY_TCP_ONLY=1 |
disable the relay UDP channel (all unreliable traffic rides the TCP relay conn) |
BT_INPUT_LOG |
binding-engine trace: CONTROLS.MAP load, button/keypad emissions, pad connect, torso twist/elev axis (pod-hardware §Desktop input remap) |
BT_MODE_LOG |
[mode] display notify / [mode] preset (g,i) — the ModeManager mask after each secondary-view swap (Gitea #6) or upper-MFD preset-page swap (SetPresetMode, Gitea #9) (btl4mppr.cpp) |
BT_VIEWCYCLE_TEST=<frame> / BT_MODECYCLE_TEST=<frame> |
scripted verify (mech4.cpp): pulse a secondary-schematic cycle / a control-mode cycle at the frame and every 300 after — pixel-verifiable with BT_DEV_GAUGES_DOCK+BT_SHOT |
BT_PRESET_TEST=<frame> |
scripted verify (mech4.cpp, Gitea #9): from the frame, pulse an upper-MFD preset-page cycle on all three MFDs every 120 frames (Quad → populated Eng pages → Quad) — pixel-verifiable with BT_DEV_GAUGES_DOCK+BT_SHOT |
BT_PRESET_HOLD=<n> |
(with BT_PRESET_TEST, Gitea #11) stop after n pulses so a page is HELD — steady-state pixel verification; the 120-frame cycle phase-locks with BT_SHOT's 90-frame cadence, so cycling shots periodically capture the 1-frame page-switch transition |
BT_LOOK_TEST=<frame> |
scripted verify (mech4.cpp, Gitea #12): from the frame, HOLD the rear-view button 300 frames / release 300, repeating — drives mapper→BTCommitLookState→viewFireEnable so the projectile denied-shot blip (stay-Loaded, no launch) is log-verifiable headless |
BT_SEEK_LOG |
[seek] BecameActive/replot — SeekVoltageGraph activation sentinel + clear/replot events with the response-at-12kV value (Gitea #11, btl4gau2.cpp) |
BT_CTRLMAP_LOG |
streamed .CTL record audit (L4CTRL.cpp CreateStreamedMappings): Direct records with the resolved member offset + EVENT records (button→subsystem message; found the pod's secondary-panel Cycle buttons 0x15/0x18) |
BT_CAM_LOG |
camera-seat/broadcast trace: streamed camera network count, director pick + Players-group census, ship follow state, PlayerLink dispatch/receive, ranking-window render (multiplayer camera seat) |
BT_SPEC_LOG |
manual spec-audit dump at subsystem ctor: torso speed/limits, heat-sink count, reservoir capacity, per-subsystem coolant loop (pod-hardware §Spec cross-check) |
BT_TORSO_LOG |
[torso] gate probe — why effectiveTwistRate is zero: hmOff/elec/heatState/rate/base/limits/axis/twist/wIdx/wElec/wSrc/gOut/gRated/minV (torso.cpp ~592). The elec field is the #70 tell: 4=Ready, 1=NoVoltage |
BT_MYOMERS_LOG |
[myo] per-subsystem line — elec / outV / speed / dmg / gear. Samples EVERY frame while the electrical state ≠ Ready (the NoVoltage window is ~1s and a 1 Hz probe steps right over it), else ~1 Hz |
BT_MYOMERS_REPAIR_TEST=<lvl> |
⚠ bring-up scaffolding. At frame 300 seeds this myomer's OWN zone damage to <lvl>, detaches its voltage source and forces ManualConnect so the AutoConnect hunt cannot restore power a frame later. The only way to hold the un-powered window open long enough to watch @004b8bb9. (That branch is inert — see combat-damage.) |
BT_SELF_DAMAGE=<dps> |
dispatch an unaimed TakeDamage at your OWN mech once a second, through the real Entity::Dispatch path, so the whole RESPAWN family is bench-testable solo (nothing else can kill the local pilot: BT_MP_FORCE_DMG only targets replicants). Latches off at first death so everything after the respawn is the respawn's doing, not the harness still shooting you |
BT_POWER_DETACH_TEST=<name|1> |
drop a subsystem's voltage link + force Auto, so the auto-hunt must recover it. 1 = first powered subsystem to tick; a NAME (PPC_1, Myomers) targets one, which is what proves FAILOVER to a different generator rather than a same-generator re-attach |
BT_AUDIO_SOURCES=<n> |
request n OpenAL mono sources instead of the driver default (~256). Opt-in on purpose — the cap doubles as a governor, and with EFX reverb live a higher ceiling means more simultaneous voices mixing during heavy combat. Measure frame time. See wintesla-port |
BT_SELF_DAMAGE_ZONE=<n> |
aim the BT_SELF_DAMAGE harness at an explicit zone (-1/unset = the lottery) -- ramps one zone deterministically past thresholds (#78 bench) |
BT_ARMOR_LOG |
mech ARMOUR-DARKENING trace (#87): per-zone .DZM list -> draw-op binding counts at tree build, any unmatched material name, and every change of a bound zone's level with the resulting brightness |
BT_ARMOR_FORCE=<0..1> |
pin EVERY bound armour zone to a fixed damage level, so the same scene can be rendered pristine and fully-damaged and diffed pixel-wise (scratchpad/night7/armor_ab.sh) |
BT_DRIVE_LOG |
the demand-site drive scale: [drive] n/drive/mm/dmd/mech ~1Hz (drive = myomers speedEffect x gimp; mm = the gimp level via BTMechGimpLevel) |
BT_GIMP_SPEED=<0..1> |
EXTRA gimp demand multiplier (default 1.0 = authentic since 2026-07-30: the real slowdown is the gimp gait machines' clamp to the wg clip speed — see locomotion §visible limp; the old 0.5 T3 stand-in is retired) |
BT_PICK_LOG |
#73 aimed-pick diagnostics: [segpick] the segment→zone map at tree build (index, name, zone, sphere), [pickwin] the winning zone/score/t per pick (score ~0 = threading the part core, ~1 = envelope graze) |
BT_CRIT_LOG |
#80 crit diagnostics: [subarmor] per-subsystem armour/scales/critBonus at ctor (proves the resource keys parsed + the zone got REAL scales), [critroll] per landed crit (zone, subsystem, its resulting own-zone level). NB the type-0x1e loader's [crit] tag is a different, older log |
BT_DEVICELOST_TEST=<frame>[,crashrepro] |
#35 bench hook. <frame> forces the D3D9 DEVICELOST branch at that render frame (+600/+1200 = 3 cycles), driving the REAL BTResetLostDevice recovery. ,crashrepro runs the field null-teardown shape (double ParticleEngine::Destroy) — pre-fix this reproduced the field crash byte-for-byte (Destroy +0x11, target=0x0); post-fix it must log SURVIVED. See wintesla-port §Device-loss |
Full render/locomotion gates (BT_RAMP, BT_MATPRI, BT_CULL, BT_SHADOW_*, BT_LODSEL, BT_ADDLOD, BT_PUNCH, …) are catalogued in rendering. Warp visuals: translocation-warp.
Translocation-warp addresses [T1]: POVTranslocateRenderable = L4VIDRND.cpp:1749 / .h:638; its
per-frame Execute (spin about local Z + submit, NO texcoord/ramp work) = FUN_00453dc4
(reference/decomp/all/part_007.c), using axis-rotation helper FUN_0040998c (axis idx 2 = Z).
Gyro damage fan-out [T1, re-disassembled from raw bytes] (full story: cockpit-view):
FUN_004b2980 (0x4b2980-0x4b2d8b, unexported gap) = Gyroscope::ApplyDamageResponse — args =
(Gyroscope*, Damage BY VALUE 12 dwords), caller cleans 0x34; constants: dir-eps 1e-4, sign 0.5f
(@0x4b2d84), clamp 1.3f (@0x4b2d88); vibrationDirection = gyro+0x390 (0,1,0). Call sites: the
take-damage hub @0x4a02fb (hub = FUN_004a0230, gyro kick FIRST); crushable crunch @4aa81e (torque
0.4)/@4aa86c (up 0.2); gait jolt/rumble @4aa254/@4aa288/@4aa342 (DEFERRED [T3 gates]); firing
recoil @4bc136-4bc19c (damage>3 → (0,0.6,−1.5) × damage/16; gyroRumbleTimer = mech+0x5c4 float,
ex-"clipLoadGuard"). Byte tooling: scratchpad/dis_4b2980.py / dis_range.py over
reference/decomp/section_dump.txt.
ARMOUR-DARKENING addresses [T1] (#87; full story: rendering): .DZM res compiler
FUN_0041e4e0 (part_002.c:7341, reads the material key list per [dz_*] section); runtime
consumer inside BTL4VideoRenderer::MakeMechRenderables FUN_004cef28 (part_014.c:5190-5272);
per-zone node ctor FUN_00455c7c(...,0.04f); the per-material watcher
FUN_004573e4(material, &zone->damageLevel, 0.1f) (part_007.c:9133, vtable PTR_LAB_004f1730,
232 bytes) with Perform FUN_00457784 (lerp constant @0x4579a4 = 1.0; damaged factor
0x3dcccccd = 0.1). Material accessors: dpl_GetMaterialAmbient FUN_0048baf4 /
Set FUN_0048ba98, Emissive FUN_0048bbb0/FUN_0048bb54, Diffuse FUN_0048bc6c/
FUN_0048bc10, Specular(4) FUN_0048bd2c/FUN_0048bccc, Opacity FUN_0048bdf0 (READ ONLY --
lerped but never pushed), flush FUN_0048d4d4; dpl_SetMaterialRamp @0x48bfb0 (writes
material+0x1c; ONE caller, load-time only). Material lookup by name FUN_00497678. The UNUSED
sibling path: dpl_Damagize FUN_004902b0 (tokens {0x92,0x05,0x04}), SV_SPECIAL DAMAGE
dispatch @0x4599ae -- zero shipped BGFs carry a DAMAGE token.
PUNCH firmware decode [T1] (the definitive mechanism, 2026-07-11): cmd 0x20 = vr_damage_action (VR_PROT.H enum); the i860 firmware content/VREND.MNG (code @0xf0400000) handler 'damageize' @0xf040f6f8 writes the token triple onto the geogroup's first THREE geometry-list nodes' geometry_type@+8 IN FILE ORDER (superlist appends at tail @0xf040c620); DPL_PRIV.H names the roles: f_t_dam(0x92)/sca(0x05)/undamage(0x04)/f_t_undam(0x17)/ damage_set(0x03). Punchize triple = mask, hull, reset-twin -- the visible surface is the HULL with the MASK shapes stencil-cut through it. Disassembler: scratchpad/i860dis.py.
Cockpit-eye / punch addresses [T1] (full story: cockpit-view): eye ctor
FUN_004579a8 (part_007.c:9274 = DPLEyeRenderable; offset = segment GetBaseOffset
segment+0x74, parent = parent-segment DCS; caller part_014.c:5525-66 gates on "siteeyepoint");
matrix multiply FUN_0040b104(dst,A,B) = B × A (translation from B — operand-order trap!);
euler→rotation FUN_0040ac6c; per-frame view FUN_004c22c4 (part_013.c:11742): eyeWorld from
the live view-DCS then FUN_0040b244 = affine INVERSE → VIEW (+ uniform zoom) — NO LookAt;
eye euler-change refresh FUN_00457b48 (part_007.c:9360). Punch: dpl_Punchize =
FUN_00490308 (part_011.c:6219) → FUN_0048e5b0 → FUN_00492580(0x20,…) board send, constant
tokens {0x80000003,0x80000017,0x80000004} (sibling dpl_Damagize FUN_004902b0 = same cmd,
different triple); SV_SPECIAL PUNCH dispatch part_011.c:10209/10252 → callback LAB_004596ac.
Respawn SIM path: DropZoneReply FUN_004bffd0 → Mech::Reset @0x4009fb74; CreatePlayerVehicle
FUN_004bfcac. See translocation-warp, multiplayer.
7. Tools
tools/disas2.py <VA> [len]— capstone disassembly of BTL4OPT.EXE at a VA (recovers x87 math Ghidra drops; folds known call targets + float constants). THE tool for anyFUN_the assert-anchored exporter didn't capture (e.g.@0x4ae464MoveValve,@0x4ca07cExecute).tools/disas2.py-style PE reads:scratchpad/rdva.py(4-byte VA read),rdtbyte.py(80-bit float10).reference/ghidra_scripts/ExportBTSource.java— the headless assert-anchored decompiler.reference/ghidra_scripts/DecompVSS.java— headless address-list decompiler (any skipped fn).tools/btconsole.py— the MP console emulator.tools/mapscan.py/resscan.py— content scanners.
THE ENGINE FRAME-RATE GLOBAL — DAT_0052140c = 28.0f (2026-08-02) [T1]
The DOS binary's per-frame<->per-second conversion scalar, set ONCE at startup:
0x401ace: mov [0x52140c], 0x41E00000 ; 28.0f (nominal rate)
0x401ada: mov [0x52140c], 0x4191A6E0 ; 18.2065f (BIOS-tick fallback, PIT/65536)
and passed straight into the ApplicationManager ctor by the DOS main
(0x401189: push [0x52140c]). ~90 code sites fmul/fdiv this global —
whenever the decomp shows an unexplained multiply/divide by _DAT_0052140c,
it is per-second <-> per-frame conversion at the pod's 28 Hz.
So the pod's nominal simulation cadence was 28 fps, not the i860 board's 30 Hz. Every "per tick with no dt" accumulation in the binary (the myomer kinetic heat term; candidate: particle trail density) was calibrated against 28 nominal — and less under load (the 18.2 fallback existing at all says slow paths were expected; the EFFECTIVE in-pod rate is an open question, see open-questions). Our port's frame pacer note in btl4main.cpp claimed 30 as "the pod's authentic rate" — corrected to 28.
Key Relationships
- Feeds: subsystems (factory + ClassIDs), combat-damage (damage delivery), gauges-hud (attribute binding).
- Verified against: reconstruction-method (the decomp loop), reconstruction-gotchas (why raw offsets fail).