Full 5-path audit of the damage economy vs the decomp (5-finder + adversarial-verify workflow), resolving the KB self-contradiction the binary-coverage audit flagged. CONFIRMED AUTHENTIC as-is (no change needed): - Energy beam (emitter.cpp): damagePortion = authored DamageAmount x (charge/seekV)^2; the ctor x1e7 and fire x1e-7 cancel (_DAT_004bafbc dumped from the exe = x87 80-bit 1e-7 exactly). - Autocannon (projweap.cpp): full authored DamageAmount from resource +0x19C delivered unmodified; the 0.0625 at :667 is the shooter's own gyro recoil, not the round. - Zone-armor BASE model: damageLevel += amount x damageScale[type] (engine DAMAGE.cpp:379, called mechdmg.cpp:427), legs x0.5, 1.0=dead. 3 STAND-INS FIXED (all byte-verified against the decomp): - A. mechdmg.cpp:451 -- read the phantom `stance` member (no binary offset, zero writers -> perma-0), so the leg-shot-out -> fall/death branch was DEAD. Now MovementMode() (mech+0x40, @part_012.c:6910). - B. mechdmg.cpp:458 -- guarded the leg partial-failure graphic on the always-0 IsAirborne() stub where the binary calls IsDisabled() (@0049fb54 = movementMode 2||9). On a wreck the binary SUPPRESSES the write; the stub let it corrupt graphicAlarm 9->4/3 -- the task-#52 wreck-graphic bug, now fixed AT SOURCE (was only masked by the IsMechDestroyed latch). - C. mech4.cpp:1551 -- flat kShotDamage=12 fed as the kill-score damageAmount (the KB self-contradiction: task #8 claimed it retired, but it was live). The score handler @0x4c02e4 derives the whole kill award from it, so every kill scored identically regardless of weapon. Now lastInflictingDamage -- the real killing-blow magnitude, latched in TakeDamageMessageHandler (mech.cpp:624), mirroring the per-hit path (mech4.cpp:1207). The phantom `int stance` slot is reused for the new Scalar member (size-neutral, no layout shift); init 0 in the ctor. DEFERRED (task #60-D, documented): the missile CLUSTER model -- the port fires N flying rounds (net armor total authentic) vs the binary's ONE missile with a random burstCount cluster roll (loses cluster variance + single-zone concentration). Blocked on an OPEN decomp semantic (does burstCount multiply armor or only the gyro kick? settle at FUN_004bef78 -> FUN_004be078 -> EXPLODE.cpp:209-210). VERIFIED live: clean build; 2-node fight -> clean center-mass kill (no crash, kills 0->1); [zone-armor] dump confirms per-zone armor 50-140 + legs x0.5. NB the displayed POINTS score still reads 0 -- a SEPARATE open gap (scoreAward + role/team/tonnage multipliers unwired); fix C corrected the damage INPUT to that formula. KB swept: open-questions.md (self-contradiction resolved + task #60 summary + deferred missile item), combat-damage.md (damageScale is type-indexed not even/odd; task-#52 source fix; kill-score section), RECONCILE.md (missile = ONE spawn not N), stale comments in mechweap.cpp (SendDamageMessage is LIVE), mislanch.hpp, mechdmg.cpp (FUN_0049fb54 = IsDisabled). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1560 lines
113 KiB
Markdown
1560 lines
113 KiB
Markdown
# BT compile-wave reconciliation notes
|
||
|
||
Cross-family header needs + link-time externs reported by family agents.
|
||
Resolve AFTER all 7 families report (Stage 1 reconciliation), then again at link (Stage 3).
|
||
|
||
## From controls/player (mechmppr, btplayer) — DONE, 0 errors
|
||
|
||
### Cross-family header changes needed
|
||
- **mech.hpp (mech-core family)**: expose on `Mech` — `tonnage` (+0x4bc), `damageBiasFactor` (+0x354),
|
||
`owningPlayer` (+0x190, `BTPlayer*`). Currently reached via raw offset macros in btplayer.cpp.
|
||
- **ScenarioRole (engine SCNROLE.h)**: needs decrement of `returnFromDeath`
|
||
(used `SetReturnFromDeath(GetReturnFromDeath()-1)`); no `returnDelay` field (role+0xf0) →
|
||
`roleReturnDelay` stubbed to 0.
|
||
- **Player__VehicleDeadMessage**: BT build carried `killerName` (+0x1c) the engine base lacks; read via offset.
|
||
|
||
### Stubbed (no WinTesla analog) — revisit at integration
|
||
- BTPlayer ctor role/mission-registry resolution: `GetMissionRegistry()` / `GetRoleRegistry()->Lookup()` /
|
||
`GetMission()->GetGameModel()` have no WinTesla analog → stubbed; base-set `scenarioRole` stands.
|
||
|
||
### Link-time externs to be PROVIDED by owning modules (Stage 3)
|
||
- `ToggleVoiceAssist`, `Is_Destroyed` (mechmppr)
|
||
- `Set_Alarm_Level` (GaugeAlarm has no `SetLevel` in WinTesla)
|
||
- `Notify_Objective_Reached`, `StatusMessagePool`
|
||
- `BTPlayerVTable`, `BTStatusMessageVTable`, `SelfDestructName`, `ResetDelay`, `TicksPerSecond`
|
||
|
||
## From gauges/HUD (btl4gaug, gau2, gau3, galm, hud) — DONE, 0 errors
|
||
|
||
### Cross-family header changes needed
|
||
- **mech family**: `LWord BT_GetSegmentFlags(Mech*)` — declared `extern` in hud.cpp (mirrors powersub's
|
||
`BT_GetMessageManager` pattern); needs a real definition in the mech layer.
|
||
- **powersub/heat family**: HUD inherited watchdog accessors stubbed to return 0 — bind to real inherited
|
||
members once exposed: heat-state level @0x140, watched-voltage alarm level @0x198, heat-watcher flag @0x40.
|
||
|
||
### Foundation fix worth doing properly
|
||
- `DebugStream` is double-defined incompatibly: `heat.hpp` (`std::ostream&`) vs `mechrecon.hpp` (`ReconStream`).
|
||
Gauge .cpps work around it by pre-defining `BT_DEBUGSTREAM_DEFINED` to suppress heat.hpp's variant.
|
||
→ settle on ONE definition in the shared foundation. (NOTE: a `mechrecon.hpp` exists — investigate.)
|
||
|
||
### Stubbed (unrecovered .data) — revisit
|
||
- Gauge resource-pool config (rates/modes/colours/image-names) not recovered from the optimised image →
|
||
typed placeholder statics. HUD `FindAttributeIndex` shim (no engine analog).
|
||
|
||
## From heat/power (heat, heatfamily_reslice, powersub, gnrator) — DONE, 0 errors
|
||
|
||
OWNS the subsystem base classes. Established the authoritative base virtual surface (HeatableSubsystem/
|
||
HeatSink/Condenser/Reservoir/PoweredSubsystem/Generator/Myomers/PowerWatcher/HeatWatcher) — see report:
|
||
GetClassDerivations()/DefaultData 4-arg idiom; HeatAlarm/HeatFilter/SubsystemConnection wrappers in heat.hpp.
|
||
|
||
### Cross-family header changes needed
|
||
- **mech family** — provide on `Mech` (declared as extern prototypes `BT_GetMessageManager/BT_IsBusLive/
|
||
BT_ClearMyomers(Mech*)` in powersub.hpp so TUs link):
|
||
- `Mech::GetMessageManager()` → subsystem message manager (`owner+0x190`)
|
||
- `Mech::IsBusLive()` → power-bus-live flag (`*(owner+0x190)+0x274`)
|
||
- `Mech::ClearMyomers()` → clears myomer back-pointer (`owner+0x374`)
|
||
- Cockpit HUD gauge fields `field_438`/`field_5b4` used by `Condenser::MoveValve` (stubbed null).
|
||
|
||
## From btl4-app (btl4app/mssn/rdr/grnd/vid/pb/mppr, btdirect) — DONE (4 clean, 4 only cross-family residual)
|
||
|
||
btl4mssn/grnd/vid/direct = 0. btl4app=1, btl4mppr=2, btl4pb=3, btl4rdr=1 — ALL residuals are in
|
||
mech.hpp / mechrecon.hpp (not owned). Will clear when mech family + mechrecon are reconciled.
|
||
|
||
### Cross-family header changes needed
|
||
- **mech.hpp — add to `class Mech`** (offsets from binary oracle):
|
||
- `void SetMappingSubsystem(Subsystem*);` (btl4app.cpp:567)
|
||
- `void Reset();` + `void MissionReviewReset();` (btl4pb.cpp:555)
|
||
- `Logical GetMissionReviewMode();` (reads mech+0x414, btl4mppr.cpp:366)
|
||
- `void SetTargetRange(Scalar);` (writes mech+0x404, btl4mppr.cpp:407)
|
||
- (non-blocking) egg fields `vehicleColor`/`vehicleBadge`/`vehiclePatch` (mech+0x848/0x844/0x84c)
|
||
- **mechrecon.hpp(89)** — declares `Random` as a FUNCTION, collides with engine `extern RandomGenerator
|
||
Random;` (RANDOM.h:40). RENAME/REMOVE it — sole residual blocking btl4rdr.cpp. *Investigate what
|
||
mechrecon.hpp is + who owns it (mentioned by gauges/HUD + btl4-app).*
|
||
|
||
## From mech-subsystems (gyro/torso/myomers/sensor/mechtech/searchlight/thermalsight/messmgr) — DONE, 0 errors
|
||
|
||
`mechrecon.hpp` confirmed = emergent SHARED COMPAT-SHIM header (provides ReconAlarm etc.); families lean
|
||
on it. → CONSOLIDATE + settle ownership in reconciliation (also the `Random`-function bug above).
|
||
|
||
### Cross-family header changes needed (locally shimmed; real homes are shared bases)
|
||
- **Mech/Entity/Subsystem**: `Word GetSegmentFlags() const` (used by 6 files); `Mech::ResolveJoint(const
|
||
char*)`; segment-roster accessors; `Mech::GetSegmentCount()`.
|
||
- **HeatSink**: `HeatState HeatStateLevel() const`, `Logical HeatModelOff() const`, public `heatEnergy` accessor.
|
||
- **PoweredSubsystem**: `ElectricalState ElectricalStateLevel() const`.
|
||
- **PowerWatcher**: confirm `ResetToInitialState()`/`HandleDeathMessage(Message&)`/`UpdateWatch()`↔
|
||
`Simulation(Scalar)`/`WatchedVoltageLevel()`/`HostShutDown()` signatures (now derives HeatableSubsystem).
|
||
- **Subsystem**: `GetExplosionID()`; renderer/clock globals for messmgr explosion submission.
|
||
- **powersub.hpp**: DELETE stale mislabeled `Myomers`; **RegisteredClass**: add `MyomersClassID == 0xBC6`
|
||
(enum still has the 0xBC3 mislabel = really Sensor).
|
||
|
||
## From mech-core (mech/mech2/3/4/mechsub/mechdmg/dmgtable) — PARTIAL (4 of 7 clean)
|
||
|
||
CLEAN: mech2, mech4, mechsub, dmgtable. BLOCKED: mech.cpp, mech3.cpp, mechdmg.cpp.
|
||
**OWNS `mechrecon.hpp`** (the shared compat shim) → fix the `Random`-function collision HERE.
|
||
Added ~60 Mech members (gait/locomotion/clip/sim) to mech.hpp.
|
||
|
||
### The big remaining integration task — THE SUBSYSTEM FACTORY
|
||
mech.cpp ctor + mech3.cpp `CreateSubsystemStream`/`CreateSubsystemsStream`/`SubsystemDefaultData` dispatch
|
||
over ~21 sibling subsystem classes. Each needs, uniformly:
|
||
`static <X> CreateStreamedSubsystem(NotationFile*, const char*, const char*, SubsystemResourceBlob*,
|
||
NotationFile*, const ResourceDirectories*, int)` AND `static SharedData DefaultData`
|
||
Classes: HeatSink, Reservoir, Condenser, PoweredSubsystem, Myomers, Generator, HUD, Searchlight,
|
||
ThermalSight, Sensor, Gyroscope, Torso, Emitter, PPC, ProjectileWeapon, MissileLauncher, GaussRifle,
|
||
AmmoBin, MechControlsMapper, MechTech, SubsystemMessageManager (+ mech.cpp-only: Cockpit, HeatSinkSource,
|
||
Actuator, JumpJet, LegSubsystem, MechDisplay).
|
||
→ This is a dedicated follow-up pass AFTER all sibling headers are final (needs their headers included in
|
||
mech.cpp/mech3.cpp + the uniform static signatures present). mech.cpp also has ~180 raw `this[0xNN]`
|
||
pseudo-assignments to convert, and ChainOf<> default-ctor/SetOwner/iteration semantics.
|
||
- mechdmg.cpp: mostly reducible (Mech__DamageZone members structureLevel/graphicState/flags + word
|
||
accessor; Read→ReadBytes); + Generator cross-family dep (powersub) + engine skeleton-walk types
|
||
(SegmentRecord/SegmentIterator/DZRef).
|
||
|
||
## From weapons (mechweap/emitter/projweap/mislanch/missile/seeker/projtile/misthrst/ammobin) — DONE, 0 errors
|
||
|
||
No blocking cross-family needs (adapted to heat/powersub header changes live). Watch points:
|
||
- `mechweap.cpp` calls `PoweredSubsystem::GetClassDerivations()` — must track if powersub reverts.
|
||
- `ammobin.hpp` includes `<heatfamily_reslice.hpp>` (where HeatWatcher is actually defined), NOT
|
||
`<heatfamily.hpp>` (forwards to heat.hpp, no HeatWatcher). Follow if HeatWatcher moves.
|
||
- Added per-class `X__UpdateRecord : Base::UpdateRecord`; `UpdateTargeting()` now returns `Logical`.
|
||
|
||
## ============ WAVE COMPLETE: 36/43 clean. Remaining: 4 btl4-app (reconcile) + 3 mech-core (factory) ============
|
||
|
||
## RECONCILIATION PASS 1 done → 40/43 clean
|
||
- mechrecon.hpp `Random()` template → renamed `RandomUnit()` (collided w/ engine `extern RandomGenerator
|
||
Random`); updated dmgtable.cpp 2 call sites. btl4rdr now 0.
|
||
- mech.hpp: added Mech app/cockpit interface — `SetMappingSubsystem(Subsystem*)`, `GetMissionReviewMode()`,
|
||
`SetTargetRange(Scalar)`, `Reset(const Origin&, int mode)`, and `enum ResetMode { MissionReviewReset=0 }`.
|
||
CORRECTION: MissionReviewReset is a MODE CONSTANT (0), not a method (call site proved it).
|
||
→ btl4app/mppr/pb all 0. (Definitions of these Mech methods still owed by mech.cpp at link time.)
|
||
|
||
REMAINING: mech.cpp, mech3.cpp, mechdmg.cpp (the subsystem factory) → dedicated agent.
|
||
|
||
## RECONCILIATION PASS 2 + STAGE 2 PREP (overnight)
|
||
- Factory agent #1 stalled (stream watchdog) with mech.cpp at 67 (from >100); mech3/mechdmg untouched.
|
||
It regressed hud.cpp (0→1) via a `ZeroVector` it added to mechrecon.hpp:181 → FIXED by renaming
|
||
hud.cpp's local `ZeroVector`→`HudZeroVector` (kept mechrecon's for the mech files). hud.cpp back to 0.
|
||
- Resumed factory agent (a52bd3052af4ade1a) from transcript with file-by-file + verify-often guidance.
|
||
- STAGE 2 (executable) staged, all non-conflicting:
|
||
- `btbuild/btl4main.cpp` = WinMain launcher cloned from RP_L4/RPL4.CPP, BTL4Application path, BTL4.RES,
|
||
800x600 window, RP-only review/playback branch dropped. COMPILES CLEAN (0 errors).
|
||
- `btbuild/CMakeLists.txt` now also defines exe target `btl4` (WIN32): links bt410_l4 + munga_engine.lib
|
||
(enginebuild/build/Debug) + OpenAL32/libsndfile-1 (trunk/lib) + d3d9/d3dx9/dinput8/dxguid/DxErr
|
||
(DXSDK Lib/x86) + winmm/dbghelp/shell32/user32/gdi32/ole32. CMake CONFIGURES clean (Win32 Debug).
|
||
- Version bytes hardcoded {4,1,0} — TODO verify vs BTL4.RES header at runtime.
|
||
|
||
## NEXT (once factory lib is green): cmake --build → if lib green, link btl4.exe → triage undefined
|
||
symbols (Stage 3) → define/reconstruct or stub-with-flag → iterate to a linked exe → attempt boot.
|
||
|
||
## ====== ALL 43 RECONSTRUCTED MODULES COMPILE (factory done by stopped-but-finished agent) ======
|
||
## FULL BUILD #1: 0 compile errors, 45 unresolved externals at link. Buckets:
|
||
## - ~25 Winsock (L4NET) → FIXED: added ws2_32 to btl4 link.
|
||
## - Surviving BT source not compiled in (BTMission/BTRegistry/BTL4ModeManager/Gauss/PPC/BTTeam/
|
||
## BTScenarioRole/BTConsole) → FIXED: added BTMSSN/BTREG/BTTEAM/BTSCNRL/BTCNSL/GAUSS/PPC/
|
||
## BTL4MODE/BTL4ARND .CPP to the bt410_l4 sources.
|
||
## FULL BUILD #2: surviving files have 12 compile errors (410-era ClassDerivations→GetClassDerivations
|
||
## API drift; BTSCNRL needs btscnrl.hpp; BTMSSN Exit()). Link not reached yet.
|
||
## STILL-UNDEFINED (buckets 3+4, expected after survivors compile) — provide real def or flagged stub:
|
||
## Mech::{GetMissionReviewMode,SetTargetRange,SetMappingSubsystem,Reset,IsAirborne,RaiseStatusAlarm},
|
||
## Mech__DamageZone::LoadCriticalSubsystems, MechSubsystem::{TakeDamage,OnAlarmChanged,HandleMessage},
|
||
## Is_Destroyed, ToggleVoiceAssist, Generator::{ClassDerivations,ForceShortRecovery},
|
||
## FrameTimeScale, allPresets, BTL4MethodDescription, BTL4AudioRenderer ctor, BTL4VideoRenderer ctor.
|
||
|
||
## ====== FULL BUILD #3: ✅ btl4.exe LINKS (build/Debug/btl4.exe, 1,849,344 bytes, valid PE) ======
|
||
## 0 compile errors, 0 unresolved externals. Achieved via:
|
||
## TASK 1 (12 compile errors in surviving 410-era files — all FIXED):
|
||
## - GAUSS.CPP/PPC.CPP: ClassDerivations/DefaultData now pass ADDRESSES (&Emitter::ClassDerivations,
|
||
## &X::ClassDerivations) — matches reconstructed emitter.cpp member-idiom (Derivation ctor now takes
|
||
## Derivation*). Headers unchanged (still `static Derivation ClassDerivations`).
|
||
## - BTTEAM.CPP/.HPP: migrated to GetClassDerivations()/GetAttributeIndex() accessor idiom (mirrors
|
||
## engine TEAM.cpp); TestInstance uses *GetClassDerivations().
|
||
## - BTL4ARND.CPP:77: JointedMover::ClassDerivations → *JointedMover::GetClassDerivations().
|
||
## - BTMSSN.CPP:53: Exit(AbortExitCodeID) → Fail(...) (WinTesla engine has no free Exit(); Fail is the
|
||
## fatal-abort macro already used in this file). FLAGGED.
|
||
## - btscnrl.hpp AUTHORED (decomp/reconstructed/) — minimal BTScenarioRole : ScenarioRole (BTSCNRL.CPP
|
||
## is an empty TU; nothing else references the class).
|
||
## TASK 2 (link): three link waves. Wave A=21 unresolved, Wave B=34 (defining the BTL4VideoRenderer ctor
|
||
## pulled btl4vid.obj fully in, exposing the unreconstructed BT renderable module + dpl2d + renderer
|
||
## helpers), Wave C=6 (engine VideoComponent never implemented in WinTesla).
|
||
## REAL fixes (co-located, not stubs):
|
||
## - mechdmg.cpp:358 Generator::ClassDerivations → *Generator::GetClassDerivations() (idiom).
|
||
## - btl4grnd.cpp: defined BTL4MethodDescription[] = { &BTL4ChainToPrevious } (minimal real table).
|
||
## - btl4vid.cpp: defined BTL4VideoRenderer ctor/dtor/TestInstance (ctor adapts old IG calibration
|
||
## args → DPLRenderer(GetActiveWindow(),800,600,false,...); FLAGGED window/size).
|
||
## FLAGGED STUBS (new file decomp/reconstructed/btstubs.cpp; added to bt410_l4 in CMakeLists):
|
||
## data globals: FrameTimeScale(=1.0f), allPresets[2][100], StatusMessagePool, BTPlayerVTable,
|
||
## BTStatusMessageVTable.
|
||
## free fns: Is_Destroyed, ToggleVoiceAssist, Set_Alarm_Level, Notify_Objective_Reached.
|
||
## Mech: GetMissionReviewMode, IsAirborne, SetTargetRange, SetMappingSubsystem, RaiseStatusAlarm.
|
||
## Mech__DamageZone::LoadCriticalSubsystems; MechSubsystem::TakeDamage/OnAlarmChanged;
|
||
## Generator::ForceShortRecovery.
|
||
## BT_GetMessageManager/BT_IsBusLive/BT_ClearMyomers (Mech<->subsystem bridge).
|
||
## dpl2d_* (NewDisplayList/Begin/SetColor/Circle/Push/PopMatrix/MoveTo/End/Compile) +
|
||
## dpl_SetMaterialNameCallback (libDPL 2D layer never ported to L4D3D).
|
||
## BTL4VideoRenderer::GetScene/LoadObject/AttachToEyeDCS/AddDynamicRenderable/LoadMissionImplementation.
|
||
## ENTIRE BT RENDERABLE MODULE (unreconstructed): BTWorldContainer/Root/DCSObject/Hinge/BallJoint/
|
||
## BallTranslateJoint/Eye/DeathEffect/MarkerWatcher/Translocation/POVStartEnd/Tracer/EmitterBeam/
|
||
## PPCBeam/LightConnection ctors (chain to BTRenderableBase) + BTReticleRenderable ctor/dtor/Execute.
|
||
## ENGINE VideoComponent ctor/dtor/Add/Connect/ReceiveControl/Execute (declared in L4VIDRND.h but
|
||
## NEVER implemented in WinTesla — superseded by VideoRenderable; stubbed to link).
|
||
## LINK FLAG: target_link_options(btl4 PRIVATE /FORCE:MULTIPLE) absorbs ~124 LNK2005 / 200 LNK4006 —
|
||
## 1995 header-defined non-inline globals/free-fns (operator==(sockaddr_in&), EnumAxes/JoysticksCallback,
|
||
## gOpNames, gReplacementData) compiled into multiple TUs. Identical defs → linker keeps first.
|
||
## ⚠ RUNTIME BRING-UP WORKLIST = every stub above (all carry TODO(bring-up) comments; grep btstubs.cpp).
|
||
## The exe LINKS but the BT renderable pipeline + 2D HUD + several Mech accessors are inert until the
|
||
## stubs get real bodies (from BTL4OPT.EXE oracle / RP analog). First boot will likely reach init/menu
|
||
## but not render a mech. NEXT: attempt boot; reconstruct the BT renderable module + dpl2d-over-D3D.
|
||
|
||
## MISSING-FILE LEDGER (from BT.MAK+BTL4.MAK = 53 modules): ~10 survived, ~43 reconstructed,
|
||
## 4 unaccounted: btl4(main→recreated as btl4main.cpp), path(AI pathfinding-not referenced in link),
|
||
## mechbld(offline tool), btl4tool(offline tool). Nothing runtime-critical lost except possibly `path`.
|
||
|
||
## ============ STAGE 4: IT BOOTS. btl4.exe runs to a stable window. ============
|
||
FULL BUILD: 0 errors, 0 unresolved → btl4.exe (1.85MB PE32) LINKS.
|
||
Boot smoke-test (run/ dir: exe + stub OpenAL32.dll/libsndfile-1.dll + x86 debug CRT; cwd = pod content
|
||
tree tesla4pod_extracted/.../REL410/BT; arg `-egg TEST.EGG`):
|
||
|
||
PROGRESS LADDER (each was a real fix):
|
||
1. exit 3, "no source for egg" → wired L4Application::ParseCommandLine + ParseToken in btl4main.cpp
|
||
(egg via `-egg <file>`; single-user mode = no real net needed).
|
||
2. exit 3, "btl4.res v1.1.0.6 obsolete" → version check compares our version[i] vs file versionArray[i+1]
|
||
(skips format byte). File=1.1.0.6 → set version[3]={1,0,6}. PASSES.
|
||
3. NOW: boots → window "BattleTech" (800x600) appears → resources OK → ApplicationManager OK →
|
||
BTL4Application constructs OK → **BLOCKS inside ApplicationManager::StartApplication** (idle, CPU~0,
|
||
no crash, stable 16s+). Waiting on a mission/network state-machine event the inert stubs don't produce.
|
||
|
||
### RUN RECIPE (for morning / interactive)
|
||
exe: C:\git\nick-games\btbuild\run\btl4.exe (+ stub DLLs + debug CRT already there)
|
||
cwd: C:\git\nick-games\tesla4pod_extracted\Tesla4PodPCNovell\CopyofNovellDisk\REL410\BT
|
||
arg: -egg TEST.EGG (a BattleTech/cavern/freeforall/night mission)
|
||
log: btl4.log in the cwd (has the [boot] markers from btl4main.cpp).
|
||
|
||
### NEXT (bring-up, best done interactively + with a debugger):
|
||
- Trace why StartApplication blocks: the mission/net state machine (InitializingState→WaitingForEgg→
|
||
LoadingMission→...) likely waits for a step a STUB no-ops. Prime suspects = the btstubs.cpp list
|
||
(BT*Renderable, dpl2d_*, BTL4VideoRenderer::Load*/GetScene, network/host local-establishment).
|
||
- Replace stubs with real bodies from the BTL4OPT.EXE oracle / RP analog, in dependency order of the
|
||
boot state machine. Each real body lets the state machine advance one step.
|
||
- Audio is stubbed (no real OpenAL/libsndfile DLLs) — fine for now; swap real DLLs later.
|
||
|
||
## ============ STAGE 4 BRING-UP (debugger-driven, 2026-06-30) ============
|
||
Method: run under cdb (x86), catch fault/abort, fix, repeat. Run recipe unchanged
|
||
(run\btl4.exe, cwd=pod BT dir, -egg TEST.EGG; symbols via -y run\ + btl4.pdb).
|
||
|
||
PROGRESS LADDER (each a real fix; all advance the boot):
|
||
1. HANG in video init: DPLRenderer(=D3D)->loadTables fread(NULL) -> CRT assert MessageBox.
|
||
loadTables reads VIDEO\REPLACEMATS.tbl + VIDEO\MATREPLACETABLE.tbl (WinTesla-era, absent).
|
||
FIX: created empty (numMats=0, 4 zero bytes) tables in pod VIDEO\. -> video init passes,
|
||
monitor detection runs (windowed mode), StartApplication returns, enters RunMissions.
|
||
2. AV in mission load: BTPlayer ctor -> ScenarioRole::GetReturnFromDeath on NULL (role lookup
|
||
stubbed/commented at btplayer.cpp:630). FIX: null-guard btplayer.cpp:644 (default index 2).
|
||
3. AV in frame loop: Entity::Execute->PerformAndWatch called NULL fn ptr; the BTPlayer's vtable
|
||
was overwritten with the zeroed stub `BTPlayerVTable`. ROOT: btplayer.cpp:568/698 had decompiler
|
||
artifact `*(void**)this = &BTPlayerVTable` (+447 status msg). FIX: removed those 3 artifacts
|
||
(C++ ctor sets the real vtable). -> **D3D WINDOW APPEARS**, game simulates.
|
||
4. abort() in frame loop: Player::HuntForDropZone (spawn logic) -> DropZone reply ->
|
||
base Player::DropZoneReplyMessageHandler = Fail("...base player class!"). ROOT: BTPlayer never
|
||
defines DropZoneReplyMessageHandler, so MESSAGE_ENTRY(BTPlayer,DropZoneReply) resolves to the
|
||
inherited base Fail-stub. <-- CURRENT FRONTIER.
|
||
|
||
NEXT: reconstruct BTPlayer::DropZoneReplyMessageHandler (the spawn handshake: place mech at dropzone +
|
||
create player vehicle) from the BTL4OPT.EXE oracle. Likely a small CLUSTER of missing BTPlayer/Mech
|
||
gameplay overrides will follow (now genuine game-logic reconstruction, not mechanical link fixes).
|
||
|
||
## ============ STAGE 4 BRING-UP (cont., 2026-06-30 session 2) ============
|
||
The SPAWN PIPELINE is now reconstructed end-to-end. Boot advances from the DropZoneReply abort all the
|
||
way INTO Mech construction. PROGRESS LADDER (each a real fix):
|
||
5. abort() Player::DropZoneReplyMessageHandler base Fail. ROOT: BTPlayer had no override.
|
||
FIX (REAL reconstruction): reconstructed the whole spawn handshake -
|
||
- BTPlayer::DropZoneReplyMessageHandler <- binary oracle FUN_004bffd0 (handler table @00512fd8,
|
||
msg id 0x15; confirmed VehicleDead=FUN_004c05c4 NOT 004c012c, and the real 6-entry table:
|
||
DropZoneReply/VehicleDead/Score/ScoreUpdate/MissionStarting/MissionEnding). !playerVehicle ->
|
||
CreatePlayerVehicle+InitializePlayerLink+SetPerformance(by classID: Mech 0xbb9 / CameraShip 0x45)
|
||
+AlwaysExecute+deathCount=0; else-same-death -> MissionEnding ignore / !DropZoneAcquired ->
|
||
VehicleDead(reset); then mech->Reset(dropZoneLocation,True).
|
||
- BTPlayer::CreatePlayerVehicle(Origin) <- vtable slot +0x40 (FUN_004bfcac, not in the
|
||
assert-anchored decomp) modelled on RPPlayer::CreatePlayerVehicle: base CreatePlayerVehicle
|
||
(camera ships) then make a Mech via MakeAndLinkViewpointEntity from playerMission->GetGameModel()
|
||
ModelList resource, names = GetBadgeName/GetColorName/((BTMission*))->GetPatchName.
|
||
- BTPlayer::InitializePlayerLink <- FUN_004bfee0 / RPPlayer analog (PlayerLink dispatch).
|
||
- Mech::MakeMessage ctor (mech.hpp) <- mirror of VTV__MakeMessage entity_ID form + 3 name strings.
|
||
- Mech::Reset(const Origin&,int) (btstubs.cpp) <- MINIMAL of FUN_0049fb74: set localOrigin/
|
||
localToWorld + ForceUpdate. TODO(bring-up): full ~50-field state + per-subsystem reset deferred.
|
||
RESULT: HuntForDropZone->AssignDropZone->DropZoneReply->CreatePlayerVehicle->MakeAndLinkViewpoint->
|
||
BTL4Application::MakeViewpointEntity->Mech::Make->Mech::Mech runs. Spawn pipeline FUNCTIONAL.
|
||
6. AV mech.cpp:407 (Mech ctor) deref Wword(0xbb)+0xc, Wword(0xbb)=0 (engine segment record).
|
||
FIX: guarded the NULL deref (heatLevel default 0). TODO(bring-up) - real value needs the segment
|
||
record wired (see frontier below).
|
||
7. AV mech.cpp:591 (Mech ctor) gyro<->sinkSource cross-link wrote through gyroSubsystem=0xcdcdcdcd.
|
||
ROOT (REAL BUG): the ctor's subsystem-pointer zero-inits at mech.cpp:359-361 targeted the Wword
|
||
SCRATCH BANK (Wword(0x14a/0x10e/0x16d)=0) instead of the real members, so gyroSubsystem/
|
||
sinkSourceSubsystem/mechTechSubsystem were never zeroed -> debug-fill passed the !=0 guard.
|
||
FIX (REAL): zero the real members gyroSubsystem/sinkSourceSubsystem/mechTechSubsystem/sensorSubsystem
|
||
(matches binary @9873-9875). NOT a guard - a genuine init bug.
|
||
8. AV mech.cpp:641 (Mech ctor Pass 2) model = ResourceFind(Wword(0x6f),0xf) -> NULL -> model->base deref.
|
||
<-- CURRENT FRONTIER (ROOT, structural).
|
||
|
||
### THE FRONTIER = the Mech object-layout / resource-streaming "factory" (deferred big task)
|
||
ROOT CAUSE, definitively localised: in `decomp/reconstructed/mechrecon.hpp` the resource-streaming layer
|
||
is a set of NO-OP placeholder shims:
|
||
- `ResourceFind(...) -> Recon()` (mechrecon.hpp:86) [stubs ResourceFile::Find FUN_00407064]
|
||
- `ResourceStream` (mechrecon.hpp:117-119) = template-ctor no-op; SegmentCount()=0 -> 0 subsystems
|
||
- `ResourceFindByName`, `LoadSkeleton`, `WorldTransform`, `ResourceManagerHas` = same no-op shims
|
||
- `Wword(i)` (mechrecon.hpp:188) = a single shared `static BTVal bank[0x400]` SCRATCH bank: every raw
|
||
`this[0xNN]` in mech.cpp reads/writes a throwaway slot, NOT real object memory. ~145 sites in mech.cpp.
|
||
CONSEQUENCE: the Mech ctor compiles and runs but never streams its model (segment 0xf), subsystem list
|
||
(0x11), damage zones (0x14) or skeleton; `model` is NULL and the model-resource handle `Wword(0x6f)`
|
||
(set by the BASE ctor in the binary, an inherited field) is 0. So NO functional mech can be constructed.
|
||
Every further mech-ctor fault (407, 591, 641, ...) is one symptom of this same root - guarding them one
|
||
by one yields only a hollow, non-functional mech. This is exactly the "THE SUBSYSTEM FACTORY" + "~180 raw
|
||
this[0xNN]" item flagged for a dedicated pass (see CLAUDE.md mech-core / Roadmap Track 2).
|
||
NEXT (dedicated factory pass, NOT a fault-grind): (a) map the ~145 Wword(0xNN) sites + the segment-record
|
||
fields (0xba/0xbb) and model handle (0x6f) to REAL Mech/base members at the documented offsets; (b)
|
||
implement ResourceFind/ResourceStream/ResourceFindByName against the real engine ResourceFile::Find over
|
||
the BT .res segment format (0x11/0xf/0x14/0x1e/0x1d); (c) verify the ModelResource/SubsystemSegment field
|
||
layouts vs the binary; (d) then the per-class subsystem factory (CreateStreamedSubsystem/DefaultData)
|
||
actually instantiates subsystems. Oracle = Mech ctor FUN_004a1674 (part_012.c @9696, 5690 bytes).
|
||
Files touched this session: decomp/reconstructed/btplayer.{hpp,cpp}, mech.hpp (MakeMessage ctor),
|
||
mech.cpp (lines ~359-361 real zero-init, ~406 heat guard), btstubs.cpp (Mech::Reset). Build GREEN, 0 errs.
|
||
|
||
## ============ STAGE 5: MECH STREAMS + CONSTRUCTS (resource-streaming pass, 2026-06-30 sess 3) ============
|
||
GOAL of this pass: replace the no-op resource-streaming shims with REAL engine streaming + map the
|
||
raw object-layout fields to real members, then grind faults until the Mech is fully constructed.
|
||
RESULT: **the Mech now constructs end-to-end from real streamed resources**, the spawn pipeline +
|
||
control-mapping binding complete, and the game advances into mission-entity RENDER setup.
|
||
|
||
FAULT LADDER CLEARED (each = real reconstruction unless noted):
|
||
8. mech.cpp:641 model = ResourceFind(...) == NULL (THE documented frontier). ROOT: mechrecon
|
||
ResourceFind/ResourceStream were no-op shims (SegmentCount()=0). FIX (REAL streaming): added
|
||
MechFindResource() = application->GetResourceFile()->SearchList(creation_message->resourceID, type)
|
||
-- the exact engine idiom the JointedMover base ctor + RP/VTV use. Rewrote BOTH passes:
|
||
- Pass 1 (subsystems, segment 0x11 SubsystemModelStreamResourceType): MemoryStream over the
|
||
locked resource; header int = subsystem count; per-blob loop reads Subsystem::SubsystemResource
|
||
via GetPointer()/AdvancePointer(subsystemModelSize). (mirrors VTV::VTV @1379-1583)
|
||
- Pass 2 (model, segment 0xf GameModelResourceType): model = res->resourceAddress (kept locked).
|
||
`Wword(0x6f)` handle -> creation_message->resourceID (authoritative; base ctor already streamed
|
||
the skeleton from it). -> model non-NULL, subsystem stream real.
|
||
9. mech.cpp:729 *(float*)(this + 0x15d) = ... AV. ROOT (decompiler artifact): typed-pointer
|
||
arithmetic on Mech* (this + 0x15d == this + 0x15d*sizeof(Mech) == +0x89028). FIX: route to the
|
||
Wword(0x15d/0x15e) scratch bank (the decomp intent is word slot this[0x15d], an unnamed pose angle).
|
||
10. **OBJECT-LAYOUT BUG (the big one):** mech.hpp re-declared `int subsystemCount; Subsystem **subsystems;`
|
||
-- these SHADOWED the inherited Entity base members `subsystemCount` / `subsystemArray` that the
|
||
engine reads in Entity::GetSimulation/GetSubsystem. The ctor streamed into the shadow members, so
|
||
the engine never saw the roster (GetSimulation returned NULL). FIX (REAL layout mapping): removed
|
||
the shadow decls; renamed all mech.cpp `subsystems` -> inherited `subsystemArray`; `subsystemCount`
|
||
now resolves to the Entity base field. this[0x49]/this[0x4a] === Entity::subsystemCount/subsystemArray.
|
||
11. MechTech::MechTech (mechtech.cpp:213) AV walking owner->GetSubsystem(0..count): array TAIL was
|
||
uninitialised heap garbage (loop only zeroed the current slot). FIX: zero the WHOLE subsystemArray
|
||
right after allocation (every not-yet-filled slot must read NULL for roster scans).
|
||
12. MechTech roster walk AV: the streamed roster entries were HOLLOW stubs (RECON_SUBSYS = empty
|
||
ReconSubsystemStub, no vtable) -> sub->IsDerivedFrom() faulted. FIX (REAL base wiring): made
|
||
ReconSubsystemStub DERIVE FROM the real MechSubsystem base (proven ctor -- MechTech uses it), so
|
||
every streamed entry is now a genuine registered, vtable-valid Subsystem WITH a DamageZone.
|
||
-> **Mech::Mech now RETURNS (full construction: model + 31 streamed subsystems + MechTech walk).**
|
||
13. btl4app.cpp:554 Fail("Mech has no controls mapping"): primaryControlType==None on a dev box (no
|
||
RIO/Thrustmaster joystick). FIX (bring-up): default case falls back to MechRIOMapper (the pod's
|
||
real L4 device) instead of Fail. FLAGGED -- keyboard/gamepad remap is a pod Phase-8 task.
|
||
14. mechmppr.cpp:327 (MechControlsMapper ctor) AV: torso-recenter reads a ptr at the BINARY byte
|
||
offset owner+0x438 then derefs it -- invalid because the RECONSTRUCTED Mech layout != binary's
|
||
0x854. FIX (bring-up GUARD): skipped (the Mech ctor already identity-inits the torso aim, so it
|
||
starts centered). FLAGGED -- re-enable via a named accessor once cross-module layout is mapped.
|
||
15. L4CTRL CreateStreamedMappings -> GetSimulation(0) == NULL -> GetAttributePointer on NULL. ROOT:
|
||
the streamed control-mapping resource binds DirectMappings to subsystemID 0 (the ControlsMapper
|
||
slot, cf. VTV::ControlsMapperSubsystem==0), but Mech::SetMappingSubsystem was a no-op STUB, so
|
||
subsystemArray[0] stayed NULL. FIX (REAL, mirrors VTV::SetMappingSubsystem): SetMappingSubsystem
|
||
now installs the mapper into subsystemArray[0] (+ mirrors controlsMapper). -> control binding runs.
|
||
16. btplayer.cpp:225 VehicleDeadMessageHandler AV: scenarioRole->GetReturnFromDeath() on a NULL role
|
||
(scoring role not looked up for the minimal TEST.EGG; btplayer.cpp:815 deferred). FIX (guard,
|
||
consistent w/ the existing :644 null-role handling): only debit a life when scenarioRole != 0.
|
||
|
||
### END STATE
|
||
The Mech is **fully constructed from streamed resources** and spawned; controls bind; the mission loads
|
||
entities and enters the RENDER path. Boot now reaches:
|
||
"Entity 1:NN classNN couldn't figure out how to MakeEntityRenderables" (the stubbed BT renderable
|
||
module) then AV at **btl4vid.cpp:808** BTL4VideoRenderer::SetupMaterialSubstitutionList ->
|
||
NotationFile::GetEntry -> NotePage::FindNote. CAUSE: the renderer looks up the mech's egg
|
||
color/badge/patch via entity->GetAttributePointer("VehicleColor"/"VehicleBadge"/"VehiclePatch") -- a
|
||
FLAGGED cross-family gap (those egg strings aren't in the attribute index; the call returns garbage
|
||
that FindNote then derefs). This is the BT video-renderer / material-substitution module (btl4vid.cpp,
|
||
NOT a mech-construction file) -- the "ideally renders" frontier, a separate reconstruction task.
|
||
|
||
### REMAINING STUBS / GUARDS introduced or relied on this pass (bring-up worklist):
|
||
- mech.cpp RECON_SUBSYS subsystems: now real MechSubsystem-derived objects but with MINIMAL class
|
||
behaviour (Cockpit/Sensor/Generator/Gyro/Myomers/weapons/... all share the base body). Per-class
|
||
reconstruction (use the existing sensor.cpp/gyro.cpp/gnrator.cpp/powersub.cpp/emitter.cpp/... real
|
||
classes in the switch) is the next refinement. 6 classes have NO real recon at all (Cockpit,
|
||
Actuator, JumpJet, LegSubsystem, HeatSinkSource, MechDisplay).
|
||
- mech.cpp Pass 3 (damage-zone segment 0x14) + crit-subsystem (0x1e) + standing-anim (0x1d): still use
|
||
the mechrecon shim proxies (damageZoneCount scratch==0 -> loops inert). Not yet real-streamed.
|
||
- mech.cpp model field reads (Pass 2): write mostly to the Wword scratch bank; the Mech__ModelResource
|
||
struct offsets vs the on-disk GameModel record are unverified (reads are buffer-safe, values unused
|
||
on the critical path). The capability sub-rosters (heatable/powered/damageable) stay EMPTY because
|
||
the SubProxy::IsDerivedFrom shim returns 0 (no per-frame consumer reached yet).
|
||
- mechmppr.cpp:327 torso-recenter: GUARDED/skipped (layout-dependent raw offset).
|
||
- btl4app.cpp default controls case: MechRIOMapper fallback (no-joystick dev box).
|
||
- btplayer.cpp:225 scenarioRole debit: NULL-guarded (no scoring role in TEST.EGG).
|
||
- btstubs.cpp SetMappingSubsystem: now REAL (slot 0 install). Remaining btstubs stubs unchanged.
|
||
|
||
### NEXT (for whoever continues): the render path. btl4vid.cpp SetupMaterialSubstitutionList needs the
|
||
mech's egg color/badge/patch (resourceNameA/B/C, set in the Mech ctor from the MakeMessage) exposed to
|
||
the renderer -- either via the attribute index ("VehicleColor"/...) or a named Mech accessor -- and the
|
||
VEHICLE table entries to match. Then the BT renderable module (BTWorldContainer/DCSObject/... ctors,
|
||
currently btstubs) must be reconstructed for MakeEntityRenderables to actually build geometry.
|
||
|
||
## ============ STAGE 6: RENDER PATH RECONSTRUCTED — mech tree builds + presents (black: asset gap) ============
|
||
GOAL: clear the render-path faults until a frame draws (mech in cavern) or reach the deepest render fault.
|
||
RESULT: the BT world-render path is reconstructed and runs end-to-end to a STABLE PRESENTING FRAME with no
|
||
crash; the mech's 51-segment renderable tree + cockpit camera build correctly. The frame is currently
|
||
BLACK — honest status: nothing visibly draws — but the cause is now precisely localised to a CONTENT
|
||
(asset-pipeline) gap, NOT a render-code fault. Files touched: decomp/reconstructed/btl4vid.{cpp,hpp}.
|
||
|
||
FAULT LADDER CLEARED (each a real fix):
|
||
R1. btl4vid.cpp:808 SetupMaterialSubstitutionList AV (strcmp NULL) — GetEntry("color",egg_color,..)
|
||
with egg_color from GetAttributePointer("VehicleColor") returns garbage; NotePage::FindNote
|
||
deref-crashed. FIX (mirrors RP RPL4VID.cpp:1562 which tolerates a missing colour/badge): the egg
|
||
color/badge/patch reads are NULL-guarded and a table miss is no longer fatal (was Fail("Exiting"))
|
||
— veh_color/badge/patch stay NULL and the %placeholder% substitution drops to default materials.
|
||
TODO(bring-up): re-wire the real egg names (Mech::resourceNameA/B/C @0x844/0x848/0x84c) via a named
|
||
accessor once the Mech layout is mapped; the attribute-index path is not wired.
|
||
R2. ARCHITECTURE PIVOT — the recovered MakeMechRenderables built a 1996 pre-DPL renderable hierarchy
|
||
(BTRootRenderable/BTHingeRenderable/... parenting on raw dpl_DCS*) on top of VideoComponent, which
|
||
WinTesla never implemented (it was superseded by VideoRenderable; btstubs.cpp had to stub
|
||
VideoComponent + every BT*Renderable just to link → all inert → "couldn't MakeEntityRenderables").
|
||
FIX (the real work): REWROTE BTL4VideoRenderer::MakeMechRenderables to build the tree with the
|
||
engine's WORKING D3D renderables — RootRenderable / HingeRenderable / BallJointRenderable /
|
||
BallTranslateJointRenderable / DPLStaticChildRenderable / DPLEyeRenderable (MUNGA_L4/L4VIDRND) —
|
||
exactly as RP's port does (RPL4VideoRenderer::MakeJointedMoverRenderables / RecurseSKLFile). These
|
||
self-register (RootRenderable→AddRenderable; VideoRenderable→AddDynamicRenderable), build their own
|
||
DCS, parent on the PARENT RENDERABLE (HierarchicalDrawComponent*, not a dpl_DCS*), and hook the root
|
||
to entity->localToWorld. Walks the EntitySegment table; per segment loads geometry via
|
||
d3d_OBJECT::LoadObject(GetDevice(), "<name>.bgf"), picks Hinge/Ball/BallTranslate/Static by joint
|
||
type, and builds the cockpit camera (DPLEyeRenderable→mCamera) at the "siteeyepoint" site.
|
||
MakeMechRenderables return type VideoComponent* → HierarchicalDrawComponent* (.hpp updated); the
|
||
caller no longer double-registers via the AddDynamicRenderable stub. The BT*Renderable stubs in
|
||
btstubs.cpp are now DEAD CODE for the body (kept for link; the 2D reticle path still references the
|
||
BTReticleRenderable stub but that block is deferred — see below).
|
||
R3. _RTC_UninitUse at L4VIDEO.cpp:5211 (DPLRenderer::MakeEntityRenderables) — exposed when the default
|
||
(non-mech) case was routed to DPLRenderer::MakeEntityRenderables. ROOT (pre-existing ENGINE port
|
||
bug): `this_instance` is used uninitialised (the DPL-stubbed engine left its assignment commented
|
||
out) and /RTC1 traps it. FIX (faithful + safe): the default case calls the generic base
|
||
VideoRenderer::MakeEntityRenderables (FUN_0045e744) — which is exactly what the shipped BT renderer
|
||
did (it deferred to the grandparent "couldn't figure out how to MakeEntityRenderables" handler, NOT
|
||
the DPL prop builder). Non-mech props/terrain pieces therefore log + draw nothing for now.
|
||
TODO(bring-up): route world props through DPLRenderer::MakeEntityRenderables once that engine
|
||
uninitialised-`this_instance` gap is fixed (out of scope here — shared engine source).
|
||
|
||
END STATE / VERIFIED BY DIAGNOSTICS (btl4.log, flagged [BTrender] lines):
|
||
"[BTrender] MakeMechRenderables: 51 segments, view=1" <- render path runs, inside (cockpit) view
|
||
"[BTrender] no mesh for 'blx_cop.bgf' (expects VIDEO\*.x)" <- geometry load FAILS
|
||
"[BTrender] mech tree built: meshes 0/1 loaded, eye=1" <- tree + camera built OK; 0 meshes loaded
|
||
Window: 804x600 "BattleTech", stable, ALIVE@20s+, no crash, captured frame = solid black.
|
||
|
||
DEEPEST RENDER FAULT REACHED = ASSET PIPELINE GAP (not code):
|
||
d3d_OBJECT::LoadObject (MUNGA_L4/L4D3D.cpp:47) loads `VIDEO\<name>.x` via D3DXLoadMeshFromXA — i.e. the
|
||
WinTesla D3D renderer consumes Direct3D .x meshes produced by the port's MeshExp exporter. The BT pod
|
||
content tree ships 879 `.bgf` (DIV-BIZ2) files and ZERO `.x` files (VIDEO\ + VIDEO\GEO\). So EVERY
|
||
segment mesh resolves to NULL → renderables are built with a NULL graphicalObject → nothing is drawn →
|
||
black clear. Inside (cockpit) view requests only the cockpit shell (blx_cop); the 50 body segments have
|
||
no inside-view object name (correct — you're inside the mech). To actually SEE geometry the .bgf models
|
||
must be exported to .x into VIDEO\ (the MeshExp/ASSET_PIPELINE task), and/or use the OUTSIDE view to load
|
||
the full body. Terrain/cavern likewise needs either the prop path (R3 TODO) or environment build.
|
||
|
||
REMAINING STUBS/GUARDS in the render path (bring-up worklist):
|
||
- SetupMaterialSubstitutionList: egg color/badge/patch forced NULL (default materials). TODO: wire real
|
||
Mech::resourceNameA/B/C.
|
||
- Inside-view targeting reticle (BTReticleRenderable + AddWeapon pips) and per-subsystem weapon/effect
|
||
renderables (PPC/emitter beams, projectile tracers, coolant, searchlight) NOT built — they depend on
|
||
the unported dpl2d_ 2D layer (btstubs.cpp, all inert) + the BT effect renderables (btstubs.cpp). These
|
||
are the HUD/weapons render bring-up; not needed for the mech body.
|
||
- dpl2d_* (9 fns) + dpl_SetMaterialNameCallback: still inert stubs (btstubs.cpp).
|
||
- Non-mech entity default → VideoRenderer base (no-op + log); props/terrain not rendered (R3 TODO).
|
||
- [BTrender] DEBUG_STREAM diagnostics left in MakeMechRenderables (low volume; remove at cleanup).
|
||
|
||
================================================================================
|
||
PASS: .BGF MESH LOADER (close the asset-pipeline gap so meshes actually load)
|
||
================================================================================
|
||
PROBLEM (from prior pass): d3d_OBJECT::LoadObject loaded VIDEO\<name>.x via
|
||
D3DXLoadMeshFromXA, but the pod ships 879 original .bgf (DIV-BIZ2) models and ZERO
|
||
.x -> every segment mesh resolved NULL -> "meshes 0/1 loaded" -> black frame.
|
||
|
||
FIX (engine mesh loader now reads .bgf natively):
|
||
- NEW MUNGA_L4/bgfload.{h,cpp}: C++14 port of the proven port/src/bgf.cpp loader.
|
||
Parses DIV-BIZ2 (OBJECT->LOD->PATCH->PMESH; quads triangulated; per-PATCH
|
||
SV_SPECIAL material name), resolves .bmf material libs + texture image basenames,
|
||
produces L4VERTEX-compatible verts (XYZ|NORMAL|DIFFUSE|TEX1) + 32-bit index list
|
||
+ per-material draw batches. Directory scanning uses Win32 FindFirstFile (NOT
|
||
<filesystem>) so it builds under the engine's C++14 target. Asset resolution:
|
||
recursively indexes VIDEO\ (covers VIDEO\GEO, VIDEO\GEO\<theme>, VIDEO\MAT...),
|
||
basename(lower)->path; LoadObject("blx_cop.bgf") -> VIDEO\GEO\BLX_COP.BGF.
|
||
Geometry is emitted DOUBLE-SIDED (both windings) so it is visible regardless of
|
||
the engine's global D3DCULL_CW (no .x exporter survives to confirm winding);
|
||
smooth normals computed from the forward winding only.
|
||
- L4D3D.cpp: new static d3d_OBJECT::LoadObjectBGF — builds an ID3DXMesh
|
||
(D3DXCreateMeshFVF, MANAGED|32BIT, L4VERTEX_FVF), fills VB/IB, writes one
|
||
attribute id per face = batch index, GenerateAdjacency + OptimizeInplace(ATTRSORT)
|
||
to build the attribute table, then constructs the mesh-backed d3d_OBJECT and fills
|
||
mDrawOps per batch: material Diffuse(+Ambient) from the resolved .bmf color, and a
|
||
decoded texture (port/src/image.cpp decodeImage -> D3DFMT_A8R8G8B8 managed tex).
|
||
LoadObject now falls through to LoadObjectBGF when D3DXLoadMeshFromXA fails (the
|
||
.x path is kept as a fallback), before LoadSpheres.
|
||
- port/src/image.cpp (tga/vtx/bsl decoder, C++14-clean) added to the engine build;
|
||
enginebuild/CMakeLists.txt: + bgfload.cpp, + image.cpp, + port/src include dir
|
||
(no header-name collisions with the engine; verified).
|
||
|
||
VERIFIED (btl4.log, flagged [BTrender]/[BTdraw] lines):
|
||
"[BTrender] mech tree built: meshes 1/1 loaded, eye=1" <- inside view: cockpit
|
||
shell blx_cop.bgf now LOADS (was 0/1). N>0 achieved.
|
||
Forced-outside experiment (skeletonType_N, reverted): "meshes 12/12 loaded"
|
||
-> loader handles the full mech body too (12 segments w/ geometry, 0 failures).
|
||
[BTdraw] diagnostic (added then removed): opaqueList=1, batches=9, every frame
|
||
-> the cockpit mesh REACHES the opaque pass list and issues 9 DrawSubset calls
|
||
per frame. The load->D3D-buffer->rasterize pipeline works end to end.
|
||
|
||
FRAME STATE: still BLACK in the default inside view (captured the BattleTech client
|
||
region via screen grab; solid black even with materials forced fully Emissive
|
||
and with the full body loaded). So this is NOT a loader/lighting problem -- the
|
||
geometry IS rasterized but projects outside the eyepoint camera's frustum.
|
||
ROOT (out of scope here, R2/R3 render-tree + camera): the inside view places the
|
||
camera at the cockpit "siteeyepoint"; the player's own cockpit shell / body sit
|
||
at/behind that eyepoint, and there is NO world to see ahead (terrain not built;
|
||
other entities still hit the default VideoRenderer base = "couldn't figure out
|
||
how to MakeEntityRenderables", R3 TODO). Original BT drew the world through the
|
||
canopy + a 2D gauge HUD (dpl2d, unported). So black is expected for the current
|
||
port state -- a VISIBLE mech needs either an external/chase camera looking at the
|
||
body, or routing enemy JointedMovers through MakeMechRenderables so a mech renders
|
||
ahead. Neither is the asset pipeline; both are render-tree/camera work.
|
||
|
||
HONEST MILESTONE READ: the asset-pipeline gap is CLOSED (meshes load: 1/1 cockpit,
|
||
12/12 body; rasterized 9 batches/frame). The window is still black because the
|
||
only loaded geometry is the player's own shell behind the eyepoint with no world
|
||
ahead -- a camera/world-render gap, not a mesh gap. Per the strict milestone
|
||
("window shows geometry, not black"), the on-screen mech is NOT yet achieved.
|
||
|
||
REMAINING GAPS (next pass, in priority order):
|
||
- Camera/visibility: add an external view (or capture an enemy mech) so loaded body
|
||
geometry lands in the frustum; verify winding (currently double-sided as insurance).
|
||
- Textures: most BT materials resolve to .bmf diffuse COLORS; only ~47 .vtx/.bsl exist
|
||
in the tree, so most batches are flat-colored (no texture). Texture coverage + the
|
||
egg color/badge/patch substitution (SetupMaterialSubstitutionList, still NULL-forced)
|
||
remain for a faithful skin.
|
||
- World/props/terrain rendering (R3) + 2D cockpit HUD (dpl2d_, btstubs inert).
|
||
|
||
---
|
||
|
||
## MECH VISIBLE ON SCREEN — first rendered BattleMech (external chase camera)
|
||
|
||
**WIN:** `btbuild/run/frame.png` shows a flat-shaded Avatar BattleMech (torso, both arms +
|
||
gun pods, both legs + feet/toes, hip) centered in the BattleTech window. 12/12 body meshes
|
||
assembled and rasterized.
|
||
|
||
### What was wrong (diagnosis chain)
|
||
1. The default player view is `insideEntity` -> `MakeMechRenderables` built the INSIDE
|
||
skeleton (SkeletonType_A) and put the camera AT the cockpit eyepoint; no body geometry,
|
||
nothing ahead -> black.
|
||
2. Forcing the OUTSIDE body + adding an external camera still gave black. Instrumented
|
||
`DPLEyeRenderable::Execute` (L4VIDRND.cpp): camera WAS correct — ~29 units in front of the
|
||
mech (mech localToWorld vs camera pos), look-dir within ~5deg of the mech centre.
|
||
3. Instrumented the opaque pass: the 12 mech body meshes WERE submitted (DrawSubset) with
|
||
correct world matrices clustered at the mech's world position. Still black. CULLMODE=NONE
|
||
did not help -> not backface culling.
|
||
4. **Root cause:** logged the device transforms at draw time — the PROJECTION matrix was
|
||
uninitialised garbage (every entry ~ -4.3e8). `BTL4VideoRenderer::LoadMissionImplementation`
|
||
was an **empty stub** (btstubs.cpp), so `DPLReadEnvironment` never ran and
|
||
`DPLRenderer::mProjectionMatrix` (a private member) was never built -> all geometry clipped.
|
||
Compounded by `D3DXMatrixPerspectiveFovLH` mismatching the `D3DXMatrixLookAtRH` view.
|
||
|
||
### Changes made
|
||
- `decomp/reconstructed/btl4vid.cpp` `MakeMechRenderables`:
|
||
- For the player's own mech (originally `insideEntity`) force the OUTSIDE body build
|
||
(loads all 12 body meshes) and, after the tree is built, install a fixed external chase
|
||
camera: `DPLEyeRenderable` parented to `this_root` with a hand-built `LinearMatrix`
|
||
offset (camera at mech-local (0,9,-28), looking at (0,6,0); +Z=look, +Y=up, W=pos).
|
||
- `MUNGA_L4/L4VIDEO.cpp`:
|
||
- `DPLReadINIPage` projection: `PerspectiveFovLH` -> `PerspectiveFovRH` (match the RH view),
|
||
dropped the `mProjectionMatrix(0,0)*=-1` X-flip.
|
||
- Added `DPLRenderer::EnsureValidProjection()` (decl in L4VIDEO.h) — builds a valid RH
|
||
projection from the private members + bright ambient + fog off.
|
||
- Opaque pass: `CULLMODE=NONE` and `LIGHTING=FALSE` (flat-shaded so the mech shows without
|
||
scene lights).
|
||
- `decomp/reconstructed/btstubs.cpp`: `BTL4VideoRenderer::LoadMissionImplementation` now calls
|
||
`EnsureValidProjection()` instead of being empty.
|
||
- Temporary diagnostics still present (gated to first few calls): `[EYE]`, `[DRAW]`, `[VP]`
|
||
logging in L4VIDRND.cpp / L4D3D.cpp / L4VIDEO.cpp.
|
||
|
||
### Remaining work for a proper external view
|
||
- The current camera is a hard-coded DEBUG chase cam forced on the player mech. A real
|
||
spectator/chase view-mode should be a toggle wired through `BTL4Application` (view mode),
|
||
not a forced override inside `MakeMechRenderables`.
|
||
- Mech sits slightly low/small; tune offset (raise target, pull closer) or frame from the
|
||
mech bounding box.
|
||
- `EnsureValidProjection` + opaque-pass `LIGHTING=FALSE`/`CULLMODE=NONE` are debug shortcuts.
|
||
Proper fix: implement the real `LoadMissionImplementation` (call `DPLReadEnvironment`) so
|
||
the per-mission viewangle/clip/fog/ambient/lights/paths from BTDPL.INI are honoured, then
|
||
restore lighting + correct cull winding (and verify the RH projection against RP).
|
||
- Strip the `[EYE]`/`[DRAW]`/`[VP]` diagnostics once stable.
|
||
- Intermittent: ~1 run in 2 crashes during the segment loop before "mech tree built"
|
||
(pre-existing reconstructed-code instability, unrelated to the camera/projection fix).
|
||
|
||
## ============ STARTUP-CRASH FIX (the intermittent ~1-in-6 heap corruption) ============
|
||
Symptom: exit 0xC0000374 (STATUS_HEAP_CORRUPTION), intermittent (2/12 runs), in the Mech ctor
|
||
subsystem loop. Diagnosis: debugger masked it (corruption, not AV); nailed via **gflags PageHeap**
|
||
(/p /enable btl4.exe /full) which faults at the exact bad access.
|
||
ROOT (two object-layout overflows in mech.cpp's subsystem construction):
|
||
1. mech.cpp:654 HeatableSubsystem allocated only **0x104** bytes, but the shared MechSubsystem base
|
||
ctor WRITES this[0x44] (byte 0x110, vitalSubsystemIndex) -> 16-byte write past the allocation =
|
||
heap corruption. FIX: bumped to 0x140 (base extent is 0x114; all other subsystems were already
|
||
>=0x130). THIS was the corruption. (PageHeap caught it at mechsub.cpp:161.)
|
||
2. mech.cpp:497 the subsystem-resource MemoryStream wrapped the .res segment at exact size; the base
|
||
ctor reads each record to res+0xe0, overrunning the buffer for short/last records (controls mapper).
|
||
FIX: stream over a zero-padded (+0x100) copy. (Read-overflow; PageHeap caught at mechsub.cpp:165.)
|
||
VERIFIED: PageHeap off, **30/30 runs clean, 0 crashes** (was ~17%). Crash fixed.
|
||
RESIDUAL (NOT the crash; latent over-reads PageHeap flags but that read valid adjacent heap in normal
|
||
runs, e.g. mech.cpp:741 reading +0x108): object-layout debt -> fold into the Mech layout reconstruction.
|
||
Tooling note: gflags needs an ELEVATED prompt; user enabled/disabled. btl4main.cpp has a dormant
|
||
BT_HEAPCHECK #ifdef (CRT heap validation) for future hunts.
|
||
|
||
## ============ WORLD/TERRAIN VISIBLE — mech now stands in the cavern(des) world ============
|
||
**WIN:** `btbuild/run/world.png` shows the player mech standing on TEXTURED DESERT TERRAIN
|
||
(the `cavern`->`des_day` environment) with a horizon + midground structures, instead of the
|
||
black void. 3/3 stability runs clean, no crash. Engine + game both build green.
|
||
|
||
### Diagnosis chain
|
||
1. Set `L4DPLCFG=BTDPL.INI` + wired the real env load (see changes) -> the DPL per-entity
|
||
builder began processing world props (`bld08_lp`, `dish_lp`, `mslr_LP`, `ctar/dtar/calp_lp`)
|
||
but the run then crashed (0xC0000005) in `DPLRenderer::ConsolidateSingleObject`
|
||
(L4VIDEO.cpp:2138), `mov eax,[edx]` edx=0.
|
||
2. ROOT: the pod ships `.bgf`, loaded by `d3d_OBJECT::LoadObjectBGF` with **D3DXMESH_32BIT**
|
||
(L4D3D.cpp:212). `ConsolidateStaticObjects` (the post-load static-mesh merge) called
|
||
`D3DXConcatenateMeshes` with only `D3DXMESH_MANAGED` (16-bit output) -> concatenating
|
||
32-bit-index source meshes into a 16-bit output FAILS, leaving `outMesh==NULL`, then
|
||
`outMesh->GetNumFaces()` dereferenced NULL. RP never hit this (its world loads from `.x`
|
||
as 16-bit). This static-consolidation path had NEVER run for BT before, because the world
|
||
default-case was previously a no-op (no static objects existed to consolidate).
|
||
|
||
### Changes made (all in owned files)
|
||
- `btbuild/btl4main.cpp`: added `putenv("L4DPLCFG=BTDPL.INI")` (alongside the other env
|
||
defaults) so `DPLReadEnvironment` reads BT's real renderer config (paths/clip/fog/lights/
|
||
ambient/background/cached shapes), not `dpldflt.ini`. (RP set `RPDPL.INI`.)
|
||
- `decomp/reconstructed/btstubs.cpp` `BTL4VideoRenderer::LoadMissionImplementation`: now calls
|
||
`DPLRenderer::LoadMissionImplementation(mission)` (-> `DPLReadEnvironment` + `LoadNameBitmaps`,
|
||
exactly like `RPL4VideoRenderer::LoadMissionImplementation`), then `EnsureValidProjection()`
|
||
as a safety net (mProjectionMatrix is private here; this guarantees a valid RH projection +
|
||
fog off so the mech/terrain are not clipped/fogged). Was previously an empty stub that only
|
||
called EnsureValidProjection -> env never loaded.
|
||
- `decomp/reconstructed/btl4vid.cpp` `MakeEntityRenderables` default case: route non-mech
|
||
entities (terrain/props/landmarks) to `DPLRenderer::MakeEntityRenderables` (the DPL prop
|
||
builder) instead of the no-op `VideoRenderer::MakeEntityRenderables` grandparent -- this is
|
||
what RP's default does (RPL4VID.cpp:1436) and is what actually loads world `.bgf` into the
|
||
scene.
|
||
- `MUNGA_L4/L4VIDEO.cpp`:
|
||
- `ConsolidateSingleObject`: pass `D3DXMESH_MANAGED | D3DXMESH_32BIT` to
|
||
`D3DXConcatenateMeshes` (match the 32-bit source meshes); init `outMesh=NULL` and return
|
||
NULL on failure with a log line instead of dereferencing NULL. Both callers in
|
||
`ConsolidateStaticObjects` now skip a NULL result. **This was the crash fix.**
|
||
- `MakeEntityRenderables` CulturalIcon/Landmark arm: initialise `dpl_INSTANCE *this_instance
|
||
= NULL` (the per-object instance assignment is DPL-stubbed/commented out) so the /RTC1
|
||
uninitialised-variable check can't fault if a CulturalIcon/Landmark entity is built.
|
||
|
||
### What renders / what doesn't
|
||
- RENDERS: desert ground/terrain (textured), horizon, midground structures, the player mech
|
||
positioned on the terrain at world ~(275,-0.35,48). Static terrain meshes consolidate cleanly
|
||
(no D3DXConcat-skip log).
|
||
- NOTE: `BT/VIDEO/GEO/CAVERN/` is EMPTY in this content tree; `cavern` branches to `des_day`
|
||
in BTDPL.INI, so the "world" is the desert/arena prop set in `VIDEO/GEO/` (buildings, dishes,
|
||
missile launchers, targets) -- which the recursive `.bgf` index finds and loads.
|
||
- STILL FAILS (harmless): 28 `*_LP.bgf` (low-poly distant-LOD variants: bld08_lp/dish_lp/
|
||
mslr_lp/ctar_lp/dtar_lp/calp_lp). Their high-poly counterparts (BLD08/DISH/MSLR/CALP) ARE
|
||
present and load; the `_LP` files are tiny (216-750B) valid DIV-BIZ2 that the bgf parser
|
||
(bgfload.cpp) rejects -- this is the pre-existing BGF-loader gap (CLAUDE: 621/663 load), NOT
|
||
a terrain-path regression. Fixing the LOD parse would add the distant-LOD billboards.
|
||
|
||
### Remaining / next
|
||
- Capture gotcha (for future sessions): the D3D9 window is windowed; `PrintWindow` returns
|
||
BLACK (can't read the D3D backbuffer) and plain `CopyFromScreen` grabs whatever occludes it.
|
||
WORKS: `SetWindowPos(h, HWND_TOPMOST, 0,0,820,640, SWP_SHOWWINDOW)` then `CopyFromScreen` at
|
||
the client rect -> real frame.
|
||
- The chase camera + `EnsureValidProjection`/opaque-pass `LIGHTING=FALSE`/`CULLMODE=NONE` are
|
||
still the debug shortcuts from last pass; now that the env loads, the INI lights/fog/ambient
|
||
could be honoured (restore lighting + correct cull winding) once verified against RP.
|
||
- Fix the `_LP` BGF parse (bgfload.cpp) to add distant-LOD props; wire the dpl2d_ HUD.
|
||
|
||
---
|
||
|
||
## Tier 2 — DRIVABLE mech (locomotion + camera follow) [this session]
|
||
|
||
**WIN: the player mech walks under control and the chase camera follows.**
|
||
Verified with forced throttle (headless): position log Z 493.156 -> 160.363
|
||
(monotonic, ~30 u/s at full throttle), and the per-render `[EYE] Execute
|
||
entityL2W.t` tracks the moving mech each frame (camera follows by
|
||
construction). Two warm screenshots (`run/walk_t1.png`, `run/walk_t2.png`) show
|
||
the mech centered while the background structures shift. Default (no-throttle)
|
||
run is stable: mech spawns idle, no drift, no crash.
|
||
|
||
### Root cause: mech rendered but never SIMULATED
|
||
The mech rendered every frame (EYE/DRAW) but its `localToWorld` was static. The
|
||
reconstructed per-frame tick (`Mech::Simulate` via `activePerformance`, set from
|
||
a `Recon`->null member-fn-ptr, and full of `this+0xNN` byte-offset-as-index
|
||
bugs) is unsafe and was never reached. Two engine gates also block a fresh
|
||
master entity from being executed:
|
||
1. `Entity::Execute` only calls `PerformAndWatch` when the app is in
|
||
`RunningMission`/`EndingMission` **or** the entity `IsPreRunnable()`
|
||
(ENTITY.cpp ~549). The bring-up never reaches RunningMission.
|
||
2. `UpdateManager::Execute` skips a master unless `IsInteresting()`
|
||
(`interestCount!=0`) `&& IsNonReplicantExecutable()` (UPDATE.cpp:148).
|
||
|
||
### What was changed (4 files; engine untouched)
|
||
- `decomp/reconstructed/mech.hpp` / `mech4.cpp`: added
|
||
`Mech::PerformAndWatch(const Time&, MemoryStream*)` — an **override of the
|
||
engine virtual** `Simulation::PerformAndWatch` (the director calls it every
|
||
frame on every entity; stasis/subsystem/activePerformance machinery lives in
|
||
the *base* `Mover::PerformAndWatch`, which we deliberately bypass). It reads
|
||
the drive input, integrates the engine-base `localOrigin` (turn -> yaw about
|
||
world +Y; forward = mech-local -Z from the rebuilt matrix; advance in the
|
||
ground plane preserving Y) and rebuilds `localToWorld` — the matrix the render
|
||
tree AND the chase camera (btl4vid.cpp) are bound to. Bypasses the unsafe
|
||
Simulate/subsystem chain (that's a later step). Logs world pos at 1 Hz while
|
||
moving. Tuning: 30 u/s full throttle, 1.2 rad/s full turn (hand-picked; the
|
||
animation `.ani` `[RootTranslation]` clip-speed path is future work).
|
||
- `decomp/reconstructed/btstubs.cpp`: `Mech::Reset` (spawn hook) now sets
|
||
`SetPreRunFlag()` + forces `interestCount = 1` so the update manager ticks the
|
||
mech regardless of mission-run state. Added `Mech::IsDisabled()` stub
|
||
(return False) — was declared but undefined; the new vtable pulls mech2/mech4
|
||
into the link which referenced it.
|
||
- `btbuild/btl4main.cpp`: keyboard drive input wired in `WndProc`
|
||
(WASD / arrow keys -> `gBTDrive.throttle`/`turn`). `gBTDrive.forced` is a
|
||
DEBUG toggle (env `BT_FORCE_THROTTLE`, default OFF) used only for headless
|
||
verification (full-ahead, no keypress).
|
||
|
||
### Notes / caveats (flagged)
|
||
- Single local-player assumption: heading is a file-static in mech4.cpp. AI/
|
||
multiplayer mechs would need per-entity state (out of scope; Phase 7/8).
|
||
- Idle (no input) the mech re-asserts yaw=0 each frame, overriding any egg
|
||
spawn facing (minor; documented). Position is preserved.
|
||
- The bypass skips collision + the real animation-driven gait/`SequenceController`
|
||
clips. Next: drive `commandedSpeed` through the controls-mapper subsystem and
|
||
integrate the `.ani` `[RootTranslation].z` per-clip speed (the established
|
||
animation-driven locomotion model) instead of a flat tuning constant.
|
||
- Render is slow in the Debug build (~0.6 fps on the big desert scene); the
|
||
simulation runs ~real-time (dt~0.017). Capture gotcha unchanged: grab a WARM
|
||
frame (>=8 s) — early frames are black (backbuffer not presented yet).
|
||
|
||
---
|
||
|
||
## Wave: per-frame SUBSYSTEM SIMULATION TICK wired into Mech::PerformAndWatch
|
||
|
||
Goal: turn the drivable mech into a live combat harness by ticking the mech's
|
||
subsystem roster every frame, WITHOUT calling the unsafe reconstructed
|
||
`Mech::Simulate` (raw `this+0xNN`-as-index bugs, null `activePerformance`).
|
||
|
||
### What was wired (how the tick reaches the subsystems)
|
||
Mirrored the shipped engine's `Entity::PerformAndWatch` roster loop
|
||
(`MUNGA/ENTITY.cpp` ~698-790; RP's `VTV` ticks its roster the identical way).
|
||
In `Mech::PerformAndWatch` (mech4.cpp), AFTER the existing drive/movement
|
||
integration and BEFORE `WriteSimulationUpdate`, we now walk the inherited Entity
|
||
roster `subsystemArray[0..subsystemCount-1]` and, for each non-null subsystem,
|
||
call the **engine** virtual `subsystem->PerformAndWatch(till, update_stream)`.
|
||
That base (`Simulation::PerformAndWatch`, SIMULATE.cpp:448) computes the
|
||
subsystem's OWN time slice from its own `lastPerformance` and dispatches
|
||
`activePerformance` — the reconstructed `<Class>::<Class>Simulation` method each
|
||
subsystem's ctor installed via `SetPerformance` (heat/gyro/sensor/power/weapons/
|
||
…). We deliberately do NOT call `Mech::Simulate`. Guards: null check +
|
||
`IsNonReplicantExecutable()` (mirrors Entity).
|
||
|
||
### Per-frame confirmation (verified live, headless, BT_FORCE_THROTTLE=1)
|
||
btl4.log over a 30 s run (cwd = pod REL410\BT, `-egg TEST.EGG`):
|
||
- `[tick] first frame: dispatched to 30 executable subsystem(s) of 30 present / roster 33`
|
||
→ the dispatch genuinely reaches **all 30 real subsystems** on frame 1.
|
||
- `[tick] subsystems simulated: 1 (executable) of 30 present / roster 33` (1 Hz, steady).
|
||
After frame 1 the subsystems still on the engine default `DoNothingOnce`
|
||
latch `NeverExecute` (DontExecuteFlag) on their first call and drop out of
|
||
`IsNonReplicantExecutable()` — **faithful engine behavior**. The persistent
|
||
N=1 is the single subsystem with an always-executable real Performance. As the
|
||
upcoming per-subsystem wave deepens each subsystem (its ctor calls
|
||
`SetPerformance(realMethod)`), it automatically stays executable and the live
|
||
count climbs — the tick path is already wired to carry it.
|
||
- Driving NOT regressed: `[drive] pos=...` advances on Z at ~30 u/s·dt each frame.
|
||
- Stability NOT regressed: full 30 s run, no fault (re-verified under cdb).
|
||
|
||
### Roster shape (from the run)
|
||
roster 33 = slot 0 (controls mapper, skipped — see below) + slot 1 (resolved
|
||
voltage bus) + 30 streamed subsystems + (slots that streamed NULL). 30 present,
|
||
all ticked frame 1.
|
||
|
||
### One subsystem we could NOT safely reach (flagged, skipped by design)
|
||
The **controls-mapping subsystem** (`MechRIOMapper`/`MechControlsMapper`,
|
||
installed at roster slot 0 by `Mech::SetMappingSubsystem`, mirrored to the
|
||
`controlsMapper` member). Initial wiring crashed here on the FIRST tick:
|
||
`MechControlsMapper::FillPilotArray` (mechmppr.cpp:779) does
|
||
`pilotIDs[0] = *(int*)(local_pilot+0x1e0)` where `local_pilot` is read through
|
||
UNDOCUMENTED WinTesla `Application` byte offsets (`application+0x6c → +0x190`)
|
||
that don't exist in the port → wild pointer → 0xC0000005 (its own null guards
|
||
can't catch garbage-non-null). This is an **engine gap**, not a stub.
|
||
Resolution: skip `subsystem == controlsMapper` in the tick loop. Justified —
|
||
in bring-up the player drives the mech DIRECTLY via `gBTDrive` in this same
|
||
override, so the input mapper is bypassed by design (the "bring-up RIO fallback"
|
||
path). Reaching it safely = pod-bring-up / input-remap work (CLAUDE.md Phase 8).
|
||
All OTHER subsystems tick normally.
|
||
|
||
### Gait bonus (animation-driven speed): SKIPPED (flagged)
|
||
Not landed — not quick. The flat 30 u/s `kDriveMaxSpeed` slide remains. Wiring
|
||
`.ani [RootTranslation].z` per-clip speed (as `port/` viewer does:
|
||
`g_walkPos += dt*forwardSpeedAt(anim,t)`) needs either (a) independently loading
|
||
the mech's walk `.ani` and exposing its root-translation track, or (b) calling
|
||
the reconstructed `Mech::AdvanceBodyAnimation` (which lives inside the bypassed,
|
||
still-unsafe `Simulate`/`IntegrateMotion` chain). Both are a dedicated step, not
|
||
a quick add. Deferred to the locomotion/gait wave.
|
||
|
||
### Files changed (4; engine untouched)
|
||
- `decomp/reconstructed/mech4.cpp`: added the subsystem-roster tick loop +
|
||
`#include <subsystm.hpp>`; controls-mapper skip; 1 Hz + one-shot first-frame
|
||
tick logs; new file-statics `gTickLogAccum`, `gTickFirstLogged`.
|
||
- `decomp/reconstructed/mech.hpp`, `mechsub.*`, `btstubs.cpp`: not modified
|
||
(the roster + `controlsMapper` member + `IsNonReplicantExecutable` were all
|
||
already present/sufficient).
|
||
|
||
Build: `bt410_l4` + `btl4.exe` green (only the pre-existing /FORCE:MULTIPLE
|
||
LNK4006/4088 warnings). Engine lib untouched.
|
||
|
||
## COMBAT WAVE — agent 4/7 (sensor, mechtech) — DONE, both compile-clean (exit 0)
|
||
|
||
Per-frame TICK behavior verified against binary oracle + surviving headers. Both subsystems
|
||
install a real `activePerformance` in their ctor, so neither falls back to `DoNothingOnce`
|
||
(they tick every frame, not just frame 1).
|
||
|
||
### Sensor (classID 0xBC3, ctor @004b1d18, vtable @0050fb0c)
|
||
- **Behavior reconstructed:** `SensorSimulation(Scalar)` @004b1c4c — faithful to oracle
|
||
(part_013.c:1823). Runs `PoweredSubsystem::PoweredSubsystemSimulation` first, then derives
|
||
the radar-effectiveness readout: `radarPercent = 1.0 - heatMaster.heatEnergy`; zeroed +
|
||
selfTest cleared when thermal model off; `badVoltage`/blind when electrical state != Ready;
|
||
heat-state alarm trims radar (Normal→ok, Degradation→×0.5, Failure→0). This IS the whole of
|
||
the Sensor subsystem's per-frame work in the binary.
|
||
- **NOTE on "target sweep/acquisition/lock-on":** confirmed NOT in this class. Oracle
|
||
FUN_004b1c4c does only the radar-effectiveness model above; target acquisition/lock/range
|
||
lives in the HUD/radar brick (other family), not the Sensor subsystem. Did not invent it.
|
||
- **SetPerformance wired:** ctor calls `SetPerformance(&Sensor::SensorSimulation)` gated on
|
||
`(GetSegmentFlags() & 0xC) != 4` (damaged-copy segments don't tick) — matches oracle
|
||
`param_1[7]=PTR_FUN_0050fa94`. Also sets has-performance instance flag bit 0x8.
|
||
|
||
### MechTech (classID 0xBDC, ctor @004ad228, vtable @0050e3a0)
|
||
- **Behavior reconstructed:** `TechnicalAssistance(Scalar)` @004ad33c — faithful to oracle
|
||
(part_012.c:16435). Iterates `subsystemMonitors`; per monitor reads watched
|
||
`MechSubsystem::GetStatusFlags()` (vt+0x30), loops the 7 TechStatusTypes, clears expired
|
||
wooHoo latches, edge-detects each status bit, and posts cleared/set cockpit messages on
|
||
change (set carries `alarmModel`). Monitor roster is built in the ctor over damageable
|
||
segments (segment bit 0x8). Faithful to oracle ctor flag gate `(entity+0x28 & 0xC) == 0`.
|
||
- **SetPerformance wired:** ctor calls `SetPerformance(&MechTech::TechnicalAssistance)` for
|
||
live primaries; copies/templates flag dormant (this[10] |= 2) and do NOT install a
|
||
Performance — matches oracle (MechTech sets no has-performance bit, just the perf pointer).
|
||
|
||
### CROSS-FAMILY NEEDS (locally shimmed; real homes are shared bases — unchanged from Stage 1)
|
||
- **HeatSink (heat family):** `HeatStateLevel()`, `HeatModelOff()`, `heatEnergy` accessor —
|
||
Sensor backs these locally (heatStateLevel/heatModelOff/heatEnergy members) pending real
|
||
inherited accessors. SensorSimulation reads them; base sim must write them once exposed.
|
||
- **PoweredSubsystem (power family):** `ElectricalStateLevel()` — backed locally (electricalState).
|
||
- **Mech/Entity/Subsystem (mech family):** `GetSegmentFlags()`, `GetSubsystemCount()`,
|
||
`GetSubsystem(i)`, `MechSubsystem::damageZone`/`GetStatusFlags()`, `Entity::simulationFlags`.
|
||
Currently satisfied via the shared mechrecon.hpp compat shim + entity accessors.
|
||
|
||
### Stubs (cross-family, revisit at integration — unchanged from Stage 1)
|
||
- MechTech cockpit message dispatch: `StatusMessageSink()` returns 0,
|
||
`ReportStatusCleared`/`ReportStatusSet` are no-ops. Real impl routes through
|
||
`*(theApplication+0x38)` + HUD message builders (FUN_004366b8/00436688/004364e4) in the
|
||
btl4gaug/app brick (not this family). Edge-detection + latch-clear in TechnicalAssistance
|
||
are live; only the message emission is stubbed.
|
||
- `MechTech::GetTime()` returns `Now()`; oracle compared the subsystem's cached current-time
|
||
field (this+0x10). Behaviorally equivalent for latch expiry; revisit if a per-frame cached
|
||
clock is wired.
|
||
|
||
## From combat/energy-weapons (mechweap, emitter; +surviving PPC/GAUSS) — DONE, 0 errors
|
||
|
||
OWNS the energy/beam weapon family: MechWeapon (abstract base), Emitter (charge/discharge beam base),
|
||
PPC + GaussRifle (surviving real source, `: public Emitter`). compile1.cmd mechweap.cpp + emitter.cpp
|
||
both exit 0.
|
||
|
||
### Per-weapon fire/heat behavior + SetPerformance wiring
|
||
|
||
- **MechWeapon (base)** — abstract; `FireWeapon()` is the "should not be fired" assert stub (matches
|
||
oracle @004b95ec / RP GenericGun). No own performance: ctor @004b99a8 only chains to PoweredSubsystem
|
||
+ installs vtable PTR_FUN_00511d2c (verified in oracle — does NOT write this[7..9]). Per-frame
|
||
helpers live (UpdateTargeting heat-degrades effectiveRange = (1-heatLoad)*weaponRange; CheckFireEdge
|
||
rising-edge on fireImpulse; recharge/heat-cost fields parsed from resource). Relies on a derived
|
||
class to install the tick → correct, since only Emitter/ProjectileWeapon are instantiable.
|
||
|
||
- **Emitter (energy base) — FIXED the missing tick wiring.** The ctor @004bb120 was mis-reconstructed
|
||
as choosing a "fire message template" (SetFireMessage). The binary oracle proves `this[7..9]` is the
|
||
`Simulation::activePerformance` member-pointer (same slot every sibling fills via SetPerformance) and
|
||
the ctor installs ONE of two performances, resolved from section_dump:
|
||
- default : member-ptr @00511e84 = `88 aa 4b 00` -> **0x004baa88 = EmitterSimulation**
|
||
- external model (flags&0xC==4): @00511e90 = `d0 a8 4b 00` -> **0x004ba8d0 = ServiceDischarge**
|
||
Replaced the stub with `SetPerformance(&Emitter::EmitterSimulation)` / `&Emitter::ServiceDischarge`.
|
||
Without this the chained PoweredSubsystem ctor's PoweredSubsystemSimulation would tick instead and the
|
||
Loading->Loaded->Firing charge/fire machine would never run (would drop out via DoNothing-style after
|
||
frame 1). EmitterSimulation (the full state machine), FireWeapon (slot 18, vtables.tsv 005120b8 col3 =
|
||
4bace8 — confirmed: E=½·currentLevel²·energyCoefficient split by damageFraction into damage + self-heat,
|
||
beam build muzzle->target, pip), TrackSeekVoltage (charge integrate + I²R heat back into source),
|
||
ServiceDischarge / ContinueDischarge / ResetFiringState / ComputeOutputVoltage / ReadyToDischarge all
|
||
reconstructed and live. Removed the dead SetFireMessage stand-in + the two fire-template statics.
|
||
|
||
- **PPC (surviving source, unchanged)** — ctor chains to Emitter ctor → inherits EmitterSimulation tick.
|
||
`FireWeapon()` tail-calls `Emitter::FireWeapon()` (the post-`return` block is `#if 0` dead code in the
|
||
original). Fires + charges per frame via the inherited Emitter machine. No edit needed.
|
||
|
||
- **GaussRifle (surviving source, unchanged)** — ctor chains to Emitter ctor → inherits EmitterSimulation
|
||
tick. `FireWeapon()` override sets `outputVoltage = 0.0f` (drain). Declares GaussRifleSimulation +
|
||
SetPerformance in GAUSS.HPP but the ctor never calls them → intentionally uses the inherited Emitter
|
||
performance (faithful to shipped GAUSS.CPP). No edit needed.
|
||
|
||
### CROSS-FAMILY NEEDS
|
||
- **messaging (messmgr/SubsystemMessageManager)**: Emitter beam emit + keepalive use
|
||
`Subsystem::SendMessage(template,a,b)` (FUN_0041c350) with message templates PTR_LAB_00511e6c/e78
|
||
(beam-keepalive) and the fire message. ServiceDischarge/ContinueDischarge/FireWeapon currently stub the
|
||
actual SendMessage call (templates retained as statics). Bind to the real message manager at integration.
|
||
- **heat/power (heat.hpp/powersub.hpp — NOT edited)**: Emitter charge + self-heat depend on the linked
|
||
VoltageSource (Generator). `FUN_00417ab4` (follow SharedData link to the voltage source) is stubbed →
|
||
TrackSeekVoltage / ReadyToDischarge / ctor seekVoltage-scaling (×source->ratedVoltage) and energyCoefficient
|
||
derivation need the real Generator link. `FUN_004ad7d4` (HeatableSubsystem online?) stubbed to 1 and the
|
||
self-heat dump (`heatAccumulator += heatPortion`) needs to feed the inherited HeatableSubsystem load owned
|
||
by the heat family. EMITTER.TCP confirms intended coupling (Condenser/Generator/PowerBar feed the Emitter).
|
||
- **hud**: `DrawWeaponPip` AddElement into the cockpit HUD manager is stubbed in mechweap.cpp (gated on
|
||
pipSegment != -1) — wire to the HUD module's AddElement (FUN_0049b6d8).
|
||
|
||
### Stubs remaining (engine-internal stand-ins, local to emitter.cpp; bind at link)
|
||
- FUN_004ad7d4, FUN_0049fb54, FUN_004b0bd0 (PoweredSubsystem step), FUN_004b0d50 (dt scale),
|
||
FUN_004ac9c8, FUN_00417ab4, FUN_00421070, ResolveSource, VoltageCurve, CopyColor — local stand-ins.
|
||
- Firing-physics tuning constants `_DAT_004bafbc` (kDamageScale=1.0f) and `_DAT_004bb3b8` (folded into
|
||
VoltageCurve()→1.0f) are double-precision operands the optimised image left non-finite in the dump →
|
||
left symbolic; a human should re-derive from the FPU stream for exact damage/recharge tuning.
|
||
|
||
---
|
||
|
||
## Agent 6/7 — damage / messaging (mechdmg, dmgtable, messmgr)
|
||
|
||
All three modules were already substantially reconstructed (prior pass); this pass DEEPENED the
|
||
damage math against the binary oracle + the WinTesla engine DAMAGE.cpp/.h and re-verified each
|
||
compiles clean single-file via `compile1.cmd`. **mechdmg.cpp / dmgtable.cpp / messmgr.cpp → 0 errors.**
|
||
|
||
### Damage model reconstructed (armor -> structure -> crit)
|
||
- **Base armour model confirmed & documented** (`DamageZone::TakeDamage` @0041e4e0, oracle part_002.c;
|
||
identical to the WinTesla engine `MUNGA/DAMAGE.cpp` body we link against). There is **no separate
|
||
armour-then-structure pool**: a single `damageLevel` [0,1] accumulates
|
||
`damageLevel += damage.damageAmount * damageScale[damage.damageType]`, is clamped to [0,1], sets the
|
||
DamageLevelChanged flag (|=4), and at `>= 1.0` marks the zone destroyed (BurningState, DestroyedGraphicState,
|
||
flag |=8). Documented verbatim in `Mech__DamageZone::TakeDamage`.
|
||
- **Key correctness fix in the ctor:** the 5 scalars at words 0x51..0x55 were mislabelled "per-facing
|
||
armour". The oracle base TakeDamage indexes them `this[0x51 + damage.damageType]`, so they are
|
||
`damageScale[Damage::DamageTypeCount==5]` indexed by damage TYPE
|
||
(Collision/Ballistic/Explosive/Laser/Energy), NOT by facing. Relabelled the field, loop var
|
||
(`facing`->`type`, bound `< Damage::DamageTypeCount`) and comments. Normalisation math unchanged
|
||
(1/(scale*count); leg zones ×0.5) — verified against oracle FUN_0049ce50.
|
||
- **Leg-failure threshold fix:** the non-standing / still-grounded branch of `Mech__DamageZone::TakeDamage`
|
||
compares structure against `_DAT_0049c9a0 == 0.5` (partial leg-failure graphic: right->level 4,
|
||
left->level 3), not `StructureMax (1.0)`. Was using 1.0; now uses new `LegHalfStructure = 0.5f`.
|
||
The standing/walking branch correctly keeps 1.0 -> level 9 (fall). Verified vs oracle FUN_0049c690.
|
||
- **Critical-hit roll verified** (`CriticalHit` @0049ccc4): half the damage amount (×0.5, clamped to 1.0)
|
||
flows through the normal `TakeDamage`, then a single uniform draw walks the cumulative `criticalWeight`
|
||
list (`roll <= cum/criticalWeightSum`) to pick one MechCriticalSubsystem, applying `ApplyDamage` and
|
||
accumulating `damagePercentageUsed` (capped at `damagePercentage`). Reconstruction matches oracle
|
||
field offsets (+0x10 pct, +0x14 pctUsed, +0x18 weight) exactly.
|
||
- **DMGTABLE roll logic verified** (dmgtable.cpp): three-level resolver
|
||
DamageLookupTable(layers/penetration depth) -> PieSlice(angular sector, optionally torso-relative) ->
|
||
DamageZonePercentTable(weighted zone list summing to 1.0). `SelectZone()` = single uniform draw against
|
||
ascending cumulative %, returns first zone whose cumulative exceeds the draw. Constants confirmed
|
||
(1.0 sum target, 2*PI wrap, 1e-4 sort epsilon). No change needed.
|
||
|
||
### Message dispatch (SubsystemMessageManager) verified
|
||
- `AddDamageMessage` (@0049b6d8) re-verified against oracle: first-of-frame wins for entityHit (+0xE4),
|
||
impactPoint (+0xEC, gated on AlmostEqual-to-origin), damageZoneIndex (+0xE8); each message also queues a
|
||
0x14-byte DamageInformation Plug (damageType=msg+0x2C, subsystemID=msg+0x5C) into the damageInformation
|
||
chain keyed by damage amount (msg+0x30). Field offsets match exactly.
|
||
- `ConsolidateAndSendDamage` (Performance @0049b784): walks queued damage, resolves each damaged subsystem
|
||
from the owner roster, queues a per-zone weapon-explosion ResourceID (dedup via chain Find), then flushes
|
||
via `SendQueuedExplosions` (staggered 0.1s/explosion) and clears the common record. Logic intact.
|
||
|
||
### CROSS-FAMILY NEEDS
|
||
- **renderer (btl4vid/L4D3D — NOT edited):** `messmgr::SubmitExplosion` is stubbed. Original built an
|
||
explosion render command (ClassID 0x5C sub-type 3, FUN_004336f0) and submitted it to the global IG
|
||
graphics pipe (`theGraphicsPipe->Submit`, FUN_0041acbc). WinTesla replaced the IG pipe with L4D3D and
|
||
the submission entry point is not in these headers — wire to the L4D3D explosion/effect submit at
|
||
integration, using commonDamageInformation.impactPoint + rendererCompensateTime.
|
||
- **subsystem base (subsystm.hpp — NOT edited):** `messmgr::ResolveExplosionID` should read the
|
||
per-subsystem explosion id off the engine `Subsystem` (subsystem+0x3E4, `Subsystem::GetExplosionID()`).
|
||
The WinTesla Subsystem doesn't expose it yet; currently returns the manager's own terrainHitExplosionID
|
||
as a stand-in. Add the accessor to the base, then bind.
|
||
- **mech roster (mech.cpp / mech3.cpp — NOT edited):** ConsolidateAndSendDamage relies on
|
||
`owner->GetSubsystem(id)` (mech+0x128 roster). Confirm the reconstructed Mech exposes that accessor with
|
||
the same id->slot indexing the message manager assumes.
|
||
- **damage notify message (net/entity — NOT edited):** the consolidated TakeDamage/notify message build
|
||
(ClassID 0x13, vtable @0050b984, target field +0x184) in ConsolidateAndSendDamage is left as a recovered
|
||
note — the message ctor (FUN_00419e18) and target wiring are net/renderer-side and absent from headers.
|
||
|
||
### Random / stubs remaining
|
||
- **`RandomUnit()` (mechrecon.hpp) returns 0** — the unit-random ([0,1)) used by the CriticalHit weighted
|
||
roll, DamageZonePercentTable::SelectZone, LODDamageRouter hysteresis and DamageLookupTable degenerate-angle
|
||
fallback. With it pinned at 0 every weighted roll selects the FIRST entry and LOD always re-uses the same
|
||
child. **Must be backed by the real engine RNG (FUN_00408050) before damage distribution is correct.**
|
||
Flagged here as the single highest-impact bring-up item for this family.
|
||
- mechdmg local stand-ins (TheTime/TicksPerSecond clock, SegTable/SegmentIterator segment-tree walk,
|
||
SubProxy2 subsystem facets, NoteX2/RFileX2 offline notation readers) and the dmgtable ReconTable/
|
||
ReconTableIter/notation proxies are behaviour-neutral compile shims — bind to the real engine
|
||
clock / EntitySegment tree / Subsystem / NotationFile + TableOf at integration.
|
||
- `LODReuseHysteresis = 0.82f` (_DAT_0049c514) is a TODO: the exact reuse-probability operand wasn't
|
||
cleanly recoverable from the pseudo-C; behaves as a high "stay on the same spot" bias. Re-derive from the
|
||
FPU constant stream for exact LOD damage clustering.
|
||
|
||
---
|
||
|
||
## COMBAT WAVE — agent 1/7 (heat/power) [heat.cpp/.hpp, powersub.cpp/.hpp, gnrator.cpp/.hpp]
|
||
|
||
Status: all three files compile clean (compile1.cmd → 0 `error C`). The per-frame
|
||
TICK is wired for every master subsystem in this family (SetPerformance installed in
|
||
ctor; engine `Simulation::PerformAndWatch`→`Perform`→`(this->*activePerformance)` persists
|
||
it, no auto-reset — verified against RP MUNGA/SIMULATE.HPP:117-182).
|
||
|
||
### Per-class per-frame behavior + SetPerformance wiring
|
||
- **HeatSink** — `HeatSinkSimulation` (@004ad924): heatEnergy += pendingHeat; currentTemp =
|
||
heatEnergy/thermalMass; UpdateHeatLoad (radiatedHeat = currentTemp*coolant, scaled+clamped+
|
||
15-sample filtered); ConductHeat→ComputeHeatFlow (conductive exchange w/ linked sink via
|
||
expf decay) + BalanceCoolant; UpdateCoolant (draw=master.heatEnergy*heatLoad, top-up via
|
||
DrawCoolant, draw state machine); degradation/failure alarm. WIRED: ctor installs
|
||
`SetPerformance(&HeatSink::HeatSinkSimulation)` for master segments (flags 0x100, copy-bits 0).
|
||
Faithful to FUN_004ad924/7f0/8ac/9ec/da94/dbf8 in heat_cluster.c.
|
||
- **PoweredSubsystem** — `PoweredSubsystemSimulation` (@004b0bd0): runs HeatSink sim, resolves
|
||
voltage bus from powering Generator, advances electrical state machine
|
||
(Starting/NoVoltage/Shorted/GeneratorOff/Ready) off generator state + startTimer, AutoConnect
|
||
scan of GeneratorClassID segments. WIRED: ctor installs it for masters. Voltage state per frame
|
||
is live.
|
||
- **Generator** — `GeneratorSimulation` (@004b1f7c): spin-up (startTimer→startTime), short
|
||
recovery (shortTimer→shortRecoveryTime), idle/recovered transitions, outputVoltage =
|
||
(1 - sourceLevel)*ratedVoltage, percentVoltageAvailable clamped [0,1]. WIRED: ctor installs it
|
||
for masters. ALSO folded in the one recovered method the powersub cluster omitted:
|
||
`Generator::ResetToInitialState()` (verbatim from GNRATOR.TCP — chains
|
||
HeatableSubsystem::ResetToInitialState, outputVoltage=0), declared in powersub.hpp, defined in
|
||
powersub.cpp.
|
||
- **Myomers / PowerWatcher** (in powersub) — already wired (`MyomersSimulation`,
|
||
`PowerWatcher::Simulation`) in their ctors; left as-is.
|
||
- **HeatableSubsystem** (base) — owns the damageable-subsystem virtual surface; base
|
||
edits kept additive (Generator::ResetToInitialState chains it). No regressions to derived families.
|
||
- **gnrator.cpp/.hpp** — alias TUs (Generator lives in powersub); the recovered method they
|
||
documented is now folded into powersub. Left as alias-only, no code emitted.
|
||
|
||
### Constants RECOVERED from the oracle (section_dump.txt raw bytes; corrected placeholders)
|
||
- `_DAT_004ad870` HeatLoadScale: **0.002f** (was 1.0f placeholder) — 80-bit extended
|
||
`3b df 4f 8d 97 6e 12 83 f6 3f` = 1.023992 × 2^-9. Real: normalises radiatedHeat (~300) into
|
||
the [0,1] heatLoad band; the 1.0f placeholder would have pinned heatLoad at the 1.0 clamp.
|
||
- `_DAT_004ad87c`=0.0f, `_DAT_004ad880`=1.0f, `_DAT_004ada90`=1.0f — confirmed correct.
|
||
- `_DAT_004adbf4` HeatEqualizeEpsilon & `_DAT_004adcfc` CoolantDrawEpsilon: **1.0e-4f**
|
||
(0x38d1b717) — was 0.0005f JM_CLOSE_ENOUGH placeholder. Now exact.
|
||
- Power constants verified already-correct from bytes: VoltageEpsilon 1.0e-4f (@4b0bcc),
|
||
RatedVoltageRef 1.0f (@4b1d10), DegradedDriveScale 0.5f (@4b1d14), MinOutputVoltage 0.0f
|
||
(@4b2158), MaxOutputVoltage 1.0f (@4b2154/4b2258).
|
||
|
||
### CROSS-FAMILY NEEDS
|
||
- `BT_GetMessageManager(Mech*)`, `BT_IsBusLive(Mech*)`, `BT_ClearMyomers(Mech*)` (declared in
|
||
powersub.hpp, definitions belong in the mech family) — used by PoweredSubsystem/Generator/Myomers
|
||
message handlers. Mech family must provide these (owner+0x190 message manager, +0x274 bus-live
|
||
flag, +0x374 myomer back-ptr).
|
||
- `Mech::GetSegment(int)` / `EntitySegment` segment roster — relied on for voltage/heat-sink linkage
|
||
resolution and the AutoConnect generator scan. Owned by mech family.
|
||
|
||
### Remaining stubs / not deepened (out of my 3 DEEPEN targets)
|
||
- **Condenser** (RefrigerationSimulation @4ae4d8) — body lives in heatfamily_reslice.cpp (not my
|
||
file). Condenser currently inherits HeatSink ctor → ticks via HeatSinkSimulation (live, not
|
||
DoNothingOnce), but does NOT install RefrigerationSimulation. Deferred to whoever owns
|
||
heatfamily_reslice.cpp; flagged so refrigeration output isn't yet per-frame.
|
||
- PowerWatcher MinVoltageScale (`_DAT_004b1924`) left at 1.0f — appears to be a double in the dump
|
||
and is non-load-bearing (PowerWatcher only delegates per frame); not re-derived.
|
||
- `MyomersSimulation` SourceLevel/voltage-curve details depend on cross-family Mech back-ptrs above.
|
||
|
||
---
|
||
|
||
## COMBAT WAVE — agent 5/7: movement subsystems (gyro / torso / myomers)
|
||
|
||
Goal: real per-frame behavior + `SetPerformance(&<Class>::<Class>Simulation)` so each subsystem
|
||
ticks through `Mech::PerformAndWatch → subsystem->PerformAndWatch → Simulation::PerformAndWatch →
|
||
activePerformance` (instead of the default `DoNothingOnce` that drops out after frame 1).
|
||
|
||
### ROOT-CAUSE FOUND (all three were silently NOT ticking as the master)
|
||
The binary registers the per-tick Performance in each ctor by storing the pointer-to-member into
|
||
`activePerformance` (`this[7..9]`) under the live-MASTER guard `(segFlags & 0xC)==0 && (segFlags &
|
||
0x100)`. Proven identical shape in all three ctors:
|
||
- Gyro @004b3778: `this[7..9] = PTR_FUN_0050fe08` (GyroscopeSimulation), guard bit **0x100**.
|
||
- Torso @004b6b0c: master → `PTR_FUN_00510c10` (TorsoSimulation), else → `PTR_FUN_00510c1c`
|
||
(TorsoCopySimulation); `this[0x93]`=isDamagedCopy.
|
||
- Myomers @004b8fec: `this[7..9] = PTR_LAB_00511620` (MyomersSimulation), guard bit **0x100**.
|
||
The earlier reconstructions (a) tested the wrong bit (`SegmentLiveFlag` 0x01 instead of
|
||
`MasterHeatSinkFlag` 0x100) AND (b) primed the local `segmentFlags` shim to 0 → guard always false.
|
||
Net effect before this pass: **Gyro never registered its sim; Torso registered the slaved COPY sim
|
||
(no command processing); Myomers mislabeled the store a "graphic-state hook" and omitted
|
||
registration entirely.** All three would have fallen back to DoNothingOnce.
|
||
|
||
### Gyroscope (gyro.cpp/.hpp) — balance / tip-over
|
||
- Behavior: per-frame `GyroscopeSimulation` verified faithful to @004b275c — PowerWatcher watch
|
||
update, idle-sway ramp toward (impaired? percentageOnDestruction : percentageOnNormal)+swayBias at
|
||
rotationPerSecond·dt with overshoot clamp + [min,max] noise band, then IntegrateEyeJoint
|
||
(@004b2ec0) + IntegrateBody (@004b30ec) spring-dampers. (Sim integrates only; the binary does NOT
|
||
write the joints in-sim — WriteEyeJoint/WriteMechJoint are driven from the render/animate pass.)
|
||
- SetPerformance wired: guard fixed to `MasterHeatSinkFlag` (0x100); `segmentFlags` primed to live-
|
||
master so `GyroscopeSimulation` registers. Compile-clean (exit 0).
|
||
|
||
### Torso (torso.cpp/.hpp) — twist (yaw) + elevation (pitch) aim
|
||
- Behavior: `TorsoSimulation` verified faithful to @004b5cf0 — watch update; effective rates gated by
|
||
power/heat (Degradation halves, Failure/not-Ready/HeatModelOff zero); button-accel ramp; per-axis
|
||
command integration (rotate L/R, elevate U/D) clamped to software limits; center/recenter; velocity
|
||
+ settle/dirty-bit; statusFlags limit latches. `TorsoCopySimulation` (@004b65f8) retained for the
|
||
copy path.
|
||
- SetPerformance wired: guard fixed to `MasterHeatSinkFlag` (0x100); `segmentFlags` primed to master
|
||
so the **command-processing** TorsoSimulation registers (was registering TorsoCopySimulation).
|
||
Compile-clean (exit 0).
|
||
|
||
### Myomers (myomers.cpp/.hpp, classID 0xBC6) — artificial-muscle drive/actuation
|
||
- Behavior: `MyomersSimulation` verified faithful to @004b8d18 — advanced-damage gate (FUN_004ad7d4),
|
||
then `pendingHeat(@0x1C8) += ratio² · (1+damage) · [(1-accelEff)·|vy|·k·m·dt + (1-velEff)·(k·|v|²·½)
|
||
+ (1-accelEff)·|v|·|a|·k·dt]` (k=motionGain@0x20c, m=*mass@0x250). Drive helpers AvailableOutput/
|
||
RegisterMaxOutput/GetSpeedReading/HasAdequateVoltage already faithful.
|
||
- SetPerformance wired: ADDED the missing registration in the ctor (added a local `segmentFlags`
|
||
Word shim + `SegmentCopyMask`/`MasterHeatSinkFlag` enum to myomers.hpp), primed to live-master,
|
||
guard `(flags&0xC)==0 && (flags&0x100)` per @004b8fec, → `SetPerformance(&Myomers::MyomersSimulation)`.
|
||
Compile-clean (exit 0).
|
||
|
||
### CROSS-FAMILY NEEDS (movement subsystems)
|
||
- **Real segment flags** (`owner+0x28`, read by all three ctors): the live-master vs damaged-copy
|
||
decision and thus which Performance is installed must come from the owning Mech segment, not the
|
||
locally-primed `segmentFlags` shim. Mech family must set these on the subsystem/segment before
|
||
construction (master = 0x100 set, copy-mask 0xC clear). Until then all three are primed to the
|
||
master value so the master sims tick.
|
||
- Gyro skeleton-joint I/O: `eyeJointNode`/`mechJointNode` resolution + the node get/set-transform API
|
||
(FUN_0041cfa0/0041d020/0041d0a8/0041d11c) and `Mech::ResolveJoint(name)` are stubbed (NodeType/
|
||
NodeScalar/SetNode*). WriteEyeJoint/WriteMechJoint integrate values are inert until the
|
||
Skeleton/JointedMover node API is reachable from the subsystem.
|
||
- Torso: `Mech::ResolveJoint`, skeleton-node scalar writer (`PushTwist`), Simulation dirty/replicate
|
||
bit (`SetMovedFlag`), and PowerWatcher base virtuals (`WatcherResetToInitialState`/
|
||
`WatcherHandleDeathMessage`/`WatcherUpdateWatch`, FUN_004b1804/179c/181c) are stubbed — owned by
|
||
Mech / Simulation / PowerWatcher families.
|
||
- Myomers: owner-Mech accessors (base/top speed @0x34C/0x7A0, velocity/drive vectors @0x1C4/0x82C,
|
||
motionGain @0x20C, mass @0x250, advanced-damage gate FUN_004ad7d4), JointedMover roster
|
||
Attach/Detach/Release (mover vtable +0x38/+0x3C/+0x44, fed `&speedEffect`@0x31C), and this segment's
|
||
DamageZone structureLevel @0x158 are all backed by neutral local shims. The drive/heat formulas are
|
||
intact but read placeholder zeros until the Mech/JointedMover/DamageZone accessors land.
|
||
|
||
### Stubs / not deepened
|
||
- Gyro `UpdateAnimationNoise` (@004b357c) procedural-noise generator: structure recovered, node
|
||
injection bodies left as branch stubs (best-effort; not on the per-frame tick path).
|
||
- Gyro `WriteEyeJoint`/`WriteMechJoint` quantise-write logic faithful but inert pending the
|
||
skeleton-node API (above).
|
||
- Myomers `ToggleSeekVoltage` (@004b8a48) command handler: not in the captured shards (untagged gap);
|
||
cycles currentSeekVoltageIndex within [min,max] — left as a documented stub.
|
||
|
||
---
|
||
|
||
## COMBAT WAVE — agent 3/7: projectile / missile weapons
|
||
|
||
Edited only the 7 assigned module pairs. All 7 compile clean (`compile1.cmd`,
|
||
EXIT=0). The per-frame TICK is now wired end-to-end for the launcher and the
|
||
flight entity.
|
||
|
||
### Per class
|
||
|
||
- **ProjectileWeapon** (`projweap.cpp/.hpp`) — behavior DEEPENED + tick wiring
|
||
FIXED.
|
||
- **Bug fix (matters for the live tick):** the ctor's Performance-install gate
|
||
read `subsystem_resource->subsystemFlags`; the oracle (FUN_004bc3fc @8375)
|
||
reads the **owner's** flags `*(param_2+0x28)` = `owner->simulationFlags`
|
||
(`&0xC==0 && &0x100`) — the *same* authoritative-mech gate the working AmmoBin
|
||
uses. Corrected so launchers actually arm their Performance on the bring-up
|
||
mech instead of dropping out as `DoNothingOnce`.
|
||
- `ProjectileWeaponSimulation` (was an empty stub; body genuinely
|
||
unrecovered @004bbd04) reconstructed from the recovered helpers
|
||
(DrawFiringCharge @004bbc78, UpdateEject @004bbb50, CheckForJam @004bbfcc,
|
||
ComputeTimeOfFlight @004bc06c) + the RP analog `RivetGun::RivetGunSimulation`
|
||
(WEAPSYS.cpp): recharge recoil, advance eject, edge-trigger via
|
||
`CheckFireEdge()`, a Jammed/NoAmmo/ready state machine, jam roll, then
|
||
`FireWeapon()` through the vtable.
|
||
- `FireWeapon` (slot 18, stub @004bc104) reconstructed: `ConsumeRound()` (→
|
||
AmmoBin::FeedAmmo @004bd4f4), start recoil cooldown, `GetMuzzlePoint` +
|
||
`Projectile::New`. New shared helpers `ConsumeRound()` / `ReadyToFire()`.
|
||
- `SetPerformance(&ProjectileWeapon::ProjectileWeaponSimulation)` — wired (ctor).
|
||
|
||
- **MissileLauncher** (`mislanch.cpp/.hpp`) — `FireWeapon` (override @004bcc60)
|
||
reconstructed: `ConsumeRound()`, recoil cooldown, then spawn ONE guided
|
||
`Missile::New` entity per salvo (task #60 correction — NOT `missileCount`
|
||
entities; the single missile carries `burstCount` = the random cluster-hit
|
||
roll, and per-missile damage = authored/missileCount pre-split in the ctor).
|
||
The PORT re-expresses this as N flying rounds (net armor total identical;
|
||
loses cluster variance + single-zone concentration — see [[open-questions]]
|
||
task #60-D). Ticks via the inherited `ProjectileWeaponSimulation`; its vtable
|
||
`FireWeapon` override is dispatched from there.
|
||
|
||
- **Missile** (`missile.cpp/.hpp`) — flight-entity tick wiring ADDED.
|
||
- **`SetPerformance(&Missile::MoveAndCollide)` installed in the ctor** (was
|
||
MISSING — the Projectile base ctor only installed the unguided base pass, so
|
||
a spawned missile would not have homed). This is the "flight entity
|
||
simulates" wiring.
|
||
- `MoveAndCollide` now explicitly drives its hosted subsystems each frame
|
||
(`seeker->FindTarget(dt)`, `thruster->MissileThrusterSimulation(dt)`) — the
|
||
behavior the shipped binary folds inline — before the steering/integrate/
|
||
collide pass.
|
||
|
||
- **Seeker** (`seeker.cpp/.hpp`) — `LeadTarget`/`FindTarget` homing already
|
||
reconstructed; now actually RUN each frame from `Missile::MoveAndCollide`.
|
||
Faithful to the binary, the Seeker installs **no** custom Performance (its
|
||
vtable is all engine slots; its work is folded into the host missile), so it
|
||
is driven by the missile rather than a standalone `SetPerformance`. No change
|
||
needed.
|
||
|
||
- **Projectile** (`projtile.cpp/.hpp`) — base integrate/collide pass already
|
||
present; `SetPerformance(&Projectile::MoveAndCollide)` already installed by the
|
||
ctor. No change.
|
||
|
||
- **MissileThruster** (`misthrst.cpp/.hpp`) — burn/steer sim already present;
|
||
`SetPerformance(&MissileThruster::MissileThrusterSimulation)` already installed
|
||
by the ctor, and now also invoked per frame by the host missile. No change.
|
||
|
||
- **AmmoBin** (`ammobin.cpp/.hpp`) — feed (FeedAmmo @004bd4f4), DumpAmmo
|
||
@004bd588 and CookOff @004bd300 already reconstructed; `AmmoBinSimulation`
|
||
Performance already installed by the ctor under the correct
|
||
`owner->simulationFlags` gate. Verified the feed/dump identities against the
|
||
oracle (FeedAmmo dispenses one round Loaded→Feeding; DumpAmmo empties the bin).
|
||
No change.
|
||
|
||
### CROSS-FAMILY NEEDS
|
||
- **MechWeapon base (agent 2):** the fire/sim path relies on `recoil`,
|
||
`rechargeRate`, `weaponAlarm` (`SetLevel`/`GetState`), `CheckFireEdge()`,
|
||
`UpdateTargeting()`, `GetMuzzlePoint()`, `GetTargetPosition()`, `damageData`,
|
||
`effectiveRange`, `rangeToTarget`. All present in `mechweap.hpp` today; if any
|
||
are renamed/retyped, my two FireWeapon paths + `ProjectileWeaponSimulation`
|
||
need a matching touch-up.
|
||
- **Mech (mech family):** the AmmoBin roster lookup (`OwnerSubsystemCount`/
|
||
`OwnerSubsystem`) and the cockpit "live-fire" flag (`LiveFireEnabled`) are still
|
||
isolated stubs in `projweap.cpp` — the real Mech subsystem-roster + controls
|
||
query must wire them so the launcher resolves its bin and the jam roll honors
|
||
the live-fire control.
|
||
- **Entity / world (engine):** `Projectile`/`Missile` world-segment registration,
|
||
motion integration (`IntegrateMotion`/`AdvanceModel`), `CollisionQuery`
|
||
(FUN_0042291c) and detonation spawn (FUN_004be078, ClassID 0x5C) remain
|
||
structural stubs in `projtile.cpp`/`missile.cpp` — engine-side, not weapon
|
||
logic. Spawned entities also need the entity-manager registration that ticks
|
||
their installed Performance (engine-side).
|
||
|
||
### Stubs still standing (flagged, not blocking the tick)
|
||
- `projweap.cpp`: `GetStatusFlags` (slot 12 @004bbf88) and `UpdateWeaponState`
|
||
(slot 16 @004bbc20) — bodies genuinely unrecovered; left deferring to base /
|
||
benign. `OwnerSubsystem*` / `LiveFireEnabled` cross-family stubs (above).
|
||
- `missile.cpp`: `GetForward`, `ResolveTargetConnection`, `LaunchAimOffset`,
|
||
`ResolveModel`, `SpawnDetonation` — engine/model plumbing stubs.
|
||
- `projtile.cpp`: `InitSegmentLink`/`ResolveWorld`/`IntegrateMotion`/
|
||
`CollisionQuery`/`Retire` etc. — Entity-engine stubs.
|
||
|
||
---
|
||
|
||
## WAVE agent 7/7 — HUD / 2D cockpit layer (dpl2d_ over D3D9 + heat gauge feed)
|
||
|
||
### What was done
|
||
1. **dpl2d_ 2D vector display-list layer reimplemented over Direct3D 9.**
|
||
Moved the 9 inert `dpl2d_*` stubs OUT of `btstubs.cpp` into a NEW file
|
||
`decomp/reconstructed/dpl2d.cpp` (+ `dpl2d.hpp`), added to the `bt410_l4`
|
||
CMake source list. The recorder calls (`dpl2d_NewDisplayList/Begin/SetColor/
|
||
Circle/PushMatrix/PopMatrix/MoveTo/End/Compile` — prototypes in btl4vid.hpp)
|
||
now build a real in-memory command list (colour, filled disc, outline ring,
|
||
translation-matrix stack + pen). A real **`dpl2d_ExecuteList(list, device)`**
|
||
executor rasterises a compiled list as pre-transformed 2D primitives
|
||
(`D3DFVF_XYZRHW|D3DFVF_DIFFUSE`, mirroring engine `L4VERTEX_2D`/`_2D_FVF`):
|
||
filled circle = `D3DPT_TRIANGLEFAN`, outline = `D3DPT_LINESTRIP`, 32-seg
|
||
tessellation, alpha-blend on, z/lighting/fog off, full render-state
|
||
save/restore, normalised-[0,1]→viewport-pixel mapping. This matches exactly
|
||
what the reconstructed reticle/PIP builder in `btl4vid.cpp` (lines ~563-616)
|
||
issues (`dpl2d_Circle(list,x,y,0.012f,1)` filled blip + `0.014f,0` dark ring).
|
||
|
||
2. **Heat gauge wired to real subsystem state.**
|
||
Implemented `HeatConnection::Update()` (the per-frame data feed, orig
|
||
"Transfer" @004c3720) in `btl4gaug.cpp`. It reads the live
|
||
`HeatableSubsystem::currentTemperature` (@0x114) of the gauge's bound heat
|
||
subsystem and writes the colour index that `ColorMapper::Execute` pushes into
|
||
the hardware palette slot (no subsystem / destroyed → 100/full tint, else
|
||
`Round(currentTemperature)`). `Update()` is the real base virtual
|
||
(`GaugeConnection::Update`, gauge.h) the gauge framework calls each frame —
|
||
so the cockpit heat tint art is now driven by actual heat state.
|
||
|
||
### 2D primitives implemented
|
||
Coloured filled circle, coloured outline circle, translation matrix stack
|
||
(Push/Pop), pen move — over D3D9 via `DrawPrimitiveUP` with pre-transformed
|
||
verts. (The port's dpl2d_ API surface is the small reticle/PIP subset; the full
|
||
libDPL board API — polylines/polygons/clip regions/AddSetMatrix etc. — is NOT
|
||
used by the reconstructed code and was not reimplemented.)
|
||
|
||
### Gauges wired to real state
|
||
- **ColorMapperHeat** ← `HeatableSubsystem::currentTemperature` (DONE, live).
|
||
The ColorMapper draw path itself uses the engine's real L4 gauge raster layer
|
||
(`Video16BitBuffered`/`SVGA16`/`L4GraphicsPort` in MUNGA_L4/L4VB16.cpp), which
|
||
is **already fully implemented over D3D9** in WinTesla (rasters to a PixelMap16
|
||
then uploads changed lines to a D3D texture) — so this path genuinely draws.
|
||
|
||
### Compile-clean status
|
||
All 7 owned files compile clean (exit 0) via `compile1.cmd`:
|
||
`hud.cpp btl4gaug.cpp btl4gau2.cpp btl4gau3.cpp btl4galm.cpp btstubs.cpp dpl2d.cpp`.
|
||
|
||
### Honest "draws vs compiles" assessment
|
||
- **Gauge raster path: REAL** (engine L4VB16 D3D path is implemented). The heat
|
||
gauge's data feed is now live and correct.
|
||
- **dpl2d_ reticle: records + has a real D3D9 executor, but NOT yet on-screen.**
|
||
No caller invokes `dpl2d_ExecuteList` yet — `BTReticleRenderable::Execute()`
|
||
(btstubs.cpp) is still empty and `btl4vid.cpp` builds the PIP lists but never
|
||
executes them. Hooking execution into the per-frame world render is the last
|
||
mile and lives in btl4vid.cpp / the reticle renderable (NOT my files).
|
||
- **ColorMapperHeat::Make() uses placeholder .data resource constants** (empty
|
||
palette names, port 0, colour slot) — the gauge-config resource pool
|
||
(`l4gauge.cfg` / DAT_005150xx) was not recovered from the optimised image, so
|
||
the heat gauge constructs degenerately at runtime until those constants are
|
||
supplied. Data FEED is correct; the gauge's art binding is the gap.
|
||
- **Other gauge widgets** in btl4gaug/gau2/gau3 (bars, OneOfSeveral,
|
||
HeadingPointer, arcs) remain comment-summaries (no code bodies); they only
|
||
fail at link if the config references them (currently not constructed).
|
||
- **btl4galm.cpp** (alarm/lamp stream virtuals) is serialization glue — no live
|
||
gauge to wire; left as the existing best-effort reconstruction.
|
||
- **hud.cpp** is the HUD *subsystem* (torso-horizon gimbal logic), not a 2D
|
||
drawing surface — left as-is (already reconstructed).
|
||
|
||
### CROSS-FAMILY NEEDS
|
||
1. **Reticle execution hookup (btl4vid.cpp / BTReticleRenderable — NOT my files):**
|
||
wire `dpl2d_ExecuteList(pipDisplayListA[i]/B[i], renderer->GetDevice())` into
|
||
the world-view per-frame render (after the 3D scene) so the recorded reticle/
|
||
PIP display lists actually draw. `DPLRenderer::GetDevice()` (L4VIDEO.h) returns
|
||
the main-view device; `dpl2d_ExecuteList` is declared in `decomp/reconstructed/
|
||
dpl2d.hpp`.
|
||
2. **Gauge-config resource pool** (`l4gauge.cfg` / the DAT_005150xx constants for
|
||
ColorMapperHeat::Make): needed so the heat gauge binds real palette names /
|
||
port / colour slot. Likely recoverable from the pod content tree or a deeper
|
||
.data slice (binary oracle). Owner: whoever owns gauge-config / content.
|
||
3. **Heat temperature→percent scaling** (flagged in btl4gaug.cpp): the recovered
|
||
feed rounds `currentTemperature` (seeded 300.0) straight into a 0..255 index;
|
||
the exact ramp scaling should be reconciled against the original palette ramp.
|
||
Touches heat subsystem semantics (heat.cpp owned by another agent) — read-only
|
||
here; no edit made to heat.* .
|
||
|
||
---
|
||
|
||
## INTEGRATION PASS (post-7-agent wave) — subsystem tick reconcile
|
||
|
||
### TL;DR
|
||
- Tasks 1/2/3 landed; build GREEN, stable (3+ clean runs), driving intact.
|
||
- Tick count stayed 1 -> 1. Root cause is NOT the gate-source variance the brief
|
||
described. The player mech's subsystem roster is built from local RECON_SUBSYS
|
||
stub classes (mech.cpp:145-181), not the reconstructed real classes. The real
|
||
classes (heat.cpp::HeatSink, gyro.cpp::Gyroscope, powersub.cpp::Generator/
|
||
PoweredSubsystem, sensor.cpp::Sensor, ...) are compiled but never instantiated,
|
||
so their (now-reconciled) gates never run.
|
||
|
||
### What actually gates the tick (empirically verified, not theory)
|
||
The base Simulation ctor installs activePerformance = &DoNothingOnce
|
||
(MUNGA/SIMULATE.cpp:352). DoNothingOnce runs once then calls NeverExecute() (sets
|
||
DontExecuteFlag 0x2), so a subsystem that never SetPerformance-s a real sim drops
|
||
out after frame 1. Frame-1 log = 30 executable, steady-state = 1: i.e. 29
|
||
subsystems entered with DoNothingOnce and self-disabled.
|
||
|
||
A one-time per-slot roster dump (temp diag, since removed) showed the lone
|
||
survivor is slot 18 'HUD' = the real MechTech (mech.cpp:643), whose gate
|
||
(entity->simulationFlags and 0xC)==0 reads the OWNER's flags and passes. Every
|
||
other slot (HeatSink, Reservoir, Condenser1-6, GeneratorA-D, Avionics, Myomers,
|
||
Gyroscope, Torso, Searchlight, ThermalSight, PPC_1/2, ERMLaser_1-3, AmmoBinSRM6,
|
||
SRM6, MessageManager) is a RECON_SUBSYS/local-struct stub -> DoNothingOnce ->
|
||
exec=0. (The brief's claim that AmmoBin is the survivor is incorrect; AmmoBin
|
||
isn't instantiated as the real class -- the 'AmmoBinSRM6_*' slots are stubs.)
|
||
|
||
### Binary-oracle finding on the gate (Task 1, done correctly)
|
||
decomp/recovered/all/part_*.c shows the canonical subsystem master-gate ~17x:
|
||
(*(uint*)(param_2+0x28) and 0xc)==0 && (*(uint*)(param_2+0x28) and 0x100)!=0
|
||
where param_2 is the owner (Mech*), so owner->simulationFlags (owner+0x28) is the
|
||
authoritative source -- matching the working AmmoBin/projweap path. The
|
||
reconstruction had drifted to per-file sources (subsystem_resource->subsystemFlags
|
||
[streams 0], GetSegmentFlags() shim, primed-local segmentFlags,
|
||
entity->simulationFlags without 0x100). Aligned all real-class SetPerformance
|
||
master gates to owner->simulationFlags: heat.cpp (HeatSink), powersub.cpp
|
||
(PoweredSubsystem, Generator, PowerWatcher), gyro.cpp, torso.cpp, myomers.cpp,
|
||
sensor.cpp. (mechtech.cpp already reads entity->simulationFlags = owner -> left
|
||
as-is; it's the one that works.) Heat/power linked-sink/voltage-source logic left
|
||
on per-segment flags (that linkage is genuinely per-segment, and leaving it keeps
|
||
the master-missing Verify() paths dormant). These edits are correct and
|
||
oracle-matched but LATENT -- they only take effect once the real classes are
|
||
instantiated (see blocker below).
|
||
|
||
### Task 2 -- mech-core accessors (btstubs.cpp)
|
||
The reconstructed Mech is NOT byte-exact (named members + a Wword scratch bank),
|
||
so the shipped raw offsets (+0x190 msg-mgr, *(+0x190)+0x274 bus, +0x374 myomers)
|
||
do NOT address the right members; a raw read would deref garbage. Done against
|
||
intent, deref-safe:
|
||
- BT_IsBusLive -> True (running player mech is a powered master; un-gates the
|
||
power-dependent paths in powersub when wired). REAL change (was False).
|
||
- BT_GetSegmentFlags(Mech*) -> defined = owner->simulationFlags. This symbol was
|
||
declared in hud.cpp but had NO definition -- it only linked via /FORCE (undefined
|
||
return if ever called). Now resolves the dangling extern and feeds the HUD/
|
||
segment gate the same authoritative source. REAL fix.
|
||
- BT_GetMessageManager -> still 0 (the real MessageManager is a roster slot but
|
||
can't be located without the byte-exact layout; a bogus non-null would be worse
|
||
-- callers deref). BT_ClearMyomers -> no-op (no known-offset member). Documented.
|
||
- Mech::GetSegmentFlags() / EntitySegment roster (owner+0x28) / GetExplosionID
|
||
(+0x3E4): NOT implemented -- no safe byte-exact source in the reconstructed Mech;
|
||
latent like the rest. Flagged.
|
||
|
||
### Task 3 -- RandomUnit backed by real RNG (DONE, verified)
|
||
mechrecon.hpp: RandomUnit() was return 0 (pinned every damage/crit/zone roll to
|
||
entry 0). Now #include <RANDOM.hpp> (fwd -> engine RANDOM.h) and returns
|
||
(Scalar)Random -- RandomGenerator::operator Scalar() in [0,1), matching
|
||
FUN_00408050. Variadic tail + Scalar return keep dmgtable call sites unchanged.
|
||
Verified with a temp 1 Hz log (since removed): printed varying triples e.g.
|
||
0.00662 0.1477 0.2346 / 0.2919 0.1782 0.3686 / 0.0590 0.0847 0.4086 across runs.
|
||
This genuinely de-pins damage/crit/zone selection -- but its consumers (dmgtable
|
||
via weapon impacts) are exercised only when the stub weapons fire, so the combat
|
||
effect is latent until the weapon roster is real.
|
||
|
||
### THE REAL BLOCKER to N->~30 (honest)
|
||
Replacing the RECON_SUBSYS stubs (mech.cpp:145-181 + the switch @550-672) with the
|
||
real reconstructed classes is the genuine remaining work. It was NOT done in this
|
||
pass because it cannot be landed stably piecemeal:
|
||
1. Two parallel ClassID schemes. The streamed segments use the switch's 0xBBD-0xBDE
|
||
IDs; the real classes stamp a different family (heat.cpp HeatSinkClassID,
|
||
heatfamily Condenser/Reservoir/HeatWatcherClassID, powersub, ...). The class<->
|
||
resource mapping must be reconciled first or a class gets constructed over a
|
||
resource blob meant for another (garbage fields).
|
||
2. Unverified per-class alloc sizes. The switch hand-codes Memory::Allocate sizes
|
||
per class; the real objects are larger (e.g. HeatSink/AmmoBin reach >=0x224).
|
||
Under-allocation -> heap corruption (this family already caused the
|
||
"~1-in-6 startup crash", mechsub.cpp:165 note).
|
||
3. Real ctors Verify()-assert on segment links. HeatSink @004adda0 asserts
|
||
"Bad subsystem resource ->heatSink" if owner->GetSegment(heatSinkIndex) is null;
|
||
PoweredSubsystem @004b0f74 asserts on GetSegment(voltageSourceIndex). These
|
||
depend on the streamed indices landing at the right resource offsets in our
|
||
(approximate, +0x100-padded) stream.
|
||
4. Interdependence. Even a single real class wired alone won't show useful
|
||
computation: HeatSink's HeatSinkSimulation does
|
||
currentTemperature = heatEnergy/thermalMass but pendingHeat stays 0 unless the
|
||
heat sources (Generator/weapons -- all stubs) feed it. The roster must be made
|
||
real together to compute real combat state.
|
||
|
||
Ordered safe path (next pass): (a) reconcile the streamed ClassID -> real-class
|
||
table; (b) recover each real class's exact object size from the binary and size the
|
||
allocations; (c) confirm owner->GetSegment(index) returns valid skeleton segments
|
||
for the streamed indices (it's the real engine JointedMover::GetSegment(int), so
|
||
likely yes) and that resource fields sit at the expected offsets; (d) wire ONE
|
||
family at a time (heat first -- it's the pilot), build+run, watch for the asserts,
|
||
keep only if green+stable; (e) repeat. The gate alignment (Task 1) and accessors
|
||
(Task 2) above are the prerequisites already in place, so each newly-real class
|
||
will tick the moment it's instantiated.
|
||
|
||
### What genuinely runs today (no overclaim)
|
||
- Driving: real ([drive] pos advances every second; stable).
|
||
- 1 real subsystem ticks: MechTech::TechnicalAssistance every frame (the
|
||
[tick] ... 1 (executable) of 30). The other 29 are stubs decaying to
|
||
DoNothingOnce -- they tick frame 1 only, then opt out (by design of the stub).
|
||
- RNG: real and verified (Task 3). Bus-live / segment-flags accessors: real values
|
||
now, but their consumers are stubs -> latent.
|
||
- Stability: 3+ headless runs to timeout, no asserts, no faults, gflags PageHeap OFF.
|
||
- Build: GREEN (bt410_l4.lib + btl4.exe), links under the pre-existing /FORCE (now
|
||
with one fewer unresolved extern -- BT_GetSegmentFlags resolved).
|
||
|
||
## ============ HEAT-FAMILY PILOT (instantiate real heat classes) — 2026-06-30 ============
|
||
GOAL: make the mech construct the REAL heat-family subsystem classes (heat.cpp/heat.hpp/
|
||
heatfamily_reslice: HeatSink/HeatableSubsystem/Condenser/Reservoir/HeatWatcher) instead of the
|
||
inert RECON_SUBSYS stubs, proving the instantiation+tick pattern for the other families.
|
||
RESULT: **REVERTED to known-good (all stubs).** A real Condenser DOES instantiate and its
|
||
per-frame Performance DOES tick (the gate/mechanism works) — but it then crashes in the heat
|
||
SIMULATION on a missing inter-sink linkage that cannot be fixed from the allowed files. Build is
|
||
back GREEN, 3/3 runs stable, tick back to 1/30, driving intact. This is the conservative outcome
|
||
the brief sanctions ("if a class can't be made stable, keep the stub, document, report").
|
||
|
||
### #1 finding — the factory ClassID enum NAMES (mech.hpp:253-273) are MISLABELED.
|
||
The streamed ClassID VALUES are right, but the human-readable enum names + the class each switch
|
||
case constructs are guesses that DISAGREE with the authoritative CLASSMAP.md. Cross-referencing the
|
||
ctor address each case cites against CLASSMAP gives the TRUE class. Authoritative map (streamed ID
|
||
-> ctor -> REAL class // current switch label):
|
||
0xBBD @4ae568 -> Condenser // labelled CockpitClassID (builds stub Cockpit)
|
||
0xBBE @4ae8d0 -> HeatSink bank (agg) // labelled SensorClassID (builds stub Sensor; ALSO
|
||
// wrongly sets sensorSubsystem — pre-existing mislabel)
|
||
0xBC0 @4af408 -> Reservoir // labelled CondenserClassID (builds stub Condenser)
|
||
0xBC1 @4b225c -> Generator // GeneratorClassID (correct)
|
||
0xBC2 @4b0f74 -> PoweredSubsystem // correct
|
||
0xBC3 @4b1d18 -> Sensor // labelled MyomersClassID
|
||
0xBC4 @4b3778 -> Gyroscope // correct
|
||
0xBC5 @4b6b0c -> Torso // labelled SinkSourceClassID
|
||
0xBC6 @4b8fec -> Myomers // labelled ActuatorClassID
|
||
0xBC8 @4bb120 -> Emitter // WeaponEmitterClassID (correct)
|
||
0xBCB @4bd5c4 -> AmmoBin // labelled JumpJetClassID
|
||
0xBCD @4bc3fc -> ProjectileWeapon // MechWeaponClassID (correct family)
|
||
0xBD0 @4bcff0 -> MissileLauncher // labelled BallisticWeaponClassID
|
||
0xBD3 @49bca4 -> SubsystemMessageManager // labelled MechControlsMapperID
|
||
0xBD4 @4bb888 -> PPC // labelled GaussWeaponClassID
|
||
0xBD6 @4b7f94 -> HUD // labelled MechTechClassID — and the case builds the REAL
|
||
// MechTech class over it (size 0x2a4 == HUD size, so safe);
|
||
// THIS is the lone steady-state tick survivor.
|
||
0xBD8 @4b84dc -> Searchlight // labelled LegSubsystemClassID
|
||
0xBDC @4ad228 -> MechTech // labelled HeatableClassID (builds stub HeatableSubsystem,
|
||
// and that case does NOT even assign subsystemArray[id])
|
||
0xBDE @4b8718 -> ThermalSight // labelled DisplayClassID
|
||
ACTION for the next pass: re-label this enum + re-target every switch case to the CLASSMAP class
|
||
BEFORE wiring any real class — a class built over the wrong streamed blob = garbage fields. (I did
|
||
NOT renumber the enum this pass to avoid churn/regression; documented here instead.)
|
||
|
||
### Heat-family streamed in TEST.EGG (from the roster dump): Condenser x6 (0xBBD), Reservoir x1
|
||
(0xBC0), "HeatSink" x1 (0xBBE aggregate). The 0xBBE aggregate has NO reconstructed class (only
|
||
prose in heatfamily_reslice.hpp), so it is not instantiable. That leaves Condenser and Reservoir.
|
||
|
||
### Assert/crash analysis (heat.cpp, read-only):
|
||
- Condenser is the only ctor-safe heat class. HeatSink ctor @004adda0 (heat.cpp:415-428) resolves
|
||
a linked sink via owner->GetSegment(heatSinkIndex) and Verify-aborts ("Bad subsystem resource
|
||
->heatSink") if null — BUT it SKIPS that whole block when IsDerivedFrom(Condenser) (true during a
|
||
Condenser ctor iff shared_data == Condenser::DefaultData). So Condenser never hits the segment assert.
|
||
- Reservoir runs the segment block (it is HeatSink-derived, not Condenser) -> Verify-abort risk if
|
||
heatSinkIndex doesn't resolve; PLUS its own master branch (heatfamily_reslice.cpp:446-448) does
|
||
link->Attach(this) with no null guard; PLUS btl4vid MakeMechRenderables walks the roster touching
|
||
ClassID 0xBC0 (Reservoir) at BINARY offsets (reconstructed layout != binary) — three independent
|
||
risks. Not attempted past the Condenser result.
|
||
|
||
### What I actually did + the debugger-confirmed blocker:
|
||
Wired the REAL Condenser into case 0xBBD: new (Memory::Allocate(sizeof(Condenser))) Condenser(this,
|
||
id,(Condenser::SubsystemResource*)seg, Condenser::DefaultData) (used C++ sizeof(Condenser) for the
|
||
alloc — the reconstructed object size, guaranteed to cover every member the ctor writes; NO binary
|
||
hand-size, NO under-alloc). Added #include <heat.hpp>; renamed the two colliding local stubs
|
||
(Condenser->ReservoirStub, HeatableSubsystem->HeatableStub) so heat.hpp's real names win.
|
||
- Build: GREEN. Construction: SUCCEEDED — all 6 Condensers built, mech spawned (no ctor assert; the
|
||
Condenser skip-path held).
|
||
- The master gate DID arm (owner->simulationFlags has &0xC==0 AND &0x100 — the Task-1 gate
|
||
reconcile is correct): the Condenser installed HeatSink::HeatSinkSimulation and it TICKED frame 1.
|
||
- CRASH (0xC0000005) on frame 1, cdb stack (PageHeap off):
|
||
btl4!HeatSink::UpdateCoolant+0x35 heat.cpp:693
|
||
btl4!HeatSink::HeatSinkSimulation+0xa4 heat.cpp:537<-531
|
||
btl4!Simulation::Perform / PerformAndWatch
|
||
btl4!Mech::PerformAndWatch mech4.cpp:554
|
||
ROOT: heat.cpp:692-693 `HeatSink *master = linkedSinks.Resolve(); coolantDraw = master->heatEnergy
|
||
* heatLoad;` — linkedSinks is EMPTY for a Condenser (its HeatSink ctor skipped the linkage block),
|
||
so master==NULL and master->heatEnergy derefs null. NB the sibling methods ConductHeat (heat.cpp:595)
|
||
and BalanceCoolant (heat.cpp:646) DO null-guard linkedSinks.Resolve(); UpdateCoolant does NOT — a
|
||
reconstruction omission (the shipped binary @4adbf8 must guard, since real condensers ran). Even the
|
||
"correct" Condenser sim (RefrigerationSimulation @4ae4d8, which the reconstructed Condenser ctor fails
|
||
to install) derefs master the same way (heatfamily_reslice.cpp:125-127) -> same crash.
|
||
|
||
### Why it can't be made stable from the allowed files (mech.cpp/mech.hpp + heat.hpp/powersub.hpp):
|
||
The fix is EITHER (a) a one-line null-master guard in heat.cpp::UpdateCoolant (mirroring its own
|
||
ConductHeat/BalanceCoolant guards) — but heat.cpp is outside the allowed edit set and UpdateCoolant's
|
||
body can't be patched from heat.hpp; OR (b) give the Condenser a VALID linked master heat sink — which
|
||
requires the 0xBBE master heat-sink bank (no reconstructed class) to be real AND the inter-sink linkage
|
||
to be wired, i.e. the whole heat family at once. Fabricating a self/peer link to dodge the null deref
|
||
would produce meaningless heat numbers (the exact "garbage" the brief warns against). This is precisely
|
||
the interdependence blocker already called out in "THE REAL BLOCKER" point 4 above — now proven with a
|
||
debugger trace rather than predicted.
|
||
|
||
### Before/after: tick stayed 1 (executable) of 30 — the Condenser tick CRASHED rather than surviving;
|
||
reverted -> 1/30 baseline restored. Stability after revert: 3/3 runs alive to 14s, bad-pattern count 0,
|
||
[drive] pos advances every frame. Build GREEN (mech.cpp only recompiled). NO files left modified.
|
||
|
||
### Reverted-to-stub: Condenser (0xBBD) — blocker = heat.cpp::UpdateCoolant null-master deref +
|
||
absent inter-sink linkage (interdependence). Reservoir (0xBC0) and HeatSink bank (0xBBE) — not attempted
|
||
(0xBBE has no class; Reservoir carries 3 independent crash risks).
|
||
|
||
### EXACT path forward for a (necessarily family-wide) heat pass:
|
||
1. Re-label the SubsystemClassID enum + re-target every switch case to the CLASSMAP class (table above).
|
||
2. Reconstruct the 0xBBE master HeatSink-bank class (heatfamily_reslice.hpp documents it: parses
|
||
HeatSinkCount/AmbientTemperature, ctor @4ae8d0) so a real master exists.
|
||
3. Wire the heat inter-sink linkage in the Mech ctor (after the subsystem loop, like the existing
|
||
gyro<->sinkSource cross-link @ mech.cpp ~683): link each Condenser/Reservoir's linkedSinks to the
|
||
master bank. (linkedSinks is public on HeatSink — reachable from mech.cpp.)
|
||
4. Add the missing null-master guard to heat.cpp::UpdateCoolant (and install Condenser's real
|
||
RefrigerationSimulation in its ctor) — REQUIRES heat.cpp edits (out of scope for this pilot's file set).
|
||
5. THEN instantiate the heat family together (Condenser+Reservoir+bank) and verify the tick rises by
|
||
their count without the UpdateCoolant fault.
|