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>
111 lines
6.5 KiB
Markdown
111 lines
6.5 KiB
Markdown
# 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.
|