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>
216 lines
8.1 KiB
Python
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]))
|