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:
1 parent
86fe26d786
commit
526f6072f6
36 files changed
+1553
-1544
No files matched your search
+59
-59
@@ -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
|
||||
|
||||
Reference in new issue
Block a user