Files
TeslaRel410/dpl3-revive/parser/gamemap.py
T
CydandClaude Fable 5 afc3fd839e Vendor dpl3-revive: the Division/DPL3 renderer, now ours
Bring the graphics-dev collaborator's dpl3-revive into the repo as first-class
project code (they've handed it off; it's ours now). This is the proven
Division renderer that our in-process rt_draw has been trying to be.

What's here:
- parser/  B2Z/V2Z/SVT/SCN/SPL/BGF/BMF/BSL decoders (pure Python).
- spec/    reverse-engineered format + the definitive VelociRender wire
           protocol (from the original DIVISION source, matches our live
           VPX node/action tables exactly).
- source-ref/  read-only copies of the original DIVISION C (BIZREAD.C,
           DPLTYPES.H, DPL.H) that define the formats.
- patha/   the "virtual VelociRender board": vrboard.py (24-action protocol
           server), vrview.py (numpy software rasterizer, the reference),
           vrview_gl.py (moderngl GPU backend, 832x512@60Hz), plus the
           run/replay/regress tooling and evidence renders. Drives FLYK/BLADE/
           Star Trek demos AND our btl4opt/rpl4opt game binaries.
- viewer/  WebGL archive generators (.py); prebuilt HTML/data regeneratable.
- samples/ small test models/textures.
- bt*.raw.bin  real BTL4OPT arena wire captures (kept for offline renderer
           testing/regression against OUR game).

.gitignore keeps the multi-hundred-MB demo capture dumps + debug logs +
regeneratable viewer bundles out of history (they stay on disk).

Phase 0 of the integration is validated: their board decodes our bt8 capture
with zero errors (3748 nodes, 507 instances, 4 mechs) and renders our arena
(terrain/dome/sky, correct Division DAC gamma). Plan + status in memory;
integration continues in emulator/RENDERER-COLLAB.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 22:06:25 -05:00

