The MadCat torso twists, the view turns with it, and targeting follows. Three reconstruction fronts closed: THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up): - PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog MIRRORS the watched subsystem's electrical level (+0x278), brownout downgrade when gen output <= minVoltage% x rated. @004b1804 relabeled ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong. - The factory watcher-CONNECT pass reconstructed (vtable slot +0x38, @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes): watchedLink.Add(roster[watchedSubsystem]) on the master node. Was the SubProxy::Start() no-op -- every watchdog sat at 0 forever. - MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f = permanent brownout) and PowerWatcher's Derivation chains its REAL base HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the whole Torso/Searchlight/ThermalSight family). - KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not "HeatSink"); the btl4gaug heat-widget gate now tests it via the BTIsHeatWatcher bridge. THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes): - The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in every twist-capable .SKL; the camera + canopy ride the same hinge subtree through HingeRenderable's live matrix-stack compose -- ALREADY WORKING in the port. The crosshair stays screen-centered (center IS the boresight); the twist reads on the tape carets/compass/radar. - The real bug was the port's gBTAimX = tan(twist) slew (the falsified "body-mounted view" model): the camera already carried the twist, so the crosshair counter-slid to hull-forward and the fire ray with it. Deleted; the pick ray inherits the twist from the yawing eye basis. - Two instrumentation traps documented (chase-eye-as-default-camera, BT_FORCE_TORSO clobbering real joints -> the hook now only fills unresolved ones); an over-correcting explicit eye compose was added on those false readings and retired the same day. CONTROLS + REPLICATION: - Q/E spring-center on release (the axis is a twist-RATE demand; the old hold-deflection model drifted forever); X also zeroes the axis and pulses the authentic torso Recenter (@004b6918). M cycles control mode via the real CycleControlMode body. - Torso update-record DIRECTION fixed: engine truth is Write=serialize / Read=apply; @004b6a78 is the READ (was mislabeled Write) and the missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C, twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD -140-degree ghost twist. - Marching-ghost desync: 4 Standing-case guards zero stale reverse cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate). - Kill credit rerouted to the OBSERVED killer (lastInflictingID -> killer's player link) -- kills count, target K/D populates. KB: subsystems.md (watcher chain), multiplayer.md (record direction), combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view re-correction), decomp-reference.md (new addresses + tag fix), open-questions.md (dead capability-roster loops 2-4, snapshot CD read). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
22 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 |
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; INERT un-stub |
| 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 (recon MISLABELS it controlsMapper; the drive squats there — the real mapper is roster slot 0). NOT the valve/Myomers gate (that's the owning BTPlayer @mech+0x190). WAVE 8 = untangle + damage-hub. |
| 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) + Myomers gate (FUN_004ad7d4 →player+0x260) read the owning player's game-mode flags (NOT the messmgr @0x434). MECH_OWNING_PLAYER. |
| +0x434 (word 0x10d) | SubsystemMessageManager cache |
the 0xBD3 damage/explosion hub (factory @10142). Our recon MISLABELS this controlsMapper + the drive squats here; 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]
4. Damage delivery
Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)→target->Dispatch(&msg).- Base
Entity::TakeDamageMessageHandlerIGNORES zone==-1 — send a VALID zone index (the Mech cylinder-lookup override for unaimed hits is not reconstructed). [T1] 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) opens withcall 0x4b0bd0(the powered/heat step), like @004baa88. 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.
- 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; SetColor @004c6ee0 has NO caller (gauge dormant in the shipped build).
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_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_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 |
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.
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.
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).