translated the remaining german notes, python comments, help and messages to english (in-game strings, parsed report lines and cli values stay german)

This commit is contained in:
MineTec committed 2026-11-07 10:51:14 +01:00
1 parent 86fe26d786
commit 526f6072f6
36 files changed
+1553 -1544

No files matched your search

+79 -77
View File
@@ -1,26 +1,28 @@
#!/usr/bin/env python3
"""Debug-Visualisierung fuer Level-WADs von "Ein Koenigreich fuer ein Lama".
"""Debug visualisation for level WADs of "Ein Koenigreich fuer ein Lama".
Macht Kollisionsgeometrie (auch unsichtbare Waende/Boeden) und unsichtbare Logik-Objekte
(Kameras, Trigger, Ausgaenge, Checkpoints, Kraftzonen ...) im Spiel sichtbar.
Makes collision geometry (including invisible walls/floors) and invisible logic objects
(cameras, triggers, exits, checkpoints, force zones ...) visible in the game.
debugviz.py IN.wad OUT.wad [Optionen] ein Level
debugviz.py --all OUTDIR [--src CD-wads-Ordner] alle Level-WADs (t1l*, t6l*)
debugviz.py IN.wad OUT.wad [options] one level
debugviz.py --all OUTDIR [--src CD-wads-folder] all 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
Options: --no-coll --no-markers --no-fill (edges only) --opaque (opaque fill)
--width 0.03 (edge width) --offset 0.012 (offset along the normal)
--report file.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).
Principle (details: notes/debugviz.md):
* Collision: render triangles are appended to every TRAK model (a frame along the
edges of each collision polygon, offset slightly along the normal; invisible
collision additionally gets a semi-transparent fill). This way tile transforms,
zones/visibility and tile swaps are automatically correct. Collision polygons are NOT
changed/added. The vertex colour block in MAP grows accordingly per tile and colour set.
* Objects: markers (cubes, lines, boxes) as new tiles without collision, one tile per
grid cell (so in the right zone). Strats stay unchanged.
* Colours: appended flat colour textures (flags 0x105 opaque, 0x1105 = blend mode 1
alpha blending, alpha in the 4th byte).
Report/JSON keys and category names (boden, wand, ausgang, koll_polys, ...) are German
identifiers and kept as they are.
"""
import argparse
import collections
@@ -40,24 +42,24 @@ import strats as S # noqa: E402
CD_WADS = os.path.expanduser('~/.wine-lama/iso-dk/Setup/Eng/Cd/wads')
# ------------------------------------------------------------------ Farben
# ------------------------------------------------------------------ colours
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
'boden': (40, 255, 40), # walkable (normal steeply upwards)
'schraege': (200, 255, 0), # slope
'wand': (0, 150, 255), # wall
'decke': (255, 130, 0), # ceiling / overhang
}
INVIS_FILL = (255, 0, 0) # unsichtbare Kollision (keine Render-Flaeche)
INVIS_FILL = (255, 0, 0) # invisible collision (no render surface)
FILL_ALPHA = 96
# Material -> (Name, Farbe). Unbekannte Materialien bekommen Farben aus PALETTE.
# material -> (name, colour). Unknown materials get colours from 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)),
16: ('exit (LevelDoor floor)', (190, 60, 255)),
17: ('selective 0x11 (only objects with flag 0x8000000)', (255, 255, 0)),
18: ('selective 0x12 (not for objects with flag 0x8000000)', (255, 200, 120)),
30: ('no floor / fall/death plane (0x1e)', (255, 0, 255)),
}
# feste Farben fuer die uebrigen Materialien (Bedeutung im Code nicht geklaert, siehe notes/debugviz.md)
# fixed colours for the remaining materials (meaning in the code not yet known, see 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),
@@ -79,8 +81,8 @@ MARK_COL = {
'objekt': (90, 90, 90),
}
MARK_H = 0.35 # Markerwuerfel ueber dem Strat-Ursprung
MARK_SIZE = 0.05 # halbe Kantenlaenge
MARK_H = 0.35 # marker cube above the strat origin
MARK_SIZE = 0.05 # half edge length
START_T = {'Llama', 'KuscoAndPachaTogetherOnATree', 'RollerCoaster', 'LlamaChase'}
EXIT_T = {'LevelDoor', 'VillageEndDoor', 'EndGameDoor', 'TeleportDoor'}
FORCE_T = {'FallOffNoHurt', 'ObjectDontDropInBox', 'RiverCurrentBox', 'RiverRebound',
@@ -117,10 +119,10 @@ def mark_cat(name):
return None
# ------------------------------------------------------------------ TEXT: Flachfarben anhaengen
# ------------------------------------------------------------------ TEXT: append flat colours
def text_append_flats(d, entries):
"""entries: Liste (r,g,b,alpha|None). Liefert (neues TEXT, erster neuer Index)."""
"""entries: list of (r,g,b,alpha|None). Returns (new TEXT, first new index)."""
o = 0
npal = struct.unpack_from('<I', d, o)[0]; o += 4
for _ in range(npal):
@@ -139,7 +141,7 @@ def text_append_flats(d, entries):
for r, g, b, a in entries:
if a is None:
add += struct.pack('<HbbBBBB', 0x105, -1, -1, r, g, b, 0)
else: # Blendmodus 1 (Bits 12-14) = SRCALPHA/INVSRCALPHA, Alpha = 4. Byte
else: # blend mode 1 (bits 12-14) = SRCALPHA/INVSRCALPHA, alpha = 4th byte
add += struct.pack('<HbbBBBB', 0x1105, -1, -1, r, g, b, a)
e = o + 4 + 8 * nt
return d[:o] + struct.pack('<I', nt + len(entries)) + d[o + 4:e] + add + d[e:], nt
@@ -156,10 +158,10 @@ class Palette:
if k not in self.idx:
self.idx[k] = len(self.entries)
self.entries.append((*rgb, alpha))
return self.idx[k] # relativ; + base beim Schreiben
return self.idx[k] # relative; + base when writing
# ------------------------------------------------------------------ Geometrie-Helfer
# ------------------------------------------------------------------ geometry helpers
def norm(v):
n = np.linalg.norm(v)
@@ -167,7 +169,7 @@ def norm(v):
class Mesh:
"""Dreiecksliste mit Paletten-Index; Verts (pos, normal)."""
"""Triangle list with palette index; verts (pos, normal)."""
def __init__(self):
self.verts = []
@@ -178,13 +180,13 @@ class Mesh:
return len(self.verts) - 1
def quad(self, p, n, col):
"""4 Punkte (Ring), eine Normale."""
"""4 points (ring), one normal."""
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."""
"""Line a->b as a cross of two double-sided strips."""
a = np.asarray(a, float); b = np.asarray(b, float)
d = b - a
L = np.linalg.norm(d)
@@ -215,7 +217,7 @@ def seg_split(a, b, maxlen=1.0):
return [(a + (b - a) * k / n, a + (b - a) * (k + 1) / n) for k in range(n)]
# ------------------------------------------------------------------ Kollision klassifizieren
# ------------------------------------------------------------------ classify collision
def kind_of(ny):
if ny >= 0.7:
@@ -236,7 +238,7 @@ def decode_model_coll(m):
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
# check the surface
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
@@ -249,7 +251,7 @@ def decode_model_coll(m):
class RenderIndex:
"""Render-Dreiecke der Welt (Exportraum), nach Rasterzellen gebuckelt."""
"""Render triangles of the world (export space), bucketed by grid cell."""
def __init__(self, trak, mp):
cells = collections.defaultdict(list)
@@ -280,7 +282,7 @@ class RenderIndex:
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)?"""
"""Does the ray p + t*n (|t| <= dist) hit a render triangle (angle to the normal < ~66 degrees)?"""
ids = set()
for q in (p, p + n * dist, p - n * dist):
k = (int(math.floor(q[0])), int(math.floor(q[2])))
@@ -316,9 +318,9 @@ class RenderIndex:
def classify(trak, mp, decoded):
"""Setzt pro Polygon 'invis' (True = keine Render-Flaeche deckt es)."""
"""Sets 'invis' per polygon (True = no render surface covers it)."""
ri = RenderIndex(trak, mp)
votes = collections.defaultdict(lambda: [0, 0]) # (model, k) -> [sichtbar, unsichtbar]
votes = collections.defaultdict(lambda: [0, 0]) # (model, k) -> [visible, invisible]
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']):
@@ -326,7 +328,7 @@ def classify(trak, mp, decoded):
if i in altmap:
inst[altmap[i][4]].append(i)
for mi, polys in enumerate(decoded):
tiles = inst.get(mi, [])[:6] # wenige Instanzen reichen
tiles = inst.get(mi, [])[:6] # a few instances are enough
for k, pl in enumerate(polys):
if pl['pts'] is None:
continue
@@ -334,7 +336,7 @@ def classify(trak, mp, decoded):
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
votes[(mi, k)][1] += 1 # never placed: no world reference -> count as invisible
continue
for ti in tiles:
x, y, z, rot, _, _ = mp['A'][ti]
@@ -348,7 +350,7 @@ def classify(trak, mp, decoded):
pl['invis'] = vs[1] > vs[0]
# ------------------------------------------------------------------ Overlay je Modell
# ------------------------------------------------------------------ overlay per model
def mat_color(mat, matcols):
if mat in MAT_INFO:
@@ -367,7 +369,7 @@ def poly_overlay(mesh, pl, pal, opt, matcols):
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
# incircle estimate: smallest distance centre -> edge
rmin = 1e9
for k in range(len(P)):
a, b = P[k], P[(k + 1) % len(P)]
@@ -377,7 +379,7 @@ def poly_overlay(mesh, pl, pal, opt, matcols):
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
# inner corners: edge lines shifted inwards by w, intersected with the surface plane
E = c['edges']
m = len(E)
ok = True
@@ -395,7 +397,7 @@ def poly_overlay(mesh, pl, pal, opt, matcols):
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)
O = [mesh.v(q + off, n) for q in P] # create corners once (frame + fill share them)
I = [mesh.v(q + off, n) for q in inner]
ec = pal(ecol)
for k in range(len(P)):
@@ -416,7 +418,7 @@ def poly_overlay(mesh, pl, pal, opt, matcols):
def append_mesh(model, mesh, pal_base, limit=7900):
"""Haengt Mesh an Modell an (Batch-Flag 0x8 bei Texturwechsel / alle 256 Faces)."""
"""Appends a mesh to a model (batch flag 0x8 on texture change / every 256 faces)."""
nv0 = len(model.verts)
if nv0 + len(mesh.verts) > limit:
return 0
@@ -437,7 +439,7 @@ def append_mesh(model, mesh, pal_base, limit=7900):
return len(mesh.verts)
# ------------------------------------------------------------------ Marker fuer Objekte
# ------------------------------------------------------------------ markers for objects
def fx(v):
v = v - (1 << 32) if v >= 1 << 31 else v
@@ -445,15 +447,15 @@ def fx(v):
def strat_markers(ch, wf, pal, stats):
"""Liefert Liste (welt-Mesh-Teile) als dict cell -> Mesh in Weltkoordinaten (Exportraum)."""
"""Returns the world mesh parts as dict cell -> Mesh in world coordinates (export space)."""
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)
segs = [] # (a, b, width, colour) world space, game coordinates (z positive)
cubes = [] # (c, h, colour)
quads = [] # (4 points, colour, alpha)
per_cat = collections.Counter()
per_type = collections.Counter()
@@ -478,7 +480,7 @@ def strat_markers(ch, wf, pal, stats):
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)
# all other objects: small dark dot at the origin (nothing gets lost)
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
@@ -487,11 +489,11 @@ def strat_markers(ch, wf, pal, stats):
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
# marker: cube + post
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
# arrow flat on the ground (doesn't block the start camera)
d = np.array([math.sin(rot), 0, math.cos(rot)]) # 0 = +z, 90 degrees = +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))
@@ -530,16 +532,16 @@ def strat_markers(ch, wf, pal, stats):
prev = q
stats['pfadpunkte'] += len(ch_)
# Umgebungszonen (MAP-Block 4, 90 Byte: Zelle x/y/z, Nebel, Musik ...) -> Drahtwuerfel je Zelle
# environment zones (MAP block 4, 90 bytes: cell x/y/z, fog, music ...) -> wire cube per cell
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)
continue # default entry (whole 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)
# bucket everything into cells (export space: negate z)
cells = collections.defaultdict(Mesh)
def cell_of(q):
@@ -554,7 +556,7 @@ def strat_markers(ch, wf, pal, stats):
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
# split into <=1x1 pieces
(x0, y, z0), _, (x1, _, z1), _ = pts
xs = np.arange(math.floor(x0), math.ceil(x1))
zs = np.arange(math.floor(z0), math.ceil(z1))
@@ -575,7 +577,7 @@ def strat_markers(ch, wf, pal, stats):
return cells
# ------------------------------------------------------------------ Hauptablauf
# ------------------------------------------------------------------ main flow
def process(src, dst, opt):
ch = G.wad_read(src)
@@ -592,7 +594,7 @@ def process(src, dst, opt):
if not opt.no_coll:
decoded = [decode_model_coll(m) for m in trak]
classify(trak, mp, decoded)
# Statistik je Kachelinstanz
# statistics per tile instance
inst = collections.Counter(mp['midx'])
for mi, polys in enumerate(decoded):
for pl in polys:
@@ -610,11 +612,11 @@ def process(src, dst, opt):
add_v[mi] = len(mesh.verts)
mesh_store = mesh
trak[mi]._ov = mesh_store
# Marker
# markers
cells = {}
if not opt.no_markers:
cells = strat_markers(ch, wf, pal, stats)
# TEXT erweitern
# extend TEXT
text, base = text_append_flats(G.wad_get(ch, 'TEXT'), pal.entries)
G.wad_set(ch, 'TEXT', text)
for mi, m in enumerate(trak):
@@ -624,7 +626,7 @@ def process(src, dst, opt):
if got == 0:
stats['modelle_zu_gross'] += 1
add_v[mi] = 0
# neue Marker-Kacheln
# new marker tiles
W, H = mp['W'], mp['H']
new_tiles = []
for (cx, cz), mesh in sorted(cells.items()):
@@ -640,7 +642,7 @@ def process(src, dst, opt):
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
# split cells that are too large
if len(loc.verts) > 7900:
stats['marker_gekuerzt'] += 1
keep = [t for t in loc.tris if max(t[:3]) < 7900]
@@ -651,7 +653,7 @@ def process(src, dst, opt):
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
# rebuild MAP
altmap = dict(zip([i for i in range(mp['N']) if mp['A'][i][5]], mp['anim']))
tiles = []
for i in range(mp['N']):
@@ -666,7 +668,7 @@ def process(src, dst, opt):
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
# cross-check: MAP parses completely, strats unchanged
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)
@@ -693,7 +695,7 @@ def main():
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('--only', help='comma-separated level names (with --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')
@@ -712,12 +714,12 @@ def main():
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' % (
print('%-13s %5d KB -> %5d KB coll %5d (invisible %4d) marker tiles %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')
ap.error('IN.wad OUT.wad or --all OUTDIR')
info = process(opt.inp, opt.out, opt)
infos.append(info)
print(json.dumps(info, indent=1, ensure_ascii=False))