"""Parsers for Division Ltd. dVS/DPL file formats (1994-1996), reverse-engineered from the VWE 'Glaze' drive. Binary BIZ2 layout comes from Division's own reader source (DPL3/BIZREAD.C, PJA 1994); text formats from surviving examples. Formats: .VGF/.VMF DIV-VIZ2 text geometry / materials .BGF/.BMF DIV-BIZ2 binary geometry / materials (b2z block stream) .SVT raw square texture, 4 B/texel, size implies 64/128/256 edge .TGA type-2 uncompressed 24/32-bit .SPL text spline paths .SCN text scene scripts """ import os, re, struct, zlib, base64 # ================================================================ helpers def strip_comments(text): text = re.sub(r"/\*.*?\*/", " ", text, flags=re.S) text = re.sub(r"//[^\n]*", " ", text) return text def png_datauri(w, h, rgb_rows): raw = b"".join(b"\x00" + r for r in rgb_rows) def chunk(tag, data): c = struct.pack(">I", len(data)) + tag + data return c + struct.pack(">I", zlib.crc32(tag + data) & 0xffffffff) png = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b"")) return "data:image/png;base64," + base64.b64encode(png).decode() def r3(x): return round(x, 3) def r4(x): return round(x, 4) # ================================================================ VGF (text geometry) def parse_vgf(path): text = strip_comments(open(path, "r", errors="replace").read()) scale = 1.0 hm = re.search(r"HEADER\s*\((.*?)\)", text, re.S) if hm: sm = re.search(r"SCALE\s*=\s*([\d.eE+-]+)", hm.group(1)) if sm: scale = float(sm.group(1)) groups = [] for gm in re.finditer(r"GEOGROUP\s*\(([^)]*)\)\s*\{", text): params = gm.group(1) mmat = re.search(r'F_MATERIAL\s*=\s*"([^"]+)"', params) material = mmat.group(1) if mmat else None vflags = re.search(r"VERTEX\s*=\s*([^;]+);", params) flags = vflags.group(1).upper() if vflags else "" has_n = "NORMALS" in flags has_uv = "2D_TEXTURE" in flags or "3D_TEXTURE" in flags depth, i = 1, gm.end() while depth > 0 and i < len(text): if text[i] == "{": depth += 1 elif text[i] == "}": depth -= 1 i += 1 body = text[gm.end():i - 1] g = {"material": material} sph = re.search(r"SPHERELIST\s*\{(.*?)\n\s*\}", body, re.S) if sph: pts = [] for row in re.finditer(r"\{([^}]*)\}", sph.group(1)): nums = [float(x) for x in re.findall(r"[-\d.eE+]+", row.group(1))] pts.append([r3(v * scale) for v in nums[:3]]) g["mode"] = "points"; g["points"] = pts groups.append(g); continue vp = re.search(r"VERTEX_POOL\s*\{(.*?)\n\s*\}", body, re.S) if not vp: continue verts, norms, uvs = [], [], [] for row in re.finditer(r"\{([^}]*)\}", vp.group(1)): nums = [float(x) for x in re.findall(r"[-\d.eE+]+", row.group(1))] k = 0 verts.append([r3(v * scale) for v in nums[k:k+3]]); k += 3 if has_n: norms.append([r3(v) for v in nums[k:k+3]]); k += 3 if has_uv: uvs.append([r4(v) for v in nums[k:k+2]]); k += 2 indices = [] for cm in re.finditer(r"CONNECTION_LIST\s*(?:\(\s*PCOUNT\s*=\s*\d+\s*\))?\s*\{(.*?)(?:\n\s*\}|(?=CONNECTION_LIST))", body, re.S): for row in re.finditer(r"\{([^}]*)\}", cm.group(1)): idx = [int(x) for x in re.findall(r"\d+", row.group(1))] for t in range(1, len(idx) - 1): indices += [idx[0], idx[t], idx[t + 1]] g["mode"] = "mesh"; g["v"] = verts g["n"] = norms if has_n else None g["uv"] = uvs if has_uv else None g["i"] = indices groups.append(g) return groups # ================================================================ VMF (text materials) def parse_vmf(path, prefix): text = strip_comments(open(path, "r", errors="replace").read()) textures, materials = {}, {} for tm in re.finditer(r"TEXTURE\s*\(\s*NAME\s*=\s*\"?([\w-]+)\"?\s*;?([^)]*)\)\s*\{(.*?)\n\}", text, re.S): name, params, body = tm.group(1), tm.group(2), tm.group(3) mmap = re.search(r'MAP\s*\{\s*"([\w-]+)"\s*\}', body) mslice = re.search(r"BITSLICE\s*\{\s*(\d+)\s*\}", body) textures[name] = {"map": mmap.group(1).lower() if mmap else None, "slice": int(mslice.group(1)) if mslice else None} ramps = {} for rm in re.finditer(r"RAMP\s*\(\s*NAME\s*=\s*\"?([\w-]+)\"?\s*\)\s*\{(.*?)\n\}", text, re.S): nums = [float(x) for x in re.findall(r"[-\d.eE+]+", rm.group(2))] if len(nums) >= 6: ramps[rm.group(1)] = [nums[0:3], nums[3:6]] for mm in re.finditer(r"MATERIAL\s*\(\s*NAME\s*=\s*\"?([\w-]+)\"?\s*;?([^)]*)\)\s*\{(.*?)\n\}", text, re.S): name, params, body = mm.group(1), mm.group(2), mm.group(3) def col(tag, n=3): m = re.search(tag + r"\s*\{([^}]*)\}", body) return [float(x) for x in re.findall(r"[-\d.eE+]+", m.group(1))][:n] if m else None mtex = re.search(r'TEXTURE\s*\{\s*"([\w-]+)"\s*\}', body) mramp = re.search(r"RAMP\s*\{\s*\"?([\w-]+)\"?\s*\}", body) materials[prefix + ":" + name] = { "ambient": col("AMBIENT"), "diffuse": col("DIFFUSE"), "emissive": col("EMISSIVE"), "texture": mtex.group(1) if mtex else None, "ramp": mramp.group(1) if mramp else None, "immune": "IMMUNE" in params} return textures, materials, ramps # ================================================================ BIZ2 (binary container) class B2Z: def __init__(self, path): self.d = open(path, "rb").read() self.p = 0 def u8(self): v = self.d[self.p]; self.p += 1; return v def u16(self): v = struct.unpack_from("> 12) & 0xC if mode == 0x8: ln = self.i32() elif mode == 0x4: ln = self.u16() elif mode == 0x0: ln = self.u8() else: raise ValueError("bad block header 0x%x @%d" % (hdr, self.p)) return hdr & 0x3fff, ln # strip length-size bits, keep 0x2000 flag VTX_LAYOUT = { # tag: (floats_per_vertex, n_off, uv_off, uv3_off, col_off, lum_off) 0x80: (3, -1, -1, -1, -1, -1), 0x81: (6, 3, -1, -1, -1, -1), 0x82: (7, -1, -1, -1, 3, -1), 0x83: (10, 3, -1, -1, 6, -1), 0x84: (5, -1, -1, -1, -1, 3), 0x85: (8, 3, -1, -1, -1, 6), 0x88: (5, -1, 3, -1, -1, -1), 0x89: (8, 3, 6, -1, -1, -1), 0x8a: (9, -1, 7, -1, 3, -1), 0x8c: (7, -1, 5, -1, -1, 3), 0x90: (6, -1, -1, 3, -1, -1), 0x91: (9, 3, -1, 6, -1, -1), 0x92: (10, -1, -1, 7, 3, -1), 0x94: (8, -1, -1, 5, 3, -1), # lum@3, 3dtex@5 per BIZREAD.C } def _tri_from_strip(nv): out = [] for t in range(nv - 2): if t % 2 == 0: out += [t, t + 1, t + 2] else: out += [t + 1, t, t + 2] return out def _read_vertex_block(bz, tag, ln): fpv, n_off, uv_off, uv3_off, col_off, lum_off = VTX_LAYOUT[tag] if tag == 0x94: fpv, lum_off, uv3_off = 8, 3, 5 nv = (ln // 4) // fpv verts, norms, uvs, cols = [], [], [], [] for _ in range(nv): f = bz.floats(fpv) verts.append([r3(f[0]), r3(f[1]), r3(f[2])]) if n_off >= 0: norms.append([r3(f[n_off]), r3(f[n_off+1]), r3(f[n_off+2])]) if uv_off >= 0: uvs.append([r4(f[uv_off]), r4(f[uv_off+1])]) if uv3_off >= 0: uvs.append([r4(f[uv3_off]), r4(f[uv3_off+1])]) if col_off >= 0: cols.append([r3(f[col_off]), r3(f[col_off+1]), r3(f[col_off+2])]) if lum_off >= 0: cols.append([r3(f[lum_off])] * 3) return verts, norms or None, uvs or None, cols or None def _parse_bgf_vertices(bz, length, geotype, out_groups, material): end = bz.p + length pend_v = pend_n = pend_uv = pend_c = None conns = [] while bz.p < end: tag, ln = bz.block() nxt = bz.p + ln if tag == 0x47: # triangle connectivity for _ in range(ln // 12): conns.append(list(bz.ints(3))) elif tag == 0x4d: # polygon connectivity vpp = bz.u8() n = (ln - 1) // (vpp * 4) for _ in range(n): conns.append(list(bz.ints(vpp))) elif tag in VTX_LAYOUT: v, n, uv, c = _read_vertex_block(bz, tag, ln) if geotype == "pmesh": if pend_v is None: pend_v, pend_n, pend_uv, pend_c = v, n, uv, c else: pend_v += v if pend_n and n: pend_n += n if pend_uv and uv: pend_uv += uv if pend_c and c: pend_c += c else: # strip / polygon: emit immediately idx = (_tri_from_strip(len(v)) if geotype == "tristrip" else [x for t in range(1, len(v)-1) for x in (0, t, t+1)]) out_groups.append({"material": material, "mode": "mesh", "v": v, "n": n, "uv": uv, "c": c, "i": idx}) bz.p = nxt if geotype == "pmesh" and pend_v is not None: idx = [] for poly in conns: for t in range(1, len(poly) - 1): idx += [poly[0], poly[t], poly[t + 1]] out_groups.append({"material": material, "mode": "mesh", "v": pend_v, "n": pend_n, "uv": pend_uv, "c": pend_c, "i": idx}) def _parse_bgf_patch(bz, length, out_groups, obj_mtl): end = bz.p + length f_mtl = obj_mtl pend = [] # geometry blocks seen before the material tag while bz.p < end: tag, ln = bz.block() nxt = bz.p + ln if tag == 0x2008: bz.cstr() elif tag in (0x2030, 0x2031): t = bz.u8() name = bz.cstr() if t == 1 else (None if t != 2 else "DEFAULT") if tag == 0x2030: f_mtl = name or f_mtl elif tag == 0x43: _parse_bgf_vertices(bz, ln, "polygon", pend, None) elif tag == 0x44: _parse_bgf_vertices(bz, ln, "tristrip", pend, None) elif tag == 0x45: _parse_bgf_vertices(bz, ln, "tristrip", pend, None) elif tag == 0x46: _parse_bgf_vertices(bz, ln, "pmesh", pend, None) bz.p = nxt for g in pend: g["material"] = f_mtl out_groups.append(g) def parse_bgf(path): """Parse DIV-BIZ2 geometry -> groups like parse_vgf. Also returns any embedded materials/textures/ramps.""" bz = B2Z(path) if bz.d[:8] != b"DIV-BIZ2": raise ValueError("not a DIV-BIZ2 file: " + path) bz.p = 8 groups, materials, textures, ramps = [], {}, {}, {} while bz.p < len(bz.d) - 2: tag, ln = bz.block() nxt = bz.p + ln top = tag & 0xfff if top == 0x005: break # trailer elif top == 0x003 or top == 0x004: pass elif top == 0x010: _parse_biz_texture(bz, ln, textures) elif top == 0x020: _parse_biz_material(bz, ln, materials) elif top == 0x030: _parse_biz_ramp(bz, ln, ramps) elif top == 0x040: _parse_bgf_object(bz, ln, groups) bz.p = nxt return groups, materials, textures, ramps def _parse_bgf_lod(bz, length, groups, obj_mtl): end = bz.p + length while bz.p < end: tag, ln = bz.block() nxt = bz.p + ln if tag == 0x42: _parse_bgf_patch(bz, ln, groups, obj_mtl) bz.p = nxt def _parse_bgf_object(bz, length, groups): end = bz.p + length obj_mtl = None lods = [] # (size, offset) of nested LOD blocks while bz.p < end: tag, ln = bz.block() nxt = bz.p + ln if tag == 0x2008: bz.cstr() elif tag == 0x2030: t = bz.u8() if t == 1: obj_mtl = bz.cstr() elif tag == 0x2031: t = bz.u8() if t == 1: bz.cstr() elif tag == 0x42: _parse_bgf_patch(bz, ln, groups, obj_mtl) elif tag == 0x41: lods.append((ln, bz.p)) bz.p = nxt if lods: # newer writers nest patches inside LODs (Division's 1994 reader # predates this); take the largest LOD = highest detail ln, off = max(lods) bz.p = off _parse_bgf_lod(bz, ln, groups, obj_mtl) bz.p = end def _parse_biz_material(bz, length, materials): end = bz.p + length m = {"ambient": None, "diffuse": None, "emissive": None, "texture": None, "ramp": None, "immune": False} name = "unnamed" while bz.p < end: tag, ln = bz.block() nxt = bz.p + ln if tag == 0x2008: name = bz.cstr() elif tag == 0x0021: mode = bz.u8() if mode == 2: m["texture"] = bz.cstr() elif tag == 0x0023: m["ambient"] = [r3(x) for x in bz.floats(3)] elif tag == 0x0024: m["diffuse"] = [r3(x) for x in bz.floats(3)] elif tag == 0x0026: m["emissive"] = [r3(x) for x in bz.floats(3)] elif tag == 0x0028: m["ramp"] = bz.cstr() bz.p = nxt materials[name] = m def _parse_biz_texture(bz, length, textures): end = bz.p + length name, mapname = None, None while bz.p < end: tag, ln = bz.block() nxt = bz.p + ln if tag == 0x2008: name = bz.cstr() elif tag == 0x0011: mapname = bz.cstr() bz.p = nxt if name: textures[name] = {"map": (mapname or "").lower() or None, "slice": None} def _parse_biz_ramp(bz, length, ramps): end = bz.p + length name, data = None, None while bz.p < end: tag, ln = bz.block() nxt = bz.p + ln if tag == 0x2008: name = bz.cstr() elif tag == 0x0031: f = bz.floats(6); data = [list(f[0:3]), list(f[3:6])] bz.p = nxt if name and data: ramps[name] = data # ================================================================ textures def tga_datauri(path): d = open(path, "rb").read() idlen, imgtype = d[0], d[2] w, h = struct.unpack("= 3: pts.append([r3(v) for v in nums[:3]]) return pts def parse_scn(path): scene = {"lights": [], "placements": []} last = None for raw in open(path, errors="replace"): line = raw.split("//")[0].split("#")[0].strip() if not line: continue t = line.split() k = t[0].upper() try: if k == "VIEWANGLE": scene["fov"] = float(t[1]) elif k == "CLIP": scene["clip"] = [float(t[1]), float(t[2])] elif k == "FOG": scene["fog"] = [float(x) for x in t[1:6]] elif k == "BACKGND": scene["bg"] = [float(x) for x in t[1:4]] elif k == "AMBIENT": scene["ambient"] = [float(x) for x in t[1:4]] elif k == "LIGHT": scene["lights"].append([float(x) for x in t[1:7]]) elif k == "START": scene["start"] = [float(x) for x in t[1:7]] elif k in ("GEOMETRY", "MATERIAL", "TEXTURE"): scene[k.lower()] = t[1] elif k == "STATIC": last = {"kind": "static", "geo": t[1].lower(), "scale": float(t[2]), "pos": [float(x) for x in t[3:6]], "rot": [float(x) for x in t[6:9]], "children": []} scene["placements"].append(last) elif k == "DYNAMIC": last = {"kind": "dynamic", "geo": t[1].lower(), "scale": float(t[2]), "spl": os.path.basename(t[3].replace("\\", "/")).lower(), "phase": float(t[4]), "speed": float(t[5]), "children": []} scene["placements"].append(last) elif k in ("SCHILD", "DCHILD") and last is not None: last["children"].append({"geo": t[1].lower(), "scale": float(t[2])}) except (ValueError, IndexError): pass return scene