# 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.