216 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""
gamemap.py -- reader for the game-side ".MAP" arena/track format and its ".MOD"
game-object definitions (BTDAVE / BTLIVE / BTRAVINE / RPDAVE / RPLIVE MAPS dirs).
A .MAP is INI-style text:
[hall2] ; declare an asset section
type=model
modelfile=hall2.mod
[one]
type=dropzone ; pod spawn ring
dropzone= x y z rx ry rz ; rotations in RADIANS
[arena1] ; the map itself
type=map
instance=hall2 x y z rx ry rz
instancedropzone=one x y z rx ry rz
[inc]
type=include ; pull in another .MAP's sections
file=other.map
A .MOD is the game object: `[video] object=X.bgf` (render geometry, DIV-BIZ2),
`[collision] name=X.sld`, `[gamedata]` physics/damage/animations. We only need
the video geometry.
expand(path) resolves everything to a flat list of world-space placements:
[(bgf_name, matrix4x4), ...], plus dropzone positions.
Maps may instance other maps; expansion is recursive with matrix composition
(row-vector convention, as everywhere in DPL -- see SCN_FORMAT.md).
"""
import math
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import scn
def _parse_ini(path):
"""-> ordered {section: {'type':..,'modelfile':..,'file':..,
'instances':[(name,[6f])], 'dropzones':[[6f]],
'instancedropzones':[(name,[6f])]}}"""
sections = {}
cur = None
try:
lines = open(path, encoding="latin-1").read().splitlines()
except OSError:
return sections
for raw in lines:
line = raw.split("//")[0].strip()
if not line:
continue
if line.startswith("[") and line.endswith("]"):
name = line[1:-1].strip().lower()
cur = sections.setdefault(name, {"type": None, "instances": [],
"dropzones": [], "instancedropzones": []})
continue
if cur is None or "=" not in line:
continue
key, _, val = line.partition("=")
key = key.strip().lower()
val = val.strip()
if key == "type":
cur["type"] = val.lower()
elif key == "instance":
p = val.split()
cur["instances"].append((p[0].lower(), [float(x) for x in p[1:7]]))
elif key == "instancedropzone":
p = val.split()
cur["instancedropzones"].append((p[0].lower(), [float(x) for x in p[1:7]]))
elif key == "dropzone":
cur["dropzones"].append([float(x) for x in val.split()[:6]])
else:
cur[key] = val.lower() # generic key (modelfile, MotionExtent, ...)
return sections
def load_sections(path, _seen=None):
"""Parse a .MAP plus its type=include files (same directory)."""
_seen = _seen or set()
key = os.path.normcase(os.path.abspath(path))
if key in _seen:
return {}
_seen.add(key)
sections = _parse_ini(path)
merged = dict(sections)
for name, sec in sections.items():
if sec.get("type") == "include" and sec.get("file"):
inc = os.path.join(os.path.dirname(path), sec["file"])
for n, s in load_sections(inc, _seen).items():
merged.setdefault(n, s)
return merged
def _matrix(vals):
"""instance 6-tuple: x y z rx ry rz (RADIANS) -> row-vector matrix,
same composition as the SCN STATIC transform (unit scale)."""
x, y, z, rx, ry, rz = (vals + [0.0] * 6)[:6]
return scn.instance_matrix(1.0, x, y, z,
math.degrees(rx), math.degrees(ry), math.degrees(rz))
def pick_root(sections, path):
"""The map section to expand: prefer one named like the file, else the
map-typed section with the most instances."""
stem = os.path.splitext(os.path.basename(path))[0].lower()
maps = {n: s for n, s in sections.items() if s.get("type") in ("map",)}
if stem in maps:
return stem
if maps:
return max(maps, key=lambda n: len(maps[n]["instances"]))
return None
def expand(path, mod_resolver, max_depth=6):
"""Flatten a .MAP to world space.
mod_resolver(modfile_basename) -> bgf_name or None (looks up the .MOD's
[video] object=). Returns (placements, dropzones):
placements = [(bgf_name, matrix)]
dropzones = [[x,y,z,rx,ry,rz]] (world space)"""
sections = load_sections(path)
root = pick_root(sections, path)
placements, dropzones = [], []
if root is None:
return placements, dropzones
def emit(sec_name, parent_mtx, depth):
sec = sections.get(sec_name)
if sec is None or depth > max_depth:
return
t = sec.get("type")
if t == "model":
# mod_resolver -> list of (bgf_name, anim_or_None); a MOD may carry
# several video objects (door frame + sliding leaves, laser drill...)
for item in (mod_resolver(sec.get("modelfile", "")) or []):
placements.append((item, parent_mtx))
elif t == "map":
for name, vals in sec["instances"]:
emit(name, scn.matmul(_matrix(vals), parent_mtx), depth + 1)
for name, vals in sec["instancedropzones"]:
dz = sections.get(name)
if dz:
m = scn.matmul(_matrix(vals), parent_mtx)
for d in dz["dropzones"]:
wx, wy, wz = scn.xform_point(m, d[0], d[1], d[2])
dropzones.append([wx, wy, wz, d[3], d[4], d[5]])
elif t == "dropzone":
for d in sec["dropzones"]:
wx, wy, wz = scn.xform_point(parent_mtx, d[0], d[1], d[2])
dropzones.append([wx, wy, wz, d[3], d[4], d[5]])
emit(root, scn.ident(), 0)
return placements, dropzones
def parse_mod_objects(path):
""".MOD -> list of [video] object= geometry basenames (no extension).
A MOD may carry several objects ("number and order is significant!"), e.g.
doors: frame + leaves (+ low-poly LOD twins, which we drop: any name equal to
an earlier name + 'l'). Also returns the [gamedata] class and Subsystems file.
-> (objects, gameclass, subsystems_file)"""
sec = _parse_ini(path)
vid = sec.get("video") or {}
raw = vid.get("object") or vid.get("name") or ""
if not raw:
for s in sec.values():
if s.get("object"):
raw = s["object"]
break
names = []
for tok in raw.split():
base = os.path.splitext(os.path.basename(tok))[0].lower()
if not tok.lower().endswith((".bgf", ".b2z", ".biz")):
continue # skip node-name tokens
if base[:-1] in names and base.endswith("l"):
continue # low-poly LOD twin of an earlier object
names.append(base)
game = sec.get("gamedata") or {}
return names, (game.get("class") or "").lower(), game.get("subsystems")
def parse_mod(path):
"""Back-compat: the first video object basename, or None."""
names, _, _ = parse_mod_objects(path)
return names[0] if names else None
def parse_sub(path):
""".SUB subsystems -> {collision_stem: {'extent':[x,y,z], 'travel':s, 'dead':s}}
for DoorClassID sections. The collision stem (dr1_cv -> dr1) links a subsystem
to its video object by name."""
out = {}
for name, sec in _parse_ini(path).items():
if "motionextent" not in sec:
continue
ext = [float(x) for x in sec["motionextent"].split()[:3]]
coll = sec.get("collision", name)
stem = os.path.splitext(os.path.basename(coll))[0].lower()
if stem.endswith("_cv"):
stem = stem[:-3]
out[stem] = {"extent": ext,
"travel": float(sec.get("traveltime", 10.0)),
"dead": float(sec.get("deadtime", 3.0))}
return out
if __name__ == "__main__":
path = sys.argv[1]
secs = load_sections(path)
types = {}
for n, s in secs.items():
types.setdefault(s.get("type"), []).append(n)
print("root:", pick_root(secs, path))
for t, names in types.items():
print(" %-10s x%-3d %s" % (t, len(names), names[:8]))