Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.
Layout:
engine/ MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
models) + image codec; the minimal rp/ headers the audio HAL needs
game/ reconstructed BT logic + surviving-original BT source + fwd shims
+ WinMain launcher
content/ full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
docs/ format specs + reconstruction ledgers
reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
tools/ MP console emulator + map/resource scanners
One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# Asset Pipeline — Textures, Materials & Gauges
|
||||
|
||||
Companion to `BGF_FORMAT.md`. Covers how a model's surface resolves to pixels, and the cockpit
|
||||
gauge/HUD formats. Verified by hexdump against real content; header cites are `file:line` under
|
||||
`…/CODE/RP/MUNGA_L4/libDPL/`.
|
||||
|
||||
## Format inventory (CONTENT/BT)
|
||||
| Ext | Count | Kind | Role |
|
||||
|---|---|---|---|
|
||||
| `.bgf` | 1275 | `DIV-BIZ2` FILETYPE=0 | 3D geometry (see BGF_FORMAT.md) |
|
||||
| `.bmf` | 2222 | `DIV-BIZ2` FILETYPE=1 | material + texture **libraries** (no pixels) |
|
||||
| `.vtx` | 5 | `DIV-VTX2` | raw RGB texel image |
|
||||
| `.tga` | 8 | Truevision TGA2 type-2 | 24-bit truecolor texel image |
|
||||
| `.sgi` | 0 | (loader exists) | RGBA texel image (none shipped) |
|
||||
| `.bsl` | 3 | `DIV-BSL2` | bit-slice / multi-image + mip container |
|
||||
| `.pcc` | 434 | ZSoft PCX, 8-bit RLE | cockpit HUD/gauge **raster** bitmaps |
|
||||
| `.gim` | 311 | ASCII INI | MFD/radar **vector** line-gauges (NOT raster) |
|
||||
| `.gat` | 3 | ASCII INI | gauge color/attribute table |
|
||||
|
||||
Layout: `VIDEO/GEO/*.bgf`, `VIDEO/MAT/*.bmf`, `VIDEO/TEX/*.vtx,*.bsl` (+ `TEX/BUILD/*.tga`),
|
||||
`VIDEO/BUILD/*` staging, `GAUGE/*.gim,*.gat,*.pcc`, `MODELS/*.mod` (INI tying bgf+gim+sld).
|
||||
|
||||
## Name-resolution chain (verified)
|
||||
```
|
||||
BGF geogroup SV_F_MATERIAL "library:material"
|
||||
→ open library.bmf
|
||||
→ MATERIAL chunk whose NAME == "material"
|
||||
→ its MATERIAL_TEXTURE (0x0021): [u8 type=2 NAMED][texture-name string]
|
||||
→ TEXTURE chunk whose NAME == that texture-name
|
||||
→ its TEXTURE_MAP (0x0011): image basename (no ext)
|
||||
→ loader appends ext, searches texmap path → .vtx/.tga/.sgi/.bsl
|
||||
```
|
||||
`dpfCreateMaterialName(extPath,library,mname)` `PFILE.H:821`.
|
||||
|
||||
## .BMF chunk layout (same TLV grammar as BGF; tags in `PFBIZTAG.H:43-62`)
|
||||
**TEXTURE `0x0010`** (`dpfTEXTURE`, `__PFILE.H:120-137`): NAME `0x2008` (string); TEXTURE_MAP `0x0011`
|
||||
(basename string); MINIFY `0x0012`/MAGNIFY `0x0013` (mip/filter enums, `PFILE.H:155-162`); ALPHA `0x0014`
|
||||
{blend/cut/blendcut}; WRAP_U/V `0x0015/16` {repeat/clamp/select}; DETAIL `0x0017`; BITSLICE `0x0018` (u8 → .bsl slice).
|
||||
**MATERIAL `0x0020`** (`dpfMATERIAL`, `__PFILE.H:152-169`): NAME `0x2008`; MATERIAL_TEXTURE `0x0021`
|
||||
[`u8 type`(2=named)+name]; AMBIENT `0x0023`/DIFFUSE `0x0024` = 3×f32 RGB (verified); SPECULAR `0x0025`/
|
||||
EMISSIVE `0x0026`/OPACITY `0x0027` (optional, inferred 3-4 f32); RAMP `0x0028` (ramp-name string).
|
||||
**RAMP `0x0030`**: NAME + RAMP_DATA `0x0031` (float color data).
|
||||
|
||||
## Pixel formats (normalize all to 32-bit RGBA at load)
|
||||
- **.VTX (`DIV-VTX2`):** TLV; size tag `0x2062` = two int32 (w,h, e.g. 128×128); body = **uncompressed RGB
|
||||
3 B/px**, top-to-bottom. Type enum `dpiBSLTYPE` (`PIMAGE.H:104-116`): MONO0-5/BILINEAR/RGB/RGBA.
|
||||
- **.TGA:** standard type-2, 24-bit BGR, TGA2 footer. `dpl_tgaRead`.
|
||||
- **.SGI:** RGBA (`dpl_sgiRead`; none shipped).
|
||||
- **.BSL (`DIV-BSL2`): DECODED — a BIT-SLICED multi-image container** (corpus-verified on all 66 archive
|
||||
BSLs; reference = the shipping WinTesla loader `DivLoader/VGCDivLoader.cpp:323-410`
|
||||
`LoadBSLFile`/`getBSLData` + Division `dsys/PIMAGE.H` `dpiBSLTYPE` + the content build scripts
|
||||
`img2vtx.exe -b -mN <tga>`). Header (int32 LE): w `@0x08`, h `@0x0C`, depth `@0x10` (bits per slice,
|
||||
always 4), tableBytes `@0x14` (directory length counted from the nEntries field; imageDataOffset =
|
||||
`0x18 + tableBytes`), nEntries `@0x18`; directory entries `{int32 recLen; int32 sliceType; char
|
||||
name[recLen-4]}` — **entry names are authoring records only, read-and-discarded at runtime**; then
|
||||
`w*h` little-endian 32-bit texel WORDS. Byte 0 of each word = pad (structurally unused, 0 in all 66
|
||||
files); the other 3 bytes = SIX independent **4-bit GRAYSCALE slices**, nibble PAIR-SWAPPED (even slice
|
||||
= HIGH nibble): slice0=bits12-15, 1=8-11, 2=20-23, 3=16-19, 4=28-31, 5=24-27 (`shift=(c+((c+1)%2)*2)*4`,
|
||||
value returned `<<4` = 0x00..0xF0). sliceType 7 = **RGB444** (r=s5,g=s4,b=s3), 8 = **RGBA4444** (+a=s2);
|
||||
both coexist with mono slices in one file (BDAM/BEXP/BDET). Slice SELECTION = the BMF TEXTURE record's
|
||||
`BITSLICE` tag `0x18` (u8; **absent = slice 0**) — e.g. BLHSKIN.BMF blkhwk1_tex=absent(0),
|
||||
blkhwk2/3/4_tex=01/02/03, all mapping image basename `blkhwk` → BLKHWK.BSL. Mono slices are colorized
|
||||
downstream by material RAMP (+ diffuse tint on neutral ramps) — all mech skins, vehicle atlases
|
||||
(BASEV = 6 gray sheets, NOT truecolor), logos, effects grit. ⚠ The previous "base image = trailing
|
||||
w*h*bpp, pixel order [pad,R,G,B]" claim was WRONG — it overlaid 2-3 different gray slices as RGB
|
||||
channels (the rainbow "graffiti" mech-skin bug); it survives as the `BT_BSL=0` diagnostic fallback in
|
||||
`port/src/image.cpp decodeBSL`.
|
||||
|
||||
## Cockpit gauges
|
||||
- **.GIM** = ASCII INI, **vector** top-down line drawing for MFD/radar: `[lods] level0=3000…`,
|
||||
`[vertices] v0=x,y,z`, `[level0] linelist=<group> v0 v1 v2…`. First token = color/attr group resolved
|
||||
via `.gat`. Feeds the `dpl2d_*` 2D line path. Verified `AB01_GA.GIM`.
|
||||
- **.GAT** = ASCII INI: `[colors]`,`[groups] a_bld=amber1…` → group→palette-index. Verified `GAUGE.GAT`.
|
||||
- **.PCC** = ZSoft **PCX**, 8-bit RLE, 256-color palette (palette after `0x0C` EOF marker); 640×480-class.
|
||||
The actual HUD raster bitmaps → need 8-bit-palette → RGBA expansion. Verified `ADPAL.PCC`.
|
||||
|
||||
## ⚠ Dependencies / open items
|
||||
1. **The retail texel raster set is NOT in this archive** — only the BUILD/logo subset of `.vtx/.tga/.bsl`
|
||||
ships. The 2222 `.bmf` reference maps that aren't here. **Need the full texture content from Nick**, or
|
||||
the renderer must tolerate missing maps and fall back to material DIFFUSE color.
|
||||
2. **No source for the `.gim/.gat/.pcc` loaders** — inside the closed `LIBDPL.LIB`. `.pcc`(PCX) and
|
||||
`.gim/.gat`(INI) are standard/simple. `.bsl` and `.vtx` are now FULLY decoded (see Pixel formats above;
|
||||
the shipping `DivLoader/VGCDivLoader.cpp` turned out to carry the reference reader for both).
|
||||
@@ -0,0 +1,110 @@
|
||||
# BGF (`DIV-BIZ2`) Model Format — Parsing Spec
|
||||
|
||||
Reverse-engineered for the BattleTech pod port (replacing the closed `libDPL`/Division-IG
|
||||
renderer). Verified by parsing **all 1275 `.BGF` files** in `CONTENT/` with a prototype
|
||||
parser — every file closes to the exact byte. Header authority cited inline; source headers
|
||||
under `…/CODE/RP/MUNGA_L4/libDPL/`.
|
||||
|
||||
## 1. Header & global encoding
|
||||
- **Magic:** 8 bytes ASCII `DIV-BIZ2` (`__PFILE.H:60-61` `dpfB2ZIDstr`). Siblings: `DIV-BMF2`
|
||||
(binary material), `DIV-VIZ2`/`DIV-VMF2` (ascii).
|
||||
- **Endian:** little-endian (`FILELIB.H:55`). All ints/tags + IEEE-754 **float32** (not double,
|
||||
`PFILE.H:127-130`).
|
||||
- **No directory.** After the magic, a flat-to-walk **nested chunk (TLV) tree**. First chunk is
|
||||
always `HEADER (0x0003)`, last always `BIZ_DONE (0x0005)`.
|
||||
- Content written by format v2.8 (lib 2.07).
|
||||
|
||||
## 2. Chunk grammar (TLV tree)
|
||||
```
|
||||
Chunk { uint16 tagword; (uint8|uint16) len; byte payload[len]; }
|
||||
```
|
||||
- **Tag id = `tagword & 0x2fff`.** Namespaces: `0x00xx` = structural/DEF, `0x20xx` = attribute/CON
|
||||
(all leaves). Validity macro `dpfValidTag` (`PFBIZTAG.H:134-137`).
|
||||
- **Length width flag = `tagword >> 14`:** `0` → 1-byte len; `1` (`0x4000`) → 2-byte LE len;
|
||||
`0x8000` (4-byte) reserved, unused in this content. (Census: 1-byte 38890, 2-byte 8525, 4-byte 0.)
|
||||
- **Containers** (payload = child chunks): `HEADER, BOUND, OBJECT, LOD, PATCH, PMESH, SPHERE_LIST`
|
||||
(+ spec'd MATERIAL/TEXTURE/RAMP/POLYGON/TRISTRIP/POLYSTRIP/LINE/TEXT). Everything else is a leaf.
|
||||
|
||||
### Tag IDs actually used by this game (full table in `PFBIZTAG.H`)
|
||||
Structural `0x00xx`: `0003 HEADER`, `0005 BIZ_DONE`, `0040 OBJECT`, `0041 LOD`, `0042 PATCH(=GEOGROUP)`,
|
||||
`0046 PMESH` (**only mesh type used**), `0047 CONNECTION_LIST`, `004d PCONN_LIST`, `0048/0049 SPHERE_LIST/SPHERE`,
|
||||
`0070/0071/0072 BOUND/BBOX/BSPHERE`, vertex blocks `0080 XYZ`, `0081 XYZ_N`, `0082 XYZ_RGBA`,
|
||||
`0088 XYZ_UV`, `0089 XYZ_N_UV`, `008A XYZ_RGBA_UV`.
|
||||
Attribute `0x20xx`: `2002 VERSION`, `2003 DATE`, `2004 TIME`, `2005 SCALE`, `2007 FILETYPE`(0=geom/1=mtl),
|
||||
`2008 *_NAME`, `2009 UNIT`(1=metre), `2030/2031 SV_F/B_MATERIAL`, `2034 SV_DECAL`, `2035 SV_FACETED`,
|
||||
`2036 SV_VERTEX`, `2037 SV_SPECIAL` (articulation/part names — see §6), `2046 LOD_DISTANCE`(2×f32 in,out),
|
||||
`2048 LOD_TRANSITION`.
|
||||
|
||||
## 3. Geometry
|
||||
**Vertex blocks** — interleaved packed float32, count = `len / stride`:
|
||||
| tag | fields | stride |
|
||||
|---|---|---|
|
||||
|0080 XYZ|pos[3]|12|
|
||||
|0081 XYZ_N|pos[3],n[3]|24|
|
||||
|0082 XYZ_RGBA|pos[3],rgba[4]|28|
|
||||
|0088 XYZ_UV|pos[3],uv[2]|20 ← dominant (2595 blocks)|
|
||||
|0089 XYZ_N_UV|pos[3],n[3],uv[2]|32|
|
||||
|008A XYZ_RGBA_UV|pos[3],rgba[4],uv[2]|36|
|
||||
|
||||
Positions = 3 floats, units per `UNIT` (metres). Flags map 1:1 to `dpl_VERTEX_TYPE` (`DPLTYPES.H:189-199`).
|
||||
|
||||
**Indices** (inside a `PMESH`, after its vertex block):
|
||||
- **`PCONN_LIST` (0x004d)** — `uint8 pointsPerFace` (4 = quads is typical; **6 = hexagons occur**,
|
||||
e.g. CALPB) then `int32 index[]` (0-based into this geometry's vertex block).
|
||||
`nFaces = ((len-1)/4)/ppf`; fan-triangulate each face.
|
||||
- **`CONNECTION_LIST` (0x0047)** — a **flat triangle list** (3 indices per face, no leading byte;
|
||||
corpus: all 3488 CONN chunks have `n%3==0`).
|
||||
- ⚠ **CONN and PCONN are NOT alternatives — a single PMESH can carry BOTH** (quads/hexes in PCONN
|
||||
*plus* plain triangles in CONN): 370 of 841 pod GEO models mix them in one pmesh. A loader that
|
||||
prefers PCONN and skips CONN silently drops those triangles (found via the calliope turret base
|
||||
`calpb`: 78 PCONN tris + 24 CONN tris — the missing base panels). Process both, always.
|
||||
|
||||
> **D3D9 note:** faces are **quads** → triangulate each quad `[a b c d]` → `[a b c][a c d]` at load.
|
||||
|
||||
## 4. Materials & textures (NOT in the BGF)
|
||||
- Geometry refs material via parent `PATCH`'s `SV_F_MATERIAL`/`SV_B_MATERIAL` leaf:
|
||||
`uint8 mattype` (`PFILE.H:169-170`; 1=named) + NUL-terminated `"library:material"` string,
|
||||
e.g. `basev:shadow_mtl`.
|
||||
- The part before `:` = a **`.BMF` material-library** file (`basev` → `BASEV.BMF`). BMF uses the
|
||||
**identical chunk grammar** (magic `DIV-BMF2`/`DIV-BIZ2`, `FILETYPE=1`, `MATERIAL`/`TEXTURE` chunks) —
|
||||
so the same parser reads it.
|
||||
- Texture **pixels** live in further files named by the BMF's `TEXTURE_MAP` (loaders
|
||||
`dpl_vtxRead/sgiRead/tgaRead`, `DPLUTILS.H:210-223`). **Load chain: BGF → BMF → image.**
|
||||
(Open: this archive has 2222 `.BMF` but few `.vtx/.sgi/.tga` — confirm where pixels actually live.)
|
||||
|
||||
## 5. Hierarchy → dpl model
|
||||
```
|
||||
HEADER
|
||||
BOUND { BSPHERE, BBOX }
|
||||
OBJECT → dpl_OBJECT
|
||||
SV_VERTEX
|
||||
LOD → dpl_LOD (LOD_DISTANCE in/out; may be absent = 1 implicit LOD)
|
||||
PATCH → dpl_GEOGROUP (SV_F/B_MATERIAL, SV_FACETED, SV_DECAL, SV_SPECIAL)
|
||||
PMESH → dpl_GEOMETRY
|
||||
VERTEX_xxx
|
||||
PCONN_LIST / CONNECTION_LIST
|
||||
BIZ_DONE
|
||||
```
|
||||
`PATCH == GEOGROUP` (`VCELTYPE.H:481-492`). Multi-LOD objects (1947 LODs/1275 files) are
|
||||
distance-switched; blend per `LOD_TRANSITION`/runtime `dpl_MORPH_MODE`.
|
||||
|
||||
## 6. Articulation / damage — name-driven, not geometric
|
||||
No morph-target/joint chunk exists. Articulation & damage key off **`SV_SPECIAL` (0x2037)** string
|
||||
tokens on geogroups, e.g. `dz_hip`, `dz_utorso`, `dz_larm`, `dz_rgun`, `dz_lfoot`, `dz_searchlight`,
|
||||
and behaviour tokens `PUNCH`, `BLINK`, `WIREFRAME`, `ADDITIVE_LODS`, `GEOMETRIZE 0x8000001a`.
|
||||
`dpl_FlushDCSArticulations`/`dpl_MorphObject`/`dpl_Damagize` match these names to attach DCS joints /
|
||||
morph / hide-swap on damage. **Loader must expose each geogroup's special string verbatim.**
|
||||
|
||||
## 7. Open items (need a known-good render or more samples)
|
||||
1. ~~`PCONN_LIST` vs `CONNECTION_LIST` semantics~~ **RESOLVED (twice-corrected — see §"Indices"):**
|
||||
`PCONN_LIST` = many faces with a leading points-per-face byte (4 or 6); `CONNECTION_LIST` =
|
||||
a FLAT TRIANGLE LIST (the earlier "ONE polygon per chunk" reading was wrong — task #20), and
|
||||
the two **coexist in one PMESH** (the earlier "prefer PCONN" reading dropped CONN's triangles —
|
||||
the turret-base missing-panels bug). The `*_LP` "light-point/FX" objects (materials `btfx:*`)
|
||||
carry only `SPHERE_LIST`/`POINT_LIST` geometry — no triangle mesh — and render as sprites
|
||||
later, not solids. Loader result on this corpus: **621/663 triangle meshes load; 42 are
|
||||
point/sphere FX.**
|
||||
2. Winding order (CW/CCW) + `SV_FACETED` ↔ backface culling — confirm visually.
|
||||
3. Full `SV_SPECIAL` token grammar (multi-word commands).
|
||||
4. `SPHERE`/`SPHERE_LIST` per-sphere radius source.
|
||||
5. 4-byte length variant (`0x8000`) — unused here, handle defensively.
|
||||
@@ -0,0 +1,187 @@
|
||||
# BattleTech (Tesla 4 / rel 4.10) — Source Status & Reconstruction
|
||||
|
||||
**Purpose:** brief for a meeting with an original BT programmer. What source we have, what was
|
||||
missing, where each piece came from, and how we're patching the gaps to get BT running on modern
|
||||
Windows. **The most useful thing you can tell us is at the bottom (§7).**
|
||||
|
||||
**One-line status:** the port now **compiles, links, and boots to a window**, and is running into the
|
||||
mission/spawn simulation. The engine/renderer/audio came from an existing Windows port (Red Planet);
|
||||
the BT *game logic* was lost and is being **reconstructed from the shipped binary** + the surviving
|
||||
headers + Red Planet's parallel code.
|
||||
|
||||
---
|
||||
|
||||
## 1. The situation in one picture
|
||||
|
||||
| Layer | Have it? | Source |
|
||||
|---|---|---|
|
||||
| **MUNGA engine + L4 HAL** (renderer, mover, math, scene, net, audio) | ✅ complete | The **Windows port** ("WinTesla", Red Planet 4.11 source) — already bypasses the Division IG board with Direct3D 9 (`L4D3D`) and replaces HMI SOS audio with OpenAL. |
|
||||
| **BT headers** (`.hpp`: class layouts, vtables, signatures) | ⚠️ **partial** | ~14 survived in the BT source archive (`410srczipped`); the **core ~28 (`mech`, `heat`, `powersub`, weapons, `btplayer`, `hud`, gauges…) were lost** — see §3. |
|
||||
| **BT game logic** (`mech`, subsystems, weapons, HUD, app…) `.cpp` | ❌ **LOST** | Reconstructed from the shipped binary `BTL4OPT.EXE` (decompiled). |
|
||||
| **Deployed content** (models, skeletons, skins, animations, missions/eggs) | ✅ present | The pod machine disk image. |
|
||||
|
||||
Where a header survived it's an *answer key* for class layout/vtable order (then the binary just
|
||||
supplies the bodies). Where it didn't (the core mech/weapon/player classes), the layout itself is
|
||||
inferred from the binary's vtables + `this+offset` usage, cross-checked against the Red Planet analog —
|
||||
a harder reconstruction, and the part most worth validating against original source if any exists.
|
||||
|
||||
---
|
||||
|
||||
## 2. Where everything lives (the four archives, in the repo root)
|
||||
|
||||
| Archive | What it is | Key contents |
|
||||
|---|---|---|
|
||||
| `410srczipped.zip` | **BT source tree** | All `.hpp` headers, geometry/material defs, **428 animations**, and a *partial* set of `.cpp`. Build manifests `CODE/BT/BT/BT.MAK` + `CODE/BT/BT_L4/BTL4.MAK` (the authoritative file lists). |
|
||||
| `Tesla4NovellTechPC-*.zip` | Pod **technician/test PC** image | The compiled game `btrel410.exe`, Division VPX board diagnostics, shared textures. |
|
||||
| `Tesla4PodPCNovell-*.zip` | The pod **game machine** image | The full **deployed content tree** at `…/CopyofNovellDisk/REL410/BT/` — per-mech skeletons/skins, models, textures, gauges, **and the mission eggs** (`TEST.EGG`, `LAST.EGG`). Also a copy of the game binary. |
|
||||
| `Elsewhen RP411 Source-*.zip` | **The Windows port** ("WinTesla", VS2008) | Modernized engine: `WinTesla.sln`, MUNGA + MUNGA_L4 (incl. `L4D3D.cpp` = IG→Direct3D), OpenAL/libsndfile audio, and the **complete Red Planet game logic**. The solution references a `BT410_L4` target **but the BT game source folder is absent** — confirming BT was mid-port and its code isn't here. |
|
||||
|
||||
The shipped binary `btrel410.exe` is a PKZIP self-extractor; unwrapped it yields **`BTL4OPT.EXE`**
|
||||
(1.24 MB, clean PE32) — that's our decompilation + behavioral oracle.
|
||||
|
||||
---
|
||||
|
||||
## 3. What was missing, precisely (from `BT.MAK` + `BTL4.MAK` = 53 modules)
|
||||
|
||||
Every module in the original build manifests is accounted for as follows:
|
||||
|
||||
### 3a. Survived as real source (~10) — used as-is
|
||||
From `CODE/BT/BT/` and `CODE/BT/BT_L4/`:
|
||||
`BTMSSN` (mission), `BTREG` (registry), `BTTEAM`, `BTSCNRL` (scenario role), `BTCNSL` (console),
|
||||
`BTTOOL`, `GAUSS` (Gauss rifle), `PPC`, `BTL4ARND` (arena), `BTL4MODE` (mode manager).
|
||||
|
||||
### 3b. Lost → reconstructed from the binary (~43 modules)
|
||||
The heart of the game. **Two cases, depending on whether the header also survived:**
|
||||
|
||||
**(i) Header AND `.cpp` lost → both reconstructed** (class layout/vtable inferred from the binary +
|
||||
RP analog — these are the ones whose layout is *our inference*, not ground truth):
|
||||
`mech`, `mech2`, `mech3`, `mech4`, `mechsub`, `dmgtable`, `heat`, `heatfamily_reslice`, `powersub`,
|
||||
`gnrator`, `gyro`, `torso`, `myomers`, `searchlight`, `thermalsight`, `mechweap`, `emitter`,
|
||||
`projweap`, `missile`, `projtile`, `ammobin`, `btplayer`, `mechmppr`, `btl4vid`, `btl4mppr`, `hud`,
|
||||
`btl4gaug`, `btl4gau2`, `btl4gau3`.
|
||||
|
||||
**(ii) `.hpp` survived, only `.cpp` lost → implementation reconstructed against the real header**
|
||||
(lower risk — layout is ground truth):
|
||||
`mechdmg`, `mechtech`, `sensor`, `messmgr`, `mislanch`, `misthrst`, `seeker`, `btl4app`, `btl4mssn`,
|
||||
`btl4rdr`, `btl4grnd`, `btl4pb`, `btl4galm`, `btdirect`.
|
||||
|
||||
(Surviving headers with no reconstruction needed — interface-only or their `.cpp` also survived:
|
||||
`bt`, `btl4`, `btl4ver`, `btmssn`, `btreg`, `btteam`, `bttool`, `gauss`, `ppc`, `btl4arnd`,
|
||||
`btl4mode`, `testbt`, `turret`.)
|
||||
|
||||
### 3c. Genuinely unaccounted-for (4) — **please confirm with us (§7)**
|
||||
| Module | Our handling | Question for you |
|
||||
|---|---|---|
|
||||
| `btl4` (the `WinMain`/main app) | **Recreated** as `btl4main.cpp`, cloned from RP's launcher `RPL4.CPP`. | Is the real BT main app meaningfully different from RP's? |
|
||||
| `path` | **Not reconstructed.** Not referenced on the current code path. | What is `path.cpp` — AI pathfinding? Is it needed for single-player bots? |
|
||||
| `mechbld` | Assumed **offline tool** (mech/model builder), not runtime. | Correct? |
|
||||
| `btl4tool` | Assumed **offline tool**, not runtime. | Correct? |
|
||||
|
||||
### 3d. Missing files, organized by the directory you'd look in
|
||||
|
||||
This is the concrete "check your backup drive" list. `BOTH` = `.cpp` and `.hpp` both gone;
|
||||
`.cpp only` = header survived; `.hpp only` = implementation survived.
|
||||
|
||||
**`CODE/BT/BT/` (game logic)**
|
||||
|
||||
| File (module) | What's missing |
|
||||
|---|---|
|
||||
| `mech`, `mech2`, `mech3`, `mech4`, `mechsub` | **BOTH** |
|
||||
| `heat`, `powersub`, `gnrator` | **BOTH** |
|
||||
| `mechweap`, `emitter`, `projweap`, `missile`, `projtile`, `ammobin` | **BOTH** |
|
||||
| `gyro`, `torso`, `myomers`, `hud`, `dmgtable` | **BOTH** |
|
||||
| `btplayer`, `mechmppr` | **BOTH** |
|
||||
| `path` | **BOTH** (AI pathfinding — see §3c) |
|
||||
| `mechdmg`, `mechtech`, `sensor`, `messmgr`, `mislanch`, `misthrst`, `seeker`, `btdirect` | `.cpp only` |
|
||||
| `btcnsl` | `.hpp only` |
|
||||
|
||||
**`CODE/BT/BT_L4/` (app / cockpit / L4 integration)**
|
||||
|
||||
| File (module) | What's missing |
|
||||
|---|---|
|
||||
| `btl4vid`, `btl4gaug`, `btl4gau2`, `btl4gau3`, `btl4mppr` | **BOTH** |
|
||||
| `mechbld`, `btl4tool` | **BOTH** (assumed offline tools — see §3c) |
|
||||
| `btl4` (main app), `btl4app`, `btl4mssn`, `btl4rdr`, `btl4grnd`, `btl4pb`, `btl4galm` | `.cpp only` |
|
||||
|
||||
**Present and intact (no help needed):** `CODE/BT/BT/`: `btmssn`, `btreg`, `btteam`, `bttool`,
|
||||
`gauss`, `ppc`, `turret.hpp`, `bt.hpp` · `CODE/BT/BT_L4/`: `btl4arnd`, `btl4mode`, `btl4.hpp`,
|
||||
`btl4ver.hpp`, `testbt.hpp`.
|
||||
|
||||
---
|
||||
|
||||
## 4. How the BT game logic is being reconstructed
|
||||
|
||||
`BTL4OPT.EXE` is a near-ideal target: class names, `Class::Method` strings, and **original source
|
||||
paths in asserts** (`d:\tesla_bt\bt\mech.cpp`) are left in the binary. Per module:
|
||||
|
||||
1. Locate the module's function cluster in the decompiled output (asserts + address contiguity).
|
||||
2. Map vtable slots → methods (vtable order = declaration order, from the surviving header).
|
||||
3. Map `this+0xNN` offsets → named members (from the header layout).
|
||||
4. Rewrite the Ghidra pseudo-C into compilable C++, **cross-checking the Red Planet analog** for
|
||||
idioms — `VTV`≈`Mech`, `VTVMPPR`≈`mechmppr`, `RPPLAYER`/`BLOCKER`≈`btplayer`, `WEAPSYS`≈weapons,
|
||||
`RPL4*`≈the `BT_L4` app layer.
|
||||
5. Verify behavior against the binary as an oracle.
|
||||
|
||||
Result is a **behavior-equivalent reconstruction**, not the original text. Reconstructed files carry a
|
||||
header banner citing the originating `@ADDR` and the cross-references used.
|
||||
|
||||
---
|
||||
|
||||
## 5. How it's assembled & patched into a running build
|
||||
|
||||
- **Engine:** the WinTesla MUNGA + MUNGA_L4 (193 files) compiles green to `munga_engine.lib`
|
||||
(modern MSVC, Win32, targeting the legacy DirectX SDK June 2010 for `d3dx9`/`dinput`).
|
||||
- **BT game lib (`bt410_l4`):** the 43 reconstructed `.cpp` + the ~10 surviving BT `.cpp` + a small
|
||||
`btstubs.cpp` (see below) + a header-forwarding shim (the DOS-era code uses `.hpp` include names;
|
||||
WinTesla renamed engine headers to `.h`).
|
||||
- **Launcher:** `btl4main.cpp` (`WinMain`, cloned from `RPL4.CPP`), drives `BTL4Application`.
|
||||
- **Link:** `bt410_l4` + `munga_engine.lib` + OpenAL/libsndfile + D3D9/d3dx9/dinput8 → **`btl4.exe`**.
|
||||
|
||||
### The patch points we know about (the bring-up worklist)
|
||||
These are deliberately isolated and flagged `// TODO(bring-up)`:
|
||||
- **`btstubs.cpp`** — first-link placeholders for: a handful of engine data globals not pulled from the
|
||||
static lib (`allPresets`, `FrameTimeScale`); the BT renderable pipeline (`BT*Renderable`); the 2D HUD
|
||||
layer (`dpl2d_*`, which libDPL provided and `L4D3D` hasn't yet re-implemented); a few `Mech`/subsystem
|
||||
accessors. Each is inert until given a real body.
|
||||
- **Two stubbed video data files** — `VIDEO\REPLACEMATS.tbl` / `MATREPLACETABLE.tbl` (WinTesla-era
|
||||
material-substitution tables, absent from the 1995 content) created empty so video init proceeds.
|
||||
- **A few reconstructed `BTPlayer`/`Mech` methods** still missing real bodies (currently being
|
||||
recovered, in dependency order driven by what the running game asks for next).
|
||||
|
||||
---
|
||||
|
||||
## 6. Current runtime state (live)
|
||||
|
||||
`btl4.exe` boots cleanly: opens `BTL4.RES`, parses the `-egg TEST.EGG` mission (BattleTech / cavern /
|
||||
freeforall / night), creates the application, initializes the D3D video renderer, **opens its window**,
|
||||
and enters the mission simulation. The **player-spawn pipeline now executes end-to-end** —
|
||||
`HuntForDropZone → DropZone reply → BTPlayer::CreatePlayerVehicle → MakeAndLinkViewpointEntity →
|
||||
Mech::Make → Mech::Mech` (all reconstructed from the binary + RP analog this session).
|
||||
|
||||
**Current frontier = the `Mech` object-layout + resource-streaming reconstruction.** The Mech
|
||||
constructor runs but doesn't yet *stream* its model/subsystems/skeleton: in the reconstruction the
|
||||
resource layer (`ResourceFind`/`ResourceStream`) is still a no-op placeholder, and ~145 raw
|
||||
`this[0xNN]` field accesses in `mech.cpp` are backed by a shared scratch bank instead of named members.
|
||||
This is the single biggest remaining reconstruction task — and, notably, it's **exactly the module
|
||||
whose original `.hpp` *and* `.cpp` were both lost** (`mech`, see §3d). Recovering the real `mech.*`
|
||||
would short-circuit this entire pass.
|
||||
|
||||
---
|
||||
|
||||
## 7. What would help most from you
|
||||
|
||||
1. **The real BT game source** — if a backup/dev drive has `CODE/BT/BT/*.cpp` (the lost `mech`,
|
||||
subsystems, weapons, HUD, app), it would replace the entire reconstruction. Even a partial set
|
||||
shrinks the work dramatically.
|
||||
2. **`path.cpp`** — what is it (AI pathfinding?), and is it needed for single-player bot missions?
|
||||
3. **`mechbld` / `btl4tool`** — confirm these are offline tools, not runtime modules.
|
||||
4. The **mission/egg flow** for standalone (non-networked) play — anything special about how a pod
|
||||
launched a single mission vs. the multiplayer console handshake.
|
||||
5. The **2D HUD / `dpl2d_*` layer** — how the cockpit gauges/radar were drawn, since that's the part of
|
||||
libDPL the Direct3D port hasn't reproduced yet.
|
||||
6. Confirmation that the pod disk image is the **complete content master**, and whether the full
|
||||
walk/run **animation set** exists beyond what shipped in the source archive.
|
||||
|
||||
---
|
||||
*Reference docs in this repo: `CLAUDE.md` (overall project), `btbuild/RECONCILE.md` (the detailed
|
||||
reconstruction + bring-up ledger), `decomp/README.md` (decompilation method).*
|
||||
@@ -0,0 +1,199 @@
|
||||
# BT Port — Hardest-Problems Front-Load Plan (scope-hardest-problems workflow + verification)
|
||||
|
||||
Goal: tackle the hardest, highest-LEVERAGE problems first so the rest of the port gets easier.
|
||||
6 candidates were scoped by independent deep-dive agents, then the load-bearing claims were VERIFIED
|
||||
against the code (several agent claims + several CLAUDE.md facts turned out wrong — see Verification).
|
||||
|
||||
## Ranked front-load sequence (post-verification)
|
||||
|
||||
| # | Problem | Verdict | Why here |
|
||||
|---|---------|---------|----------|
|
||||
| **1** | **P5 — Entity lifecycle / collision teardown** ★ | do-first | Hardest (4) + highest leverage (4) + hard prereq for multiplayer & multi-entity combat & real hit detection. Forces Entity/Mover base-region layout correctness that ALSO de-risks P3. |
|
||||
| **2** | **P3 — Locomotion cutover** (SequenceController → world transform) | do-early (adjacent to P5) | The gait pipeline is ALREADY reconstructed in source (mech2/3/4.cpp: legAnimation@0x65c, bodyAnimation@0x6bc SequenceControllers) — this is a CUTOVER from the procedural-slide stand-in, not greenfield. Shares P5's Mech/Mover layout de-risk. Produces the speed/turn-demand consumer P2 & P6 need. |
|
||||
| 3 | **P2 — Authentic input** (MechControlsMapper + RIO) | do-later | Downstream of P3 (mapper output is inert without the locomotion consumer). Software plumbing = days once P3 lands; physical RIO wiring defers to a Phase-8 session with Nick. |
|
||||
| 4 | **P6 — Multiplayer** (integrate existing TCP stack) | do-later | Terminal integration. The hard part is DONE (L4NET = 3446-line WinSock TCP; master/replicant core complete). Gated on P3 + P5 + subsystem waves. Do a 2-instance smoke test early to de-risk; save fidelity for last. |
|
||||
| 5 | **P1 — dpl2d reticle/PIP overlay** | opportunistic | Easy + isolated + additive (btl4vid.cpp already calls dpl2d_Circle for the PIP). Slot in whenever a visible aiming win is wanted; blocks nothing. |
|
||||
| — | **P4 — Build /FORCE cleanup** | optional cosmetic | ⚠ NOT a front-load enabler. The agent's premise was REFUTED (see Verification): /FORCE hides ZERO runtime symbols. Pure cosmetic; do opportunistically, not first. |
|
||||
|
||||
**Prerequisite gates:** P5 gates P6 · P3 gates P2 & P6 · P5 and P3 share a hidden prereq = **Entity/Mover
|
||||
base-region field-layout correctness** (do that audit once, up front, both benefit).
|
||||
|
||||
## Verification results (adversarial — several claims corrected)
|
||||
|
||||
- ✅ **P5 root cause corrected:** CLAUDE.md said "collision solids were never built." VERIFIED WRONG —
|
||||
`Mover::Mover` (RP/MUNGA/MOVER.cpp:1756) allocates `collisionLists = new BoxedSolidCollisionList[2]`
|
||||
UNCONDITIONALLY; `~Mover`:2050 deletes it. So the crash is a **clobber / dangling / teardown-order** bug.
|
||||
- ✅ **P6 corrected:** CLAUDE.md §8 said "reimplement over UDP." VERIFIED WRONG — L4NET.CPP is 3446 lines of
|
||||
WinSock **TCP** (`SOCK_STREAM`×4, `ReliableMode`, zero `SOCK_DGRAM`). Already reimplemented, not greenfield.
|
||||
- ❌ **P4 claim REFUTED:** agent claimed `/FORCE` swallows ~10-15 genuine runtime unresolveds (e.g.
|
||||
`Mech::WorldToLocal`). The linker emits EXACTLY 40 unresolveds = 20 `DefaultData` + 20
|
||||
`CreateStreamedSubsystem` (dead offline factory), **zero runtime symbols**. `WorldToLocal` resolves via the
|
||||
engine lib. CLAUDE.md's "dead offline-factory, cleanup TODO" characterization was correct. P4 is cosmetic.
|
||||
- ✅ **P3 confirmed less-greenfield:** mech2.cpp carries the full SequenceController gait reconstruction → cutover.
|
||||
- ✅ **P1 confirmed small/isolated:** dpl2d gap ≈ the reticle/PIP vector overlay only (btl4vid.cpp:563+ already
|
||||
calls dpl2d_NewDisplayList/Begin/Circle for the PIP; weapon beams are a separate 3D-renderables module;
|
||||
gauges/MFDs/radar are the separate L4GAUGE path — MUNGA_L4/L4GAUGE.cpp — already ported, OFF in BT).
|
||||
|
||||
## P5 — ⚠ AUDIT PIVOT: it is a TEARDOWN-SEQUENCE bug, NOT a base-region stomp (verified)
|
||||
|
||||
The base-region audit **disproved the base-region-stomp hypothesis** and redirected P5:
|
||||
- **The enemy's engine base region is FULLY VALID at death** (`BT_ENABLE_TEARDOWN` dump): `collisionLists@0x2e4
|
||||
=0D8C8C04`, `segmentTable@0x2f0=00872CE8` (segmentCount `51`), `jointSubsystem@0x30c=00872D68` all valid;
|
||||
`lastCollisionList`/`collisionAssistant` NULL (fine). Nothing corrupts it during the enemy's life.
|
||||
- **Every raw-offset base-region stomp is DEAD CODE.** `Mech::Simulate` (collision-cluster reads/writes +
|
||||
telemetry overflow past sizeof) and `FeedHeat*Gauge` (writes through `+0x2ec`) are DEFINED but NEVER
|
||||
CALLED (grep-confirmed; mech4.cpp:858 "our drivable override bypasses the unsafe Mech::Simulate"). So the
|
||||
`0x2d4-0x2f0` stomps and the `0x7e0-0x828` over-sizeof writes do not run — they are not the cause.
|
||||
- **The crash is in the teardown SEQUENCE** (`FryDeathRow → ~Mech → ~JointedMover → ~Mover`): cdb_dmg5 shows
|
||||
`collisionLists`'s array already **freed** (`0xdd` cookie / `count=0xdddddddb`) by the time `~Mover`'s
|
||||
`delete[]` runs — a **double-free / destruction-order** bug (crash site varies run-to-run: `~Mover`
|
||||
collisionLists, or `~JointedMover` deleting a `0x0ccd1210` value). ~Mech (reconstructed) runs BEFORE the
|
||||
engine base dtors, so the prime suspect is a Mech member dtor / the enemy's minimal-spawn collision setup
|
||||
freeing (or aliasing) a base resource that the engine base dtor then frees again.
|
||||
|
||||
**Latent finding (real but not the crash cause):** compiled `sizeof(Mech)=0x638` < binary `0x854`, and
|
||||
`Mech::Make` allocates the compiled size; the code that would write `0x7e0-0x828` past it is the dead
|
||||
`Simulate`, so no live overflow today — but if that code is ever revived, the Mech must first be padded to 0x854.
|
||||
|
||||
**✅ DOUBLE-FREE PINNED (trace done):** NOT collisionLists — it's the **skeleton SEGMENT teardown**.
|
||||
- `collisionLists` is LIVE at `~Mech` entry (`*cl` not `0xDD`) — ruling out the collision path.
|
||||
- Real crash stack: `~JointedMover` (JMOVER.cpp:436 `SegmentTableIterator(segmentTable).DeletePlugs()`) →
|
||||
`SocketIterator::DeletePlugs` (SOCKET.cpp:157-161 `delete plug`) → `EntitySegment` scalar-deleting-dtor →
|
||||
`_free_base` → **`STATUS_HEAP_CORRUPTION` (0xC0000374)** with **`0xFEEE` freed-fill** present. So one of the
|
||||
enemy's **51 EntitySegments is freed twice**.
|
||||
- Mechanism: `DeletePlugs(defeat_release_node=1)` (SOCKET.cpp:151-154) NULLs the socket's release node and
|
||||
force-`delete`s every plug, BYPASSING the ref-count release path. If the enemy's segments are ref-counted/
|
||||
shared (owned by the skeleton resource, or shallow-aliased by the minimal `Mech::Make` spawn), the normal
|
||||
release already freed them → `DeletePlugs` double-frees. (Earlier `~Mover` collisionLists `0xDD` crash was
|
||||
the same heap corruption surfacing at a different free — the segment double-free is the root.)
|
||||
|
||||
**Fix-trace (deep, still open):** segment-deletion trace (cdb bp on `EntitySegment::~scalar-deleting-dtor`)
|
||||
shows `DeletePlugs` deletes segment #1 OK (`0d3d3f30`), then crashes on segment #2 (`0d3d5200`) —
|
||||
`STATUS_HEAP_CORRUPTION` freeing its block. Only 2 of 51 deleted; **segment #2's block is invalid** and its
|
||||
scalar-dtor never fired earlier, so it was freed via ANOTHER path before `~JointedMover`. Ruled out:
|
||||
- `EntitySegment::~EntitySegment` (SEGMENT.cpp:116) only DeletePlugs its OWN child-index/damage/video plugs
|
||||
(integers) — it does NOT free sibling segments, so deleting #1 didn't free #2.
|
||||
- The reconstructed `~Mech` body (mech.cpp:952-1008) does NOT explicitly free `subsystemArray` or segments.
|
||||
- No live overflow past `sizeof(Mech)=0x638` (the over-sizeof writes are all in dead `Simulate`).
|
||||
|
||||
**Leading hypothesis — DUAL segment/joint DeletePlugs ownership:** the `JointSubsystem` dtor (JOINT.cpp:499-505)
|
||||
`DeletePlugs()`es its **`jointTable`**, and `~JointedMover` (JMOVER.cpp:436) `DeletePlugs()`es **`segmentTable`**;
|
||||
the JMOVER `#if 0` comment ("deleted by the entity subsystemArray[jointSubsystem]") flags the ownership overlap.
|
||||
If the enemy's minimal `Mech::Make` spawn puts the SAME segments in both tables (or sets release-node/ref-counts
|
||||
differently than a normal entity), both force-delete them → the second is the double-free.
|
||||
|
||||
**✅ DECISIVE TEST RUN (spawn-vs-rp-creation workflow + cdb):**
|
||||
- **Dual-ownership hypothesis DISPROVEN.** Engine source confirms `segmentTable` (owns EntitySegments,
|
||||
`(this,True)`) and `jointTable` (owns Joints, `(NULL,False)`) are DISJOINT — `DeletePlugs` on one never
|
||||
touches the other (JMOVER.cpp:171/310, JOINT.cpp:501-504, SEGMENT.cpp:116-126). So there is no
|
||||
segment/joint double-delete.
|
||||
- **Double-destruction DISPROVEN.** cdb bp on `JointedMover::~JointedMover` shows it runs EXACTLY ONCE for
|
||||
the enemy (`~JM this=0d04b998` == the teardown-log enemy this). Single teardown.
|
||||
- **=> It is HEAP CORRUPTION of an individual segment block (Hypothesis B), during a SINGLE teardown.**
|
||||
`segmentTable.DeletePlugs` frees segment #1 OK, then segment #2's block header is invalid
|
||||
(`RtlValidateHeap ... Invalid address`). So a live write during the enemy's life corrupts one segment's
|
||||
block. **The spawn-lifecycle fix (Registry::MakeEntity / BecomeInteresting) will NOT fix this** (the
|
||||
workflow's own gate: "entered once => steps 1-5 do not fix it").
|
||||
- **Source still unpinned, but these are RULED OUT:** the dead-`Simulate` raw-offset stomps (never called);
|
||||
double-destruction; subsystem writes past `sizeof(Mech)=0x638`; the segmentTable POINTER (valid at death).
|
||||
What remains: a live heap-corruptor of a segment block (candidate: the damage path mechdmg.cpp:628 which
|
||||
indexes segments while the enemy takes its 8 hits) — and it may not even be enemy-specific.
|
||||
|
||||
**⭐ P5 PREMISE WAS WRONG — dead mechs are SUPPOSED to stay (verified).** In the real game a killed mech does
|
||||
NOT vanish: RP's death path `VTV::DeathShutdown` (VTV.cpp:1681-1691) loops subsystems calling `DeathShutdown`
|
||||
(the vehicle shuts down but is NEVER removed); `CondemnToDeathRow`/entity-removal in RP is used ONLY for
|
||||
transient objects (RIVET.cpp projectiles, DEMOPACK.cpp cleanup), never for a combat kill. BT death is a
|
||||
STATE/VISUAL transition — `SetGraphicState(DestroyedGraphicState)` (mechdmg.cpp:355) + a death animation
|
||||
(`deathAnimationLatched@0x650`, mech.hpp:505) + death effect/splash. So the mech becomes a wrecked HULK and
|
||||
stays. ⇒ **Our current wreck-stays behavior IS the faithful one; `DestroyEntityMessage`-on-death is NOT a
|
||||
real behavior and should never be enabled.** The teardown crash is an artifact of forcing a removal the
|
||||
original never does (behind `BT_ENABLE_TEARDOWN`). There is NOTHING to fix here for faithfulness. The real
|
||||
future "death" work is the OPPOSITE of removal: reconstruct the DeathShutdown sequence + collapse animation
|
||||
+ destroyed skin — none of which touch the crashing teardown path.
|
||||
|
||||
**RECOMMENDATION: CLOSE P5 (not just park).** The wreck-stays death (explosion + stop-targeting) ships and
|
||||
combat is 100% unaffected; the crash only exists behind `BT_ENABLE_TEARDOWN`. Pinning the live segment-corruptor needs
|
||||
either gflags **PageHeap** (elevation — catches the overflow AT the write) or a `ba w4` drill on a segment
|
||||
block, i.e. another deep session for a cosmetic "wreck vanishes" win. Better to spend the effort on P3
|
||||
(locomotion cutover) and revisit this opportunistically (e.g. run once under PageHeap when elevation is available).
|
||||
|
||||
**(superseded) earlier next step:** hardware write-breakpoint (`ba w4`) on segment #2's block to catch the FIRST free
|
||||
(which table/dtor frees it) — capture its address at spawn, set the bp, run to the free. That names the exact
|
||||
first-owner and the fix (make that table release-not-delete, or de-duplicate the segment registration).
|
||||
**Pragmatic alternative:** the wreck-stays behavior (explosion + stop-targeting) already ships and combat is
|
||||
unaffected; full `DestroyEntityMessage` removal can stay deferred behind `BT_ENABLE_TEARDOWN` until the
|
||||
segment-ownership model is reconciled (a bounded but genuinely deep engine-archaeology task).
|
||||
|
||||
---
|
||||
### (superseded) earlier hypothesis — base-region stomp
|
||||
|
||||
Reproduced under cdb behind `BT_ENABLE_TEARDOWN=1` (mech4.cpp, default OFF, parallel to the working stand-in).
|
||||
|
||||
**Findings (all verified, not inferred):**
|
||||
- The documented cause "collision solids never built" is **WRONG**. `Mover::Mover` (MOVER.cpp:1756) allocates
|
||||
`collisionLists = new BoxedSolidCollisionList[2]` unconditionally; the `[teardown]` log shows the enemy's
|
||||
`collisionLists@0x2e4 = 0x0CCBCF8C` (a valid heap ptr) at death.
|
||||
- `collisionLists` sits at **Mover+0x2e4** (from `dt btl4!Mover`), inside the engine collision cluster
|
||||
`collisionVolumeCount@0x2d4 · collisionVolume@0x2d8 · collisionTemplate@0x2dc · containedByNode@0x2e0 ·
|
||||
collisionLists@0x2e4 · lastCollisionList@0x2e8 · collisionAssistant@0x2ec`. `sizeof(Mover)=0x2f0`,
|
||||
`JointedMover=0x318`, `Mech=0x638`.
|
||||
- The reconstructed Mech's **declared** fields for these are safely relocated (`netOrientation@0x3ec`,
|
||||
`arrivalTime@0x500`, `torsoAimTarget@0x3e0`) — BUT `Mech::Simulate`/terrain/heat code in mech4.cpp still
|
||||
uses **stale RAW binary offsets** `this+0x2d4 / +0x2e0 / +0x2e8 / +0x2ec / +0x2f0` that land ON the engine
|
||||
collision cluster. It READS them (garbage) and DEREF-WRITES through them (e.g. mech4.cpp:1020
|
||||
`*(this+0x2ec)+0xc = heatCapacity` writes through the engine `collisionAssistant` ptr). `physicsBody/
|
||||
groundRef/groundCell` aren't even declared members — they exist ONLY as these raw offsets.
|
||||
- Result: the engine base region is corrupted during the enemy's life; teardown crashes at **varying**
|
||||
base-member sites (cdb_dmg5: `~Mover`:2050 `delete[] collisionLists`, count cookie `0xdddddddb`; this run:
|
||||
`~JointedMover`:444 deleting a `0x0ccd1210` uninit member). It's the **base-region layout divergence**
|
||||
between the 1995 BT binary (raw offsets) and the 2007 RP411 engine base — the shared P3/P5 prerequisite.
|
||||
|
||||
**Remaining P5 fix (the real work):**
|
||||
1. **Base-region audit of mech*.cpp raw offsets:** enumerate every `this+0xNN` for `0xNN < 0x318` (engine
|
||||
base), map each against the `dt btl4!Mover`/`JointedMover` layout, and convert stomping accesses to the
|
||||
correct engine field or the relocated declared member. The `0x2d4-0x2f0` set is the known-bad start;
|
||||
audit the whole base range (there are likely more, given `Mech::Simulate` is raw-offset-dense).
|
||||
2. **Complete the minimal spawn** so the enemy's engine base members are all initialized (no `0xCD`).
|
||||
3. **Route death through `DestroyEntityMessage → FryDeathRow`** once teardown is clean (env flag → default).
|
||||
|
||||
Effort: the audit is mechanical but broad (Simulate/terrain/heat are raw-offset-dense) — days-to-weeks.
|
||||
Do it behind `BT_ENABLE_TEARDOWN` until green, since the shipped combat loop works by bypassing teardown.
|
||||
|
||||
## ✅ CLOSED — the BGF-load heap corruption (`bld08.bgf` / `Builder::~Builder` AV)
|
||||
|
||||
**Symptom:** mid-mission `LoadBgfFile("bld08.bgf")` → `Builder::~Builder` → `vector<float>` teardown → AV
|
||||
inside `operator delete` (ntdll `RtlpFreeHeap` dereferencing `0xDDDDDDDD`), position-dependent (one combat
|
||||
run crashed; walk-only and differently-routed runs were clean).
|
||||
|
||||
**Root cause (reconstruction TYPE CONFUSION, found by a 4-agent workflow + forensics):**
|
||||
`HeatSink`'s ctor resolved its linked sink via `owner->GetSegment(heatSinkIndex)` — the Nth skeleton
|
||||
**EntitySegment** (288 bytes compiled) — cast to `Subsystem*`/`HeatSink*`. The binary (`@004adda0`,
|
||||
part_012.c:16999) reads **`owner->subsystemArray[heatSinkIndex]`** (the subsystem ROSTER @0x128,
|
||||
bounds-checked vs subsystemCount @0x124, null-guarded before `linkedSinks.Add`). Through the bogus pointer,
|
||||
every per-frame `ConductHeat` wrote `other->pendingHeat` at compiled offset 388 = **100 bytes past the
|
||||
288-byte EntitySegment heap block** (and `BalanceCoolant` wrote `coolantLevel` +20 past) — thousands of
|
||||
4-byte OOB writes during sustained fire, smashing NT free-list metadata adjacent to the mech's segments.
|
||||
The BGF loader's big vector alloc/free churn merely DETECTED it later. Sibling bug: `PoweredSubsystem`'s
|
||||
`voltageSourceIndex` (res+0xFC; raw part_013.c:1198 = the same roster lookup) was also GetSegment-resolved,
|
||||
so `AttachToVoltageSource` wrote `currentTapCount` 136 bytes past the segment block at every mech spawn.
|
||||
|
||||
**Exoneration sweep (all CONFIRMED):** the loader itself — an exact Python mirror over all 879 content BGFs,
|
||||
zero anomalies; the crashed 0x768 block = exactly the 474-float MSVC growth capacity for bld08's 472 verts
|
||||
(healthy vector); every hard-coded placement-new alloc ≥ compiled sizeof (probe-compiled); Mech spawns use
|
||||
`sizeof(Mech)` (immune to growth); Explosion churn is pure engine code; `Mech::Simulate` over-sizeof writes
|
||||
are dead code; LoadLocomotionClips postdates the crash (timeline via /tmp mtimes) and its
|
||||
`keyframeData[keyframeCount]` read is binary-faithful.
|
||||
|
||||
**Fix (heat.cpp + powersub.cpp):** both resolutions replaced with the binary's roster lookup via the public
|
||||
`owner->GetSubsystemCount()`/`GetSubsystem(i)` (pre-checked so the engine Verify never fires; roster is
|
||||
pre-zeroed so forward refs read NULL = the binary's "missing" warn path). Powersub's else-gate also fixed to
|
||||
OWNER flags per raw. Payoff: the heat link now reaches a REAL sink (`heatEnergy=1.34e+07`, was ~0).
|
||||
|
||||
**Verification:** (1) `BT_HEAPCHECK=1` (new runtime gate, btl4main.cpp: `_CRTDBG_CHECK_ALWAYS_DF` whole-heap
|
||||
validation on every alloc/free) through 100+ shots — ZERO detections (pre-fix, the first ConductHeat write
|
||||
would trip it). (2) The im2 scenario re-run fast: **3601 shots + 10.6 km walked** — no AV. (3) `BT_PROBE_BGF`
|
||||
(new: direct-load models at boot; `=ALL` sweeps every GEO model) — bld08 clean 25×.
|
||||
|
||||
**Durable lesson (systemic checklist entry):** an owner offset `+0x128` in subsystem raw decomp is the
|
||||
subsystem ROSTER (`subsystemArray`), NOT the segment table — audit every `GetSegment(int)` call in
|
||||
reconstructed subsystem ctors (only these two existed; both fixed).
|
||||
@@ -0,0 +1,362 @@
|
||||
# P3 — Locomotion Cutover (procedural slide → animation-driven gait)
|
||||
|
||||
Plan from the `p3-locomotion-cutover-scope` workflow (4 maps, source-verified). Goal: make BT movement
|
||||
ANIMATION-DRIVEN (the walk/run clip's `[RootTranslation].z` IS the speed; feet plant by construction),
|
||||
replacing the bring-up procedural slide + disconnected `gBodyAnim`.
|
||||
|
||||
## Ground truth (verified)
|
||||
The real two-channel gait is reconstructed but **bracketed by no-op stubs on BOTH ends**, so it can't just
|
||||
be "called on":
|
||||
- **INPUT stubs:** `legAnimation@0x65c` / `bodyAnimation@0x6bc` are `ReconSeq` (mech.hpp:508-509);
|
||||
`ReconSeq::Advance()` returns 0, `SelectSequence`/`Reset` no-op, `keyframeData==NULL` (mechrecon.hpp:225-236).
|
||||
- **OUTPUT stub:** `Matrix34 == ReconMatrix` no-op (mechrecon.hpp:460); its `SetRotation/FromQuaternion` write
|
||||
nothing → `IntegrateMotion`'s world-step (mech4.cpp:206-285) commits nothing even with nonzero input.
|
||||
- `IntegrateMotion` is REAL logic (not a stub) but dead (input 0, output stub); its only caller `Mech::Simulate`
|
||||
(mech4.cpp:307) has 0 call sites. The live tick is bring-up `Mech::PerformAndWatch` (mech4.cpp:549).
|
||||
- `LoadLocomotionClips` (mech3.cpp:325) would CRASH today (reads `keyframeData[..]` on the NULL stub).
|
||||
- **The lever that already works:** `gBodyAnim->Animate(dt,True)` (mech4.cpp) runs the REAL engine
|
||||
`AnimationInstance::Animate` (JMOVER.cpp:1474-1700), cycling joints AND returning the clip's per-frame
|
||||
root-translation distance — which the stand-in threw away. That is the smallest safe cutover.
|
||||
|
||||
## Steps (each independently build+test; mech keeps moving throughout)
|
||||
- **✅ STEP 1 — DONE (commit below): forward travel animation-driven via the working `AnimationInstance`.**
|
||||
Use the `adv` that `gBodyAnim->Animate(dt,True)` returns as the forward step (`localOrigin.linearPosition
|
||||
+= facing * adv`); removed the `kDriveMaxSpeed * throttle * dt` slide (mech4.cpp). VERIFIED: mech walks at
|
||||
the clip's authentic ~60 u/s (was a guessed 30), FORWARD (−Z), feet plant (travel==stride); combat +
|
||||
damage un-regressed; no crash.
|
||||
- **✅ STEP 2 — DONE: gait clip selection by throttle** (walk/run/reverse). `|throttle| >= 0.5` → run
|
||||
(`blhrrl` id 904), else walk (`blhwwl` id 898); throttle sign → direction (negative backs up); SetAnimation
|
||||
only on gait change (tracked by `gCurrentGait`). Added a forced-throttle VALUE (`BT_FORCE_THROTTLE=<v>`,
|
||||
btl4main.cpp) for headless testing. VERIFIED: run ~60 u/s, walk ~22 u/s (walk is ~1/3 of run — authentic
|
||||
per-clip speeds, NOT a scalar of one guess), reverse backs up (pos.z increases); combat un-regressed (enemy
|
||||
DESTROYED at walk speed, 8 hits). NOTE: the blh set has no dedicated reverse CYCLE clip (stand/walk/run +
|
||||
transitions only) → reverse plays the forward clip and drives the body backward (bring-up; real reverse in STEP 7).
|
||||
- **STEP 3 — back the embedded controllers with real `AnimationInstance`** (`ReconSeq`→`AnimationInstance`,
|
||||
0x60 bytes each at 0x65c/0x6bc; adapters SelectSequence→SetAnimation, Advance→Animate, Reset, keyframe
|
||||
accessors). Infrastructure only; live path unchanged.
|
||||
- **STEP 4 — call `LoadLocomotionClips(model)` at model build** (mech3.cpp:325) to populate
|
||||
`animationClips[]`/`namedClip[]` + stride caps (`standSpeed`, `walkStrideLength`, ...). Safe after Step 3.
|
||||
- **STEP 5 — bring-up feed for `movementMode@0x40` + `bodyTargetSpeed@0x6b4` + `commandedSpeed`** from
|
||||
`gBTDrive.throttle` (the real `MechControlsMapper::InterpretControls` is deferred — reads unsafe App offsets).
|
||||
- **STEP 6 — back `Matrix34`/`ReconMatrix` with the real engine affine matrix + base-region audit** of
|
||||
`IntegrateMotion`'s transform offsets (`+0x260` dual-use pos/quat suspected mislabel; `+0x100` labeled
|
||||
maxSpeed but used as `localToWorld`) vs `part_012.c`, anchored from `damageZoneCount@0x11c`.
|
||||
- **STEP 7 — THE FULL CUTOVER:** run the real two-channel gait (`AdvanceLegAnimation`/`AdvanceBodyAnimation`
|
||||
+ `IntegrateMotion`) in `PerformAndWatch`, retiring the Step-1/2 stand-in translation + free-standing
|
||||
`gBodyAnim`. Gives the authentic locally-simulated + displayed-motion gait.
|
||||
|
||||
**Deferred polish (post-core):** airborne/fall gaits (AdvanceBody/LegAnimationAirborne), torso-twist-to-target
|
||||
(Torso is FULLY reconstructed — TorsoSimulation twist/elevation @004b5cf0 + WriteJoints; needs wiring), gyro sway.
|
||||
|
||||
**First-milestone recommendation:** Steps 1-2 give a visibly correct animation-driven walk/run (real speeds,
|
||||
feet plant) on the already-proven engine path, with near-zero risk. Steps 3-7 are the deeper "authentic
|
||||
two-channel gait" and can follow once the milestone is banked.
|
||||
|
||||
---
|
||||
|
||||
## ⭐ BASE-REGION RECONCILIATION (the STEP-6 audit — ground truth, the shared P3/P5/gyro de-risk)
|
||||
|
||||
This is THE root cause behind the P5 teardown crash, the gyro `+4` cross-link landmine, the dead `Mech::Simulate`
|
||||
stomps, AND why `IntegrateMotion` can't be revived: the reconstructed `IntegrateMotion`/`Simulate` (mech4.cpp) use
|
||||
**1995 raw binary offsets** (`this+0xNN`) that, in the 2007 RP411 engine base layout, land ON live engine fields.
|
||||
|
||||
**Ground-truth compiled layout (cdb `dt btl4!Mover` / `!JointedMover` / `!Mech`, from btl4.pdb):**
|
||||
```
|
||||
engine Mover/JointedMover: reconstructed Mech own region:
|
||||
+0x0C8 localToWorld (LinearMatrix) +0x3D4 torsoAimCurrent
|
||||
+0x0F8 localOrigin (Origin) +0x3E0 torsoAimTarget
|
||||
+0x114 damageZoneCount +0x11C subsystemCount +0x3EC netOrientation (EulerAngles)
|
||||
+0x124 updateOrigin (Origin) +0x458 movementMode +0x45C movementFlags
|
||||
+0x194 creationTime (Time) +0x468 airborneSelect +0x46C forwardCycleRate
|
||||
+0x250 projectedOrigin (Origin) +0x4A4 groundCycleRate +0x4A8 airborneCycleRate
|
||||
+0x26C previousOrigin (Origin) +0x4C8 deathAnimationLatched
|
||||
+0x288 projectedVelocity (Motion) +0x4D4 legAnimation +0x4E0 bodyAnimation (ReconSeq)
|
||||
+0x2A0 updateAcceleration (Motion) +0x500 arrivalTime +0x504 simTime +0x508 spinRate
|
||||
+0x2B8 updateVelocity (Motion) +0x598 motionEventName +0x5A4 motionEventArmed
|
||||
+0x2D0 nextUpdate (Time)
|
||||
+0x2D4..0x2EC collision cluster (Count/Volume/Template/containedByNode/Lists/last/Assistant)
|
||||
+0x2F0 segmentTable +0x308 segmentCount +0x30C jointSubsystem (JointedMover ends 0x318)
|
||||
```
|
||||
|
||||
**The stomp table — every `IntegrateMotion` raw offset vs the engine field it corrupts, and the correct target:**
|
||||
| raw offset (1995) | intended 1995 field | 2007 engine field it STOMPS | correct target (declared member) |
|
||||
|---|---|---|---|
|
||||
| `this+0x100` | motion payload A | `localOrigin` (0xF8-0x114) | (relocate) `motionPayloadA` |
|
||||
| `this+0x12c` | motion payload B | `updateOrigin` (0x124-0x140) | (relocate) `motionPayloadB` |
|
||||
| `this+0x260` | motionDelta (Quat) | `projectedOrigin` (0x250-0x26c) | **declare** `motionDelta` |
|
||||
| `this+0x26c` | worldPose (Quat) | `previousOrigin` (0x26c-0x288) | **declare** `worldPose` |
|
||||
| `this+0x298`/`0x29c` | angular accum | `projectedVelocity` (0x288-0x2a0) | **declare** `angularAccum` |
|
||||
| `this+0x2a4` | torsoAimTarget | `updateAcceleration` (0x2a0-0x2b8) | ✅ existing `torsoAimTarget`@0x3E0 |
|
||||
| `this+0x2d4` | netOrientation | `collisionVolumeCount` | ✅ existing `netOrientation`@0x3EC |
|
||||
| `this+0x598`/`0x5a4` | motionEventName/Armed | (past base, in Mech region) | ✅ existing `motionEventName`/`motionEventArmed` |
|
||||
|
||||
So `torsoAimTarget`, `netOrientation`, `motionEventName/Armed`, `arrivalTime`, `simTime`, `spinRate`, `movementMode/
|
||||
Flags`, cycle rates ARE already relocated as declared Mech members — but `IntegrateMotion` still reads the RAW
|
||||
offsets for them AND for the 3 un-declared motion fields (`motionDelta`/`worldPose`/`angularAccum`) + the 2 motion
|
||||
payloads. Reviving `IntegrateMotion` as-is would corrupt `projectedOrigin`/`previousOrigin`/`projectedVelocity`/
|
||||
`updateAcceleration`/the collision cluster → exactly the P5 teardown heap corruption.
|
||||
|
||||
## STEP-6/7 attack (revised, concrete)
|
||||
1. **Lock the ground truth** (compile-time): `friend struct MechBaseLayoutCheck` with `static_assert(offsetof(...))`
|
||||
on the engine-base fields (localOrigin@0xF8, projectedOrigin@0x250, previousOrigin@0x26c, collision cluster
|
||||
@0x2d4-0x2ec, segmentTable@0x2f0) + the relocated members (netOrientation@0x3EC, torsoAimTarget@0x3E0,
|
||||
arrivalTime@0x500, spinRate@0x508). Any future raw-offset stomp then fails the build.
|
||||
2. **Declare the 3 missing motion members** (`motionDelta`/`worldPose` Quaternion, `angularAccum` Vector3D) +
|
||||
the 2 motion payloads in the Mech's OWN region (offset > 0x318), so nothing lands on the engine base.
|
||||
3. **Convert every `IntegrateMotion` raw `this+0xNN` (0xNN < 0x318) to the declared member** per the table above.
|
||||
Same pass for the dead `Simulate` (so it's revivable) — this is the base-region audit HARD_PROBLEMS wanted.
|
||||
4. **Back the 3 stub families with real engine types:** `ReconMatrix`→`AffineMatrix` ops (Identity/FromQuaternion/
|
||||
transform-vector); `ReconQuat*`→real `Quaternion`; `ReconSeq`→real `AnimationInstance` (the two-channel gait
|
||||
input, 0x60 bytes each @legAnimation/bodyAnimation).
|
||||
5. **Revive `IntegrateMotion` in `PerformAndWatch`** (the cutover), retiring the Step-1/2 stand-in translation.
|
||||
Verify: mech walks via the real gait, no engine-base stomp, no teardown crash, combat/heat un-regressed.
|
||||
|
||||
Payoff: this ALSO fixes the gyro cross-link (write to the correct relocated field, not gyro+4), makes the dead
|
||||
`Simulate` revivable, and removes the P5 teardown corruptor — the single highest-leverage de-risk in the port.
|
||||
|
||||
### PROGRESS (this pass)
|
||||
- ✅ **STEP 1 — layout locked:** `MechBaseLayoutCheck` (mech4.cpp, friend of Mech) static_asserts the engine-base
|
||||
offsets (localOrigin@0xF8, projectedOrigin@0x250, previousOrigin@0x26c, collision cluster@0x2d4-0x2ec,
|
||||
collisionLists@0x2e4, segmentTable@0x2f0) + the relocated members (torsoAimTarget@0x3E0, netOrientation@0x3EC,
|
||||
arrivalTime@0x500, spinRate@0x508). All pass — the ground truth is proven in code; any stomp now fails the build.
|
||||
- ✅ **STEP 2/3 (partial) — declared the missing motion members** (mech.hpp, appended so no locked offset shifts):
|
||||
`motionDelta`/`worldPose`/`worldPoseBase`/`angularAccum` (Quaternion) + `motionSourceA/B`/`motionEventVector`
|
||||
(Vector3D). **Converted the LIVE-PATH functions** `IntegrateMotion` (@004ab1c8) and `DeadReckonPose` (@004ab188)
|
||||
— every raw `this+0xNN` (<0x318) now uses the declared member (0x260→motionDelta, 0x26c→worldPose, 0x298→
|
||||
angularAccum, 0x100/0x12c→motionSourceA/B, 0x598→motionEventVector, 0x2a4→torsoAimTarget, 0x2d4→netOrientation,
|
||||
0x138→worldPoseBase). Build green, combat un-regressed (TARGET DESTROYED, 0 crashes). The dead `Simulate`
|
||||
(@004ab430) still holds raw offsets — DEFERRED (off the cutover path; convert when/if reviving it).
|
||||
- ✅ **STEP 4 (partial) — `ReconMatrix` backed** with the real engine `AffineMatrix` (mechrecon.hpp): `Identity`→
|
||||
`BuildIdentity()`, `FromQuaternion`/`SetRotation`→`operator=(const Quaternion&)`. SAFE: its only live caller
|
||||
(IntegrateMotion) uses a LOCAL `bodyFrame`; other callers are the dead Simulate.
|
||||
- ✅ **STEP 4 — `ReconQuat` backed** (mechrecon.hpp): `ReconQuatIdentity`→`Quaternion::Identity`,
|
||||
`ReconQuatIntegrate`→`Quaternion::Add(source, Vector3D delta)` (the exact FUN_00409f58 integrate). SAFE: all its
|
||||
callers are dead (Simulate + the dead-until-cutover IntegrateMotion/DeadReckonPose). Also relocated the last
|
||||
live-adjacent raw offset: the `this+0x1dc` "aim/torso" clear → a declared `aimRate` (Quaternion) member. (NOTE: I
|
||||
earlier miscalled mech4.cpp:447 a LIVE PerformAndWatch call — it is actually in the dead `Simulate` telemetry tail
|
||||
(<0x551 where PerformAndWatch starts), so ReconQuat had no live caller after all.) Build green, combat un-regressed.
|
||||
- ⚠ **STEP 4 remaining — the GAIT (`ReconSeq`) is the hard final piece, NOT a drop-in.** Verified: the leg/body
|
||||
controllers use `SelectSequence`/`Advance`/`Reset` (FUN_004277a8/0042790c/004283b8) — a BT-specific **SequenceController**
|
||||
keyframe player that **does NOT exist in the RP411 engine** (grep of MUNGA found no SelectSequence/Sequence/Controller).
|
||||
So "ReconSeq→AnimationInstance" (the STEP-3 note) is inaccurate. TWO approaches, a real fork:
|
||||
(A) **Reconstruct the SequenceController** from the binary (FUN_004277a8/0042790c/004283b8 + its keyframe-table
|
||||
struct) and keep the authentic two-channel gait (AdvanceLeg/BodyAnimation + IntegrateMotion). Most faithful;
|
||||
biggest. NOTE: retyping legAnimation/bodyAnimation to the full controller GROWS the Mech (they are ~0xC now vs
|
||||
the binary's 0x60 each), shifting the relocated members after them (arrivalTime/spinRate) — update those
|
||||
MechBaseLayoutCheck locks (they are by-name relocations, safe to move; the engine-base locks must NOT move).
|
||||
(B) **Rewrite AdvanceLeg/BodyAnimation onto the engine `AnimationInstance`** (which STEP 1-2 already proved can
|
||||
play the gait clips). Less faithful to the exact controller, reuses the engine, no SequenceController rebuild.
|
||||
Also still a stub: **`FUN_00408744`** (mechrecon.hpp:391) — the world-step transform (transform angularAccum by the
|
||||
bodyFrame matrix) IntegrateMotion needs; back it (AffineMatrix·Vector3D) as part of the cutover.
|
||||
- **STEP 7 (revive) is unchanged** but gated on the gait decision above.
|
||||
|
||||
---
|
||||
|
||||
## ⭐ SequenceController RECONSTRUCTION SPEC (chosen path A — from the binary, part_003.c)
|
||||
|
||||
The BT gait controller (embedded `legAnimation`@0x65c / `bodyAnimation`@0x6bc, 0x60 bytes each). It is a BT-specific
|
||||
keyframe animation player (NOT in the RP411 engine). Full decomp read: ctor @00427768, SelectSequence @004277a8,
|
||||
Advance @0042790c, Reset @004283b8, dtor @004278d4. It parses the SAME animation-clip resource format the engine
|
||||
`AnimationInstance` uses and writes joints through the SAME `Joint` API already backed for the gyro/torso.
|
||||
|
||||
**Object layout (0x60; offsets are byte offsets into the controller):**
|
||||
```
|
||||
+0x00 vtable (Plug base, FUN_00415e90 ctor) +0x34 rootTranslation[] table (0xC/frame; .z@+8 = stride)
|
||||
+0x0c (base refcount region) +0x38 jointSubsystem (resolved from owner Mech +0x31c)
|
||||
+0x14 frameCount (resource hdr[0]) +0x3c currentFrame (int)
|
||||
+0x18 jointCount (resource hdr[1]) +0x40 owner (Mech*)
|
||||
+0x1c jointIndices[] (jointCount ints) +0x44 clipResource (ref-counted handle)
|
||||
+0x20 hdr+2 (metadata ptr) +0x48 finishedCallback (Mech method ptr)
|
||||
+0x24 frameTimes[] (frameCount floats) +0x4c cbArg2 +0x50 cbArg3
|
||||
+0x28 keyframeBase +0x2c keyframeCursor +0x54 currentTime (Scalar)
|
||||
```
|
||||
**Methods (behaviour + engine mapping):**
|
||||
- **ctor(mech):** Plug base; store owner@0x40; `jointSubsystem@0x38 = mech->GetJointSubsystem()` (binary resolves
|
||||
mech+0x31c); clipResource@0x44 = 0.
|
||||
- **SelectSequence(clipID, cb, a2, a3):** store cb@0x48/a2@0x4c/a3@0x50; reset currentFrame@0x3c + currentTime@0x54;
|
||||
release old clipResource; `FindResourceDescription(clipID)` → clipResource@0x44; parse the clip: frameCount@0x14,
|
||||
jointCount@0x18, jointIndices@0x1c, frameTimes@0x24, keyframe pointers@0x28/0x2c; compute the per-frame keyframe
|
||||
stride@0x34 by summing joint DOF sizes (hinge<3 →8B, ball==4 →0xC, balltrans==5 →0x18).
|
||||
- **Advance(dt, moveJoints) -> Scalar distance:** `t = currentTime + dt`. Walk keyframes while `frameTimes[cur] <= t`:
|
||||
for each joint (jointSubsystem->GetJoint(jointIndices[i])) snap to the keyframe pose (hinge→SetRotation(Radian),
|
||||
ball→SetRotation(EulerAngles), balltrans→ + SetTranslation) IF moveJoints; accumulate `distance += (frameTimes[cur]
|
||||
- currentTime) * rootTranslation[cur].z`; advance cur/currentTime. If clip ended (cur==frameCount): call the
|
||||
finishedCallback (owner->*cb)(clipResource, carryover, moveJoints) and add its distance. Else interpolate the
|
||||
partial frame (ratio = (t - currentTime)/(frameTimes[cur]-currentTime)); per joint LERP/SLERP between keyframes
|
||||
(FUN_00409390 slerp / FUN_00408848 translation-lerp / FUN_00408dd4 hinge-lerp) and write; accumulate the partial
|
||||
distance. Return distance. Engine helper map: joint iter→`JointSubsystem::GetJoint`; GetEulerAngles=FUN_0041cfa0;
|
||||
SetRotation(Radian)=FUN_0041d0a8; SetRotation(EulerAngles)=FUN_0041d020; SetTranslation=FUN_0041d11c;
|
||||
SetHinge/direct=FUN_0041cfc8; close-enough=FUN_00408d78(radian)/FUN_004091f4(euler)/FUN_004084fc(point);
|
||||
slerp=FUN_00409390; fabsf=FUN_004dcd00.
|
||||
- **Reset(loop):** reset each joint to a default pose (identity quat + default euler/translation) via the per-type set.
|
||||
- **dtor:** reinstall vtable + release clipResource.
|
||||
|
||||
**Wiring:** replace the `ReconSeq` stub (mechrecon.hpp) with this class (keep a `typedef ... ReconSeq` so mech2's
|
||||
`legAnimation.SelectSequence/Advance/Reset` calls are unchanged); retype is automatic. The Mech GROWS (0xC→0x60 per
|
||||
controller = +0xA8) → move + update the by-name locks after them (arrivalTime/simTime/spinRate); engine-base locks
|
||||
UNCHANGED. The Mech ctor must construct legAnimation/bodyAnimation with `this` (owner). Then STEP 7 cutover.
|
||||
|
||||
### ✅ BUILT (seqctl.cpp) — the SequenceController is reconstructed, linked, integrated (inert until the cutover)
|
||||
- `SequenceController` class in mechrecon.hpp (extends the old ReconSeq: keeps keyframeCount/keyframeTimes/keyframeData
|
||||
accessors for mech3; adds the real fields; `typedef SequenceController ReconSeq`). `seqctl.cpp` implements Init
|
||||
(ctor logic — resolves `owner->GetJointSubsystem()`), SelectSequence (find via `application->GetResourceFile()->
|
||||
SearchList(clip_id, 16)` + Lock + parse frameCount/jointCount/jointIndices/frameTimes/pose-base/rootTranslations),
|
||||
Advance (snap full keyframes + interpolate the partial frame via the engine Joint API, accumulate the
|
||||
rootTranslation.z distance, loop at end-of-clip), Reset (neutral pose per joint type), dtor (Unlock the clip).
|
||||
`Hinge`==8B confirmed (`{int axisNumber; Radian rotationAmount}`) → keyframe hinge snap `SetHinge(*(Hinge*)cursor)`
|
||||
is byte-faithful. Joint I/O reuses the same engine API the gyro/torso use (GetJointType/GetEulerAngles/GetTranslation
|
||||
/ SetHinge/SetRotation(Radian|EulerAngles)/SetTranslation).
|
||||
- Added seqctl.cpp to btbuild/CMakeLists.txt. Full build GREEN; combat un-regressed (TARGET DESTROYED, 0 crashes).
|
||||
Inert for now: its callers (mech2 AdvanceLeg/BodyAnimation via IntegrateMotion, mech3 LoadLocomotionClips) are all
|
||||
DEAD until the cutover, so the retype + link changes no live behavior.
|
||||
- **Two known simplifications to revisit at the cutover (STEP 7):** (a) end-of-clip currently loops directly (reset to
|
||||
frame 0) instead of invoking the stored member-fn-ptr finished-callback @0x48 — faithful in INTENT (that callback is
|
||||
Mech::OnBodyAnimFinished's re-arm), refine if a non-looping callback is needed; (b) partial-frame rotation uses a
|
||||
component LERP of the euler/hinge (the binary FUN_00409390 slerps) — visually identical for small gait deltas, upgrade
|
||||
to slerp if needed.
|
||||
- **Remaining for STEP 7 cutover (task 13):** (1) Mech ctor must call `legAnimation.Init(this)` + `bodyAnimation.Init(this)`
|
||||
(currently default-constructed with jointSubsystem=0 — fine while inert, REQUIRED before use); (2) call
|
||||
`LoadLocomotionClips(model)` at model build to populate the clips; (3) back the `FUN_00408744` world-step transform;
|
||||
(4) revive `IntegrateMotion` in `PerformAndWatch`, retiring the STEP-1/2 stand-in. First point where it all
|
||||
runtime-verifies together.
|
||||
|
||||
### ✅ STEP 7 CUTOVER — DONE & VERIFIED LIVE (gated `BT_GAIT_CUTOVER=1`; the real controller drives locomotion)
|
||||
Rather than reviving the FULL `IntegrateMotion` (leg+body channels + world-step + DeadReckonPose, whose position-apply
|
||||
lives in the dead `Simulate`/`MoveAndCollide`) in one leap, the cutover swaps the STEP-1/2 forward-step SOURCE to the
|
||||
real `SequenceController` on the PROVEN position path (`localOrigin += facing*adv`). This runtime-verifies the
|
||||
reconstructed controller with minimal risk and is the meaningful milestone (the authentic gait controller drives the
|
||||
mech). Implemented + verified:
|
||||
- **(1) DONE** — Mech ctor (`mech.cpp`, after the body-load `ResolveJoint("jointlocal")`, where `jointSubsystem` is
|
||||
valid) calls `legAnimation.Init(this)` + `bodyAnimation.Init(this)`. Runs always (harmless when the cutover is off).
|
||||
- **cutover branch** — `PerformAndWatch` (mech4.cpp), gated `static int s_gaitCutover = getenv("BT_GAIT_CUTOVER")`.
|
||||
On a gait CHANGE it `bodyAnimation.SelectSequence(ResolveAnimationClip("blh",suffix), (void*)1 /*loop*/, 0, 0)`; each
|
||||
frame `adv = bodyAnimation.Advance(dt,1)` (the real controller animates the skeleton AND returns the clip forward
|
||||
step) → `localOrigin.linearPosition += facing*adv*dir`. STEP-1/2 `gBodyAnim` path preserved as the `else` (default).
|
||||
- **KEY BUG (found via cdb av in `ResourceFile::SearchList+0x77`, RESOURCE.cpp:498):** `SelectSequence` fetched the
|
||||
clip with `rf->SearchList(clip_id, 16)` — but `SearchList(list_id,type)` treats its arg as a resource LIST (reads the
|
||||
resource's data as an ID array + iterates) → it walked clip 904's animation bytes as garbage resource IDs → crash.
|
||||
The `clip_id` from `ResolveAnimationClip` is already a DIRECT resource ID → fixed to `rf->FindResourceDescription(clip_id)`
|
||||
+ `Lock`, which mirrors the engine's own clip load `AnimationInstance::SetAnimation` (JMOVER.cpp:1406) EXACTLY (same
|
||||
header layout too: hdr[0]=frameCount, hdr[1]=jointCount, hdr[2]=footStepThreshold skipped, hdr[3]=jointIndices).
|
||||
- **Verified live under cdb** (`BT_FORCE_THROTTLE=1 BT_GAIT_CUTOVER=1`): `[gait] SequenceController -> blhrrl id=904
|
||||
(run)`, mech walks (adv≈1.07, matching the STEP-1/2 baseline; pos advances), 0 crashes. Combat regression
|
||||
(`+BT_SPAWN_ENEMY +BT_FORCE_FIRE`): identical to a same-harness BASELINE (no-cutover) run — both fire/heat/damage the
|
||||
same (`FIRED #1..#141`, `structure` climbs 0.133/hit) and both walk PAST the stationary dummy before 8 hits (no
|
||||
DESTROY in-window) — i.e. NOT a regression, just harness geometry. `SequenceController::Advance` runs live for the
|
||||
first time with no crash.
|
||||
- **DECOMP FACT (`Advance@0042790c` raw, part_003.c:6722):** distance is LUMPY BY DESIGN — whole keyframes add
|
||||
`(frameTimes[cur]-currentTime)*rootTrans[cur].z` (matches seqctl.cpp:217); the partial-frame else-branch (6821)
|
||||
interpolates joints but adds NO distance; end-of-clip (6814) CALLS the finished-callback `@0x48` recursively (passing
|
||||
the carryover time) and folds its return into distance. Smoothness in the real game comes from VELOCITY integration
|
||||
in `IntegrateMotion`, not per-Advance — so applying `adv` directly to position (this cutover) is inherently lumpy;
|
||||
that's expected, faithful to the distance fn, and superseded once the velocity path lands.
|
||||
- **STILL GATED (not default) pending fidelity refinements:** (a) end-of-clip `carryover*stride[last]` in place of the
|
||||
recursive finished-callback `@0x48` (source of the occasional adv spike at loop boundaries — needs `OnBodyAnimFinished`
|
||||
wired as the real member-fn-ptr callback that re-arms + recursively continues `Advance`); (b) component-LERP → slerp
|
||||
(`FUN_00409390`); (c) the deep win — the FULL `IntegrateMotion` velocity path (leg+body via mech2 `AdvanceLeg/
|
||||
BodyAnimation`, the `FUN_00408744` world-step, `DeadReckonPose`) which smooths the lumpy step and enables MP dead-reckon.
|
||||
|
||||
### ✅ STEP 7 DEEPER — the AUTHENTIC world-step (real velocity model) drives locomotion LIVE
|
||||
Took the "harder path": replaced the cutover's direct position slide with the real `IntegrateMotion`
|
||||
motion-tail (`@004ab1c8`), so the mech now moves through the authentic velocity→rotate→integrate model.
|
||||
- **`FUN_00408744` BACKED** (mechrecon.hpp; was a no-op template stub): the world-step matrix×vector
|
||||
(part_000.c:8331, `out[i]=Σ_j v[j]·M(i,j)`). Backed via the engine `AffineMatrix::GetFromAxis` column
|
||||
basis — `out = v.x·colX + v.y·colY + v.z·colZ` reproduces the raw row-dot EXACTLY, and it is the SAME
|
||||
`GetFromAxis` convention the drive facing uses, so sign-consistent. Only real caller = mech4.cpp:304
|
||||
(gyro only name-drops it in comments).
|
||||
- **KEY MAPPING (verified):** the 1995 motion transform `{ Point3D @0x260; Quaternion @0x26c }` IS the
|
||||
engine `localOrigin` — `Origin = { Point3D linearPosition; Quaternion angularPosition }` (ORIGIN.h:15);
|
||||
raw `FUN_0040ab44` builds the matrix from BOTH halves (rot from 0x26c, translation from 0x260). So the
|
||||
motion state maps directly onto `localOrigin`, NOT the parallel `motionDelta`/`worldPose` placeholders.
|
||||
- **Cutover branch now (mech4.cpp):** `adv = bodyAnimation.Advance` → `localVel = {0,0,-adv/dt}` →
|
||||
`Matrix34::FromQuaternion(orient, localOrigin.angularPosition)` → `FUN_00408744(worldVel, localVel,
|
||||
orient)` → `localOrigin.linearPosition += worldVel*dt`. == `facing*adv`, through the real machinery.
|
||||
- **Latent bug fixed** in the (still-dead) full `IntegrateMotion`: it set only `spinRate`@0x508 and left
|
||||
`angularAccum[2]` (velocity.z the world-step reads) stale → now sets `angularAccum[2] = -adv/dt`.
|
||||
- **Verified:** walk-only `BT_GAIT_CUTOVER=1` walks straight fwd ~60 u/s, 0 crashes; combat fire/damage
|
||||
identical to STEP-1/2. ⚠ A combat run faulted in the ENGINE BGF loader (`bld08.bgf`, `Builder::~Builder`)
|
||||
walking into a building's range — pre-existing heap fragility, position-dependent (STEP-1/2 fired 600+
|
||||
clean), NOT the world-step (zero heap ops). Track separately.
|
||||
|
||||
### Remaining full-`IntegrateMotion` work (the rest of the harder path)
|
||||
1. **`AdvanceBodyAnimation` gait STATE MACHINE** (mech2.cpp:506, real — stand→walk→run transitions via
|
||||
`SetBodyAnimation(state)` → `animationClips[state]`) in place of the inline `ResolveAnimationClip`+
|
||||
`SelectSequence`. Needs #2.
|
||||
2. **`LoadLocomotionClips`** (mech3.cpp:326, real) called at model build (Mech ctor) to populate
|
||||
`namedClip[]`/`animationClips[]` + gait-speed caps (standSpeed/walkStride/reverseSpeedMax/gimp…) —
|
||||
measured via `legAnimation.SelectSequence` + `keyframeData` (now that the SequenceController is real).
|
||||
OPEN: reconcile `namedClip[]`@0x5e0 vs `animationClips[]`@0x5cc + the state→clip index mapping.
|
||||
3. **Orientation INTEGRATION** — the real turn is angular-rate integration into `localOrigin.angular
|
||||
Position` (raw `FUN_00409f58(0x26c, 0x26c, angRate*dt)`), driven by control input, not the bring-up
|
||||
`gDriveHeading`. (Bring-up keeps `gDriveHeading` until the controls mapper is un-bypassed.)
|
||||
4. **Velocity STORAGE + `DeadReckonPose`** — store the world velocity (raw @0x298) for MP dead-reckon;
|
||||
`DeadReckonPose(fraction)` projects the render pose forward between sim ticks (the true render smoothing).
|
||||
5. Then call the real `Mech::IntegrateMotion` (retargeted to `localOrigin`) from the cutover, retiring the
|
||||
inline world-step.
|
||||
|
||||
### ✅ STEP 2 (steps 1-2 of the full IntegrateMotion) — LoadLocomotionClips + the gait STATE MACHINE drive the walk LIVE
|
||||
Gated `BT_GAIT_SM` (requires `BT_GAIT_CUTOVER`). The real `Mech::AdvanceBodyAnimation` now drives locomotion.
|
||||
- **Step 1 — `LoadLocomotionClips`** called in the Mech ctor (gated), completes cleanly for the Blackhawk:
|
||||
resolves the full gait set + measures the speed caps (verified `walkStride=22.02`, `standSpeed=6.83`).
|
||||
**RECONCILIATION FIX:** `namedClip[]`@0x5e0 and `animationClips[]`@0x5cc were declared as SEPARATE arrays
|
||||
but in the binary are ONE (`namedClip[i]==animationClips[i+5]`, 0x5e0==0x5cc+0x14) -> the loader's writes
|
||||
weren't visible to `SetBodyAnimation`/`MeasureClipStride`. Fixed: `namedClip` is a pointer aliased to
|
||||
`&animationClips[5]` (ctor). Same class of bug as the field-shadowing systemic issue.
|
||||
- **Step 2 — `AdvanceBodyAnimation` state machine** drives the walk: case 6/7 slews `bodyCycleSpeed` toward
|
||||
`bodyTargetSpeed` with the loaded caps, Advances `animationClips[6]=wwr`, returns the cycle distance ->
|
||||
world-step. **FIX:** `bodyAnimationState`@0x728 == `bodyStateAlarm.level` (also declared-separate) -> re-synced
|
||||
from the alarm at the top of AdvanceBodyAnimation so `SetBodyAnimation`'s level reaches the switch.
|
||||
- Verified live: mech walks forward (z 1600->-975), `adv~0.35-0.42` (authentic walk cadence, < run's 1.07),
|
||||
clip loops its 15 keyframes, 0 crashes.
|
||||
- **Bring-up substitute (MARKED, mech2.cpp):** `SetBodyAnimation` passes a non-null loop sentinel `(void*)1`
|
||||
where the binary passes `PTR_LAB_0050d6fc` = the real body finished-callback (gait TRANSITION stand->walk->run
|
||||
+ leg alternation, @0x48). Without a non-null cb the SequenceController leaves the cycle stuck at its last
|
||||
keyframe (`adv=0`). Reconstruct `PTR_LAB_0050d6fc` for authentic transitions.
|
||||
|
||||
### Remaining for the FULL gait (after steps 1-2)
|
||||
1. **Reconstruct `PTR_LAB_0050d6fc`** (+ the leg `PTR_LAB_0050d6f0`) -- the finished-callback that transitions
|
||||
gait states (stand->walk->run->reverse) + alternates legs. Replaces the `(void*)1` loop sentinel.
|
||||
2. **LEG channel** `AdvanceLegAnimation` for visible leg stepping (body channel already drives world motion).
|
||||
3. **Gait SELECTION** from throttle -> `bodyTargetSpeed`/`movementMode` (currently forces walk state 6);
|
||||
wire walk/run/reverse via the real commanded speed (the `MechControlsMapper` is still bypassed).
|
||||
4. Then orientation integration + velocity storage + DeadReckonPose (the later IntegrateMotion pieces).
|
||||
|
||||
### ✅ TRANSITION CALLBACK — reconstructed + wired (walk/run leg alternation + walk->run transition)
|
||||
The real gait finished-callback (was a `(void*)1` loop sentinel).
|
||||
- **Resolved the .data fn ptr by PE-parsing** BTL4OPT.EXE: `PTR_LAB_0050d6fc`@0x50d6fc -> body cb **0x4a6d8c**;
|
||||
`PTR_LAB_0050d6f0`@0x50d6f0 -> leg cb **0x4a6928**; cbArg2/3 = 0. Neither in the assert-anchored decomp, so
|
||||
**disassembled with capstone** + decoded the jump table (byte idx @0x4a6de9, dword targets @0x4a6e0a; 33 states
|
||||
-> 10 handlers). Reusable: `/tmp/readptr.py` (PE VA->DWORD) + `/tmp/disas.py`, `/tmp/jt.py`, `/tmp/dh*.py`.
|
||||
- **Reconstructed `Mech::BodyClipFinished` (= FUN_004a6d8c)** byte-for-byte (mech2.cpp): dispatch on
|
||||
bodyAnimationState, compare bodyTargetSpeed to the caps (standSpeed/walkStrideLength/reverseSpeedMax), pick the
|
||||
next state, `SetBodyAnimation(next)` (re-binds with THIS callback) + `bodyAnimation.Advance(carryover-derived)`
|
||||
via the shared tail `Mech::BodyTransition`. Walk 6<->7, run 12<->13, stop->8/9, walk->run 6->11->12.
|
||||
- **Plumbing:** `SequenceController::Advance` now CALLS `cbCode` as `Scalar(*)(Mech*,unsigned,Scalar,int)` at
|
||||
end-of-clip (matches the binary `**(code**)(this+0x48)`), folding the returned distance. `SetBodyAnimation`
|
||||
passes `&Mech::BodyClipFinished`; the inline cutover passes `&Mech::LoopBodyClip` (bring-up loop).
|
||||
- **Verified live (cdb):** walk alternates 6<->7 (adv~0.42), run alternates 12<->13 (adv~0.5-0.85), walk->run
|
||||
transition fires (6->11->12), pos advances, 0 crashes; inline cutover (LoopBodyClip) + combat un-regressed.
|
||||
- **Fixed a bring-up gap:** `reverseSpeedMax2`@0x7a0 (run bodyCycleSpeed clamp) unset by LoadLocomotionClips ->
|
||||
0xCDCDCDCD -> run exploded; set to reverseStrideLength (case-12 divisor -> ratio~1).
|
||||
- **Remaining:** LEG callback FUN_004a6928 (state@0x3b0 + motion source@0x128); airborne FUN_004a6344; gimp
|
||||
handlers (16-19; targets 0x70b2/0x7161 undecoded); gait SELECTION from real throttle (mapper still bypassed).
|
||||
|
||||
### ✅ PILLAR A COMPLETE — leg channel + orientation + velocity storage + DEFAULT-ON
|
||||
- **LEG finished-callback** FUN_004a6928 (== PTR_LAB_0050d6f0) reconstructed (`Mech::LegClipFinished`,
|
||||
mech2.cpp): jump table @0x4a69aa decoded (same 33-state shape as the body's); compares the LIVE mapper
|
||||
speedDemand (*(subsystemArray[0])+0x128 -> typed mirror controlsMapper), slews legCycleSpeed@0x348,
|
||||
re-arms via SetLegAnimation; gimp alternates 0x12<->0x13 with |ratio| (gimpStrideLength stored negative).
|
||||
- **Two-channel split LIVE** (real-controls): leg = joints from live demand, body = motion only
|
||||
(Advance(dt, 0)). Natural Standing entry verified: legState 0 -> 5 -> 11 -> 13<->12 @ 61.5 u/s.
|
||||
- **RAW FIX: Standing case INVERTED in BOTH channels** (raw case 0: standSpeed < commanded -> walk(5);
|
||||
commanded < 0 -> reverse(0x10); else stand). Also: AdvanceLegAnimation controlSource double-deref AV ->
|
||||
controlsMapper->speedDemand; legAnimationState==legStateAlarm.level re-sync.
|
||||
- **Orientation**: yaw rate composed via Quaternion::Add (FUN_00409f58 form) -- spawn orientation kept
|
||||
authentic (turn-rate constant still bring-up).
|
||||
- **Velocity storage every frame**: worldLinearVelocity + localVelocity (fwd -Z + yaw rate) == the
|
||||
Mover::WriteUpdateRecord publish set = the MP dead-reckon writer data.
|
||||
- **DEFAULTS FLIPPED** (BTEnvOn): BT_GAIT_CUTOVER / BT_GAIT_SM / BT_COLLISION / BT_REAL_CONTROLS default
|
||||
ON; "=0" opts back to the bring-up paths (verified). BT_SPAWN_ENEMY now places the dummy along the
|
||||
spawn FACING (the authentic orientation exposed the old fixed-offset assumption).
|
||||
- Verified: env-free default run = full authentic chain, combat damage -> 0.933, 0 crashes; all =0
|
||||
fallbacks work. P3 is CLOSED for multiplayer purposes (the update-writer data is maintained).
|
||||
+1555
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
# BT Port — Resource-Layout Audit (resource-layout-audit workflow, 33 agents, adversarially confirmed)
|
||||
|
||||
> **✅ STATUS: ALL 8 BUGS FIXED & VERIFIED** (commits 802a7a6 A, 7de66ad B, 975a397 Gyro+Searchlight).
|
||||
> Each layout locked with compile-time `static_assert(offsetof/sizeof)` guards; full build green; combat +
|
||||
> heat-flow un-regressed. The plan below is retained as the record of what was found and done.
|
||||
|
||||
**Verdict:** 24 `*__SubsystemResource` structs audited → **8 confirmed bugs** (all high-severity,
|
||||
review-upheld, none refuted), 16 clean. All 8 root to **2 broken base resources** (`HeatWatcher`,
|
||||
`PowerWatcher`) + **2 struct-specific field defects** (`Gyroscope`, `Searchlight`).
|
||||
|
||||
Same class as the heat-resource bug fixed earlier (§10d): a resource struct that doesn't mirror its
|
||||
class hierarchy reads every field from the wrong offset — SILENT garbage (non-fatal). These 6 Watcher-
|
||||
family subsystems (AmmoBin, Gyroscope, Searchlight, ThermalSight, Torso, HUD) currently TICK on garbage
|
||||
resource data. Combat is unaffected (mech/weapons/damage/heat don't depend on them) — it bites when
|
||||
WAVE 4-8 consumes gyro/torso motion, sensor/searchlight/thermalsight readouts, and ammo.
|
||||
|
||||
## Raw-decomp verification (done this session — the fix is bigger than "one-liner")
|
||||
- `HeatWatcher::CreateStreamedSubsystem` = `FUN_004aec54` chains to `FUN_004ac9ec` (the **MechSubsystem**
|
||||
parse, fills 0x30..0xE4 — armorByFacing/videoObjectName/alarmModel/... verified) then adds only
|
||||
`DegradationTemperature@0xE8` + `FailureTemperature@0xEC`. Record ends **0xF0** (MechSubsystem + 3).
|
||||
- `HeatWatcher` ctor `@4aeb40` chains to `FUN_004ac644` (MechSubsystem-level base ctor, shared with the
|
||||
HeatSink ctor) and reads only `res+0xe4/0xe8/0xec` — NO thermal-mass/heatSinkIndex reads. So HeatWatcher
|
||||
is genuinely **MechSubsystem-based**, not a full HeatableSubsystem.
|
||||
- `PowerWatcher` ctor chains to `FUN_004aeb40` = **HeatWatcher's ctor** → `PowerWatcher : HeatWatcher`
|
||||
(powersub.hpp already comments "real base is HeatWatcher"). Adds `MinVoltagePercent@0xF0`, record ends **0xF4**.
|
||||
- The reconstruction implemented BOTH classes as `: public HeatableSubsystem` (an approximation, with the
|
||||
real base in `// FUN_...` comments) → the resource was made HeatableSubsystem-based (0xFC) → every
|
||||
Watcher field slid +0x18 (HeatWatcher) / +0xC (PowerWatcher). Fixing the resource base alone will NOT
|
||||
compile (the ctor passes the resource to the HeatableSubsystem base ctor) → class+ctor+parser must change together.
|
||||
|
||||
## Fix plan (bases before children; each verified by offset logging `(char*)&res->f - (char*)res`)
|
||||
1. **Root cause A — HeatWatcher → MechSubsystem** (heatfamily_reslice.hpp/.cpp):
|
||||
- class `HeatWatcher : public MechSubsystem` (was HeatableSubsystem)
|
||||
- resource `HeatWatcher__SubsystemResource : public MechSubsystem__SubsystemResource` (was HeatableSubsystem::SubsystemResource); keep `int watchedSubsystem;`(0xE4) `Scalar degradationTemperature;`(0xE8) `Scalar failureTemperature;`(0xEC) → ends **0xF0**
|
||||
- ctor chains `MechSubsystem(...)`; parser chains the MechSubsystem parse (FUN_004ac9ec)
|
||||
- de-shadow any fields HeatWatcher inherited from HeatableSubsystem it no longer has
|
||||
- **Fixes HeatWatcher + AmmoBin** (child; fields realign to 0xF0/0xF4/0xF8/0xFC/0x100, size 0x104). Target stamp: HeatWatcher **0xF0**, AmmoBin **0x104**.
|
||||
2. **Root cause B — PowerWatcher → HeatWatcher** (powersub.hpp/.cpp; depends on A):
|
||||
- class `PowerWatcher : public HeatWatcher`; resource `... : public HeatWatcher::SubsystemResource`, keep `Scalar minVoltagePercent;`(0xF0) → ends **0xF4** (do NOT pad — would break the 0xF4 stamp)
|
||||
- powersub.hpp must `#include` the HeatWatcher resource (`heatfamily_reslice.hpp`)
|
||||
- ctor chains `HeatWatcher(...)`; parser chains HeatWatcher parse (FUN_004aec54, WatchedSubsystem/Degradation/Failure keys)
|
||||
- **Fixes PowerWatcher + HUD + ThermalSight + Torso** (children realign, no child edits). Target stamps: PowerWatcher **0xF4**, Torso **0x158**.
|
||||
3. **Gyroscope** (gyro.hpp; after B): add `Scalar field_f4;`(0xF4, ctor `FUN_004b3778` reads it, init -1.0f — best-effort name) BEFORE `exageration`(0xF8). Then fields align through 0x21C.
|
||||
4. **Searchlight** (searchlight.hpp:75 + searchlight.cpp:96; independent, after B): delete `int segmentDefault;`
|
||||
(compiles OOB at sizeof(PowerWatcher)); the binary reads `resource+0x28` = inherited `segmentIndex` →
|
||||
change searchlight.cpp:96 to `commandedOn = subsystem_resource->segmentIndex;`.
|
||||
|
||||
**Clean / no action (16):** Emitter, HeatableSubsystem (correctly 0xFC — do NOT touch; HeatSink/Generator/
|
||||
PoweredSubsystem/Reservoir/Condenser depend on it), Condenser, Reservoir, MechSubsystem, MechTech,
|
||||
MechWeapon, SubsystemMessageManager, MissileLauncher, MissileThruster, Myomers, PoweredSubsystem,
|
||||
Generator, ProjectileWeapon, Seeker, Sensor.
|
||||
|
||||
**Rejected proposal:** aliasing HeatWatcher fields onto the 0xFC HeatableSubsystem base via accessors/union
|
||||
— leaves sizeof=0xFC, contradicts the 0xF0 stamp, cascades the wrong size into PowerWatcher. Use the re-base.
|
||||
|
||||
**Verify after each step:** offset logging vs stamps (HeatWatcher 0xF0, PowerWatcher 0xF4, AmmoBin 0x104,
|
||||
Torso 0x158, Gyroscope ends 0x21C); build green; combat/heat/torso un-regressed.
|
||||
@@ -0,0 +1,218 @@
|
||||
I have verified the factory switch (mech.cpp 556-741), the stub block (145-181), the corrected ClassID map (CLASSMAP.md), and the heat base re-declarations (heat.hpp 168-242 vs mechsub.hpp 120-255). The findings are corroborated by the source. Here is the unified spec.
|
||||
|
||||
---
|
||||
|
||||
# UN-STUB THE BATTLETECH SUBSYSTEM ROSTER — SYNTHESIS PLAN
|
||||
|
||||
Ground truth used throughout: **the ctor address in each factory case's `// FUN_004xxxxx` comment, reconciled via `CLASSMAP.md`** — NOT the `case <Name>ClassID` enum label, which is systematically mislabeled. The numeric classID is correct; the label and stub-struct name are wrong.
|
||||
|
||||
## Master case → real-class table (authoritative; from mech.cpp 567-689 + CLASSMAP)
|
||||
|
||||
| factory case (line) | classID | ctor | alloc | current stub | **REAL class** | file (status) | special action |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| CockpitClassID (569) | 0xBBD | 4ae568 | 0x230 | `Cockpit` | **Condenser** | heatfamily_reslice (DONE) | — |
|
||||
| SensorClassID (574) | 0xBBE | 4ae8d0 | 0x1e4 | `Sensor` | **HeatSink aggregate/bank** | heatfamily_reslice (`#if 0` skeleton) | `sensorSubsystem=arr[id]` (0x1f7) = the heat bank, NOT a sensor |
|
||||
| CondenserClassID (580) | 0xBC0 | 4af408 | 0x230 | `Condenser` | **Reservoir** | heatfamily_reslice (DONE) | — |
|
||||
| GeneratorClassID (585) | 0xBC1 | 4b225c | 0x250 | `Generator` | **Generator** | powersub.cpp (DONE) | — |
|
||||
| PoweredSubsystemClassID (590) | 0xBC2 | 4b0f74 | 0x31c | `PoweredSubsystem` | **PoweredSubsystem** | powersub.cpp (DONE) | — |
|
||||
| MyomersClassID (595) | 0xBC3 | 4b1d18 | 0x328 | `Myomers` | **Sensor** | sensor.cpp (DONE) | — |
|
||||
| GyroClassID (600) | 0xBC4 | 4b3778 | 0x3d0 | `Gyro` | **Gyroscope** | gyro.cpp (DONE) | `gyroSubsystem=arr[id]` (0x14a) |
|
||||
| SinkSourceClassID (606) | 0xBC5 | 4b6b0c | 0x280 | `HeatSinkSource` | **Torso** | torso.cpp (DONE) | `sinkSourceSubsystem=arr[id]` (0x10e) |
|
||||
| ActuatorClassID (612) | 0xBC6 | 4b8fec | 0x358 | `Actuator` | **Myomers** | myomers.cpp (DONE) | — |
|
||||
| WeaponEmitterClassID (617) | 0xBC8 | 4bb120 | 0x478 | `EmitterWeapon` | **Emitter** | emitter.cpp (DONE) | `++weaponCount` |
|
||||
| JumpJetClassID (623) | 0xBCB | 4bd5c4 | 0x22c | `JumpJet` | **AmmoBin** | ammobin.cpp (DONE) | — |
|
||||
| MechWeaponClassID (628) | 0xBCD | 4bc3fc | 0x448 | `MechWeapon` | **ProjectileWeapon** | projweap.cpp (DONE) | `++weaponCount` |
|
||||
| MissileWeaponClassID (634) | 0xBCE | **4bdcb4** | 0x484 | `MissileLauncher` | **UNRESOLVED** (no CLASSMAP entry) | — | `++weaponCount` — **BLOCKED** |
|
||||
| BallisticWeaponClassID (640) | 0xBD0 | 4bcff0 | 0x44c | `BallisticWeapon` | **MissileLauncher** | mislanch.cpp (DONE) | `++weaponCount` |
|
||||
| MechControlsMapperID (646) | 0xBD3 | 49bca4 | 0x130 | `MechControlsMapper` | **SubsystemMessageManager** | messmgr.cpp (DONE) | `controlsMapper=arr[id]` (0x10d) — verify, see Risk 4 |
|
||||
| GaussWeaponClassID (652) | 0xBD4 | 4bb888 | 0x478 | `GaussRifle` | **PPC** (Emitter subclass) | emitter.cpp bridge | `++weaponCount` |
|
||||
| MechTechClassID (658) | 0xBD6 | 4b7f94 | 0x2a4 | **MechTech (real, MIS-WIRED)** | **HUD** | hud.cpp (DONE) | `mechTechSubsystem=arr[id]` (0x16d) — see Risk 4 |
|
||||
| LegSubsystemClassID (664) | 0xBD8 | 4b84dc | 0x238 | `LegSubsystem` | **Searchlight** | searchlight.cpp (DONE) | — |
|
||||
| HeatableClassID (669) | 0xBDC | 4ad228 | 0x140 | `HeatableSubsystem` (**ptr discarded — bug**) | **MechTech** | mechtech.cpp (DONE) | must `arr[id]=…` + cache mechTech |
|
||||
| DisplayClassID (679) | 0xBDE | 4b8718 | 0x234 | `MechDisplay` | **ThermalSight** | thermalsight.cpp (DONE) | — |
|
||||
|
||||
There is **NO real Cockpit / Sensor(@0xBBE) / Actuator / JumpJet / BallisticWeapon / LegSubsystem / MechDisplay class** — every one of those stub names is a mislabel for a different, already-reconstructed class.
|
||||
|
||||
Two confirmed wiring bugs visible in the source: (a) **case 0xBDC builds `HeatableSubsystem` and never stores the pointer** (line 676 has no `subsystemArray[id] =`); (b) **the real `MechTech` class is instantiated at case 0xBD6** (line 660, alloc 0x2a4 = HUD's size) — MechTech and HUD are swapped.
|
||||
|
||||
---
|
||||
|
||||
## 1. THE STUB → REAL SWAP RECIPE (general)
|
||||
|
||||
**Architecture decision (applies to every family): use a per-family factory-bridge function, do NOT `#include` the real subsystem headers into mech.cpp.** mech.cpp defines local bring-up stubs whose names collide with the real classes (`Condenser`, `Generator`, `PoweredSubsystem`, `Myomers`, `MechWeapon`, `MissileLauncher`, …). Including the real headers there is a redefinition storm. Instead:
|
||||
|
||||
**Per real class, in its own `.cpp` (which already includes its real header):**
|
||||
```cpp
|
||||
Subsystem *
|
||||
Create<Class>Subsystem(Mech *owner, int id, void *seg)
|
||||
{
|
||||
Check(sizeof(<Class>) <= SIZE); // SIZE = the factory alloc literal (binary's true size)
|
||||
return (Subsystem *) new (Memory::Allocate(SIZE))
|
||||
<Class>(owner, id, (<Class>::SubsystemResource *)seg
|
||||
/*, <Class>::DefaultData [, extra real ctor params] */);
|
||||
}
|
||||
```
|
||||
- `SIZE` = the exact `Memory::Allocate(0x…)` literal already in the factory case (these are the binary's real allocation sizes — keep them).
|
||||
- The `Check(sizeof <= SIZE)` guard catches a placement-new overrun, which is the source of the intermittent ~1-in-6 startup heap-corruption crash (base ctor writes `vitalSubsystemIndex@0x110` → object must be ≥ 0x114).
|
||||
- The trailing `<Class>::DefaultData` matches the real ctor signature `(Mech*, int, SubsystemResource*, SharedData& = DefaultData [, …])`. Where the factory already passes `…::DefaultData`/`,0,0`, preserve those real tail params.
|
||||
|
||||
**In mech.cpp:**
|
||||
```cpp
|
||||
extern Subsystem *Create<Class>Subsystem(Mech *, int, void *); // near the forward-decls
|
||||
...
|
||||
case <Name>ClassID: // keep the numeric classID; the LABEL stays as-is
|
||||
subsystemArray[id] = Create<Class>Subsystem(this, id, seg);
|
||||
/* preserve ++weaponCount and/or the special cache (sensorSubsystem=…/gyroSubsystem=…) */
|
||||
break;
|
||||
```
|
||||
Then delete the now-dead `RECON_SUBSYS(<StubName>);` line for that class. Leave a `RECON_SUBSYS` only where no reconstruction exists yet (the 0xBCE @4bdcb4 case).
|
||||
|
||||
### The 4 systemic checks — apply to EVERY real class before/while swapping
|
||||
1. **Shadowing (re-declared base fields).** Grep the class + its bases for fields that already exist in the engine `Subsystem`/`Simulation` chain or in the reconstructed BT base (`MechSubsystem`/`HeatableSubsystem`/`HeatSink`/`PoweredSubsystem`): `owner`/`hostEntity` (→ `Subsystem::owningEntity`, use a `GetEntity()` cast), `damageZone` (→ engine `Subsystem::damageZone`), `simulationState`/`destroyed` (→ `MechSubsystem::simulationState`), `simulationFlags`/`flags`/`statusFlags`, `subsystemID`/`subsystemId2`, `subsystemName`, `lastPerformance`/`lastUpdate`/`updateModel`, `sharedData`, `classID`. **Delete the re-declaration; use the inherited member/accessor.** A shadow both (a) leaves the engine's value unread (the recon copy is `0xCDCDCDCD`) and (b) lands at the wrong compiled offset. **NOT a shadow (leave as is):** each class's own `typedef … Performance;` + `SetPerformance(Performance)` (deliberately writes the inherited `activePerformance`), its own `typedef … SubsystemResource;`, and `static …DefaultData`. After de-shadowing, confirm `sizeof(Class) ≤ alloc SIZE`.
|
||||
2. **`Wword(N)` trap.** `mechrecon.hpp:192` `Wword(i)` is a global scratch bank (`bank[i&0x3ff]`), NOT `this+i*4`. Grep the class body for `Wword(` used as object/pointer access → replace with the named inherited member or an engine accessor (e.g. owner via `(Mech*)GetOwningSimulation()`). The heat/gyro/sensor/torso/myomers/powersub bodies are already clean here — keep them that way; `Wword` use is a mech.cpp-only concern.
|
||||
3. **MessageHandler chaining.** Each class's handler set must chain to its parent: `Receiver::MessageHandlerSet X(Parent::GetMessageHandlers());`. An empty/parentless set makes `Receiver::Receive` find no handler → inherited messages (TakeDamage, InjectCoolant, ToggleLamp, Jammed, …) silently dropped. Gyroscope/Sensor/Myomers and all heat classes currently define `GetClassDerivations` but an empty/missing `GetMessageHandlers` — fix on swap.
|
||||
4. **Performance install + validity.** Confirm the real ctor calls `SetPerformance(&Class::XxxSimulation)` **under the live-primary gate** (`(owner->simulationFlags & 0xC)==0`, some also require `& 0x100`). Without it, `activePerformance` stays `&Simulation::DoNothingOnce`, which on its first tick calls `NeverExecute()` → sets `DontExecuteFlag` → the subsystem is skipped forever (this is exactly why a stub "ticks once then goes silent"). Entity-validity for message delivery rides the mech's validity (already handled in bring-up).
|
||||
|
||||
**Tick is already wired** — no new plumbing. `mech4.cpp Mech::PerformAndWatch` (~825-871) walks `subsystemArray[0..subsystemCount-1]`, and for each `s->IsNonReplicantExecutable()` calls `s->PerformAndWatch` → `Simulation::PerformAndWatch` (SIMULATE.cpp:448) → `Perform(slice)` → `(this->*activePerformance)(slice)`. Installing `activePerformance` (check #4) is the entire difference between a stub and a running subsystem.
|
||||
|
||||
**Build/verify each swap:** `cmake --build C:/git/nick-games/btbuild/build --config Debug`; single file `C:/git/nick-games/btbuild/compile1.cmd <file>.cpp`. Watch the `[tick] first frame: dispatched to N … of M present` log — N should climb toward M (minus the bypassed `controlsMapper`) as stubs become real.
|
||||
|
||||
---
|
||||
|
||||
## 2. RECOMMENDED ORDER (rationale + dependencies + blocked)
|
||||
|
||||
The whole roster sits on the BT base chain `engine Subsystem → MechSubsystem → HeatableSubsystem → HeatSink → {PoweredSubsystem, Generator, Condenser, Reservoir, Bank}` and `PoweredSubsystem → {PowerWatcher → Gyro/Torso/HUD/Searchlight/ThermalSight, MechWeapon → Emitter/PPC, ProjectileWeapon → MissileLauncher}`. So the **heat base re-base is the universal prerequisite** — it must be correct before any leaf is trustworthy. This aligns with the user mandate (heat first).
|
||||
|
||||
- **WAVE 1 — Heat base re-base + de-shadow (PREREQUISITE, no factory change).** Reconcile `mechsub.hpp` ↔ `heat.hpp`; make `HeatableSubsystem : MechSubsystem`; delete shadow fields; fix the thermal offsets. Compile `heat.cpp`, `heatfamily_reslice.cpp`, `mechsub.cpp` green. **Unblocks every family.**
|
||||
- **WAVE 2 — HEAT family + the MechTech/HUD wiring fix (user: "heat first").** Un-stub Condenser (0xBBD), Reservoir (0xBC0), aggregate sink (0xBBE). Fold in the MechTech↔HUD swap (0xBDC→MechTech with store + cache; 0xBD6→HUD) because the 0xBDC case currently instantiates the heat base and discards the pointer — it has to change here anyway, and it validates the one already-real class in its correct slot. Verify heat builds/dissipates.
|
||||
- **WAVE 3 — Energy weapons (user: "emitter/PPC second").**
|
||||
- **3a (dependency):** power bus — Generator (0xBC1) + PoweredSubsystem (0xBC2). Low-risk, foundational; energy weapons need it to charge (`GetVoltageState()==4`).
|
||||
- **3b:** Emitter (0xBC8) + PPC (0xBD4). Construct+tick+fire underneath; **keep the explosion stand-in for the visual** (beam renderable deferred — see §4).
|
||||
- **WAVE 4 — Standalone readouts (low risk, DONE classes):** Sensor (0xBC3), Searchlight (0xBD8), ThermalSight (0xBDE), AmmoBin (0xBCB). Pure power/heat-gated readouts; run standalone.
|
||||
- **WAVE 5 — Torso (0xBC5): ✅ DONE (live, binary-exact layout, zero regression).** The joint I/O
|
||||
(`Mech::ResolveJoint`/`Torso::PushTwist` + corrected `TorsoSimulation` + per-frame `UpdateJoints()`) AND the
|
||||
base-chain re-base are complete. Hierarchy is `Torso : PowerWatcher : HeatWatcher : MechSubsystem` (a SEPARATE
|
||||
branch from the HeatSink leaves — shares only MechSubsystem, so the +156 fix stays in the Watcher branch). Fix:
|
||||
Watcher-local `WatchedConnection`(0xC) + `WatcherGaugeAlarm`(0x54) in heatfamily_reslice.hpp (never resize the
|
||||
shared `SubsystemConnection`/`HeatAlarm`); grew HeatWatcher (+84) + PowerWatcher (+72, shadow watchedLink deleted);
|
||||
Torso shim fields deleted + 0x270 slot added → `currentTwist`@0x1D8, `sizeof==0x280`, compile-time locked
|
||||
(`TorsoLayoutCheck`/`HUDLayoutCheck` + Watcher-base asserts). Real Torso un-stubbed at 0xBC5; verified no crash,
|
||||
heat/combat un-regressed (TARGET DESTROYED after 8 hits). Full detail: CLAUDE.md §10d "WAVE-4 Torso base-chain".
|
||||
NOTE: the torso doesn't visibly twist for the Blackhawk because its 0xBC5 record has `TorsoHorizontalEnabled=0`
|
||||
(a FAITHFUL result — the binary ctor @004b6b0c gates on it identically); visible twist needs a twist-enabled mech.
|
||||
**Gyroscope (0xBC4) — layout+joint-I/O DONE, un-stub DEFERRED (reverted to stub).** Shim-delete + accessor-redirect +
|
||||
node-I/O (backed by the real engine Joint API, incl. FUN_0041d11c=SetTranslation) + ResolveJoint reuse + WriteEye/
|
||||
MechJoint wiring are done and locked (`GyroLayoutCheck`: exageration@0x1D8, sizeof<=0x3D0). Verified live: gyro
|
||||
constructs, resolves real joints (jointlocal/jointeye), WriteMechJoint fires — but with GARBAGE (0xCDCDCDCD/NaN)
|
||||
because the ctor field-init + integrator field-MAPPING are incomplete/incorrect (binary @004b3778: springConstant@0x1E8,
|
||||
dampingConstant@0x1F4, ...; recon mislabels + leaves accumulators uninit). Reverted to the stub (no NaN to the root
|
||||
joint). Remaining = a full ctor+integrator reconstruction from @004b3778 (bigger than the torso). Full detail: CLAUDE.md
|
||||
§10d "WAVE-5 GYROSCOPE". Also: the mech.cpp gyro↔torso cross-link write is broken (SubProxy::linkTarget→gyro+4) — commented out.
|
||||
- **WAVE 6 — Mover-coupled:** Myomers (0xBC6). Routes `SpeedEffect` into the Mech mover (`Mech+0x128`), writes `maxSpeed@0x7A0`, loops the myomer list `@0x1EB`. Verify those Mech offsets are valid before enabling (Risk 7), else no-op/wild writes.
|
||||
- **WAVE 7 — Projectile/missile weapons:** ProjectileWeapon (0xBCD), MissileLauncher (0xBD0). **PARTIALLY BLOCKED:** case 0xBCE calls `@4bdcb4`, which has no CLASSMAP entry — decompile it first and resolve the 0xBCD/0xBCE/0xBD0 label tangle (Risk 1) before swapping these.
|
||||
- **WAVE 8 — Message hub:** SubsystemMessageManager (0xBD3). Real, but **verify the cache-slot/tick-bypass interaction** (Risk 4) before enabling its tick.
|
||||
|
||||
**DEFER / DO NOT ENABLE:**
|
||||
- **MechControlsMapper tick** — the real mapper is installed at roster slot 0 by `btl4app::MakeViewpointEntity` (not by this switch); ticking it calls `InterpretControls→FillPilotArray`, reading undocumented WinTesla Application offsets (`application+0x6c→+0x190→pilot+0x1e0`) → **access violation** (mech4.cpp:854-866 deliberately bypasses it). Keep bypassed; this is a Phase-8 input-remap task.
|
||||
- **0xBCE @4bdcb4** — leave its `RECON_SUBSYS(MissileLauncher)` stub until decompiled.
|
||||
|
||||
---
|
||||
|
||||
## 3. HEAT FAMILY — detailed ordered edit list
|
||||
|
||||
### Step 1 — heat.hpp re-base + de-shadow (PREREQUISITE; do this before any factory edit)
|
||||
All in `C:/git/nick-games/decomp/reconstructed/heat.hpp` (+ matching touch-ups in `heat.cpp` / `heatfamily_reslice.cpp`). The collisions are confirmed: `mechsub.hpp` already owns `Mech *owner` (line 134), `int simulationState;//@0x40` (251), `ReconDamageZone *damageZone;//@0xE0` (252), `Mech *hostEntity;//@0xE8` (254), `int subsystemId2;//@0xEC` (255), plus `GetStatusFlags@4ac144 / HandleMessage@4ac0bc / ResetToInitialState@4ac1d4 / PrintState@4ac8c0` virtuals — while `heat.hpp` re-declares all of these on top of `: public Subsystem`.
|
||||
|
||||
1. **Base change.** heat.hpp:33 — ensure `#include <mechsub.hpp>`. heat.hpp:168-169 — `class HeatableSubsystem: public Subsystem` → **`: public MechSubsystem`**.
|
||||
2. **Delete the shadow fields** (heat.hpp:234-238):
|
||||
- `Mech *owner` → inherited `MechSubsystem::owner` (or a `Mech* GetEntity(){return (Mech*)Subsystem::GetEntity();}` accessor, matching surviving MECHTECH.HPP).
|
||||
- `LWord flags` / `LWord statusFlags` (both @0x28) → engine `Simulation::simulationFlags`. Retarget `flags |= 8/2` (Condenser/Reservoir) to `simulationFlags`.
|
||||
- `LWord statusBits` (@0x60) → engine dirty word; Reservoir `statusBits |= 1` → `ForceUpdate()`.
|
||||
- `int destroyed` (@0x40) → `MechSubsystem::simulationState` (DestroyedState); `IsDamaged()` reads it.
|
||||
3. **Thermal offsets** (heat.hpp:240-241 + HeatSink). CLASSMAP:472 — the `0x114-0x184` block belongs to HeatableSubsystem: keep `currentTemperature@0x114`, **move `degradationTemperature@0x118` / `failureTemperature@0x11C` UP from HeatSink into HeatableSubsystem** between `currentTemperature` and `heatLoad`, so `heatLoad` lands at `@0x120` (today it compiles to 0x118).
|
||||
4. **Virtual surface** (heat.hpp:192-201) now **overrides** MechSubsystem's real slots — do not add parallel slots. Match the binary signature (watch `MechSubsystem::ResetToInitialState(Logical powered)` vs heat's `ResetToInitialState()`).
|
||||
5. **HeatSink overlap words** (~heat.hpp:382-412): `valveState@0x1D0`/`field_1d0@0x1D0` and `refrigerationOutput@0x160`/`massScale@0x160` are the SAME binary word reused by subclasses — declare each once in the base it belongs to so subclasses extend rather than re-append.
|
||||
6. **Handler chains** (systemic #3): define `GetMessageHandlers()` for each heat class to chain to its parent (`Receiver::MessageHandlerSet(HeatableSubsystem::GetMessageHandlers())`, etc.). Today only `GetClassDerivations` is defined → `HeatSink::HandleMessage(1)` and `Reservoir` InjectCoolant are silently dropped.
|
||||
|
||||
Verify: `compile1.cmd heat.cpp` and `compile1.cmd heatfamily_reslice.cpp` green; `sizeof(Condenser) ≤ 0x230`, `sizeof(Reservoir) ≤ 0x230`.
|
||||
|
||||
### Step 2 — factory bridges (in heatfamily_reslice.cpp)
|
||||
Add `CreateCondenserSubsystem` (SIZE 0x230), `CreateReservoirSubsystem` (SIZE 0x230), `CreateHeatSinkBankSubsystem` (SIZE 0x1e4) per the §1 template (Condenser/Reservoir take `…::DefaultData`).
|
||||
|
||||
### Step 3 — mech.cpp factory edits
|
||||
- `extern` the three bridges near the forward-decls.
|
||||
- **Case 0xBBD** (CockpitClassID, 569-572): `subsystemArray[id] = CreateCondenserSubsystem(this, id, seg);`
|
||||
- **Case 0xBC0** (CondenserClassID, 580-583): `subsystemArray[id] = CreateReservoirSubsystem(this, id, seg);`
|
||||
- **Case 0xBBE** (SensorClassID, 574-578): `subsystemArray[id] = CreateHeatSinkBankSubsystem(this, id, seg); sensorSubsystem = subsystemArray[id];` — **keep the cache** (slot 0x1f7 is the heat bank, per CLASSMAP:455-458; the name "sensor" is a mech.cpp mis-guess).
|
||||
- **0xBBE interim (still real, not a stand-in):** the aggregate is a `#if 0` skeleton (Performance @4ae73c, 351 bytes, unfilled). Until reconstructed, have `CreateHeatSinkBankSubsystem` instantiate the real **HeatSink** base (alloc 0x1e4) so the master gate + `HeatSinkSimulation` run. This sink is the central heat engine that relaxes stored heat toward the ~300K ambient — without it the mech has no heat dissipation (Risk 8).
|
||||
- Delete `RECON_SUBSYS(Cockpit)` (161), `RECON_SUBSYS(Sensor)` (162), `RECON_SUBSYS(Condenser)` (163). Leave the others for later waves.
|
||||
|
||||
### Step 4 — MechTech ↔ HUD un-swap (folded into this wave)
|
||||
- Add `CreateHUDSubsystem` (hud.cpp, SIZE 0x2a4) and `CreateMechTechSubsystem` (mechtech.cpp, SIZE 0x140 — ≥ real 0x104 and ≥ the 0x114 base-ctor write).
|
||||
- **Case 0xBD6** (MechTechClassID, 658-662): build **HUD** → `subsystemArray[id] = CreateHUDSubsystem(this, id, seg);`.
|
||||
- **Case 0xBDC** (HeatableClassID, 669-677): build **MechTech** → `subsystemArray[id] = CreateMechTechSubsystem(this, id, seg); mechTechSubsystem = subsystemArray[id];` (this fixes the discarded-pointer bug).
|
||||
- The `mechTechSubsystem` cache (slot 0x16d) — see Risk 4 for whether it caches MechTech or HUD; default to caching the real MechTech at 0xBDC.
|
||||
|
||||
### Step 5 — internal heat linkage (depends on the bank existing)
|
||||
The HeatSink ctor resolves `heatSinkIndex` and `linkedSinks.Add(master)`; Reservoir ctor calls `link->Attach(this)` and scales by `link->field_1d0`. These need the 0xBBE master sink (Step 3) present so `linkedSinks.Resolve()` is non-null; the null-master guards (heat.cpp:595/646/693) keep it safe-but-inert otherwise. The mech.cpp:700-704 gyro↔`+0x1d8` link is **Torso, not heat** — leave it for Wave 5.
|
||||
|
||||
### Verify heat build/dissipate
|
||||
1. In each heat ctor, after the master-gate block, log when the gate arms (`name << " heat gate armed"`). No "armed" line for the 0xBBE sink ⇒ the gate/`owner->simulationFlags` path is wrong.
|
||||
2. In `HeatSink::HeatSinkSimulation` (heat.cpp:516), gated to the viewpoint mech, log per tick: `currentTemperature, heatEnergy, pendingHeat, coolantLevel, heatLoad, coolantActive`.
|
||||
3. With `BT_FORCE_FIRE=1 BT_SPAWN_ENEMY=1`: weapons deposit into `pendingHeat` → `currentTemperature`/`heatEnergy` climb, `heatLoad` (15-sample average) ramps, `coolantActive`→1 and `coolantLevel` drains; on cease-fire the aggregate relaxes `currentTemperature` toward ~300K and `coolantLevel` recovers via `Reservoir::DrawCoolant`. Alarm transitions log via `HeatSink::PrintState` (Normal/Degradation/Failure).
|
||||
4. **Offset proof:** the cockpit Heat gauge (`btl4gaug` Heat connection @4c3720 reads `currentTemperature@0x114`, CLASSMAP:260) must track the logged temperature. Garbage/`0xCDCDCDCD` ⇒ the MechSubsystem re-base/offsets are still off.
|
||||
|
||||
---
|
||||
|
||||
## 4. EMITTER / PPC WEAPON — detailed ordered edit list (make ONE real beam weapon fire)
|
||||
|
||||
Do **heat first** (E5 below depends on it). All offsets per CLASSMAP:476-484.
|
||||
|
||||
**E1 — Bridge (emitter.cpp).** Add `CreateEmitterSubsystem(Mech*, int, void* seg)` → `new (Memory::Allocate(0x478)) Emitter(owner, id, (Emitter::SubsystemResource*)seg, Emitter::DefaultData);`. Use it for **both** 0xBC8 and 0xBD4 — PPC is an Emitter subclass whose `FireWeapon@4bb878` just tail-calls `Emitter::FireWeapon@4bace8`, and the renderer keys off the *streamed* classID, so behavior is identical now. (A `struct PPC : Emitter {…}` for vtable/classID fidelity can be added later.)
|
||||
|
||||
**E2 — ctor null-deref + uninit (emitter.cpp ~653-680).** `FUN_00417ab4` (voltage-source resolver) returns 0 until the bus is real, then `src->ratedVoltage` and `TrackSeekVoltage`'s `src->voltage` deref null = construction-time crash. Add `energyCoefficient = 1.0f;` (@0x454) **before** the electrical branch (it is read by `FireWeapon` at line 160 but assigned only inside that branch). Resolve `src` once; gate the whole electrical block on `src != 0 && src->ratedVoltage > 0.0f`; in the else path keep `seekVoltage[i]` as the raw resource fractions, dividing by `v` only when `v > 0`.
|
||||
|
||||
**E3 — inert targeting (mechweap.cpp/.hpp).** `HasActiveTarget()` (mechweap.hpp:182) returns a never-set local `hasTarget` → fire gate permanently false. Read the owner instead (mech4.cpp already writes `mech+0x388` = MECH_TARGET_ENTITY):
|
||||
```cpp
|
||||
Logical MechWeapon::HasActiveTarget() const {
|
||||
Mech *owner = (Mech *)GetOwningSimulation();
|
||||
return (owner && *(Entity **)((char *)owner + 0x388)) ? True : False;
|
||||
}
|
||||
```
|
||||
Have `GetTargetPosition` read owner `+0x37c`; drop the `hasTarget` member (systemic #1).
|
||||
|
||||
**E4 — Loaded→Firing gate reachable (emitter.cpp:259).** `firingArmed` (aliases `MechWeapon::recoil@0x3E8`) is uninitialized and written by nothing → the `if (firingArmed && HasActiveTarget())` gate reads garbage. Initialize it `True` in the ctor / `ResetToInitialState` for bring-up, pending its real semantic (Risk 3).
|
||||
|
||||
**E5 — heat self-load (emitter.hpp:283).** `heatAccumulator` is declared as an *Emitter* member → lands ~0x3f0+, NOT the inherited HeatableSubsystem accumulator `@0x1c8`, so `FireWeapon`'s `heatAccumulator += heatPortion` never reaches the heat sim. Delete the Emitter-local copy; write the inherited `@0x1c8` accumulator. **Depends on the heat module (Wave 1/2) — this is why heat is first.**
|
||||
|
||||
**E6 — mech.cpp factory.** `extern Subsystem *CreateEmitterSubsystem(Mech*,int,void*);`. Replace **case 0xBC8** (617-621) and **case 0xBD4** (652-656): `subsystemArray[id] = CreateEmitterSubsystem(this, id, seg); ++weaponCount; break;`. Delete `RECON_SUBSYS(EmitterWeapon)` (170) and `RECON_SUBSYS(GaussRifle)` (174). The Emitter ctor already installs `activePerformance = EmitterSimulation` (emitter.cpp:627), so the roster loop ticks it every frame for the authoritative mech — no extra plumbing.
|
||||
|
||||
**E7 — power bus (Wave 3a, required for the real fire path).** While Generator/PoweredSubsystem are stubs, `GetVoltageState()` never returns 4 → `currentLevel` never charges → the weapon never reaches `Loaded`. Faithful path: un-stub **Generator (0xBC1)** and **PoweredSubsystem (0xBC2)** so `FUN_00417ab4` resolves a real `VoltageSource`. Bring-up shortcut (still drives the real state machine): force-charge — `currentLevel = seekVoltage[idx]; weaponAlarm → Loaded`.
|
||||
|
||||
**E8 — trigger wiring (mech4.cpp ~707-736).** The mapper is bypassed, so nothing writes `fireImpulse@0x31c` and `CheckFireEdge()` never sees a rising edge. Where `gBTDrive.fire` is handled, locate the emitter in `subsystemArray` (classID 0xBC8/0xBD4) and set its `fireImpulse > 0` for the frame (small setter/friend on the weapon). This is the faithful replacement for the explosion stand-in's input read.
|
||||
|
||||
**What blocks a VISIBLE shot (and the honest interim).** `btl4vid.cpp::MakeMechRenderables` dispatches `0xBC8 Emitter beam` / `0xBD4 PPC beam` (CLASSMAP:334) but **defers the beam renderable to the unported `dpl2d_` 2D display-list layer** (btl4vid.cpp:244-245, 452-458). So a perfectly-firing Emitter draws nothing. **Honest interim: keep the mech4.cpp explosion stand-in (707-736) for the visual** while the real Emitter mechanics (charge → `Loaded` → `CheckFireEdge` → `Firing` → `FireWeapon` splits `damagePortion`/`heatPortion`, sets `dischargeTimer`/`beamFlag`/`targetEntity`) run underneath. The shot is real; only its rendering is borrowed until the `dpl2d_` beam lands. (Net: E1/E2/E6 = construct+tick safely; E3/E4/E5 + E7 = actually fire and self-heat; E8 = trigger; the `dpl2d_` beam = finally visible.)
|
||||
|
||||
Net dependency: **E5 needs heat (Waves 1-2); the real-fire path needs the power bus (E7 = Wave 3a); the visible beam needs `dpl2d_` (out of scope — keep the stand-in).**
|
||||
|
||||
---
|
||||
|
||||
## 5. RISKS / UNKNOWNS — and how to settle each at the binary
|
||||
|
||||
1. **0xBCE @4bdcb4 unresolved (blocks Wave 7).** No CLASSMAP/recon at that address; sits between AmmoBin (4bd5c4) and MissileLauncher (4bcff0). **Settle:** run `tools/ghidra_scripts/ExportBTSource.java` on `4bdcb4` (and its CSS/vtable); it is most likely a ballistic-autocannon variant of ProjectileWeapon. Reconcile the 0xBCD(ProjectileWeapon)/0xBCE(?)/0xBD0(MissileLauncher) label tangle before swapping any of the three. Leave `RECON_SUBSYS(MissileLauncher)` at 0xBCE until done.
|
||||
2. **HeatableSubsystem base: MechSubsystem vs Subsystem.** `mechsub.hpp` and `heat.hpp` are overlapping reconstructions of the same cluster (vtable 0x50e210, classID 0xBBB, dtor 0x4ac868, model 0xE4). **Settle:** confirm `sizeof(MechSubsystem) == 0x114` and that `currentTemperature` begins exactly at 0x114 (HeatSink temp init @4adda0). Recommended resolution (Wave 1): `HeatableSubsystem : MechSubsystem`, one DamageZone built by the MechSubsystem resource ctor (@4ac644). Cross-check via the Heat gauge offset proof (§3 verify #4).
|
||||
3. **`firingArmed` real semantic (@0x3E8, aliases MechWeapon::recoil).** **Settle:** decompile around the `Loaded→Firing` gate and the fire-group dispatch to find what writes `this+0x3e8` in the binary (a weapon-selected/enabled flag from the controls path). Until then init `True` for bring-up (E4).
|
||||
4. **Special-cache slots — re-derive each member's TRUE meaning.** The Wword index is ground truth for *which Mech member* is written, but the names are mislabeled: `sensorSubsystem`(0x1f7)=heat bank; `mechTechSubsystem`(0x16d)=MechTech-or-HUD; `controlsMapper`(0x10d)=mapper-or-message-manager; `gyroSubsystem`(0x14a)=gyro; `sinkSourceSubsystem`(0x10e)=torso. **Settle:** read which Mech offset the binary writes at each slot and who consumes it. **Critical for SubsystemMessageManager (0xBD3, Wave 8):** verify slot 0x10d is NOT the same member `mech4.cpp` bypasses in the tick (`if (s == controlsMapper) continue;`) — if the message manager lands in the bypassed slot it will never tick. And confirm whether the HUD (0xBD6) needs its own cache distinct from `mechTechSubsystem`.
|
||||
5. **MechControlsMapper tick AV (confirmed).** `InterpretControls→FillPilotArray` reads `application+0x6c→+0x190→pilot+0x1e0` (`DAT_004efc94`) → wild pilot pointer → AV (mechmppr.cpp:46). **Settle:** only as a Phase-8 input-remap task with the real WinTesla `Application` layout. Until then keep it bypassed; the factory's 0xBD3 case is the *message manager*, not this mapper.
|
||||
6. **Gyro/Torso joint writes — TORSO SIDE RESOLVED.** ~~Stubbed no-ops needing a Skeleton/DCS API.~~ The
|
||||
real engine already exposes it: `JointedMover::GetSegment(CString)` → `EntitySegment::GetJointIndex()` →
|
||||
`JointSubsystem::GetJoint(i)` → `Joint::SetRotation(Radian|EulerAngles)` (all PUBLIC, in `munga_engine.lib`).
|
||||
Torso's `ResolveJoint`/`PushTwist` are reconstructed against this (DONE). Gyro's `WriteEyeJoint`/`WriteMechJoint`/
|
||||
`ResolveJoint` should reuse the same hoisted `Mech::ResolveJoint` + the joint-write dispatch (follow-up). Not a
|
||||
separate DCS task — the write path is engine-native. (Torso's live wiring is still gated on the WAVE-5 base
|
||||
re-base, not on this.)
|
||||
7. **Myomers mover coupling (Wave 6).** `ConnectToMover` routes `SpeedEffect@0x31C` into `**(Mech+0x128)`; `RegisterMaxOutput` writes `maxSpeed@0x7A0` and loops the myomer list `@0x1EB`. **Settle:** verify those Mech offsets exist/are valid in the reconstructed Mech before enabling, else no-op/wild writes. Also note `powersub.hpp` still declares a STALE `Myomers` (the old Sensor mislabel) — `#include <myomers.hpp>` first and watch for the name collision at link.
|
||||
8. **Aggregate heat sink (0xBBE) unfinished.** Performance @4ae73c (351 bytes) is a `#if 0` skeleton; it's the load-bearing central heat engine. **Settle:** decompile `4ae8d0`/`4ae73c`, finish `AggregateHeatSink`, give it `GetClassDerivations`/`DefaultData`. Interim = real `HeatSink` base at 0xBBE (real, not a stand-in) so the master gate + `HeatSinkSimulation` run.
|
||||
9. **Placement-new overrun (general).** After de-shadowing, `sizeof(Class)` can exceed the factory alloc SIZE → heap corruption (the 1-in-6 crash). **Settle:** the `Check(sizeof(<Class>) <= SIZE)` guard in every bridge (§1) makes this a hard, immediate assert instead of an intermittent corruption.
|
||||
|
||||
Key files (all under `C:/git/nick-games/decomp/reconstructed/`): `mech.cpp` (factory 556-741, stubs 145-181, enum mech.hpp:253-272), `mech4.cpp` (tick 825-871, target slots/explosion 707-736), `heat.hpp`/`heat.cpp`/`heatfamily_reslice.{hpp,cpp}`, `mechsub.hpp`, `mechtech.{cpp,hpp}`, `hud.cpp`, `emitter.{cpp,hpp}`, `mechweap.{cpp,hpp}`, `powersub.{cpp,hpp}`, `sensor/gyro/torso/myomers/searchlight/thermalsight/ammobin/projweap/mislanch/messmgr.cpp`, `CLASSMAP.md` (authoritative). Engine bases: `C:/git/nick-games/elsewhen_extracted/Elsewhen RP411 Source/trunk/MUNGA/{SUBSYSTM,SIMULATE,RECEIVER,VDATA}.h`, `SIMULATE.cpp` (PerformAndWatch @448), `ENTITY.cpp` (roster tick @698).
|
||||
@@ -0,0 +1,63 @@
|
||||
# Parallel "Deepen the Systems" Wave — ready-to-fire plan
|
||||
|
||||
**Status:** STAGED. Do NOT launch until the TRIGGER below is met. Authored while the camera/visibility
|
||||
pass is still running.
|
||||
|
||||
## TRIGGER (fire the wave only when all true)
|
||||
1. A mech renders on screen (camera pass done).
|
||||
2. There is a world/environment to be in (terrain/cavern renders — Tier 1).
|
||||
3. The player mech moves/turns under control input with a following camera (Tier 2 — basic piloting).
|
||||
|
||||
Why wait: parallel reconstruction is only **compile-verified**; correctness shows up at **runtime**, and
|
||||
we've repeatedly hit runtime-only bugs (vtable artifact, shadowed `subsystemArray`, null scenarioRole).
|
||||
A pilotable build is the **test harness** that lets the serial integration pass validate the wave's output.
|
||||
Firing earlier just piles up plausible-but-wrong code with nothing to check it against.
|
||||
|
||||
## SERIAL pre-work that must finish first (NOT part of the wave)
|
||||
- **Tier 1 — world/terrain render:** fix `DPLRenderer::MakeEntityRenderables` `/RTC1` uninit bug
|
||||
(`L4VIDEO.cpp:5211`) + load the cavern environment so non-mech entities draw. (btl4grnd.cpp + engine.)
|
||||
- **Tier 2 — locomotion:** wire control input → mech per-frame mover/Animate consumer → chase camera.
|
||||
This touches `mech.cpp` + `mechmppr.cpp` (shared cores) so it's serial, done before the wave.
|
||||
These two are the critical path to "pilotable" and must not run concurrently with the wave (shared-file
|
||||
contention on mech.cpp / btl4vid.cpp / camera).
|
||||
|
||||
## THE WAVE — parallel agents, partitioned by DISJOINT file ownership
|
||||
Same model as the compile wave that worked (7 families, no collisions). Each agent: reconstruct the REAL
|
||||
per-frame behavior of its subsystems (the streamed objects currently have only minimal `MechSubsystem`
|
||||
base behavior) from the **binary oracle** (`decomp/recovered/all/part_*.c`, `vtables.tsv`, `functions_index.tsv`)
|
||||
+ the **RP analog** (`...\trunk\RP\`, `WEAPSYS`/`VTV*`) + surviving headers. Verify by COMPILE
|
||||
(`compile1.cmd <file>`), match the oracle, and flag cross-family needs (don't edit headers you don't own).
|
||||
|
||||
| # | Agent (track) | Owns (`decomp/reconstructed/`) | Reconstruct |
|
||||
|---|---|---|---|
|
||||
| 1 | **Heat/power** | heat.cpp/hpp, powersub.cpp/hpp, gnrator.cpp/hpp | HeatSink heat-flow/coolant per-frame, HeatableSubsystem thresholds, PoweredSubsystem voltage bus, Generator output. (Owns the subsystem BASE classes — coordinate first.) |
|
||||
| 2 | **Energy weapons** | mechweap.cpp/hpp, emitter.cpp/hpp (+ survived GAUSS.CPP/PPC.CPP) | MechWeapon fire/cooldown/heat-cost, Emitter beam (PPC/Gauss) FireWeapon (vtable slot 18). |
|
||||
| 3 | **Projectile/missile weapons** | projweap, mislanch, missile, seeker, projtile, misthrst, ammobin (.cpp/.hpp) | ProjectileWeapon/MissileLauncher fire, Missile/Seeker flight + homing, AmmoBin counts, MissileThruster. |
|
||||
| 4 | **Sensors/targeting/tech** | sensor.cpp/hpp, mechtech.cpp/hpp | Sensor target acquisition/lock, MechTech repair/diagnostics. |
|
||||
| 5 | **Movement subsystems** | gyro.cpp/hpp, torso.cpp/hpp, myomers.cpp/hpp | Gyro stability, Torso twist/aim, Myomers actuation. |
|
||||
| 6 | **Damage/messaging** | mechdmg.cpp/hpp, dmgtable.cpp/hpp, messmgr.cpp/hpp | DamageZone TakeDamage → structure/armor, damage-table rolls, SubsystemMessageManager dispatch. |
|
||||
| 7 | **HUD / 2D track** (independent, HIGHEST risk) | hud.cpp/hpp, btl4gaug/gau2/gau3/galm (.cpp/.hpp) + a NEW `dpl2d` over-D3D layer | The `dpl2d_*` 2D layer (no D3D equivalent exists — from-scratch over the D3D device) + gauge widgets reading real subsystem state + `.gim/.gat/.pcc` cockpit assets. The one genuine unknown; give it the most room. |
|
||||
|
||||
(Track 8 — world/terrain — is the SERIAL Tier-1 pre-work above, not a wave slot.)
|
||||
|
||||
## Ownership / contention rules (critical — agents run concurrently, no git worktrees)
|
||||
- Edit ONLY your owned `.cpp` + `.hpp`. Need a base-class change in a header you don't own → REPORT it
|
||||
("CROSS-FAMILY NEEDS"), don't edit it.
|
||||
- Heat/power (agent 1) owns the subsystem base classes weapons/sensors/movement derive from → if its base
|
||||
signatures change, let it settle first OR have it publish the final base surface up front.
|
||||
- Do NOT touch `mech.cpp`/`mech.hpp`/`btl4vid.cpp` (cores + render) — those are serial-integration territory.
|
||||
- Each agent runs builds against the SAME build dir → builds are serialized by the OS; that's fine for
|
||||
`compile1.cmd` single-file checks (preferred) but don't run full `cmake --build` concurrently.
|
||||
|
||||
## After the wave: SERIAL integration pass
|
||||
One bring-up loop (debugger-driven) runs the pilotable build with the deepened systems active and validates
|
||||
behavior at runtime — fix interaction bugs, wire the cross-family needs the wave reported, confirm
|
||||
weapons/heat/damage/HUD actually function together. This is where the parallel reconstruction gets its
|
||||
correctness check. THEN: Tier 5 (enemy AI / `path.cpp` reconstruction) + mission objectives/scoring.
|
||||
|
||||
## Dispatch checklist (when triggered)
|
||||
- [ ] Confirm pilotable (3 trigger conditions).
|
||||
- [ ] Re-verify build GREEN + all 43 modules compile (baseline).
|
||||
- [ ] Launch agents 1–6 (combat) as one parallel batch; launch agent 7 (HUD) as its own long-running track.
|
||||
- [ ] Collect reports → reconcile CROSS-FAMILY NEEDS → serial integration pass.
|
||||
- [ ] `path.cpp` / enemy AI is its own pass after combat is validated.
|
||||
Reference in New Issue
Block a user