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>
6.5 KiB
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-61dpfB2ZIDstr). 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 alwaysBIZ_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 macrodpfValidTag(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) thenint32 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 haven%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'sSV_F_MATERIAL/SV_B_MATERIALleaf:uint8 mattype(PFILE.H:169-170; 1=named) + NUL-terminated"library:material"string, e.g.basev:shadow_mtl. - The part before
:= a.BMFmaterial-library file (basev→BASEV.BMF). BMF uses the identical chunk grammar (magicDIV-BMF2/DIV-BIZ2,FILETYPE=1,MATERIAL/TEXTUREchunks) — so the same parser reads it. - Texture pixels live in further files named by the BMF's
TEXTURE_MAP(loadersdpl_vtxRead/sgiRead/tgaRead,DPLUTILS.H:210-223). Load chain: BGF → BMF → image. (Open: this archive has 2222.BMFbut 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)
RESOLVED (twice-corrected — see §"Indices"):PCONN_LISTvsCONNECTION_LISTsemanticsPCONN_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 (materialsbtfx:*) carry onlySPHERE_LIST/POINT_LISTgeometry — 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.- Winding order (CW/CCW) +
SV_FACETED↔ backface culling — confirm visually. - Full
SV_SPECIALtoken grammar (multi-word commands). SPHERE/SPHERE_LISTper-sphere radius source.- 4-byte length variant (
0x8000) — unused here, handle defensively.