Files
BT411/docs/BGF_FORMAT.md
T
arcattackandClaude Opus 4.8 7b7d465e5e 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>
2026-07-05 21:03:40 -05:00

6.5 KiB
Raw Blame History

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 (basevBASEV.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.