#!/usr/bin/env python3 """Debug-Visualisierung fuer Level-WADs von "Ein Koenigreich fuer ein Lama". Macht Kollisionsgeometrie (auch unsichtbare Waende/Boeden) und unsichtbare Logik-Objekte (Kameras, Trigger, Ausgaenge, Checkpoints, Kraftzonen ...) im Spiel sichtbar. debugviz.py IN.wad OUT.wad [Optionen] ein Level debugviz.py --all OUTDIR [--src CD-wads-Ordner] alle Level-WADs (t1l*, t6l*) Optionen: --no-coll --no-markers --no-fill (nur Kanten) --opaque (Fuellung deckend) --width 0.03 (Kantenbreite) --offset 0.012 (Abstand entlang der Normale) --report datei.json Prinzip (Details: notes/debugviz.md): * Kollision: an jedes TRAK-Modell werden Render-Dreiecke angehaengt (Rahmen entlang der Kanten jedes Kollisionspolygons, leicht entlang der Normale versetzt; bei unsichtbarer Kollision zusaetzlich halbtransparente Fuellung). Damit stimmen Kachel-Transformation, Zonen/Sichtbarkeit und Kachel-Wechsel automatisch. Kollisionspolygone werden NICHT veraendert/ergaenzt. Vertexfarben-Block in MAP waechst je Kachel und Farbsatz mit. * Objekte: Marker (Wuerfel, Linien, Boxen) als neue Kacheln ohne Kollision, je Rasterzelle eine Kachel (also in der richtigen Zone). Strats bleiben unveraendert. * Farben: angehaengte Flachfarb-Texturen (Flags 0x105 deckend, 0x1105 = Blendmodus 1 Alpha-Blending, Alpha im 4. Byte). """ import argparse import collections import glob import json import math import os import struct import sys import numpy as np sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import geomlib as G # noqa: E402 import mapchunk # noqa: E402 import strats as S # noqa: E402 CD_WADS = os.path.expanduser('~/.wine-lama/iso-dk/Setup/Eng/Cd/wads') # ------------------------------------------------------------------ Farben KIND_COL = { 'boden': (40, 255, 40), # begehbar (Normale steil nach oben) 'schraege': (200, 255, 0), # schraeg 'wand': (0, 150, 255), # Wand 'decke': (255, 130, 0), # Decke / Ueberhang } INVIS_FILL = (255, 0, 0) # unsichtbare Kollision (keine Render-Flaeche) FILL_ALPHA = 96 # Material -> (Name, Farbe). Unbekannte Materialien bekommen Farben aus PALETTE. MAT_INFO = { 16: ('Ausgang (LevelDoor-Boden)', (190, 60, 255)), 17: ('selektiv 0x11 (nur Objekte mit Flag 0x8000000)', (255, 255, 0)), 18: ('selektiv 0x12 (nicht fuer Objekte mit Flag 0x8000000)', (255, 200, 120)), 30: ('kein Boden / Fall-/Todesebene (0x1e)', (255, 0, 255)), } # feste Farben fuer die uebrigen Materialien (Bedeutung im Code nicht geklaert, siehe notes/debugviz.md) MAT_FIXED = { 1: (0, 200, 200), 2: (255, 255, 255), 3: (255, 120, 170), 5: (120, 255, 200), 6: (160, 120, 255), 7: (255, 170, 80), 8: (0, 130, 60), 9: (150, 200, 255), 10: (150, 150, 0), 11: (150, 90, 40), 13: (255, 130, 110), 15: (255, 255, 150), 20: (0, 120, 140), 26: (110, 40, 160), 27: (190, 190, 190), 28: (255, 0, 140), 33: (150, 0, 0), 255: (20, 20, 20), } MAT_DEFAULT = (230, 230, 230) MARK_COL = { 'start': (255, 255, 255), 'ausgang': (190, 60, 255), 'checkpoint': (0, 255, 140), 'kamera': (0, 255, 255), 'trigger': (255, 255, 0), 'kraft': (255, 90, 0), 'geheimnis': (255, 200, 0), 'sonst': (170, 170, 170), 'umgebung': (255, 100, 180), 'objekt': (90, 90, 90), } MARK_H = 0.35 # Markerwuerfel ueber dem Strat-Ursprung MARK_SIZE = 0.05 # halbe Kantenlaenge START_T = {'Llama', 'KuscoAndPachaTogetherOnATree', 'RollerCoaster', 'LlamaChase'} EXIT_T = {'LevelDoor', 'VillageEndDoor', 'EndGameDoor', 'TeleportDoor'} FORCE_T = {'FallOffNoHurt', 'ObjectDontDropInBox', 'RiverCurrentBox', 'RiverRebound', 'SuckingWPs', 'ObjectSuckPoints', 'RiverTrackChange', 'TrackSwitch', 'BackgroundRotationChanger', 'RiverWhirlpool'} MISC_T = {'CheatTeleport', 'Wompy', 'WompySimple', 'WompyRollerCoaster', 'Rain', 'DrippingWater', 'WaterfallMist', 'Smoker', 'CoinLine', 'CoinCircle', 'CoinRollerCoaster', 'CoinBat', 'CoinSpawnOracle', 'MusicBox', 'SpitPlatformTarget', 'NewBlockheadKidHelper', 'FlockOfBirds', 'Bats', 'RiverBats', 'Scorpions1', 'Scorpions3', 'Scorpions5', 'GhostCatacombs', 'LabDumbGuards', 'FlyStuck', 'AppearingPlatforms', 'AppearingPlatformsCity', 'AppearingPlatformsJungle', 'AppearingPlatformsLab', 'AppearingPlatformsVillage', 'fat_bird', 'Column2', 'TipoHelp'} TRIG_T = {'ChangeToBunny', 'ChangeToTurtle', 'RiverStartSign'} def mark_cat(name): n = name.replace('(spawn)', '') if n in START_T: return 'start' if n in EXIT_T: return 'ausgang' if n.startswith('CheckPoint'): return 'checkpoint' if 'Camera' in n: return 'kamera' if n == 'Secret': return 'geheimnis' if n in FORCE_T or n.startswith('Force') or n.startswith('WaterRebound'): return 'kraft' if 'Trigger' in n or n.startswith('Say') or n in TRIG_T: return 'trigger' if n in MISC_T or n.endswith('Spawner') or 'Helper' in n: return 'sonst' return None # ------------------------------------------------------------------ TEXT: Flachfarben anhaengen def text_append_flats(d, entries): """entries: Liste (r,g,b,alpha|None). Liefert (neues TEXT, erster neuer Index).""" o = 0 npal = struct.unpack_from(' 1e-12 else v class Mesh: """Dreiecksliste mit Paletten-Index; Verts (pos, normal).""" def __init__(self): self.verts = [] self.tris = [] # (a, b, c, palidx) def v(self, p, n): self.verts.append((float(p[0]), float(p[1]), float(p[2]), float(n[0]), float(n[1]), float(n[2]))) return len(self.verts) - 1 def quad(self, p, n, col): """4 Punkte (Ring), eine Normale.""" ids = [self.v(q, n) for q in p] self.tris.append((ids[0], ids[1], ids[2], col)) self.tris.append((ids[0], ids[2], ids[3], col)) def beam(self, a, b, w, col): """Linie a->b als Kreuz aus zwei doppelseitigen Streifen.""" a = np.asarray(a, float); b = np.asarray(b, float) d = b - a L = np.linalg.norm(d) if L < 1e-6: return d /= L up = np.array([0, 1.0, 0]) if abs(d[1]) < 0.9 else np.array([1.0, 0, 0]) s1 = norm(np.cross(d, up)) * (w / 2) s2 = norm(np.cross(d, s1)) * (w / 2) for s in (s1, s2): n = norm(np.cross(d, s)) self.quad([a - s, b - s, b + s, a + s], n, col) def cube(self, c, h, col): c = np.asarray(c, float) for ax in range(3): for sg in (-1, 1): n = np.zeros(3); n[ax] = sg u = np.zeros(3); u[(ax + 1) % 3] = h v = np.zeros(3); v[(ax + 2) % 3] = h f = c + n * h self.quad([f - u - v, f + u - v, f + u + v, f - u + v], n, col) def seg_split(a, b, maxlen=1.0): a = np.asarray(a, float); b = np.asarray(b, float) n = max(1, int(math.ceil(np.linalg.norm(b - a) / maxlen))) return [(a + (b - a) * k / n, a + (b - a) * (k + 1) / n) for k in range(n)] # ------------------------------------------------------------------ Kollision klassifizieren def kind_of(ny): if ny >= 0.7: return 'boden' if ny >= 0.2: return 'schraege' if ny > -0.2: return 'wand' return 'decke' def decode_model_coll(m): out = [] for g, grp in enumerate(m.coll): for r in grp: c = G.coll_decode(r) pts = G.coll_polygon(c) ok = len(pts) >= 3 and all(abs(x) < 12 for p in pts for x in p) P = np.array(pts, float) if ok else None if ok: # Flaeche pruefen ar = 0.0 for k in range(1, len(P) - 1): ar += np.linalg.norm(np.cross(P[k] - P[0], P[k + 1] - P[0])) / 2 if ar < 1e-5: ok = False n = norm(np.array(c['plane'][:3])) out.append(dict(c=c, pts=P if ok else None, n=n, mat=c['mat'], typ=c['typ'], grp=g, kind=kind_of(n[1]))) return out class RenderIndex: """Render-Dreiecke der Welt (Exportraum), nach Rasterzellen gebuckelt.""" def __init__(self, trak, mp): cells = collections.defaultdict(list) A, B, C = [], [], [] for i in range(mp['N']): x, y, z, rot, mi, fl = mp['A'][i] m = trak[mi] if not m.faces: continue f, _ = G.tile_xform(x, y, z, rot) W = np.array([f(*v[:3]) for v in m.verts]) for fc in m.faces: a, b, c = (W[k] for k in fc['v']) A.append(a); B.append(b); C.append(c) self.A = np.array(A); self.B = np.array(B); self.C = np.array(C) N = np.cross(self.B - self.A, self.C - self.A) ln = np.linalg.norm(N, axis=1) good = ln > 1e-10 N[good] /= ln[good, None] self.N = N self.D = np.einsum('ij,ij->i', N, self.A) lo = np.minimum(np.minimum(self.A, self.B), self.C) hi = np.maximum(np.maximum(self.A, self.B), self.C) for t in np.nonzero(good)[0]: for cx in range(int(math.floor(lo[t, 0])), int(math.floor(hi[t, 0])) + 1): for cz in range(int(math.floor(lo[t, 2])), int(math.floor(hi[t, 2])) + 1): cells[(cx, cz)].append(t) self.cells = {k: np.array(v) for k, v in cells.items()} def covered(self, p, n, dist=0.5, cosmin=0.4, tol=0.02): """Trifft der Strahl p + t*n (|t| <= dist) ein Render-Dreieck (Neigung zur Normale < ~66 Grad)?""" ids = set() for q in (p, p + n * dist, p - n * dist): k = (int(math.floor(q[0])), int(math.floor(q[2]))) if k in self.cells: ids.update(self.cells[k].tolist()) if not ids: return False ids = np.fromiter(ids, int) N = self.N[ids] cs = N @ n sel = np.abs(cs) > cosmin if not sel.any(): return False ids, N, cs = ids[sel], N[sel], cs[sel] t = (self.D[ids] - N @ p) / cs sel = np.abs(t) <= dist if not sel.any(): return False ids, t = ids[sel], t[sel] q = p[None, :] + t[:, None] * n[None, :] a, b, c = self.A[ids], self.B[ids], self.C[ids] v0, v1 = b - a, c - a v2 = q - a d00 = np.einsum('ij,ij->i', v0, v0); d01 = np.einsum('ij,ij->i', v0, v1) d11 = np.einsum('ij,ij->i', v1, v1) d20 = np.einsum('ij,ij->i', v2, v0); d21 = np.einsum('ij,ij->i', v2, v1) den = d00 * d11 - d01 * d01 den[den == 0] = 1e-12 v = (d11 * d20 - d01 * d21) / den w = (d00 * d21 - d01 * d20) / den u = 1 - v - w return bool(((u > -tol) & (v > -tol) & (w > -tol)).any()) def classify(trak, mp, decoded): """Setzt pro Polygon 'invis' (True = keine Render-Flaeche deckt es).""" ri = RenderIndex(trak, mp) votes = collections.defaultdict(lambda: [0, 0]) # (model, k) -> [sichtbar, unsichtbar] inst = collections.defaultdict(list) altmap = dict(zip([i for i in range(mp['N']) if mp['A'][i][5]], mp['anim'])) for i in range(mp['N']): inst[mp['midx'][i]].append(i) if i in altmap: inst[altmap[i][4]].append(i) for mi, polys in enumerate(decoded): tiles = inst.get(mi, [])[:6] # wenige Instanzen reichen for k, pl in enumerate(polys): if pl['pts'] is None: continue P = pl['pts'] cen = P.mean(axis=0) samples = [cen] + [cen + (q - cen) * 0.7 for q in P] if not tiles: votes[(mi, k)][1] += 1 # nie platziert: ohne Weltbezug -> als unsichtbar werten continue for ti in tiles: x, y, z, rot, _, _ = mp['A'][ti] f, fn = G.tile_xform(x, y, z, rot) nw = np.array(fn(*pl['n'])) hit = sum(ri.covered(np.array(f(*s)), nw) for s in samples) votes[(mi, k)][0 if hit >= 0.6 * len(samples) else 1] += 1 for mi, polys in enumerate(decoded): for k, pl in enumerate(polys): vs = votes.get((mi, k), [1, 0]) pl['invis'] = vs[1] > vs[0] # ------------------------------------------------------------------ Overlay je Modell def mat_color(mat, matcols): if mat in MAT_INFO: return MAT_INFO[mat][1] matcols[mat] = MAT_FIXED.get(mat, MAT_DEFAULT) return matcols[mat] def poly_overlay(mesh, pl, pal, opt, matcols): P = pl['pts'] if P is None: return n = pl['n'] c = pl['c'] mat = pl['mat'] ecol = KIND_COL[pl['kind']] if mat == 0 else mat_color(mat, matcols) off = n * opt.offset cen = P.mean(axis=0) # Inkreis-Abschaetzung: kleinster Abstand Mittelpunkt -> Kante rmin = 1e9 for k in range(len(P)): a, b = P[k], P[(k + 1) % len(P)] e = b - a L = np.linalg.norm(e) if L < 1e-9: continue rmin = min(rmin, np.linalg.norm(np.cross(e, cen - a)) / L) w = min(opt.width, 0.4 * rmin) # innere Ecken: Kantengeraden um w nach innen verschoben, mit Flaechenebene schneiden E = c['edges'] m = len(E) ok = True pts2 = [] for k in range(m): e1, e2 = E[k], E[(k + 1) % m] Am = np.array([c['plane'][:3], e1[:3], e2[:3]]) Bm = np.array([c['plane'][3], e1[3] + w * np.linalg.norm(e1[:3]), e2[3] + w * np.linalg.norm(e2[:3])]) try: pts2.append(np.linalg.solve(Am, Bm)) except np.linalg.LinAlgError: ok = False break if ok and len(pts2) == len(P) and all(np.linalg.norm(pts2[k] - P[k]) < 4 * w + 0.05 for k in range(len(P))): inner = pts2 else: inner = [cen + (q - cen) * 0.85 for q in P] O = [mesh.v(q + off, n) for q in P] # Ecken einmal anlegen (Rahmen + Fuellung teilen sie) I = [mesh.v(q + off, n) for q in inner] ec = pal(ecol) for k in range(len(P)): k2 = (k + 1) % len(P) mesh.tris.append((O[k], O[k2], I[k2], ec)) mesh.tris.append((O[k], I[k2], I[k], ec)) fill = None if not opt.no_fill: if mat == 16: fill = mat_color(mat, matcols) elif pl['invis'] and mat != 30: fill = INVIS_FILL if mat == 0 else mat_color(mat, matcols) if fill is not None: fc = pal(fill, None if opt.opaque else FILL_ALPHA) ids = I for k in range(1, len(ids) - 1): mesh.tris.append((ids[0], ids[k], ids[k + 1], fc)) def append_mesh(model, mesh, pal_base, limit=7900): """Haengt Mesh an Modell an (Batch-Flag 0x8 bei Texturwechsel / alle 256 Faces).""" nv0 = len(model.verts) if nv0 + len(mesh.verts) > limit: return 0 model.verts += mesh.verts tris = sorted(mesh.tris, key=lambda t: t[3]) last, run = None, 0 for a, b, c, col in tris: tex = pal_base + col fl = 0x0800 | 0x0400 | 0x4000 if tex != last or run >= 250: fl |= 0x8 last, run = tex, 0 run += 1 p0, p1, p2 = (model.verts[nv0 + k][:3] for k in (a, b, c)) model.faces.append(dict(flags=fl, v=(nv0 + a, nv0 + b, nv0 + c), tex=tex, pad=0, plane=G.face_plane(p0, p1, p2))) model.dirty = True return len(mesh.verts) # ------------------------------------------------------------------ Marker fuer Objekte def fx(v): v = v - (1 << 32) if v >= 1 << 31 else v return v / 4096 def strat_markers(ch, wf, pal, stats): """Liefert Liste (welt-Mesh-Teile) als dict cell -> Mesh in Weltkoordinaten (Exportraum).""" mm = mapchunk.parse(G.wad_get(ch, 'MAP'), wf, mapchunk.trak_vertcounts(G.wad_get(ch, 'TRAK'))) st = G.wad_get(ch, 'STPC') import stpcwalk lo = stpcwalk.walk(st, wf)['code'] nodes = mm['nodes'] segs = [] # (a, b, breite, farbe) Weltraum Spielkoordinaten (z positiv) cubes = [] # (c, h, farbe) quads = [] # (4 Punkte, farbe, alpha) per_cat = collections.Counter() per_type = collections.Counter() def chain(w): out = [] while 0 <= w < len(nodes) and len(out) < 200: nd = nodes[w] out.append(np.array([fx(nd[5]), fx(nd[6]), fx(nd[7])])) if nd[0] == 0: break w = nd[0] return out def box(lo_, hi_, col, w=0.04): x0, y0, z0 = lo_; x1, y1, z1 = hi_ P = [np.array(p) for p in [(x0, y0, z0), (x1, y0, z0), (x1, y0, z1), (x0, y0, z1), (x0, y1, z0), (x1, y1, z0), (x1, y1, z1), (x0, y1, z1)]] for a, b in [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)]: segs.append((P[a], P[b], w, col)) for s in mm['strats']: name = S.strat_name(st, s[6], lo) or '?' cat = mark_cat(name) if cat is None: # alle uebrigen Objekte: kleiner dunkler Punkt am Ursprung (nichts geht verloren) per_cat['objekt'] += 1 cubes.append((np.array([fx(s[3]), fx(s[4]), fx(s[5])]) + [0, 0.06, 0], 0.03, MARK_COL['objekt'])) continue per_cat[cat] += 1 per_type[name] += 1 col = MARK_COL[cat] p = np.array([fx(s[3]), fx(s[4]), fx(s[5])]) rot = s[1] / 4096 * 2 * math.pi # Marker: Wuerfel + Pfahl top = p + [0, MARK_H, 0] if cat == 'start': # Pfeil flach auf dem Boden (blockiert die Startkamera nicht) d = np.array([math.sin(rot), 0, math.cos(rot)]) # 0 = +z, 90 Grad = +x q = np.array([d[2], 0, -d[0]]) g = p + [0, 0.03, 0] segs.append((g - d * 0.3, g + d * 0.4, 0.025, col)) segs.append((g + d * 0.4, g + d * 0.2 + q * 0.15, 0.025, col)) segs.append((g + d * 0.4, g + d * 0.2 - q * 0.15, 0.025, col)) else: cubes.append((top, MARK_SIZE, col)) segs.append((p, top, 0.01, col)) if cat in ('kamera', 'checkpoint'): d = np.array([math.sin(rot), 0, math.cos(rot)]) segs.append((top, top + d * 0.3, 0.012, col)) if cat == 'ausgang': cx, cz = math.floor(p[0]), math.floor(p[2]) box((cx, p[1] - 0.05, cz), (cx + 1, p[1] + 1.0, cz + 1), col, 0.02) ch_ = chain(s[13]) if s[13] >= 0 else [] if len(ch_) == 2: a, b = ch_ lo_ = np.minimum(a, b); hi_ = np.maximum(a, b) inside = (lo_[0] - 0.4 <= p[0] <= hi_[0] + 0.4) and (lo_[2] - 0.4 <= p[2] <= hi_[2] + 0.4) if inside and hi_[0] - lo_[0] > 0.2 and hi_[2] - lo_[2] > 0.2: y0 = min(lo_[1], p[1]) - 0.02 y1 = max(hi_[1], p[1]) + 1.2 box((lo_[0], y0, lo_[2]), (hi_[0], y1, hi_[2]), col, 0.02) stats['boxen'] += 1 if (hi_[0] - lo_[0]) * (hi_[2] - lo_[2]) <= 64: quads.append(([(lo_[0], y0 + 0.03, lo_[2]), (hi_[0], y0 + 0.03, lo_[2]), (hi_[0], y0 + 0.03, hi_[2]), (lo_[0], y0 + 0.03, hi_[2])], col)) continue if ch_ and cat in ('kamera', 'trigger', 'kraft', 'sonst', 'geheimnis', 'checkpoint', 'start'): dim = tuple(int(c * 0.6) for c in col) prev = p + [0, 0.45, 0] for k, q in enumerate(ch_): cubes.append((q, 0.025, dim)) if cat == 'kraft': segs.append((prev, q, 0.008, dim)) prev = q stats['pfadpunkte'] += len(ch_) # Umgebungszonen (MAP-Block 4, 90 Byte: Zelle x/y/z, Nebel, Musik ...) -> Drahtwuerfel je Zelle col = MARK_COL['umgebung'] for L in mm['lights']: cx, cy, cz = L[0], L[1], L[2] if L[3:6] == (1, 1, 1): continue # Vorgabe-Eintrag (ganzes Level) box((cx + 0.04, cy, cz + 0.04), (cx + 0.96, cy + 0.92, cz + 0.96), col, 0.02) per_cat['umgebung'] += 1 # alles in Zellen buckeln (Exportraum: z negieren) cells = collections.defaultdict(Mesh) def cell_of(q): return (int(math.floor(q[0])), int(math.floor(q[2]))) def ex(q): return np.array([q[0], q[1], -q[2]]) for a, b, w, col in segs: for s0, s1 in seg_split(a, b, 1.0): m = (s0 + s1) / 2 cells[cell_of(m)].beam(ex(s0), ex(s1), w, pal(col)) for c, h, col in cubes: cells[cell_of(c)].cube(ex(c), h, pal(col)) for pts, col in quads: # in <=1x1-Stuecke zerlegen (x0, y, z0), _, (x1, _, z1), _ = pts xs = np.arange(math.floor(x0), math.ceil(x1)) zs = np.arange(math.floor(z0), math.ceil(z1)) for cx in xs: for cz in zs: a0, a1 = max(x0, cx), min(x1, cx + 1) b0, b1 = max(z0, cz), min(z1, cz + 1) if a1 - a0 < 1e-3 or b1 - b0 < 1e-3: continue ms = cells[(int(cx), int(cz))] q = [ex((a0, y, b0)), ex((a1, y, b0)), ex((a1, y, b1)), ex((a0, y, b1))] ids = [ms.v(t, (0, 1, 0)) for t in q] fc = pal(col, 70) ms.tris.append((ids[0], ids[1], ids[2], fc)) ms.tris.append((ids[0], ids[2], ids[3], fc)) stats['marker_kat'] = dict(per_cat) stats['marker_typen'] = dict(per_type) return cells # ------------------------------------------------------------------ Hauptablauf def process(src, dst, opt): ch = G.wad_read(src) wf = G.wfpc_flags(ch) trak = G.parse_trak(G.wad_get(ch, 'TRAK')) old_vc = [len(m.verts) for m in trak] mp = G.parse_map_tiles(G.wad_get(ch, 'MAP'), wf, old_vc) pal = Palette() matcols = {} stats = collections.Counter() info = dict(level=os.path.basename(src)) mats = collections.Counter() add_v = [0] * len(trak) if not opt.no_coll: decoded = [decode_model_coll(m) for m in trak] classify(trak, mp, decoded) # Statistik je Kachelinstanz inst = collections.Counter(mp['midx']) for mi, polys in enumerate(decoded): for pl in polys: mats[pl['mat']] += 1 stats['koll_polys'] += 1 if pl['pts'] is None: stats['koll_kaputt'] += 1 elif pl['invis']: stats['koll_unsichtbar'] += 1 stats['koll_unsichtbar_inst'] += inst.get(mi, 0) for mi, m in enumerate(trak): mesh = Mesh() for pl in decoded[mi]: poly_overlay(mesh, pl, pal, opt, matcols) add_v[mi] = len(mesh.verts) mesh_store = mesh trak[mi]._ov = mesh_store # Marker cells = {} if not opt.no_markers: cells = strat_markers(ch, wf, pal, stats) # TEXT erweitern text, base = text_append_flats(G.wad_get(ch, 'TEXT'), pal.entries) G.wad_set(ch, 'TEXT', text) for mi, m in enumerate(trak): ov = getattr(m, '_ov', None) if ov is not None and ov.verts: got = append_mesh(m, ov, base) if got == 0: stats['modelle_zu_gross'] += 1 add_v[mi] = 0 # neue Marker-Kacheln W, H = mp['W'], mp['H'] new_tiles = [] for (cx, cz), mesh in sorted(cells.items()): if not mesh.verts: continue if not (0 <= cx < W and 0 <= cz < H): stats['marker_ausserhalb'] += 1 continue ys = [v[1] for v in mesh.verts] oy = round((min(ys) + max(ys)) / 2 * 4) / 4 ox, oz = cx + 0.5, -(cz + 0.5) mdl = G.Model() loc = Mesh() loc.verts = [(v[0] - ox, v[1] - oy, v[2] - oz, v[3], v[4], v[5]) for v in mesh.verts] loc.tris = mesh.tris # zu grosse Zellen aufteilen if len(loc.verts) > 7900: stats['marker_gekuerzt'] += 1 keep = [t for t in loc.tris if max(t[:3]) < 7900] loc.tris = keep loc.verts = loc.verts[:7900] append_mesh(mdl, loc, base) trak.append(mdl) new_tiles.append(dict(x=ox, y=oy, z=oz, rot=0, model=len(trak) - 1, cols=[[0x00808080] * len(mdl.verts)])) stats['marker_kacheln'] = len(new_tiles) # MAP neu altmap = dict(zip([i for i in range(mp['N']) if mp['A'][i][5]], mp['anim'])) tiles = [] for i in range(mp['N']): x, y, z, rot, mi, fl = mp['A'][i] ext = [0x00808080] * add_v[mi] t = dict(x=x, y=y, z=z, rot=rot, model=mi, cols=[fr + ext for fr in mp['cols'][i]], flag=fl) if i in altmap: am = altmap[i][4] t['altcols'] = [fr + [0x00808080] * add_v[am] for fr in mp['altcols'][i]] tiles.append(t) tiles += new_tiles newmap = G.build_map_tiles(G.wad_get(ch, 'MAP'), wf, old_vc, tiles, zones='keep', anim=mp['anim']) G.wad_set(ch, 'TRAK', G.build_trak(trak)) G.wad_set(ch, 'MAP', newmap) # Gegenprobe: MAP vollstaendig parsen, Strats unveraendert vc = [len(m.verts) for m in trak] a = mapchunk.parse(newmap, wf, vc) b = mapchunk.parse(G.wad_get(G.wad_read(src), 'MAP'), wf, old_vc) assert a['rest'] == b['rest'] and a['strats'] == b['strats'] and a['nodes'] == b['nodes'] assert a['tiles'][:mp['N']] == b['tiles'] nv_max = max(vc) G.wad_write(dst, ch) info.update(stats) info['materialien'] = {str(k): v for k, v in sorted(mats.items())} info['materialfarben'] = {str(k): v for k, v in matcols.items()} info['max_verts_modell'] = nv_max info['max_faces_modell'] = max(len(m.faces) for m in trak) info['texturen_neu'] = len(pal.entries) info['groesse_alt'] = os.path.getsize(src) info['groesse_neu'] = os.path.getsize(dst) info['kacheln'] = mp['N'] + len(new_tiles) info['modelle'] = len(trak) return info def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument('inp', nargs='?') ap.add_argument('out', nargs='?') ap.add_argument('--all', metavar='OUTDIR') ap.add_argument('--src', default=CD_WADS) ap.add_argument('--only', help='Kommagetrennte Levelnamen (mit --all)') ap.add_argument('--no-coll', action='store_true') ap.add_argument('--no-markers', action='store_true') ap.add_argument('--no-fill', action='store_true') ap.add_argument('--opaque', action='store_true') ap.add_argument('--width', type=float, default=0.007) ap.add_argument('--offset', type=float, default=0.012) ap.add_argument('--report') opt = ap.parse_args() infos = [] if opt.all: os.makedirs(opt.all, exist_ok=True) files = sorted(glob.glob(os.path.join(opt.src, 't[1-6]l*.wad'))) if opt.only: want = {w.lower().replace('.wad', '') for w in opt.only.split(',')} files = [f for f in files if os.path.basename(f).lower()[:-4] in want] for f in files: info = process(f, os.path.join(opt.all, os.path.basename(f)), opt) infos.append(info) print('%-13s %5d KB -> %5d KB Koll %5d (unsichtbar %4d) Marker-Kacheln %3d maxV %4d' % ( info['level'], info['groesse_alt'] // 1024, info['groesse_neu'] // 1024, info.get('koll_polys', 0), info.get('koll_unsichtbar', 0), info.get('marker_kacheln', 0), info['max_verts_modell']), flush=True) else: if not (opt.inp and opt.out): ap.error('IN.wad OUT.wad oder --all OUTDIR') info = process(opt.inp, opt.out, opt) infos.append(info) print(json.dumps(info, indent=1, ensure_ascii=False)) if opt.report: json.dump(infos, open(opt.report, 'w'), indent=1, ensure_ascii=False) if __name__ == '__main__': main()