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

+59 -59
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env python3
"""Geometrie-Bibliothek fuer Level-WADs von "Ein Koenigreich fuer ein Lama" (Croc-2-Engine, PC).
"""Geometry library for level WADs of "Ein Koenigreich fuer ein Lama" (Croc 2 engine, PC).
Format siehe notes/geometry.md. Kurz:
TRAK = Kachel-Modelle (Welt-Geometrie + Kollision), u32 n, n x 0x84 Header, dann Daten
MAP = Kachel-Raster: Platzierung (Position/Rotation/Modell) + Vertexfarben (+ Strats usw.)
STPC = Objekt-Modelle (gleiches Modellformat, Header 0x8c) + Animationen + Strat-Skripte
TEXT = Texturseiten + Texturtabelle (UV-Rechtecke)
Format: see notes/geometry.md. In short:
TRAK = tile models (world geometry + collision), u32 n, n x 0x84 header, then data
MAP = tile grid: placement (position/rotation/model) + vertex colours (+ strats etc.)
STPC = object models (same model format, header 0x8c) + animations + strat scripts
TEXT = texture pages + texture table (UV rectangles)
Alle Parser sind aus groove.exe nachgebaut (FUN_005563f0 TRAK, FUN_0041f770 STPC-Modell,
FUN_0042ac50 MAP, FUN_00406d30 Texturtabelle, FUN_00556510 Kachel-Renderer).
All parsers are reimplemented from groove.exe (FUN_005563f0 TRAK, FUN_0041f770 STPC model,
FUN_0042ac50 MAP, FUN_00406d30 texture table, FUN_00556510 tile renderer).
"""
import math
import struct
@@ -19,7 +19,7 @@ import struct
def wad_read(path):
d = open(path, 'rb').read()
total = struct.unpack_from('<I', d, 0)[0]
assert total == len(d) - 4, 'WAD-Laengenfeld passt nicht'
assert total == len(d) - 4, 'WAD length field does not match'
chunks, o = [], 4
while o + 8 <= len(d):
tag = d[o:o + 4][::-1].decode('latin1')
@@ -56,21 +56,21 @@ def wfpc_flags(chunks):
return struct.unpack('<I', c)[0] if c else 0
# ---------------------------------------------------------------- Modelle
# Header (TRAK 0x84 Byte, STPC 0x8c Byte):
# +0x00 3f Mittelpunkt der Bounding-Box
# +0x0c 24f 8 Ecken der (um 1/128 vergroesserten) Bounding-Box
# ---------------------------------------------------------------- models
# Header (TRAK 0x84 bytes, STPC 0x8c bytes):
# +0x00 3f centre of the bounding box
# +0x0c 24f 8 corners of the bounding box (enlarged by 1/128)
# +0x6c u16 nVerts +0x6e u16 nFaces
# +0x70 u32 (Zeiger Verts, in Datei 0) +0x74 u32 (Zeiger Faces, 0)
# +0x78 u16 nColA +0x7a u16 nColB +0x7c u16 nColC (Kollisionspolygone, 32 Byte)
# +0x7e u16 0 +0x80 u32 (Zeiger Kollision, 0)
# nur STPC: +0x84 u16 ?(meist = nVerts oder 0) +0x86 u16 nX (n*2 Byte Daten vor den Verts)
# +0x88 u32 (Zeiger, 0)
# Daten: [STPC: nX*u16] Verts nV*24 (pos 3f, normal 3f), Faces nF*28, Kollision nCol*32
# Face (28): u16 flags, u16 v0,v1,v2, u16 tex, u16 0, 3f Ebenen-Normale, f d (n.v = d)
# Kollision (32): u8 typ (Bit0: Viereck, sonst Dreieck), u8 Material, 5 x {i8 nx,ny,nz, u8 0,
# i16 d}: Ebene 0 = Flaeche, Ebenen 1..4 = Kanten (nach innen). Normalen *127, d *4096,
# Koordinaten im Spielraum (z gespiegelt ggue. Vertexdaten!).
# +0x70 u32 (verts pointer, 0 in file) +0x74 u32 (faces pointer, 0)
# +0x78 u16 nColA +0x7a u16 nColB +0x7c u16 nColC (collision polygons, 32 bytes)
# +0x7e u16 0 +0x80 u32 (collision pointer, 0)
# STPC only: +0x84 u16 ?(usually = nVerts or 0) +0x86 u16 nX (n*2 bytes of data before the verts)
# +0x88 u32 (pointer, 0)
# Data: [STPC: nX*u16] verts nV*24 (pos 3f, normal 3f), faces nF*28, collision nCol*32
# Face (28): u16 flags, u16 v0,v1,v2, u16 tex, u16 0, 3f plane normal, f d (n.v = d)
# Collision (32): u8 type (bit 0: quad, otherwise triangle), u8 material, 5 x {i8 nx,ny,nz, u8 0,
# i16 d}: plane 0 = surface, planes 1..4 = edges (pointing inwards). Normals *127, d *4096,
# coordinates in game space (z mirrored relative to the vertex data!).
CORNERS = [(-1, -1, 1), (1, -1, 1), (-1, 1, 1), (1, 1, 1), (-1, -1, -1), (1, -1, -1), (-1, 1, -1), (1, 1, -1)]
@@ -79,11 +79,11 @@ class Model:
def __init__(self):
self.verts = [] # [(x,y,z,nx,ny,nz)]
self.faces = [] # [dict(flags,v=(a,b,c),tex,pad,plane=(nx,ny,nz,d))]
self.coll = [[], [], []] # drei Gruppen; je Eintrag 32 Byte roh (bytes)
self.hdr = None # Original-Header (bytes) zum Vergleich
self.coll = [[], [], []] # three groups; 32 raw bytes per entry (bytes)
self.hdr = None # original header (bytes) for comparison
self.extra = b'' # STPC: nX*u16
self.x84 = 0
self.dirty = False # True setzen, wenn Verts geaendert -> Bounding-Box neu
self.dirty = False # set True when verts changed -> recompute bounding box
def _parse_models(d, o, n, hsize):
@@ -115,7 +115,7 @@ def _parse_models(d, o, n, hsize):
def parse_stpc_models(d):
"""Nur die statischen Objektmodelle am Anfang von STPC (u32 n, je 1 Modell mit 0x8c-Header)."""
"""Only the static object models at the start of STPC (u32 n, 1 model each with 0x8c header)."""
n = struct.unpack_from('<I', d, 0)[0]
o = 4
out = []
@@ -139,7 +139,7 @@ def bbox_header(m):
else:
lo = hi = (0.0, 0.0, 0.0)
c = tuple((a + b) / 2 for a, b in zip(lo, hi))
e = 1 / 128 # Original vergroessert die Box um 1/128
e = 1 / 128 # the original enlarges the box by 1/128
h = tuple((b - a) / 2 + e for a, b in zip(lo, hi))
pts = []
for s in CORNERS:
@@ -148,7 +148,7 @@ def bbox_header(m):
def build_models(models, hsize, recompute_bbox=None):
"""recompute_bbox=None: Original-Box behalten, wenn vorhanden und Verts unveraendert."""
"""recompute_bbox=None: keep the original box if present and verts unchanged."""
hdr, data = b'', b''
for m in models:
if recompute_bbox is False or (recompute_bbox is None and m.hdr is not None
@@ -180,17 +180,17 @@ def build_trak(models):
def face_plane(p0, p1, p2):
ax, ay, az = (p1[i] - p0[i] for i in range(3))
bx, by, bz = (p2[i] - p0[i] for i in range(3))
# Wicklung: Original-Normale = (v1-v0) x (v2-v0)
# winding: original normal = (v1-v0) x (v2-v0)
nx, ny, nz = ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx
ln = math.sqrt(nx * nx + ny * ny + nz * nz) or 1.0
nx, ny, nz = nx / ln, ny / ln, nz / ln
return (nx, ny, nz, nx * p0[0] + ny * p0[1] + nz * p0[2])
# ---------------------------------------------------------------- Kollision
# ---------------------------------------------------------------- collision
def coll_decode(r):
"""32-Byte-Kollisionspolygon -> dict (Ebenen im Vertex-/Exportraum, d.h. z zurueckgespiegelt)."""
"""32-byte collision polygon -> dict (planes in vertex/export space, i.e. z mirrored back)."""
typ, mat = r[0], r[1]
planes = []
for k in range(5):
@@ -201,7 +201,7 @@ def coll_decode(r):
def coll_polygon(c):
"""Eckpunkte aus Flaechenebene + Kantenebenen (Schnitt benachbarter Kanten)."""
"""Corner points from surface plane + edge planes (intersection of adjacent edges)."""
import numpy as np
P = c['plane']; E = c['edges']
pts = []
@@ -217,7 +217,7 @@ def coll_polygon(c):
def coll_encode(typ, mat, plane, edges):
"""Ebenen im Vertexraum -> 32 Byte (z wird fuer den Spielraum gespiegelt)."""
"""Planes in vertex space -> 32 bytes (z is mirrored for game space)."""
def q(p):
nx, ny, nz, d = p
cl = lambda v: max(-127, min(127, int(round(v * 127))))
@@ -227,7 +227,7 @@ def coll_encode(typ, mat, plane, edges):
def coll_from_polygon(pts, mat=0, typ=None):
"""Konvexes Polygon (3 oder 4 Punkte, Vertexraum, Reihenfolge wie Faces) -> 32-Byte-Record."""
"""Convex polygon (3 or 4 points, vertex space, same order as faces) -> 32-byte record."""
import numpy as np
P = [np.array(p, float) for p in pts]
n = np.cross(P[1] - P[0], P[2] - P[0])
@@ -244,20 +244,20 @@ def coll_from_polygon(pts, mat=0, typ=None):
return None
e /= np.linalg.norm(e)
if e @ (cen - a) < 0:
e = -e # nach innen
e = -e # inwards
edges.append((*e, float(e @ a)))
if typ is None:
typ = 1 if len(P) == 4 else 0
return coll_encode(typ, mat, (*n, d), edges)
# ---------------------------------------------------------------- MAP (nur Kachelteile)
STRAT_REC = 90 # Bytes pro Eintrag des 2. Blocks (0x5c im Speicher)
NODE_REC = 34 # Bytes pro Eintrag des 3. Blocks (0x30 im Speicher)
# ---------------------------------------------------------------- MAP (tile parts only)
STRAT_REC = 90 # bytes per entry of the 2nd block (0x5c in memory)
NODE_REC = 34 # bytes per entry of the 3rd block (0x30 in memory)
def parse_map_tiles(d, wfpc, vcounts):
"""Liefert Kachel-Infos + Byte-Offsets der Abschnitte (fuer Splicing)."""
"""Returns tile info + byte offsets of the sections (for splicing)."""
N, W, H = struct.unpack_from('<3I', d, 0)
o = 12
offs = {'A': o}
@@ -299,16 +299,16 @@ def parse_map_tiles(d, wfpc, vcounts):
def build_map_tiles(d, wfpc, old_vcounts, tiles, zones='flat', anim=()):
"""Ersetzt die Kachelabschnitte im MAP-Chunk.
"""Replaces the tile sections in the MAP chunk.
tiles: Liste dict(x,y,z,rot,model,cols=[[rgba u32]*nv]) ; x/y/z in Kacheleinheiten
(Exportraum: y oben, z<=0), rot in 4096tel (Rotation um y).
Kachel-Wechsel-Animationen: tiles[i]['flag']=1 + tiles[i]['altcols'] (Farben des Alt-Modells);
anim = Liste der 5-u32-Eintraege (Zelle x, y, z, Modell, Alt-Modell), in Reihenfolge der
geflaggten Kacheln. Ohne anim werden alle Flags geloescht.
zones='flat': Sichtbarkeitszonen abschalten (Zonenraster = 0, Maske von Zone 0 = alle),
sonst ('keep') Zonenraster + 32er-Tabelle unveraendert lassen.
Alles andere (Strat-/Knoten-/Lichtbloecke) bleibt unveraendert.
tiles: list of dict(x,y,z,rot,model,cols=[[rgba u32]*nv]) ; x/y/z in tile units
(export space: y up, z<=0), rot in 1/4096 turns (rotation about y).
Tile swap animations: tiles[i]['flag']=1 + tiles[i]['altcols'] (colours of the alt model);
anim = list of the 5-u32 entries (cell x, y, z, model, alt model), in the order of the
flagged tiles. Without anim all flags are cleared.
zones='flat': disable visibility zones (zone grid = 0, mask of zone 0 = all),
otherwise ('keep') leave zone grid + 32-entry table unchanged.
Everything else (strat/node/light blocks) stays unchanged.
"""
m = parse_map_tiles(d, wfpc, old_vcounts)
of = m['offs']
@@ -320,7 +320,7 @@ def build_map_tiles(d, wfpc, old_vcounts, tiles, zones='flat', anim=()):
int(round(t['y'] * 4096)), int(round(-t['z'] * 4096))) for t in tiles)
midx = struct.pack('<%dI' % N, *[t['model'] for t in tiles])
if anim and sum(1 for t in tiles if t.get('flag')) != len(anim):
raise ValueError('Anzahl geflaggter Kacheln != Anzahl anim-Eintraege')
raise ValueError('number of flagged tiles != number of anim entries')
animb = (struct.pack('<I', len(anim)) + b''.join(struct.pack('<5I', *e) for e in anim)) if wfpc & 0x10000 else b''
cols = b''
for t in tiles:
@@ -328,13 +328,13 @@ def build_map_tiles(d, wfpc, old_vcounts, tiles, zones='flat', anim=()):
cols += struct.pack('<%dI' % len(frame), *frame)
mid = d[of['A_end']:of['B']]
if zones == 'flat':
g = of['grid'] - of['A_end'] # Offset der Zonentabelle+Raster innerhalb mid
g = of['grid'] - of['A_end'] # offset of zone table+grid within mid
tab = struct.pack('<2I', 0xffffffff, struct.unpack_from('<I', mid, g - 256 + 4)[0]) + mid[g - 248:g]
mid = mid[:g - 256] + tab + bytes(4 * m['W'] * m['H'])
return head + A + mid + B + midx + animb + cols + d[of['col_end']:]
# ---------------------------------------------------------------- TEXT (Texturtabelle + Seiten)
# ---------------------------------------------------------------- TEXT (texture table + pages)
def parse_text(d):
o = 0
@@ -362,11 +362,11 @@ def parse_text(d):
def page_rgba(p):
"""16-Bit-Seite -> numpy RGBA (Annahme: ABGR1555 wie PS1, RLE16 bei flags&0x80)."""
"""16-bit page -> numpy RGBA (assumption: ABGR1555 like PS1, RLE16 if flags&0x80)."""
import numpy as np
w, h = p['w'], p['h']
if p['flags'] == 0:
px = np.frombuffer(p['raw'], np.uint8).astype(np.uint16) * 0x421 # Graustufen-Notloesung
px = np.frombuffer(p['raw'], np.uint8).astype(np.uint16) * 0x421 # greyscale stopgap
elif p['comp']:
v = np.frombuffer(p['raw'], '<u2'); out = np.zeros(w * h, np.uint16)
i = q = 0
@@ -388,17 +388,17 @@ def page_rgba(p):
def tex_uv(t, pw=256, ph=256):
"""UV-Rechteck (U0,U1,V0,V1) wie FUN_00406880 (u1/v1 inklusiv -> +1)."""
"""UV rectangle (U0,U1,V0,V1) like FUN_00406880 (u1/v1 inclusive -> +1)."""
return (t['u0'] / pw, (t['u1'] + 1) / pw, t['v0'] / ph, (t['v1'] + 1) / ph)
def face_uvs(flags, rect):
"""Pro Face-Ecke (v0,v1,v2) die UV aus dem Texturrechteck, nach FUN_00556510.
"""Per face corner (v0,v1,v2) the UV from the texture rectangle, per FUN_00556510.
Bit 0x20 spiegelt U; Bit 0x800 + Bit 0x10 waehlen die Dreieckshaelfte des Vierecks."""
Bit 0x20 mirrors U; bit 0x800 + bit 0x10 select the triangle half of the quad."""
U0, U1, V0, V1 = rect
m = bool(flags & 0x20)
a, b = (U0, U1) if m else (U1, U0) # a = "rechts" ohne Spiegelung
a, b = (U0, U1) if m else (U1, U0) # a = "right" without mirroring
if not (flags & 0x800):
return [(a, V1), (b, V1), (a, V0)]
if not (flags & 0x10):
@@ -406,7 +406,7 @@ def face_uvs(flags, rect):
return [(b, V0), (a, V0), (a, V1)]
# ---------------------------------------------------------------- Welt zusammensetzen
# ---------------------------------------------------------------- assemble the world
def tile_xform(x, y, z, rot):
a = rot / 4096 * 2 * math.pi