"""Convert Division DIV-VIZ2 assets (VGF/VMF/TGA/SPL/SCN) to JSON for the web viewer. Formats reverse-engineered from the VWE 'Glaze' drive (STDAVE, 1996): - VGF: text geometry. OBJECT { GEOGROUP(F_MATERIAL=..;VERTEX=flags) { PMESH { VERTEX_POOL {..} CONNECTION_LIST (PCOUNT=n) {..} .. } | SPHERELIST {..} } } Vertex tuple = pos(3) [+ normal(3) if NORMALS] [+ uv(2) if 2D_TEXTURE]. - VMF: text materials. TEXTURE(NAME=..){MAP{"base"} BITSLICE{n}} selects grayscale TGA "BASE.TGA"; MATERIAL(NAME=..){AMBIENT/DIFFUSE/EMISSIVE {r,g,b} TEXTURE{"t"}}. - SPL: first line = knot count, rows = x y z (+3 unused here). - SCN: scene script (camera, lights, fog, STATIC/DYNAMIC placements). - TGA: type-2 uncompressed 24bpp, bottom-up. """ import json, re, struct, sys, zlib, base64, os ROOT = r"c:\VWE\TeslaRel410\sda4\STDAVE" BUILD = os.path.join(ROOT, "VIDEO", "BUILD") # ---------------------------------------------------------------- tokenizer def strip_comments(text): text = re.sub(r"/\*.*?\*/", " ", text, flags=re.S) text = re.sub(r"//[^\n]*", " ", text) return text # ---------------------------------------------------------------- VGF def parse_vgf(path): """Return list of groups: {material, mode, verts/normals/uvs/indices or points}.""" text = strip_comments(open(path, "r", errors="replace").read()) groups = [] pos = 0 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 body_start = gm.end() # find matching close brace for the group body depth, i = 1, body_start while depth > 0 and i < len(text): if text[i] == "{": depth += 1 elif text[i] == "}": depth -= 1 i += 1 body = text[body_start: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(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(nums[k:k+3]); k += 3 if has_n: norms.append(nums[k:k+3]); k += 3 if has_uv: uvs.append(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*\}", body, re.S): pc = int(cm.group(1)) for row in re.finditer(r"\{([^}]*)\}", cm.group(2)): idx = [int(x) for x in re.findall(r"\d+", row.group(1))] # fan-triangulate polygons 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 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) scroll = re.search(r"SCROLL\s+([-\d. ]+)", params + body) textures[name] = { "map": mmap.group(1) if mmap else None, "slice": int(mslice.group(1)) if mslice else 0, "scroll": [float(x) for x in scroll.group(1).split()] if scroll else None, } 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): m = re.search(tag + r"\s*\{([^}]*)\}", body) return [float(x) for x in re.findall(r"[-\d.eE]+", m.group(1))][:3] if m else None mtex = re.search(r'TEXTURE\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, "immune": "IMMUNE" in params, # fog-immune (self-lit stars, running lights) } return textures, materials # ---------------------------------------------------------------- TGA -> PNG data URI def tga_to_png_datauri(path): d = open(path, "rb").read() idlen, cmap, imgtype = d[0], d[1], d[2] w, h = struct.unpack(" flip to top-down rows.reverse() raw = b"".join(b"\x00" + r for r in 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() # ---------------------------------------------------------------- SPL def parse_spl(path): lines = [l for l in open(path).read().splitlines() if l.strip()] n = int(lines[0].split()[0]) pts = [] for l in lines[1:]: try: nums = [float(x) for x in l.split()] except ValueError: continue if len(nums) >= 3: pts.append(nums[:3]) return pts # ---------------------------------------------------------------- SCN def parse_scn(path): scene = {"lights": [], "statics": [], "dynamics": []} 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 == "STATIC": scene["statics"].append({"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]]}) elif k == "DYNAMIC": scene["dynamics"].append({"geo": t[1].lower(), "scale": float(t[2]), "spl": os.path.basename(t[3].replace("\\", "/")).lower(), "phase": float(t[4]), "speed": float(t[5])}) except (ValueError, IndexError): pass return scene # ---------------------------------------------------------------- drive it def main(): out = {"models": {}, "materials": {}, "textures": {}, "images": {}, "splines": {}, "scenes": {}} tex1, mat1 = parse_vmf(os.path.join(BUILD, "STSHIPS.VMF"), "stships") tex2, mat2 = parse_vmf(os.path.join(BUILD, "STAR.VMF"), "star") out["textures"] = {**tex1, **tex2} out["materials"] = {**mat1, **mat2} for name, f in [("tesall", "TESALL.VGF"), ("tesall_lp", "TESALL_L.VGF"), ("klngn", "KLNGN.VGF"), ("klngn_lp", "KLNGN_LP.VGF"), ("stars", "STARS.VGF")]: gs = parse_vgf(os.path.join(BUILD, f)) out["models"][name] = gs nv = sum(len(g.get("v", g.get("points", []))) for g in gs) nt = sum(len(g.get("i", [])) // 3 for g in gs) print(f"model {name}: {len(gs)} groups, {nv} verts, {nt} tris") # texture maps: map base 'steh'/'klng', slice n -> BUILD/.TGA used_maps = {(t["map"], t["slice"]) for t in out["textures"].values() if t["map"]} for base, sl in sorted(used_maps): f = os.path.join(BUILD, f"{base.upper()}{sl+1}.TGA") if os.path.exists(f): out["images"][f"{base}{sl}"] = tga_to_png_datauri(f) print(f"texture {base} slice {sl}: {os.path.basename(f)} ok") else: print(f"texture MISSING: {f}") for i in range(1, 7): out["splines"][f"field{i}.spl"] = parse_spl(os.path.join(ROOT, f"FIELD{i}.SPL")) for f in ["KLNGN2.SPL", "KLNGN3.SPL"]: out["splines"][f.lower()] = parse_spl(os.path.join(ROOT, f)) out["scenes"]["TREK.SCN"] = parse_scn(os.path.join(ROOT, "TREK.SCN")) out["scenes"]["KLNGVID.SCN"] = parse_scn(os.path.join(ROOT, "KLNGVID.SCN")) dst = os.path.join(os.path.dirname(os.path.abspath(__file__)), "trekdata.json") with open(dst, "w") as fh: json.dump(out, fh, separators=(",", ":")) print("wrote", dst, f"{os.path.getsize(dst)/1e6:.2f} MB") if __name__ == "__main__": main()