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

+185 -184
View File
@@ -1,221 +1,222 @@
# Debug-Ansicht: Kollision & unsichtbare Objekte im Spiel (`tools/debugviz.py`)
# Debug view: collision & invisible objects in game (`tools/debugviz.py`)
Stand: 2026-11-06. Zeigt in **allen 34 Level-WADs** die Kollisionsgeometrie (auch unsichtbare
Wände/Böden, Todesebenen) und unsichtbare Logik-Objekte (Kameras, Trigger, Ausgänge,
Checkpoints, Kraft-/Box-Zonen, Umgebungszonen …) direkt im Spiel – als farbiges Drahtgitter,
unsichtbare Flächen zusätzlich halbtransparent gefüllt. Spielmechanik bleibt unverändert.
As of: 2026-11-06. Shows, in **all 34 level WADs**, the collision geometry (including invisible
walls/floors, death planes) and invisible logic objects (cameras, triggers, exits,
checkpoints, force/box zones, environment zones …) directly in the game – as a coloured wireframe,
invisible surfaces additionally filled semi-transparently. Game mechanics stay unchanged.
## Benutzung
## Usage
```
lama-mod debug an # Debug-WADs zuschalten (baut das Overlay neu, aktiviert damit auch andere Mods)
lama-mod debug aus # wieder Original-Level (testlevel-Mod bleibt, wie sie war)
lama-mod debug # Status
lama-mod debug an # enable debug WADs (rebuilds the overlay, thereby also activating other mods)
lama-mod debug aus # back to the original levels (the testlevel mod stays as it was)
lama-mod debug # status
```
Wirkt beim nächsten Laden eines Levels (am besten Spiel neu starten). Nicht umschalten,
während ein Level lädt. Mod-Ordner: `~/.wine-lama/mods/debug/wads/*.wad` (gleiche Namen wie
auf der CD, z. B. `t1l1m001.wad`); die Datei `~/.wine-lama/mods/debug/.aus` = abgeschaltet.
Reihenfolge im Overlay: alphabetisch, spätere gewinnen → `testlevel` (T1L9*) bleibt immer,
Debug-WADs ersetzen nur die Original-Level. `lama-mod an|aus|status` funktionieren wie
vorher (`an` überspringt Mod-Ordner mit `.aus`). Sicherung des alten Skripts:
Takes effect the next time a level is loaded (best to restart the game). Do not toggle
while a level is loading. Mod folder: `~/.wine-lama/mods/debug/wads/*.wad` (same names as
on the CD, e.g. `t1l1m001.wad`); the file `~/.wine-lama/mods/debug/.aus` = disabled.
Order in the overlay: alphabetical, later ones win → `testlevel` (T1L9*) always stays,
debug WADs only replace the original levels. `lama-mod an|aus|status` work as
before (`an` skips mod folders containing `.aus`). Backup of the old script:
`deb-build/lama-mod.bak-vor-debug`.
Neu bauen (z. B. nach Änderungen an Farben):
Rebuild (e.g. after changing colours):
```
python3 tools/debugviz.py --all ~/.wine-lama/mods/debug/wads --report /tmp/dv.json
python3 tools/debugviz.py IN.wad OUT.wad [--no-fill] [--no-markers] [--no-coll] [--opaque]
[--width 0.007] [--offset 0.012]
```
Laufzeit ≈ 10–40 s je Level.
Run time ≈ 10–40 s per level.
## Farblegende
## Colour legend
Alle Farben sind RGB (0–255). **Linien** = Drahtgitter (deckend), **Füllung** = halbtransparent
(Alpha 96/255, Blendmodus 1 der Engine).
All colours are RGB (0–255). **Lines** = wireframe (opaque), **fill** = semi-transparent
(alpha 96/255, engine blend mode 1).
### Kollisionsflächen (Rahmen um jedes Kollisionspolygon der Welt-Kacheln)
Kantenfarbe nach Lage, solange das Material 0 (normal) ist:
### Collision surfaces (frame around every collision polygon of the world tiles)
Edge colour by orientation, as long as the material is 0 (normal):
| Farbe | RGB | Bedeutung |
| Colour | RGB | Meaning |
|---|---|---|
| **Grün** (Linien) | 40,255,40 | begehbarer Boden (Normale steil nach oben, ny ≥ 0,7) |
| **Gelbgrün** (Linien) | 200,255,0 | Schräge (0,2 ≤ ny < 0,7) |
| **Blau** (Linien) | 0,150,255 | Wand (−0,2 < ny < 0,2) |
| **Orange** (Linien) | 255,130,0 | Decke / Überhang (ny ≤ −0,2) |
| **Rot, halbtransparent gefüllt** | 255,0,0 | **unsichtbare Kollision**: keine Render-Fläche innerhalb ±0,5 Einheiten entlang der Normale (= unsichtbare Wand/Boden/Decke). Die Kantenfarbe zeigt weiterhin Lage bzw. Material. |
| **Green** (lines) | 40,255,40 | walkable floor (normal pointing steeply up, ny ≥ 0.7) |
| **Yellow-green** (lines) | 200,255,0 | slope (0.2 ≤ ny < 0.7) |
| **Blue** (lines) | 0,150,255 | wall (−0.2 < ny < 0.2) |
| **Orange** (lines) | 255,130,0 | ceiling / overhang (ny ≤ −0.2) |
| **Red, filled semi-transparently** | 255,0,0 | **invisible collision**: no render surface within ±0.5 units along the normal (= invisible wall/floor/ceiling). The edge colour still shows orientation or material. |
Kollision mit Sondermaterial (Byte 1 des Kollisionspolygons ≠ 0): Kanten in Materialfarbe,
bei unsichtbaren Polygonen auch die Füllung in Materialfarbe (statt Rot):
Collision with a special material (byte 1 of the collision polygon ≠ 0): edges in the material colour,
for invisible polygons the fill is also in the material colour (instead of red):
| Material | Farbe | RGB | Bedeutung |
| Material | Colour | RGB | Meaning |
|---|---|---|---|
| 30 (0x1e) | **Magenta**, nur Linien (keine Füllung) | 255,0,255 | **kein Boden / Fall- und Todesebene**: Spieler fällt hindurch (FUN_004036d0 setzt Boden = keiner). Meist Modell 0 bei y ≈ −2,75 in jeder Rasterzelle unter den Inseln → magentafarbenes Gitter = Absturz-/Todeszone. |
| 16 | **Violett**, immer gefüllt | 190,60,255 | Ausgangs-Boden: LevelDoor löst nur aus, wenn der Spieler auf Material 16 in der Türzelle steht |
| 17 (0x11) | **Gelb** | 255,255,0 | selektive Kollision: wirkt nur für Objekte mit Laufzeit-Flag 0x8000000 (Strat+0xe8) |
| 18 (0x12) | **Hellorange/Pfirsich** | 255,200,120 | selektive Kollision: wirkt NICHT für Objekte mit Flag 0x8000000 |
| 1 | Türkis | 0,200,200 | Sondermaterial, Bedeutung unbekannt (Strat-Skripte werten es aus) |
| 2 | Weiß | 255,255,255 | dto. |
| 3 | Rosa | 255,120,170 | dto. (häufig in Welt 5 Berg) |
| 5 | Mint | 120,255,200 | dto. (Fluss, Achterbahn) |
| 6 | Lavendel | 160,120,255 | dto. |
| 7 | Hellorange | 255,170,80 | dto. |
| 8 | Dunkelgrün | 0,130,60 | dto. |
| 9 | Hellblau | 150,200,255 | dto. |
| 10 | Oliv | 150,150,0 | dto. |
| 11 | Braun | 150,90,40 | dto. |
| 13 | Lachs | 255,130,110 | dto. |
| 15 | Hellgelb | 255,255,150 | dto. |
| 20 | Petrol | 0,120,140 | dto. |
| 26 | Dunkelviolett | 110,40,160 | dto. |
| 27 | Silber | 190,190,190 | dto. (Verfolgungsjagd t1l2m003) |
| 28 | Pink | 255,0,140 | dto. |
| 33 | Dunkelrot | 150,0,0 | dto. |
| 255 | Fast schwarz | 20,20,20 | dto. |
| sonstige | Hellgrau | 230,230,230 | (kommt in den Originalen nicht vor) |
| 30 (0x1e) | **Magenta**, lines only (no fill) | 255,0,255 | **no floor / fall and death plane**: the player falls through (FUN_004036d0 sets floor = none). Usually model 0 at y ≈ −2.75 in every grid cell below the islands → magenta grid = fall/death zone. |
| 16 | **Violet**, always filled | 190,60,255 | exit floor: LevelDoor only triggers when the player stands on material 16 in the door cell |
| 17 (0x11) | **Yellow** | 255,255,0 | selective collision: only affects objects with runtime flag 0x8000000 (strat+0xe8) |
| 18 (0x12) | **Light orange/peach** | 255,200,120 | selective collision: does NOT affect objects with flag 0x8000000 |
| 1 | Turquoise | 0,200,200 | special material, meaning unknown (evaluated by strat scripts) |
| 2 | White | 255,255,255 | ditto |
| 3 | Pink (rose) | 255,120,170 | ditto (common in world 5, mountain) |
| 5 | Mint | 120,255,200 | ditto (river, roller coaster) |
| 6 | Lavender | 160,120,255 | ditto |
| 7 | Light orange | 255,170,80 | ditto |
| 8 | Dark green | 0,130,60 | ditto |
| 9 | Light blue | 150,200,255 | ditto |
| 10 | Olive | 150,150,0 | ditto |
| 11 | Brown | 150,90,40 | ditto |
| 13 | Salmon | 255,130,110 | ditto |
| 15 | Light yellow | 255,255,150 | ditto |
| 20 | Petrol | 0,120,140 | ditto |
| 26 | Dark violet | 110,40,160 | ditto |
| 27 | Silver | 190,190,190 | ditto (chase t1l2m003) |
| 28 | Hot pink | 255,0,140 | ditto |
| 33 | Dark red | 150,0,0 | ditto |
| 255 | Almost black | 20,20,20 | ditto |
| other | Light grey | 230,230,230 | (does not occur in the originals) |
### Objekt-Marker (unsichtbare Strats, MAP-Strat-Einträge)
Markerwürfel (10 cm) auf einem dünnen Pfahl 0,35 über dem Objekt-Ursprung; kurzer waagrechter
Strich = Blickrichtung (Kameras, Checkpoints).
### Object markers (invisible strats, MAP strat entries)
Marker cube (10 cm) on a thin pole 0.35 above the object origin; short horizontal
line = view direction (cameras, checkpoints).
| Farbe | RGB | Kategorie | Typen |
| Colour | RGB | Category | Types |
|---|---|---|---|
| **Weiß**, Pfeil flach am Boden | 255,255,255 | Spielerstart (Richtung) | Llama, KuscoAndPachaTogetherOnATree (Fluss), RollerCoaster, LlamaChase |
| **Violett** + Drahtwürfel 1×1×1 um die Rasterzelle | 190,60,255 | Ausgang (Auslösezelle) | LevelDoor, VillageEndDoor, EndGameDoor, TeleportDoor |
| **Grüntürkis** | 0,255,140 | Checkpoint | CheckPointPost, CheckPointBox |
| **Cyan** | 0,255,255 | Kamera | Camera* (CameraSide, CameraLift, CameraForceInsideBox …), CutSceneCamera*, RiverWaterfallCamera |
| **Gelb** | 255,255,0 | Trigger | CutSceneTrigger*, *Trigger*, Say* (Sprüche), ChangeToBunny/Turtle, RiverStartSign |
| **Orange** | 255,90,0 | Kraft-/Sonderzonen | Force*, FallOffNoHurt, ObjectDontDropInBox, RiverCurrentBox, RiverRebound, WaterReboundPoints*, SuckingWPs, ObjectSuckPoints, RiverTrackChange, TrackSwitch, RiverWhirlpool, BackgroundRotationChanger |
| **Gold** | 255,200,0 | Geheimnis-Zone | Secret |
| **Grau** | 170,170,170 | sonstige unsichtbare Steuer-Objekte | CheatTeleport, Wompy*, Spawner, Helper, Coin-Linien/-Kreise, Rain, DrippingWater, WaterfallMist, MusicBox, FlockOfBirds, Bats, Scorpions*, AppearingPlatforms* … |
| **Dunkelgrau**, kleiner Punkt (6 cm) | 90,90,90 | alle übrigen Objekte (Ursprung) | Münzen, Gegner, Türen … – damit wirklich nichts fehlt |
| **Rosa** Drahtwürfel je Rasterzelle | 255,100,180 | Umgebungs-/Musik-/Nebelzone | MAP-Block 4 (90-Byte-Einträge, Zelle x/y/z) |
| **White**, arrow flat on the ground | 255,255,255 | player start (direction) | Llama, KuscoAndPachaTogetherOnATree (river), RollerCoaster, LlamaChase |
| **Violet** + wire cube 1×1×1 around the grid cell | 190,60,255 | exit (trigger cell) | LevelDoor, VillageEndDoor, EndGameDoor, TeleportDoor |
| **Green-turquoise** | 0,255,140 | checkpoint | CheckPointPost, CheckPointBox |
| **Cyan** | 0,255,255 | camera | Camera* (CameraSide, CameraLift, CameraForceInsideBox …), CutSceneCamera*, RiverWaterfallCamera |
| **Yellow** | 255,255,0 | trigger | CutSceneTrigger*, *Trigger*, Say* (voice lines), ChangeToBunny/Turtle, RiverStartSign |
| **Orange** | 255,90,0 | force/special zones | Force*, FallOffNoHurt, ObjectDontDropInBox, RiverCurrentBox, RiverRebound, WaterReboundPoints*, SuckingWPs, ObjectSuckPoints, RiverTrackChange, TrackSwitch, RiverWhirlpool, BackgroundRotationChanger |
| **Gold** | 255,200,0 | secret zone | Secret |
| **Grey** | 170,170,170 | other invisible control objects | CheatTeleport, Wompy*, Spawner, Helper, coin lines/circles, Rain, DrippingWater, WaterfallMist, MusicBox, FlockOfBirds, Bats, Scorpions*, AppearingPlatforms* … |
| **Dark grey**, small dot (6 cm) | 90,90,90 | all remaining objects (origin) | coins, enemies, doors … – so that really nothing is missing |
| **Pink** wire cube per grid cell | 255,100,180 | environment/music/fog zone | MAP block 4 (90-byte entries, cell x/y/z) |
**Zonen-Boxen:** hat ein Marker-Objekt genau 2 Wegpunkte und liegt das Objekt im Rechteck
dazwischen, ist das ein Box-Bereich (Secret, CameraSide/Lift, ObjectDontDropInBox,
FallOffNoHurt, RiverCurrentBox …): Drahtquader in der Kategoriefarbe von min(y) bis max(y)+1,2
(die Höhe ist geschätzt – der Code prüft vermutlich nur x/z), bei Flächen ≤ 64 Einheiten² mit
halbtransparentem Boden (Alpha 70).
**Wegpunkt-Ketten** anderer Marker-Objekte: kleine Würfel (5 cm) in abgedunkelter
Kategoriefarbe (60 %), bei Kraftzonen mit Verbindungslinien (z. B. WaterReboundPoints-Pfade,
CutSceneTrigger-Kamerafahrt nur als Punkte).
**Zone boxes:** if a marker object has exactly 2 waypoints and the object lies in the rectangle
between them, it is a box area (Secret, CameraSide/Lift, ObjectDontDropInBox,
FallOffNoHurt, RiverCurrentBox …): wire cuboid in the category colour from min(y) to max(y)+1.2
(the height is estimated – the code probably only checks x/z), for areas ≤ 64 units² with a
semi-transparent floor (alpha 70).
**Waypoint chains** of other marker objects: small cubes (5 cm) in a darkened
category colour (60 %), for force zones with connecting lines (e.g. WaterReboundPoints paths,
CutSceneTrigger camera moves only as points).
## Wie es gebaut ist (Entscheidungen)
## How it is built (decisions)
* **Kollisions-Overlay an die TRAK-Modelle angehängt** (nicht als eigene Kacheln): pro
Kollisionspolygon ein Rahmen aus 2·n Vertices (äußere Ecken + um die Linienbreite nach innen
versetzte Ecken, aus den Kantenebenen berechnet), 2·n Dreiecke; Füllung nutzt die inneren
Ecken mit. Alles 0,012 entlang der Flächennormale versetzt (kein Z-Fighting). Dadurch stimmen
Kachel-Transformation, Zonen-/Sichtbarkeit und Kachel-Wechsel (Alt-Modelle, WFPC 0x10000)
automatisch. Kollision wird aus dem Spielraum zurückgerechnet (geomlib.coll_decode/
coll_polygon, z gespiegelt).
* Face-Flags der Overlay-Dreiecke: 0x800 | 0x400 (doppelseitig) | 0x4000 (Vertexfarben
ignorieren) | 0x8 bei jedem Texturwechsel und spätestens alle 250 Faces (Render-Batch).
* **Farben** = angehängte Flachfarb-Texturen (TEXT-Tabelle, Seite −1): deckend Flags 0x105,
halbtransparent **0x1105** (Bits 12–14 = 1 → Tabelle 0x573eb8: Blendmodus 1 =
SRCALPHA/INVSRCALPHA, Alpha aus dem 4. Byte (v1) des Eintrags; im Spiel geprüft).
Halbtransparente Dreiecke laufen über den Sortierpuffer (FUN_0041ed90, 512 KB/Frame ≈ 5000
Dreiecke; ist er voll, fallen sie einfach weg – kein Absturz). Deshalb gibt es Füllungen nur
für unsichtbare Kollision, Material 16 und kleine Zonenböden.
* **Vertexfarben-Block** in MAP: je Kachel (und je Farbsatz/Frame, auch für Alt-Modelle) um
nV(Overlay) Einträge 0x00808080 verlängert (Byte 3 = 0, ändert die Frame-Anzahl nicht).
* **Unsichtbar-Erkennung**: Render-Dreiecke aller Kacheln in Weltkoordinaten, nach Zellen
gebuckelt; für Mittelpunkt + 4 Punkte (70 % Richtung Ecken) jedes Kollisionspolygons wird ein
Strahl entlang der Normale (±0,5) gegen Dreiecke mit ähnlicher Ausrichtung (|cos| > 0,4)
getestet; < 60 % Treffer = unsichtbar. Je Modell Mehrheitsentscheid über bis zu 6 Instanzen.
Strat-Modelle (STPC: Türen, Plattformen, Figuren) zählen NICHT als Render-Fläche – Kollision
hinter beweglichen Objekten kann daher rot erscheinen.
(Erste Version mit 0,06/0,15 Abstand markierte ganze Hauswände und Felswände rot, weil die
Kollisionshülle 0,2–0,4 vor der sichtbaren Wand liegt – zu viel Rauschen.)
* **Marker** als neue Kacheln ohne Kollision, je Rasterzelle ein Modell (Linien in ≤ 1-Stücke
zerteilt und in die Zelle ihres Mittelpunkts gelegt) → bekommen automatisch die Zone der
Zelle. Neue Kacheln werden hinten angehängt (Indizes der Original-Kacheln bleiben).
* **Spielmechanik unverändert**: keine Kollisionspolygone hinzugefügt/geändert, Strats,
Wegpunkte, Parameterpool, Zonen und Rest von MAP byte-gleich (wird nach dem Schreiben
geprüft: Strats/Knoten/Rest/Original-Kacheln identisch).
* Größe: WADs wachsen um 0,1–4 MB (max. 8,9 MB, t1l6m003). Level-Speicher (Arena 16 MB,
FUN_0041ef00) hält TRAK+STPC+MAP – reicht für alle (Ladetest).
* **Collision overlay appended to the TRAK models** (not as separate tiles): per
collision polygon a frame of 2·n vertices (outer corners + corners offset inwards by the line
width, computed from the edge planes), 2·n triangles; the fill reuses the inner
corners. Everything offset by 0.012 along the surface normal (no z-fighting). This way
tile transform, zones/visibility and tile swaps (alt models, WFPC 0x10000) are
automatically correct. Collision is converted back from game space (geomlib.coll_decode/
coll_polygon, z mirrored).
* Face flags of the overlay triangles: 0x800 | 0x400 (double-sided) | 0x4000 (ignore vertex
colours) | 0x8 at every texture change and at the latest every 250 faces (render batch).
* **Colours** = appended flat-colour textures (TEXT table, page −1): opaque flags 0x105,
semi-transparent **0x1105** (bits 12–14 = 1 → table 0x573eb8: blend mode 1 =
SRCALPHA/INVSRCALPHA, alpha from the 4th byte (v1) of the entry; checked in game).
Semi-transparent triangles go through the sort buffer (FUN_0041ed90, 512 KB/frame ≈ 5000
triangles; when it is full they are simply dropped – no crash). That is why fills exist only
for invisible collision, material 16 and small zone floors.
* **Vertex colour block** in MAP: extended per tile (and per colour set/frame, also for alt models) by
nV(overlay) entries 0x00808080 (byte 3 = 0, does not change the frame count).
* **Invisibility detection**: render triangles of all tiles in world coordinates, bucketed by
cell; for the centre + 4 points (70 % towards the corners) of every collision polygon a
ray along the normal (±0.5) is tested against triangles with similar orientation (|cos| > 0.4);
< 60 % hits = invisible. Majority vote per model over up to 6 instances.
Strat models (STPC: doors, platforms, characters) do NOT count as render surfaces – collision
behind moving objects may therefore appear red.
(The first version with 0.06/0.15 distance marked entire house walls and cliff walls red, because the
collision hull lies 0.2–0.4 in front of the visible wall – too much noise.)
* **Markers** as new tiles without collision, one model per grid cell (lines split into pieces ≤ 1
and placed in the cell of their midpoint) → automatically get the zone of the
cell. New tiles are appended at the end (indices of the original tiles stay).
* **Game mechanics unchanged**: no collision polygons added/changed, strats,
waypoints, parameter pool, zones and the rest of MAP byte-identical (checked after writing:
strats/nodes/rest/original tiles identical).
* Size: WADs grow by 0.1–4 MB (max. 8.9 MB, t1l6m003). Level memory (16 MB arena,
FUN_0041ef00) holds TRAK+STPC+MAP – enough for all (load test).
## Grenzen / bekannt
* Kollision der **Objekt-Modelle** (STPC: bewegliche Plattformen, Türen, Kisten …) wird nicht
gezeigt – die bewegen sich; nur ihr Ursprung als dunkelgrauer Punkt.
* Zonen-Höhen der Box-Bereiche sind geschätzt; ob die Engine y prüft, ist offen.
* Bedeutung der meisten Sondermaterialien unbekannt (nur Farbe + Anzahl, s. Tabelle unten);
Material 30 = kein Boden, 16 = Ausgang, 17/18 = selektiv sicher aus dem Code.
* Umgebungszonen (MAP-Block 4): Zelle sicher, Bedeutung (Nebel/Musik/Kamera) nur mittel sicher;
Vorgabe-Eintrag (Position 1,1,1) wird nicht gezeichnet.
* Liegt die Kamera dicht an einem Marker/einer Linie, zerschneidet der Near-Plane-Clip der
Engine die Dreiecke manchmal zu großen Fetzen (nur kurz sichtbar).
* Leistung: unter Wine/llvmpipe (Xvfb) ohne spürbaren Einbruch; auf echter GPU unkritisch
(≤ 2000 zusätzliche Vertices je Kachelmodell, Ø ~1–2× so viele Dreiecke wie das Original).
* Testlevel T1L9 (Testwelt) hat keine Debug-Version (testlevel-Mod hat Vorrang).
## Limits / known issues
* Collision of the **object models** (STPC: moving platforms, doors, crates …) is not
shown – they move; only their origin as a dark grey dot.
* Zone heights of the box areas are estimated; whether the engine checks y is open.
* Meaning of most special materials unknown (only colour + count, see table below);
material 30 = no floor, 16 = exit, 17/18 = selective are certain from the code.
* Environment zones (MAP block 4): cell certain, meaning (fog/music/camera) only moderately certain;
the default entry (position 1,1,1) is not drawn.
* If the camera is close to a marker/line, the engine's near-plane clip sometimes cuts the
triangles into large shreds (only briefly visible).
* Performance: no noticeable drop under Wine/llvmpipe (Xvfb); uncritical on a real GPU
(≤ 2000 extra vertices per tile model, on average ~1–2× as many triangles as the original).
* Test level T1L9 (test world) has no debug version (the testlevel mod takes precedence).
## Statistik je Level (Stand Build 2026-11-06)
## Statistics per level (build of 2026-11-06)
| Level | Welt | WAD alt→neu (KB) | Koll.-Polygone | davon unsichtbar | Sondermaterialien (Anzahl Polygone) | Marker |
Marker categories are the internal keys of `debugviz.py`: ausgang = exit, geheimnis = secret,
kamera = camera, kraft = force zone, objekt = other object, sonst = other control object,
umgebung = environment zone.
| Level | World | WAD old→new (KB) | Coll. polygons | of which invisible | Special materials (polygon count) | Markers |
|---|---|---|---|---|---|---|
| t1l1m001 | Dorf | 6158→8235 | 3474 | 215 | 2:138, 6:4, 8:35, 9:13, 16:8, 17:5, 18:84, 28:19, 30:1 | ausgang 1, checkpoint 6, geheimnis 2, kamera 6, kraft 6, objekt 95, sonst 4, start 1, trigger 1, umgebung 12 |
| t1l1m002 | Dorf | 5471→7573 | 3909 | 130 | 6:4, 8:14, 16:9, 18:65, 28:23, 30:1 | ausgang 1, checkpoint 5, geheimnis 1, kamera 8, kraft 5, objekt 89, sonst 9, start 1, trigger 1, umgebung 15 |
| t1l1m003 | Dorf | 5891→8348 | 3893 | 79 | 2:15, 6:4, 8:8, 16:5, 17:3, 18:458, 20:6, 28:23, 30:1 | ausgang 1, kamera 3, kraft 6, objekt 45, sonst 3, start 1, trigger 2, umgebung 4 |
| t1l1m004 | Dorf | 5866→7006 | 2169 | 60 | 2:15, 8:8, 16:16, 17:3, 18:90, 30:1 | ausgang 2, geheimnis 1, kamera 2, kraft 1, objekt 31, sonst 2, start 1, trigger 1, umgebung 5 |
| t1l2m001 | Dschungelnacht | 5094→7491 | 3933 | 460 | 1:26, 2:111, 6:156, 9:34, 16:22, 17:24, 18:284, 20:80, 28:28, 30:138, 33:1 | ausgang 1, checkpoint 2, geheimnis 1, kamera 3, kraft 6, objekt 39, sonst 8, start 1, trigger 1, umgebung 8 |
| t1l2m002 | Dschungelnacht | 5329→7561 | 3715 | 389 | 1:216, 2:57, 6:154, 9:24, 16:34, 17:23, 18:319, 28:28, 30:50 | ausgang 1, checkpoint 4, geheimnis 1, kamera 3, kraft 7, objekt 105, sonst 2, start 1, trigger 1, umgebung 18 |
| t1l2m003 | Dschungelnacht (Verfolgung) | 2837→5142 | 4844 | 630 | 5:3, 9:6, 10:6, 11:37, 16:26, 27:206, 30:46 | kamera 2, kraft 1, objekt 33, sonst 4, start 1, trigger 1 |
| t1l3m001 | Fluss | 4099→8220 | 8371 | 1000 | 5:26, 16:8, 18:23 | ausgang 2, geheimnis 1, kamera 13, kraft 4, objekt 140, sonst 13, start 1, trigger 5 |
| t1l3m002 | Fluss | 3799→8249 | 8824 | 1097 | 5:40, 16:5, 18:23, 30:1 | ausgang 2, geheimnis 1, kamera 19, kraft 44, objekt 115, sonst 39, start 1, trigger 6 |
| t1l3m003 | Fluss | 4497→8537 | 8387 | 868 | 5:56, 16:5 | ausgang 4, geheimnis 1, kamera 15, kraft 7, objekt 52, sonst 15, start 1, trigger 3 |
| t1l3m004 | Fluss | 2674→3754 | 2201 | 228 | 5:18, 16:5 | ausgang 2, kamera 5, kraft 4, objekt 31, sonst 7, start 1, trigger 2 |
| t1l4m001 | Dschungeltag | 5469→7380 | 3214 | 230 | 1:148, 6:105, 10:33, 16:18, 17:33, 18:158, 20:25, 28:24, 30:10 | ausgang 1, checkpoint 9, geheimnis 1, kamera 5, kraft 12, objekt 120, sonst 3, start 1, trigger 1, umgebung 10 |
| t1l4m002 | Dschungeltag | 5807→8045 | 3820 | 157 | 1:186, 6:123, 10:80, 16:33, 17:55, 18:309, 20:6, 28:98, 30:22 | ausgang 1, checkpoint 7, geheimnis 2, kamera 1, kraft 14, objekt 135, sonst 3, start 1, trigger 1, umgebung 14 |
| t1l4m003 | Dschungeltag | 6320→8886 | 4805 | 135 | 1:174, 2:52, 6:10, 10:33, 16:36, 17:110, 18:890, 20:186, 28:27, 30:33, 255:8 | ausgang 1, checkpoint 3, geheimnis 2, kamera 6, kraft 4, objekt 99, sonst 3, start 1, trigger 1, umgebung 16 |
| t1l5m001 | Berg | 6090→8613 | 4419 | 188 | 3:356, 8:96, 16:24, 17:35, 18:539, 20:66, 28:6, 30:87 | ausgang 1, checkpoint 3, geheimnis 2, kamera 5, kraft 2, objekt 83, sonst 7, start 1, trigger 1, umgebung 15 |
| t1l5m002 | Berg | 5949→8558 | 5242 | 220 | 2:20, 3:396, 8:23, 9:15, 16:19, 17:33, 18:342, 20:57, 30:120 | ausgang 1, checkpoint 3, geheimnis 4, kamera 10, kraft 3, objekt 67, sonst 6, start 1, trigger 1, umgebung 7 |
| t1l5m003 | Berg | 4989→6796 | 3399 | 98 | 3:232, 8:223, 16:17, 18:20, 20:6, 30:78 | ausgang 1, checkpoint 5, geheimnis 1, kamera 4, kraft 1, objekt 71, sonst 6, start 1, trigger 1, umgebung 2 |
| t1l6m001 | Stadt | 6210→8227 | 2857 | 169 | 6:29, 16:18, 17:140, 18:110, 30:38 | ausgang 1, checkpoint 4, geheimnis 1, kamera 5, kraft 3, objekt 83, sonst 3, start 1, trigger 5, umgebung 20 |
| t1l6m002 | Stadt | 5155→7060 | 1771 | 106 | 6:56, 16:9, 17:20, 18:39, 30:17 | ausgang 1, checkpoint 10, geheimnis 1, kamera 7, kraft 9, objekt 127, sonst 16, start 1, trigger 5, umgebung 46 |
| t1l6m003 | Stadt | 6693→8924 | 2462 | 148 | 1:39, 16:13, 17:80, 18:154, 28:3, 30:10 | ausgang 1, checkpoint 11, geheimnis 6, kamera 11, kraft 6, objekt 107, sonst 15, start 1, trigger 3, umgebung 59 |
| t1l7m001 | Katakomben (Achterbahn) | 3054→6705 | 4043 | 20 | 5:54, 11:18, 13:18, 16:9, 30:18 | kamera 1, objekt 2, sonst 36, start 1, trigger 1, umgebung 14 |
| t1l7m002 | Katakomben | 5280→7722 | 1795 | 111 | 2:1, 11:3, 16:44, 30:93 | ausgang 3, checkpoint 12, geheimnis 5, kamera 6, kraft 4, objekt 186, sonst 12, start 1, trigger 1, umgebung 23 |
| t1l7m003 | Katakomben (Achterbahn) | 2496→5140 | 1999 | 11 | 5:54, 11:90, 13:36, 15:18, 16:9, 30:9 | kamera 1, objekt 11, sonst 41, start 1, trigger 1 |
| t1l7m004 | Katakomben | 5299→6978 | 1892 | 61 | 2:7, 16:32, 30:70 | ausgang 1, checkpoint 3, geheimnis 3, kamera 4, kraft 7, objekt 93, sonst 10, start 1, trigger 2, umgebung 15 |
| t1l7m005 | Katakomben (Achterbahn) | 2390→4782 | 1885 | 9 | 5:54, 11:36, 13:36, 16:9, 30:9 | kamera 1, objekt 1, sonst 51, start 1, trigger 1, umgebung 9 |
| t1l8m001 | Labor | 6632→8824 | 3396 | 185 | 1:40, 8:9, 16:16, 17:8, 18:181, 30:56 | ausgang 1, checkpoint 2, geheimnis 2, kamera 8, kraft 2, objekt 99, sonst 3, start 1, trigger 2, umgebung 19 |
| t1l8m002 | Labor | 5427→7112 | 2204 | 76 | 16:1, 17:44, 18:496, 30:42 | ausgang 1, geheimnis 1, kamera 5, kraft 2, objekt 74, sonst 2, start 1, trigger 3, umgebung 1 |
| t1l8m003 | Labor | 4656→5546 | 1398 | 34 | 16:9, 17:10, 18:196, 30:21 | ausgang 1, checkpoint 2, geheimnis 1, kamera 2, kraft 2, objekt 36, sonst 2, start 1, trigger 3, umgebung 6 |
| t1l8m004 | Labor | 5817→8407 | 2993 | 124 | 9:10, 16:66, 17:1, 30:57 | ausgang 1, checkpoint 7, geheimnis 3, kamera 12, kraft 6, objekt 164, sonst 3, start 1, trigger 7, umgebung 46 |
| t1l8m005 | Labor | 5375→8660 | 4041 | 179 | 7:10, 16:78, 17:1, 30:82 | checkpoint 7, geheimnis 1, kamera 14, kraft 5, objekt 113, sonst 15, start 1, trigger 2, umgebung 47 |
| t1l8m006 | Labor | 5423→5948 | 738 | 44 | 2:13, 8:24, 16:3, 17:5, 18:27, 26:4 | ausgang 1, objekt 15, start 1, trigger 17 |
| t6l6m001 | Sonder (Optionen/Karte) | 1331→1482 | 333 | 0 | | kamera 1, objekt 3 |
| t6l6m002 | Sonder (Optionen/Karte) | 3605→3640 | 64 | 0 | | kamera 1, objekt 2 |
| t1l1m001 | Village | 6158→8235 | 3474 | 215 | 2:138, 6:4, 8:35, 9:13, 16:8, 17:5, 18:84, 28:19, 30:1 | ausgang 1, checkpoint 6, geheimnis 2, kamera 6, kraft 6, objekt 95, sonst 4, start 1, trigger 1, umgebung 12 |
| t1l1m002 | Village | 5471→7573 | 3909 | 130 | 6:4, 8:14, 16:9, 18:65, 28:23, 30:1 | ausgang 1, checkpoint 5, geheimnis 1, kamera 8, kraft 5, objekt 89, sonst 9, start 1, trigger 1, umgebung 15 |
| t1l1m003 | Village | 5891→8348 | 3893 | 79 | 2:15, 6:4, 8:8, 16:5, 17:3, 18:458, 20:6, 28:23, 30:1 | ausgang 1, kamera 3, kraft 6, objekt 45, sonst 3, start 1, trigger 2, umgebung 4 |
| t1l1m004 | Village | 5866→7006 | 2169 | 60 | 2:15, 8:8, 16:16, 17:3, 18:90, 30:1 | ausgang 2, geheimnis 1, kamera 2, kraft 1, objekt 31, sonst 2, start 1, trigger 1, umgebung 5 |
| t1l2m001 | Jungle night | 5094→7491 | 3933 | 460 | 1:26, 2:111, 6:156, 9:34, 16:22, 17:24, 18:284, 20:80, 28:28, 30:138, 33:1 | ausgang 1, checkpoint 2, geheimnis 1, kamera 3, kraft 6, objekt 39, sonst 8, start 1, trigger 1, umgebung 8 |
| t1l2m002 | Jungle night | 5329→7561 | 3715 | 389 | 1:216, 2:57, 6:154, 9:24, 16:34, 17:23, 18:319, 28:28, 30:50 | ausgang 1, checkpoint 4, geheimnis 1, kamera 3, kraft 7, objekt 105, sonst 2, start 1, trigger 1, umgebung 18 |
| t1l2m003 | Jungle night (chase) | 2837→5142 | 4844 | 630 | 5:3, 9:6, 10:6, 11:37, 16:26, 27:206, 30:46 | kamera 2, kraft 1, objekt 33, sonst 4, start 1, trigger 1 |
| t1l3m001 | River | 4099→8220 | 8371 | 1000 | 5:26, 16:8, 18:23 | ausgang 2, geheimnis 1, kamera 13, kraft 4, objekt 140, sonst 13, start 1, trigger 5 |
| t1l3m002 | River | 3799→8249 | 8824 | 1097 | 5:40, 16:5, 18:23, 30:1 | ausgang 2, geheimnis 1, kamera 19, kraft 44, objekt 115, sonst 39, start 1, trigger 6 |
| t1l3m003 | River | 4497→8537 | 8387 | 868 | 5:56, 16:5 | ausgang 4, geheimnis 1, kamera 15, kraft 7, objekt 52, sonst 15, start 1, trigger 3 |
| t1l3m004 | River | 2674→3754 | 2201 | 228 | 5:18, 16:5 | ausgang 2, kamera 5, kraft 4, objekt 31, sonst 7, start 1, trigger 2 |
| t1l4m001 | Jungle day | 5469→7380 | 3214 | 230 | 1:148, 6:105, 10:33, 16:18, 17:33, 18:158, 20:25, 28:24, 30:10 | ausgang 1, checkpoint 9, geheimnis 1, kamera 5, kraft 12, objekt 120, sonst 3, start 1, trigger 1, umgebung 10 |
| t1l4m002 | Jungle day | 5807→8045 | 3820 | 157 | 1:186, 6:123, 10:80, 16:33, 17:55, 18:309, 20:6, 28:98, 30:22 | ausgang 1, checkpoint 7, geheimnis 2, kamera 1, kraft 14, objekt 135, sonst 3, start 1, trigger 1, umgebung 14 |
| t1l4m003 | Jungle day | 6320→8886 | 4805 | 135 | 1:174, 2:52, 6:10, 10:33, 16:36, 17:110, 18:890, 20:186, 28:27, 30:33, 255:8 | ausgang 1, checkpoint 3, geheimnis 2, kamera 6, kraft 4, objekt 99, sonst 3, start 1, trigger 1, umgebung 16 |
| t1l5m001 | Mountain | 6090→8613 | 4419 | 188 | 3:356, 8:96, 16:24, 17:35, 18:539, 20:66, 28:6, 30:87 | ausgang 1, checkpoint 3, geheimnis 2, kamera 5, kraft 2, objekt 83, sonst 7, start 1, trigger 1, umgebung 15 |
| t1l5m002 | Mountain | 5949→8558 | 5242 | 220 | 2:20, 3:396, 8:23, 9:15, 16:19, 17:33, 18:342, 20:57, 30:120 | ausgang 1, checkpoint 3, geheimnis 4, kamera 10, kraft 3, objekt 67, sonst 6, start 1, trigger 1, umgebung 7 |
| t1l5m003 | Mountain | 4989→6796 | 3399 | 98 | 3:232, 8:223, 16:17, 18:20, 20:6, 30:78 | ausgang 1, checkpoint 5, geheimnis 1, kamera 4, kraft 1, objekt 71, sonst 6, start 1, trigger 1, umgebung 2 |
| t1l6m001 | City | 6210→8227 | 2857 | 169 | 6:29, 16:18, 17:140, 18:110, 30:38 | ausgang 1, checkpoint 4, geheimnis 1, kamera 5, kraft 3, objekt 83, sonst 3, start 1, trigger 5, umgebung 20 |
| t1l6m002 | City | 5155→7060 | 1771 | 106 | 6:56, 16:9, 17:20, 18:39, 30:17 | ausgang 1, checkpoint 10, geheimnis 1, kamera 7, kraft 9, objekt 127, sonst 16, start 1, trigger 5, umgebung 46 |
| t1l6m003 | City | 6693→8924 | 2462 | 148 | 1:39, 16:13, 17:80, 18:154, 28:3, 30:10 | ausgang 1, checkpoint 11, geheimnis 6, kamera 11, kraft 6, objekt 107, sonst 15, start 1, trigger 3, umgebung 59 |
| t1l7m001 | Catacombs (roller coaster) | 3054→6705 | 4043 | 20 | 5:54, 11:18, 13:18, 16:9, 30:18 | kamera 1, objekt 2, sonst 36, start 1, trigger 1, umgebung 14 |
| t1l7m002 | Catacombs | 5280→7722 | 1795 | 111 | 2:1, 11:3, 16:44, 30:93 | ausgang 3, checkpoint 12, geheimnis 5, kamera 6, kraft 4, objekt 186, sonst 12, start 1, trigger 1, umgebung 23 |
| t1l7m003 | Catacombs (roller coaster) | 2496→5140 | 1999 | 11 | 5:54, 11:90, 13:36, 15:18, 16:9, 30:9 | kamera 1, objekt 11, sonst 41, start 1, trigger 1 |
| t1l7m004 | Catacombs | 5299→6978 | 1892 | 61 | 2:7, 16:32, 30:70 | ausgang 1, checkpoint 3, geheimnis 3, kamera 4, kraft 7, objekt 93, sonst 10, start 1, trigger 2, umgebung 15 |
| t1l7m005 | Catacombs (roller coaster) | 2390→4782 | 1885 | 9 | 5:54, 11:36, 13:36, 16:9, 30:9 | kamera 1, objekt 1, sonst 51, start 1, trigger 1, umgebung 9 |
| t1l8m001 | Lab | 6632→8824 | 3396 | 185 | 1:40, 8:9, 16:16, 17:8, 18:181, 30:56 | ausgang 1, checkpoint 2, geheimnis 2, kamera 8, kraft 2, objekt 99, sonst 3, start 1, trigger 2, umgebung 19 |
| t1l8m002 | Lab | 5427→7112 | 2204 | 76 | 16:1, 17:44, 18:496, 30:42 | ausgang 1, geheimnis 1, kamera 5, kraft 2, objekt 74, sonst 2, start 1, trigger 3, umgebung 1 |
| t1l8m003 | Lab | 4656→5546 | 1398 | 34 | 16:9, 17:10, 18:196, 30:21 | ausgang 1, checkpoint 2, geheimnis 1, kamera 2, kraft 2, objekt 36, sonst 2, start 1, trigger 3, umgebung 6 |
| t1l8m004 | Lab | 5817→8407 | 2993 | 124 | 9:10, 16:66, 17:1, 30:57 | ausgang 1, checkpoint 7, geheimnis 3, kamera 12, kraft 6, objekt 164, sonst 3, start 1, trigger 7, umgebung 46 |
| t1l8m005 | Lab | 5375→8660 | 4041 | 179 | 7:10, 16:78, 17:1, 30:82 | checkpoint 7, geheimnis 1, kamera 14, kraft 5, objekt 113, sonst 15, start 1, trigger 2, umgebung 47 |
| t1l8m006 | Lab | 5423→5948 | 738 | 44 | 2:13, 8:24, 16:3, 17:5, 18:27, 26:4 | ausgang 1, objekt 15, start 1, trigger 17 |
| t6l6m001 | Special (options/map) | 1331→1482 | 333 | 0 | | kamera 1, objekt 3 |
| t6l6m002 | Special (options/map) | 3605→3640 | 64 | 0 | | kamera 1, objekt 2 |
(Kollisionspolygone je TRAK-Modell gezählt, nicht je Kachel-Instanz.)
(Collision polygons counted per TRAK model, not per tile instance.)
## Tests (2026-11-06)
**Offline (alle 33 Debug-WADs, `$JOB/tmp/dv/offcheck.py`):** TRAK parst und baut byte-gleich
zurück; ≤ 8000 Vertices je Modell (max. 2012, t1l4m003); Render-Batches ≤ 256 Faces; alle
Texturindizes < Tabellengröße; Vertexfarben-Block je Kachel = nV(Modell) (alle Frames);
MAP mit `mapchunk` vollständig geparst (Rest 0 Byte) und byte-gleich neu gebaut; Strats,
Wegpunkte, Parameterpool, Umgebungszonen, Zonentabelle/-raster, Alt-Block und Original-Kacheln
unverändert; Original-Modelle (Vertices, Faces, **Kollision**) byte-genau unverändert; neue
Marker-Modelle ohne Kollision; alle anderen Chunks (STPC, Sound, LGPC …) identisch;
Kacheln im Raster. TRAK+STPC+MAP+LGPC ≤ 7,6 MB (Level-Arena 16 MB).
**Im Spiel geladen** (lamatest, über Slot Welt 9 als T1L9M001.wad, ~40–90 s gespielt/gedreht,
kein Absturz, kein Hänger, keine Fehler im Wine-Log): t1l1m001 (mehrfach, Sichtprüfung),
t1l1m003, t1l1m004, t1l2m001, t1l2m002, t1l3m001–004 (Fluss läuft auch im Slot 9 normal:
Boot fährt), t1l4m001–003, t1l5m001–003, t1l6m001–003 = 19 Level.
Getestet wurde der Build direkt vor dem endgültigen: identische Geometrie, nur die Farbwerte
der Sondermaterialien wurden danach auf feste Farben je Material umgestellt (+8–16 Byte
Texturtabelle). Die Vorschaubilder stammen ebenfalls aus diesem Build – Sondermaterial-Farben
darin (z. B. Türkis-Füllung im Berg-Bild) entsprechen noch nicht ganz der Legende oben.
**Nur offline geprüft:** t1l1m002 und t1l2m003 (Ladeversuch scheiterte vorher an der
Menü-Erkennung von lamatest, nicht am Level), t1l7m001–005 (Katakomben/Achterbahn),
t1l8m001–006 (Labor), t6l6m001/002. Ein Test über den echten Spielstand-Slot wurde nicht gemacht.
lamatest-Änderung nebenbei: Spielstand-Auswahl mit mehr Versuchen und 0,3 s langen
Tastendrücken (unter CPU-Last gingen kurze Drücke verloren).
Bilder: `notes/img/debug/00_montage.jpg` (6 Welten), `01_…06_*.jpg` (1280×720).
**Offline (all 33 debug WADs, `$JOB/tmp/dv/offcheck.py`):** TRAK parses and rebuilds
byte-identically; ≤ 8000 vertices per model (max. 2012, t1l4m003); render batches ≤ 256 faces; all
texture indices < table size; vertex colour block per tile = nV(model) (all frames);
MAP fully parsed with `mapchunk` (0 bytes left over) and rebuilt byte-identically; strats,
waypoints, parameter pool, environment zones, zone table/grid, alt block and original tiles
unchanged; original models (vertices, faces, **collision**) unchanged byte for byte; new
marker models without collision; all other chunks (STPC, sound, LGPC …) identical;
tiles within the grid. TRAK+STPC+MAP+LGPC ≤ 7.6 MB (16 MB level arena).
**Loaded in game** (lamatest, via slot world 9 as T1L9M001.wad, ~40–90 s played/rotated,
no crash, no hang, no errors in the Wine log): t1l1m001 (several times, visual check),
t1l1m003, t1l1m004, t1l2m001, t1l2m002, t1l3m001–004 (the river also works normally in slot 9:
the boat moves), t1l4m001–003, t1l5m001–003, t1l6m001–003 = 19 levels.
The build tested was the one right before the final one: identical geometry, only the colour values
of the special materials were switched afterwards to fixed colours per material (+8–16 bytes
of texture table). The preview images also come from that build – special material colours
in them (e.g. turquoise fill in the mountain image) do not yet fully match the legend above.
**Only checked offline:** t1l1m002 and t1l2m003 (the load attempt previously failed at lamatest's
menu detection, not at the level), t1l7m001–005 (catacombs/roller coaster),
t1l8m001–006 (lab), t6l6m001/002. A test via the real save game slot was not done.
lamatest change on the side: save game selection with more attempts and 0.3 s long
key presses (under CPU load short presses got lost).
Images: `notes/img/debug/00_montage.jpg` (6 worlds), `01_…06_*.jpg` (1280×720).
+168 -168
View File
@@ -1,203 +1,203 @@
# Level-Geometrie (TRAK / MAP / STPC / TEXT) – Ein Königreich für ein Lama (PC)
# Level geometry (TRAK / MAP / STPC / TEXT) – Ein Königreich für ein Lama (PC)
Stand: 2026-11-06. Quelle: groove.exe (Ghidra-Dekompilat `re/groove_decomp.c`, Capstone) +
Datenanalyse aller 34 Level-WADs. Tools: `tools/geomlib.py`, `tools/geom_export.py`,
As of: 2026-11-06. Source: groove.exe (Ghidra decompilation `re/groove_decomp.c`, Capstone) +
data analysis of all 34 level WADs. Tools: `tools/geomlib.py`, `tools/geom_export.py`,
`tools/geom_import.py`, `tools/geom_demo.py`, `tools/disx86.py`.
Konfidenz: **[sicher]** = aus Code + Daten verifiziert (Round-Trip byte-identisch / Rendering),
**[wahrsch.]** = aus Daten plausibel, **[?]** = unbekannt.
Confidence: **[certain]** = verified from code + data (byte-identical round trip / rendering),
**[likely]** = plausible from data, **[?]** = unknown.
## 1. Überblick: welcher Chunk ist was
## 1. Overview: which chunk is what
Chunk-Dispatcher: `FUN_00558xxx` bei VA 0x55899e (Vergleich auf Tag-Konstanten, Tag als u32 LE
gelesen, z. B. 'CPTS' im File = 0x53545043).
Chunk dispatcher: `FUN_00558xxx` at VA 0x55899e (compares against tag constants, tag read as u32 LE,
e.g. 'CPTS' in the file = 0x53545043).
| Chunk | Loader (VA) | Inhalt |
| Chunk | Loader (VA) | Contents |
|---|---|---|
| WFPC | inline → `DAT_006da330` | u32 Feature-Flags des Levels (steuern MAP/STPC-Aufbau) **[sicher]** |
| TEXT | 0x4067b0 / 0x406d30 | Texturseiten (256x256, 16 Bit) + Texturtabelle (UV-Rechtecke) **[sicher]** |
| SMPC / AMPC / SRPC | 0x558d30 / 0x558d70 / 0x545350 | Sound (SMPC: 'cvg ' VAG-Samples; AMPC: 'ambi'/'VABp'; SRPC Sound-Referenzen) – **keine Geometrie** |
| **TRAK** | 0x42aac0 → 0x5563f0 | **Welt-Geometrie**: Kachel-Modelle (Render-Dreiecke + Kollisionspolygone) → globales Array `DAT_005846ec` **[sicher]** |
| **STPC** | 0x42ab50 | Objekt-("Strat")-Modelle (gleiches Modellformat), Skelett-Animationen, danach Strat-Skriptdaten (Namen wie "SinkingStatuePad") **[sicher für Modelle]** |
| **MAP** | 0x42ac50 | Kachel-Raster: Platzierung der TRAK-Modelle + Vertexfarben + Zonen-PVS (+ Strats/Pfade/etc., siehe objects.md) **[sicher]** |
| LGHT / LNFO / LGPC | 0x42c180 / 0x42ab10 / 0x558db0 | Lichter / Level-Info / ? – nicht Geometrie |
| WFPC | inline → `DAT_006da330` | u32 feature flags of the level (control MAP/STPC layout) **[certain]** |
| TEXT | 0x4067b0 / 0x406d30 | texture pages (256x256, 16 bit) + texture table (UV rectangles) **[certain]** |
| SMPC / AMPC / SRPC | 0x558d30 / 0x558d70 / 0x545350 | sound (SMPC: 'cvg ' VAG samples; AMPC: 'ambi'/'VABp'; SRPC sound references) – **no geometry** |
| **TRAK** | 0x42aac0 → 0x5563f0 | **world geometry**: tile models (render triangles + collision polygons) → global array `DAT_005846ec` **[certain]** |
| **STPC** | 0x42ab50 | object ("strat") models (same model format), skeletal animations, then strat script data (names like "SinkingStatuePad") **[certain for models]** |
| **MAP** | 0x42ac50 | tile grid: placement of the TRAK models + vertex colours + zone PVS (+ strats/paths/etc., see objects.md) **[certain]** |
| LGHT / LNFO / LGPC | 0x42c180 / 0x42ab10 / 0x558db0 | lights / level info / ? – not geometry |
**Die Welt ist ein Kachelsystem (Croc-Erbe):** TRAK enthält N "Kachel"-Modelle in lokalen
Koordinaten (ungefähr −0.5..0.5 in x/z, können aber bis ±2.4 hinausragen). MAP platziert sie
auf einem W×H-Raster (1 Einheit = 1 Zelle) mit Höhe y und Drehung um y. Es gibt **keinen BSP**;
die Raumaufteilung ist das Raster selbst (Zell-Listen) + eine 32-Zonen-Sichtbarkeitstabelle.
**The world is a tile system (inherited from Croc):** TRAK contains N "tile" models in local
coordinates (roughly −0.5..0.5 in x/z, but they can stick out up to ±2.4). MAP places them
on a W×H grid (1 unit = 1 cell) with height y and rotation around y. There is **no BSP**;
the spatial partition is the grid itself (cell lists) + a 32-zone visibility table.
## 2. Koordinatenräume [sicher]
## 2. Coordinate spaces [certain]
* **Exportraum** (= MAP-Floats "A", = Vertexdaten von TRAK/STPC): rechtshändig, **y oben**,
Rasterzellen liegen bei x ∈ [0,W), **z ∈ (−H, 0]**. OBJ-Export benutzt diesen Raum.
* **Spielraum** (MAP-Fixpunkt "B", Kollision, Strats): (x, y, −z), 20.12-Fixpunkt (4096 = 1).
Der Renderer rechnet B zurück: pos = (Bx, By, −Bz)/4096, Winkel = rot·2π/4096.
* Kachel-Transformation (Vertex v lokal → Welt, Exportraum), a = rot/4096·2π:
* **Export space** (= MAP floats "A", = vertex data of TRAK/STPC): right-handed, **y up**,
grid cells lie at x ∈ [0,W), **z ∈ (−H, 0]**. The OBJ export uses this space.
* **Game space** (MAP fixed point "B", collision, strats): (x, y, −z), 20.12 fixed point (4096 = 1).
The renderer converts B back: pos = (Bx, By, −Bz)/4096, angle = rot·2π/4096.
* Tile transform (local vertex v → world, export space), a = rot/4096·2π:
`X = x + vx·cos a − vz·sin a ; Y = y + vy ; Z = z + vx·sin a + vz·cos a`
(verifiziert: 93 % aller Kachel-Randvertices treffen exakt einen Nachbarvertex; die drei
anderen Vorzeichen-/Spiegelvarianten liefern 65–70 %).
* **Kollisionsdaten liegen im Spielraum** (z gespiegelt gegenüber den Render-Vertices desselben
Modells). Beim Dekodieren nz negieren (92 % der rekonstruierten Kollisionsecken liegen
< 0.02 neben einem Render-Vertex).
* rx/rz der Kacheln sind in allen Levels 0; nur Drehung um y (praktisch 0/1024/2048/3072).
(verified: 93 % of all tile edge vertices hit a neighbour vertex exactly; the three
other sign/mirror variants give 65–70 %).
* **Collision data is in game space** (z mirrored relative to the render vertices of the same
model). Negate nz when decoding (92 % of the reconstructed collision corners lie
< 0.02 from a render vertex).
* rx/rz of the tiles are 0 in all levels; only rotation around y (in practice 0/1024/2048/3072).
## 3. Modellformat (TRAK und STPC) [sicher]
## 3. Model format (TRAK and STPC) [certain]
Fixup-Funktionen: TRAK `FUN_005563f0(&ptr, n, &out)` (Header 0x84),
STPC `FUN_0041f770(&ptr, n, &out)` (Header 0x8c). Datei-Layout: **alle Header eines Aufrufs
hintereinander, dann die Daten der Modelle in Reihenfolge** (TRAK: ein Aufruf mit n=Anzahl;
STPC: n Aufrufe mit je 1 Modell → Header/Daten abwechselnd). Zeigerfelder sind in der Datei 0
und werden beim Laden gesetzt.
Fixup functions: TRAK `FUN_005563f0(&ptr, n, &out)` (header 0x84),
STPC `FUN_0041f770(&ptr, n, &out)` (header 0x8c). File layout: **all headers of one call
in a row, then the data of the models in order** (TRAK: one call with n=count;
STPC: n calls with 1 model each → header/data alternating). Pointer fields are 0 in the file
and are set on load.
### Header
| Off | Typ | Bedeutung |
| Off | Type | Meaning |
|---|---|---|
| 0x00 | 3×f32 | Mittelpunkt der Bounding-Box |
| 0x0c | 8×3 f32 | Ecken der Box, Reihenfolge (−x−y+z)(+x−y+z)(−x+y+z)(+x+y+z)(−x−y−z)(+x−y−z)(−x+y−z)(+x+y−z). Etwas größer als die Vertices (Rand nicht konstant, ~0.004–0.014; vermutl. aus Originalmesh). Für Frustum-Culling (`FUN_00402840`). |
| 0x00 | 3×f32 | centre of the bounding box |
| 0x0c | 8×3 f32 | box corners, order (−x−y+z)(+x−y+z)(−x+y+z)(+x+y+z)(−x−y−z)(+x−y−z)(−x+y−z)(+x+y−z). Slightly larger than the vertices (margin not constant, ~0.004–0.014; probably from the original mesh). Used for frustum culling (`FUN_00402840`). |
| 0x6c | u16 | nVerts |
| 0x6e | u16 | nFaces |
| 0x70 | u32 | → Verts (Datei: 0) |
| 0x74 | u32 | → Faces (Datei: 0) |
| 0x78 / 0x7a / 0x7c | 3×u16 | Anzahl Kollisionspolygone in 3 Gruppen (Code summiert immer alle drei; Gruppenbedeutung [?]) |
| 0x70 | u32 | → verts (file: 0) |
| 0x74 | u32 | → faces (file: 0) |
| 0x78 / 0x7a / 0x7c | 3×u16 | number of collision polygons in 3 groups (code always sums all three; group meaning [?]) |
| 0x7e | u16 | 0 |
| 0x80 | u32 | → Kollision (Datei: 0) |
| *nur STPC* 0x84 | u16 | = nVerts bei starren Modellen, 0 bei Skin-Modellen |
| *nur STPC* 0x86 | u16 | nBones (Skin-Modelle) – davor in den Daten nBones×u16 = Vertexzahl je Bone (Summe = nVerts, verifiziert) |
| *nur STPC* 0x88 | u32 | → Bone-Tabelle (Datei: 0) |
| 0x80 | u32 | → collision (file: 0) |
| *STPC only* 0x84 | u16 | = nVerts for rigid models, 0 for skinned models |
| *STPC only* 0x86 | u16 | nBones (skinned models) – preceded in the data by nBones×u16 = vertex count per bone (sum = nVerts, verified) |
| *STPC only* 0x88 | u32 | → bone table (file: 0) |
### Daten (in dieser Reihenfolge)
1. (nur STPC) nBones × u16
2. **Vertex** 24 Byte: `f32 x,y,z ; f32 nx,ny,nz` (Normalen geglättet; Lichtwerte kommen aus MAP-Vertexfarben)
3. **Face** 28 Byte (nur Dreiecke):
`u16 flags ; u16 v0,v1,v2 ; u16 tex ; u16 0 ; f32 nx,ny,nz,d` mit n = normalize((v1−v0)×(v2−v0)), d = n·v0
(verifiziert für alle Faces). Beim Laden wird `tex` durch Zeiger auf Texturtabelleneintrag
(`DAT_00581154 + tex*20`) ersetzt. Ebene dient dem Backface-Culling (Kamera·n − d ≥ ε).
4. **Kollisionspolygon** 32 Byte, nGruppeA+B+C Stück:
`u8 typ ; u8 material ; 5 × { i8 nx, i8 ny, i8 nz, u8 0, i16 d }`
* Ebene 0 = Fläche, Ebenen 1..4 = Kanten (Normalen **nach innen**, Punkt drin ⇔ n·p − d ≥ −Toleranz).
* Normalen ×127 (Code rechnet ×32 → 4096-Skala), d ×4096 (Fixpunkt) → lokale Koordinaten nur ±8 Einheiten!
* typ Bit0 = Viereck (4 Kanten), sonst Dreieck (3 Kanten, 5. Ebene = 0). Bit2 ≈ Normale zeigt nach unten (ny<0; Decken/Überhänge) [wahrsch.].
* material: Untergrund-Typ (0 = normal; 0x11/0x12 werden je nach WFPC 0x8000000 ignoriert, 0x1e = Wasser-/Todesebene [wahrsch.]; Wert landet im Spieler-Bodenstatus).
* Kollisionsabfragen: `FUN_004036d0` (Boden/Decke), `FUN_00403ad0` (Wände), `FUN_004042f0`, `FUN_004046e0`. Getestet werden **nur die Kacheln der Rasterzelle, in der der Spieler steht** (Zellliste aus MAP), Punkt relativ zu Kachel-B-Position, um −rot gedreht.
### Data (in this order)
1. (STPC only) nBones × u16
2. **Vertex** 24 bytes: `f32 x,y,z ; f32 nx,ny,nz` (smoothed normals; lighting comes from MAP vertex colours)
3. **Face** 28 bytes (triangles only):
`u16 flags ; u16 v0,v1,v2 ; u16 tex ; u16 0 ; f32 nx,ny,nz,d` with n = normalize((v1−v0)×(v2−v0)), d = n·v0
(verified for all faces). On load `tex` is replaced by a pointer to the texture table entry
(`DAT_00581154 + tex*20`). The plane is used for backface culling (camera·n − d ≥ ε).
4. **Collision polygon** 32 bytes, nGroupA+B+C of them:
`u8 type ; u8 material ; 5 × { i8 nx, i8 ny, i8 nz, u8 0, i16 d }`
* plane 0 = surface, planes 1..4 = edges (normals pointing **inwards**, point inside ⇔ n·p − d ≥ −tolerance).
* normals ×127 (code multiplies by 32 → 4096 scale), d ×4096 (fixed point) → local coordinates only ±8 units!
* type bit0 = quad (4 edges), otherwise triangle (3 edges, 5th plane = 0). Bit2 ≈ normal points down (ny<0; ceilings/overhangs) [likely].
* material: ground type (0 = normal; 0x11/0x12 are ignored depending on WFPC 0x8000000, 0x1e = water/death plane [likely]; the value ends up in the player's ground state).
* Collision queries: `FUN_004036d0` (floor/ceiling), `FUN_00403ad0` (walls), `FUN_004042f0`, `FUN_004046e0`. Only **the tiles of the grid cell the player stands in** are tested (cell list from MAP), point relative to the tile's B position, rotated by −rot.
### Face-Flags (u16)
| Bit | Bedeutung | Konfidenz |
### Face flags (u16)
| Bit | Meaning | Confidence |
|---|---|---|
| 0x0008 | neuer Render-Batch: Renderzustand (Seite, Blendmode) aus diesem Face neu laden, vorherigen Batch zeichnen. Sortierung nach Seite/Zustand + Bit am Wechsel. | sicher (Code) |
| 0x0010, 0x0800, 0x0020 | UV-Ecken-Auswahl (s. u.) | sicher |
| 0x0400 | kein Backface-Culling (doppelseitig) | sicher |
| 0x4000 | Vertexfarben ignorieren (weiß) | sicher |
| 0x0001, 0x0002 | Sonder-Rendermodi (Sortierung/Transparenz; zusammen mit Globalflags 0x5f6ee8) | wahrsch. |
| 0x8000, 0x1000, 0x2000, 0x0200, 0x0100, 0x0004 | in Daten vorhanden, Renderer (0x556510) wertet sie nicht aus | ? (vermutl. PS1-Reste) |
| 0x0008 | new render batch: reload render state (page, blend mode) from this face, draw the previous batch. Sorted by page/state + bit at each change. | certain (code) |
| 0x0010, 0x0800, 0x0020 | UV corner selection (see below) | certain |
| 0x0400 | no backface culling (double-sided) | certain |
| 0x4000 | ignore vertex colours (white) | certain |
| 0x0001, 0x0002 | special render modes (sorting/transparency; together with global flags 0x5f6ee8) | likely |
| 0x8000, 0x1000, 0x2000, 0x0200, 0x0100, 0x0004 | present in the data, the renderer (0x556510) does not evaluate them | ? (probably PS1 leftovers) |
### UVs – die Engine hat KEINE freien UVs [sicher]
Jedes Dreieck zeigt eine Hälfte eines **Texturrechtecks** (Eintrag der TEXT-Tabelle).
UV-Rechteck: U0=u0/256, U1=(u1+1)/256, V0=v0/256, V1=(v1+1)/256. Mit m = Flag 0x20 (spiegelt U),
a = m?U0:U1, b = m?U1:U0 bekommen die Ecken (v0,v1,v2):
* ohne 0x800: (a,V1) (b,V1) (a,V0)
* 0x800, ohne 0x10: (a,V1) (b,V1) (b,V0) ← erste Hälfte eines Vierecks
* 0x800 + 0x10: (b,V0) (a,V0) (a,V1) ← zweite Hälfte
(Renderer `FUN_00556510` ab Zeile "if ((uVar6 & 0x800) == 0)"). Texturen mit Flag Bit0 sind
Flachfarben (u0,u1,v0 = R,G,B). 96 % der Welt-Faces haben 0x800.
### UVs – the engine has NO free UVs [certain]
Every triangle shows one half of a **texture rectangle** (entry of the TEXT table).
UV rectangle: U0=u0/256, U1=(u1+1)/256, V0=v0/256, V1=(v1+1)/256. With m = flag 0x20 (mirrors U),
a = m?U0:U1, b = m?U1:U0 the corners (v0,v1,v2) get:
* without 0x800: (a,V1) (b,V1) (a,V0)
* 0x800, without 0x10: (a,V1) (b,V1) (b,V0) ← first half of a quad
* 0x800 + 0x10: (b,V0) (a,V0) (a,V1) ← second half
(renderer `FUN_00556510` from the line "if ((uVar6 & 0x800) == 0)"). Textures with flag bit0 are
flat colours (u0,u1,v0 = R,G,B). 96 % of world faces have 0x800.
## 4. TRAK-Chunk [sicher]
`u32 n ; n × Header(0x84) ; Daten Modell 0..n−1`. Alle 34 Level-WADs parsen exakt bis
Chunkende; `geomlib.build_trak(parse_trak(x)) == x` byte-identisch für alle Levels.
Typisch 100–650 Modelle, max. 422 Verts / 798 Faces pro Modell. Modell 0 ist in vielen Levels
eine unsichtbare Ebene (4 Verts, 0 Faces, 1 Kollisionspolygon, material 0x1e) bei y=−2.75 –
die Wasser-/Absturzebene unter den Inseln.
## 4. TRAK chunk [certain]
`u32 n ; n × header(0x84) ; data of model 0..n−1`. All 34 level WADs parse exactly up to the
end of the chunk; `geomlib.build_trak(parse_trak(x)) == x` byte-identical for all levels.
Typically 100–650 models, max. 422 verts / 798 faces per model. Model 0 is, in many levels,
an invisible plane (4 verts, 0 faces, 1 collision polygon, material 0x1e) at y=−2.75 –
the water/fall plane below the islands.
## 5. MAP-Chunk – Geometrieteile (Loader FUN_0042ac50) [sicher]
(Strat/Pfad/Zonen-Blöcke: siehe notes/objects.md; hier nur, was die Geometrie betrifft.)
Reihenfolge in der Datei:
## 5. MAP chunk – geometry parts (loader FUN_0042ac50) [certain]
(Strat/path/zone blocks: see notes/objects.md; only what concerns geometry here.)
Order in the file:
1. `u32 N (Kacheln), u32 W, u32 H` (Raster)
2. **A**: N × `{f32 x, f32 y, f32 z, u32 rot, u32 model, u32 animFlag}` (Exportraum; x,z = Zellmitte .5).
Nur zum Einsortieren in die Zelllisten benutzt: Zelle = floor(x) + floor(−z)·W.
3. u32 n + n×u32 (Liste, von Strats referenziert)
4. u32 n + n×90 Byte (Lichter/Kameras? – objects.md)
5. u32 n + n×34 Byte (Pfadknoten)
6. **Zonentabelle** 256 Byte = 32 × `{u32 Sichtmaske, i32 Höhenwert}` → `DAT_006da350`
7. **Zonenraster** W·H × u32 (Zonennummer 0..31 je Zelle) → world+0x48
8. **B**: N × `{i32 rx, i32 ry, i32 rz, i32 x, i32 y, i32 z}` 20.12-Fixpunkt, Spielraum (z = −A.z), ry = A.rot.
**Wird zum Rendern und für Kollision benutzt.** A und B sind in allen Levels konsistent.
9. **Modellindex**: N × u32 (= A.model)
10. falls WFPC & 0x10000: `u32 n ; n × {u32 cellX, u32 y, u32 cellZ, u32 modelA, u32 modelB}` –
Kachel-Wechsel (Türen/Brücken o. Ä.), Eintrag k gehört zur k-ten Kachel mit animFlag≠0.
11. **Vertexfarben** je Kachel i: nVerts(model) × u32 RGBA (0x80 = 1.0, Spiel ×2 mit Clamp);
Byte3 des ersten Eintrags = Anzahl Farbsätze (0/1 = einer; ≥2 → weitere nVerts×u32 folgen,
zweiter Satz wird bei `DAT_005865d4==1` benutzt). Für animFlag-Kacheln danach dieselbe
Struktur für modelB. Zeigertabelle world+0x58 (bei WFPC 0x10000000 doppelt groß).
12. Rest (u32 n2,n3 + 58-Byte-Strat-Einträge, optional Pfade (0x200000), Sichtweite (0x10), u16) – objects.md.
1. `u32 N (tiles), u32 W, u32 H` (grid)
2. **A**: N × `{f32 x, f32 y, f32 z, u32 rot, u32 model, u32 animFlag}` (export space; x,z = cell centre .5).
Only used to sort into the cell lists: cell = floor(x) + floor(−z)·W.
3. u32 n + n×u32 (list, referenced by strats)
4. u32 n + n×90 bytes (lights/cameras? – objects.md)
5. u32 n + n×34 bytes (path nodes)
6. **Zone table** 256 bytes = 32 × `{u32 visibility mask, i32 height value}` → `DAT_006da350`
7. **Zone grid** W·H × u32 (zone number 0..31 per cell) → world+0x48
8. **B**: N × `{i32 rx, i32 ry, i32 rz, i32 x, i32 y, i32 z}` 20.12 fixed point, game space (z = −A.z), ry = A.rot.
**Used for rendering and collision.** A and B are consistent in all levels.
9. **Model index**: N × u32 (= A.model)
10. if WFPC & 0x10000: `u32 n ; n × {u32 cellX, u32 y, u32 cellZ, u32 modelA, u32 modelB}` –
tile swaps (doors/bridges etc.), entry k belongs to the k-th tile with animFlag≠0.
11. **Vertex colours** per tile i: nVerts(model) × u32 RGBA (0x80 = 1.0, game ×2 with clamp);
byte3 of the first entry = number of colour sets (0/1 = one; ≥2 → another nVerts×u32 follow,
the second set is used when `DAT_005865d4==1`). For animFlag tiles the same
structure follows for modelB. Pointer table world+0x58 (twice as large with WFPC 0x10000000).
12. Rest (u32 n2,n3 + 58-byte strat entries, optional paths (0x200000), view distance (0x10), u16) – objects.md.
`geomlib.build_map_tiles(..., zones='keep', anim=orig)` reproduziert alle MAP-Chunks byte-identisch.
`geomlib.build_map_tiles(..., zones='keep', anim=orig)` reproduces all MAP chunks byte-identically.
### Raster / Sichtbarkeit (kein BSP) [sicher]
* Zelllisten: world+0x40 (W·H Zeiger, verkettete Listen von Kachelindizes), aus A gebaut.
* Rendern (`FUN_0042bf40`): Rechteck um die Kamera (Sichtweite) über das Raster; für jede Zelle
alle Kacheln der Liste, aber **nur wenn Bit zone[Zelle] in der Maske der Kamerazone gesetzt ist**
(`DAT_0058642c` = Zonentabelle[zone[Kamerazelle]].maske, `FUN_0041e020`). Zone 0 hat im
Dorf-Level Maske 0x1. → Neue Geometrie muss passende Zonen bekommen, sonst unsichtbar.
`geom_import --zones flat` setzt Raster=0 und Maske(Zone 0)=0xffffffff (alles sichtbar).
* Kollision: nur Kacheln der Spielerzelle (s. o.).
### Grid / visibility (no BSP) [certain]
* Cell lists: world+0x40 (W·H pointers, linked lists of tile indices), built from A.
* Rendering (`FUN_0042bf40`): rectangle around the camera (view distance) over the grid; for every cell
all tiles in the list, but **only if bit zone[cell] is set in the mask of the camera zone**
(`DAT_0058642c` = zonetable[zone[camera cell]].mask, `FUN_0041e020`). Zone 0 has mask 0x1 in the
village level. → New geometry must get matching zones, otherwise it is invisible.
`geom_import --zones flat` sets grid=0 and mask(zone 0)=0xffffffff (everything visible).
* Collision: only tiles of the player's cell (see above).
## 6. STPC-Chunk (Objekte) [sicher bis Animationen]
`u32 n1 ; n1 × (Header 0x8c + Daten)` statische/geskinnte Objektmodelle (33 im Dorf) ;
`u32 n2 ; n2 × Animation` (`FUN_0041f8b0`, Header 32 Byte: +0 nA (nA×4 Byte), +8 nFrames,
+0xc (0 → nFrames×8 Byte), +0x10 nDelta (Frame 0: nDelta×8, sonst nDelta×2 (+2 wenn ungerade)
Byte komprimierte Vertexdeltas), +0x18 nBones (je Frame nBones×64 Byte = 4×4-Matrix)) ;
falls WFPC & 0x100: `u32 n3 ; n3 × {16-Byte-Kopf (+4 Anzahl, +8 → n×8 Byte), Modelle}` ;
danach Strat-Daten (Skripte, Namen) – nicht vom Lader geparst, über Offsets (`DAT_006d9dbc+off`)
aus MAP referenziert. `geom_export --objects` exportiert die n1 Modelle nebeneinander.
## 6. STPC chunk (objects) [certain up to animations]
`u32 n1 ; n1 × (header 0x8c + data)` static/skinned object models (33 in the village) ;
`u32 n2 ; n2 × animation` (`FUN_0041f8b0`, header 32 bytes: +0 nA (nA×4 bytes), +8 nFrames,
+0xc (0 → nFrames×8 bytes), +0x10 nDelta (frame 0: nDelta×8, otherwise nDelta×2 (+2 if odd)
bytes of compressed vertex deltas), +0x18 nBones (per frame nBones×64 bytes = 4×4 matrix)) ;
if WFPC & 0x100: `u32 n3 ; n3 × {16-byte head (+4 count, +8 → n×8 bytes), models}` ;
then strat data (scripts, names) – not parsed by the loader, referenced from MAP via offsets
(`DAT_006d9dbc+off`). `geom_export --objects` exports the n1 models side by side.
## 7. TEXT – soweit für Geometrie nötig [sicher]
## 7. TEXT – as far as needed for geometry [certain]
`u32 nPal ; nPal×{u32, u32 n, n×u16, n×u8}` ; `u32 nPages ; nPages×{u32 flags, u32 w, u32 h,
(flags&0x80: u32 len + RLE16) | (flags≠0: w·h·2) | w·h}` ; `u32 nTex ; nTex × 8 Byte` ; Rest (40 Byte im Dorf) [?].
Textureintrag: `u16 flags, i8 page, i8 page2(-1), u8 u0, u8 u1, u8 v0, u8 v1` (u1/v1 inklusiv).
Flags: Bit0 Flachfarbe, Bit2 1-Pixel-Rand, Bit3/Bit4 Breite/Höhe ×2, Bits 12–14 Blendmodus, Bits 8–10 weiterer Modus.
Pixel: ABGR1555 (R = Bits 0–4), 0 = transparent. Details: notes/textures.md (anderer Agent).
(flags&0x80: u32 len + RLE16) | (flags≠0: w·h·2) | w·h}` ; `u32 nTex ; nTex × 8 bytes` ; rest (40 bytes in the village) [?].
Texture entry: `u16 flags, i8 page, i8 page2(-1), u8 u0, u8 u1, u8 v0, u8 v1` (u1/v1 inclusive).
Flags: bit0 flat colour, bit2 1-pixel border, bit3/bit4 width/height ×2, bits 12–14 blend mode, bits 8–10 another mode.
Pixels: ABGR1555 (R = bits 0–4), 0 = transparent. Details: notes/textures.md (other agent).
## 8. Neue Geometrie schreiben – was konsistent sein muss
Umgesetzt in `tools/geom_import.py` (OBJ → TRAK + MAP-Kachelteil, Rest des WADs unverändert):
1. **TRAK**: u32 Anzahl; Header mit korrekten nVerts/nFaces/nKoll; Box+Mittelpunkt (für Culling,
wird neu berechnet); Face-Ebenen aus Vertices; Vertexnormalen. Keine Prüfsummen.
2. **Face-Texturindex** < Anzahl Texturtabelleneinträge; UVs nur über Rechteck-Ecken (+Flags).
Bit 0x8 am Batch-Anfang (Seiten-/Zustandswechsel, spätestens alle 256 Faces).
3. **Limits**: ≤ 8000 Vertices pro Modell (Transformpuffer 0x58b0f0..0x5d15f8, 36 Byte/Vertex),
Batch-Ausgabepuffer 0x5d15f8 ≈ 4800 Vertices; Kollisions-d als i16/4096 → Kollisionspolygone
nur innerhalb ±8 Einheiten vom Kachelursprung. u16-Indizes.
4. **MAP**: N in Kopf, A (Floats) und B (Fixpunkt, z negiert) identisch halten, Modellindizes
< TRAK-Anzahl, Kachelzellen innerhalb W×H, **Vertexfarben-Block exakt nVerts(Modell) pro
Kachel** (sonst verschiebt sich der Rest von MAP = Strats!), Animationsblock (WFPC 0x10000)
passend zu den Flags (Import setzt ihn auf 0 Einträge), Zonen (s. o.).
5. Kollision gehört zur Zelle: ein Polygon wirkt nur, wenn es in der Kachelliste der Zelle steht,
in der der Spieler steht → Import trägt jedes Polygon in jede überdeckte Zelle ein
(auch als reine Kollisionskachel ohne Faces, wie Original-Modell 0).
6. WAD-Gesamtlänge (wadtool/geomlib schreiben sie neu). Keine weiteren Prüfsummen gefunden.
7. Strats (Startpunkt, Gegner, Kamera-Pfade) liegen weiter an ihren alten Positionen (objects.md).
## 8. Writing new geometry – what must be consistent
Implemented in `tools/geom_import.py` (OBJ → TRAK + MAP tile part, rest of the WAD unchanged):
1. **TRAK**: u32 count; header with correct nVerts/nFaces/nColl; box+centre (for culling,
recomputed); face planes from vertices; vertex normals. No checksums.
2. **Face texture index** < number of texture table entries; UVs only via rectangle corners (+flags).
Bit 0x8 at the start of a batch (page/state change, at the latest every 256 faces).
3. **Limits**: ≤ 8000 vertices per model (transform buffer 0x58b0f0..0x5d15f8, 36 bytes/vertex),
batch output buffer 0x5d15f8 ≈ 4800 vertices; collision d as i16/4096 → collision polygons
only within ±8 units of the tile origin. u16 indices.
4. **MAP**: keep N in the header, A (floats) and B (fixed point, z negated) identical, model indices
< TRAK count, tile cells within W×H, **vertex colour block exactly nVerts(model) per
tile** (otherwise the rest of MAP = strats shifts!), animation block (WFPC 0x10000)
matching the flags (import sets it to 0 entries), zones (see above).
5. Collision belongs to the cell: a polygon only takes effect if it is in the tile list of the cell
the player stands in → import enters every polygon into every cell it covers
(also as a pure collision tile without faces, like original model 0).
6. Total WAD length (wadtool/geomlib rewrite it). No other checksums found.
7. Strats (start point, enemies, camera paths) remain at their old positions (objects.md).
### Tests (ohne Spielstart)
* Round-Trip TRAK und MAP aller 34 WADs byte-identisch.
* Export → Import (`--coll-obj`) → Export t1l1m001: Rendering identisch (img/), 26435/26442
Dreiecke (7 entartete fallen weg), Bodenhöhe an 2800 Zufallspunkten: 99 % < 5 mm Abweichung,
10 Punkte anders (Polygone jetzt auch in Nachbarzellen eingetragen).
* Unabhängiger Gegencheck: `tools/mapchunk.py` (anderer Agent) parst die erzeugten MAPs vollständig.
* Noch NICHT im Spiel getestet: Demo-WADs in $CLAUDE_JOB_DIR/tmp: `demo_add.wad` (Original +
2 Blöcke + Rampe, Zonen original), `demo.wad` (alles ersetzt durch Bodenplatte y=0 + Blöcke).
### Tests (without starting the game)
* Round trip TRAK and MAP of all 34 WADs byte-identical.
* Export → import (`--coll-obj`) → export t1l1m001: rendering identical (img/), 26435/26442
triangles (7 degenerate ones dropped), floor height at 2800 random points: 99 % < 5 mm deviation,
10 points differ (polygons now also entered in neighbouring cells).
* Independent cross-check: `tools/mapchunk.py` (other agent) parses the generated MAPs completely.
* NOT yet tested in game: demo WADs in $CLAUDE_JOB_DIR/tmp: `demo_add.wad` (original +
2 blocks + ramp, original zones), `demo.wad` (everything replaced by a floor plate y=0 + blocks).
## 9. Offen / unbekannt
* Bedeutung der drei Kollisionsgruppen (Summe wird immer zusammen benutzt) und vollständige Materialliste.
* Face-Flags 0x8000/0x1000/0x2000/0x200/0x100/0x4; genaue Wirkung 0x1/0x2.
* Exakte Herkunft des Box-Randes im Header (Neu-Berechnung mit 1/128 Rand reicht vermutlich).
* Zonentabelle 2. Wert (Höhe, `DAT_00586424`) – evtl. Wasser-/Nebelhöhe.
* STPC-Animation: Bedeutung von nA-Block und Delta-Kompression im Detail (`FUN_0041fa30`).
* Ob der Spieler-Startpunkt/Kamera an Geometrie gekoppelt ist (objects.md).
## 9. Open / unknown
* Meaning of the three collision groups (the sum is always used together) and the complete material list.
* Face flags 0x8000/0x1000/0x2000/0x200/0x100/0x4; exact effect of 0x1/0x2.
* Exact origin of the box margin in the header (recomputing with a 1/128 margin is probably enough).
* Zone table 2nd value (height, `DAT_00586424`) – possibly water/fog height.
* STPC animation: meaning of the nA block and the delta compression in detail (`FUN_0041fa30`).
* Whether the player start point/camera is coupled to geometry (objects.md).
## 10. Werkzeuge
* `geom_export.py WAD OUT [--collision] [--objects]` → level.obj/mtl (UV, Vertexfarben, ein
Objekt pro Kachel), page_NN.png, collision.obj (Gruppen matNN_grpG), objects.obj, preview_top/persp.png
(texturierter Software-Renderer).
* `geom_import.py BASIS.wad MESH.obj NEU.wad [--coll-obj C.obj] [--keep-tiles] [--zones flat|keep] [--default-tex N]`
* `geom_demo.py BASIS.wad demo.obj [--plaza]` – Beispielgeometrie aus Texturkachel-Vierecken.
* `geomlib.py` – Parser/Builder (TRAK, STPC-Modelle, MAP-Kachelteil, TEXT-Tabelle/Seiten, Kollision).
## 10. Tools
* `geom_export.py WAD OUT [--collision] [--objects]` → level.obj/mtl (UV, vertex colours, one
object per tile), page_NN.png, collision.obj (groups matNN_grpG), objects.obj, preview_top/persp.png
(textured software renderer).
* `geom_import.py BASE.wad MESH.obj NEW.wad [--coll-obj C.obj] [--keep-tiles] [--zones flat|keep] [--default-tex N]`
* `geom_demo.py BASE.wad demo.obj [--plaza]` – example geometry made of texture-tile quads.
* `geomlib.py` – parser/builder (TRAK, STPC models, MAP tile part, TEXT table/pages, collision).
+222 -222
View File
@@ -1,31 +1,31 @@
# Levelbau mit `tools/levelbuild.py`
# Level building with `tools/levelbuild.py`
Stand: 2026-11-06 (Ausgang + Parkour überarbeitet). Baut komplette Level-WADs aus einer Level-Beschreibung (Python-Skript).
Basis ist immer ein Original-Level (Standard: Dorf `t1l1m001.wad`): dessen Strat-Code/Objektmodelle
(STPC), Texturen (TEXT), Sound usw. werden übernommen; **Geometrie (TRAK), Kachelraster,
Vertexfarben, Objekte und Wegpunkte (MAP) werden komplett neu erzeugt.**
Ergebnis im Spiel geprüft: `out/testwelt.wad` (= installierte Testwelt, siehe unten).
As of: 2026-11-06 (exit + parkour reworked). Builds complete level WADs from a level description (Python script).
The base is always an original level (default: village `t1l1m001.wad`): its strat code/object models
(STPC), textures (TEXT), sound etc. are taken over; **geometry (TRAK), tile grid,
vertex colours, objects and waypoints (MAP) are generated completely from scratch.**
Result checked in game: `out/testwelt.wad` (= installed test world, see below).
## Aufruf
## Invocation
```
python3 tools/levelbuild.py levels/testwelt.py out/testwelt.wad # bauen
python3 tools/levelbuild.py levels/testwelt.py out/testwelt.wad --install # + als TestLevel installieren
python3 tools/levelbuild.py levels/testwelt.py out/t.wad --set start=ziel # Variablen ans Skript
python3 tools/levelbuild.py --types # platzierbare Strat-Typen des Basislevels
python3 tools/levelbuild.py --textures tex.png # Textur-Kontaktabzug + Namensliste
python3 tools/levelbuild.py levels/testwelt.py out/testwelt.wad # build
python3 tools/levelbuild.py levels/testwelt.py out/testwelt.wad --install # + install as TestLevel
python3 tools/levelbuild.py levels/testwelt.py out/t.wad --set start=ziel # pass variables to the script
python3 tools/levelbuild.py --types # placeable strat types of the base level
python3 tools/levelbuild.py --textures tex.png # texture contact sheet + name list
python3 tools/texsheet.py WAD out.png [--all] [--only 1,2,3] [--size 96]
```
`--install` kopiert nach `~/.wine-lama/mods/testlevel/wads/T1L9M001.wad` (legt einmalig
`.orig` an) und legt die Kapitel-Symlinks 2..49 an (s. „Ausgang“). Test im Spiel: `tools/lamatest.py run out/x.wad ordner` (s. testharness.md).
`--install` copies to `~/.wine-lama/mods/testlevel/wads/T1L9M001.wad` (creates
`.orig` once) and creates the chapter symlinks 2..49 (see "Exit"). Test in game: `tools/lamatest.py run out/x.wad outdir` (see testharness.md).
## Level-Skript
## Level script
```python
LEVEL_ARGS = dict(name='Testwelt', seed=7) # NAME-Chunk, Zufallssaat (Texturdrehung)
LEVEL_ARGS = dict(name='Testwelt', seed=7) # NAME chunk, random seed (texture rotation)
def build(L): # L = levelbuild.Level
L.sea(0, 0, 88, 72, y=-2.0, kill=(6, 4, 80, 66)) # Meer + Todesebene
L.sea(0, 0, 88, 72, y=-2.0, kill=(6, 4, 80, 66)) # sea + death plane
L.box(14, 12, 26, 24, -0.4, 0, top='stein', side='stein')
L.stairs(26, 18, 29, 22, 0, 1.5, 3, '+x')
L.player(20, 14.5, rot=0)
@@ -34,240 +34,240 @@ def build(L): # L = levelbuild.Level
L.strat('Bunny', 66, 34)
L.coins([(27, 20), (28, 20)])
def report(L): # optional, nach dem Bauen
def report(L): # optional, after building
...
```
`L.opts` enthält die `--set`-Werte (Strings).
`L.opts` contains the `--set` values (strings).
### Koordinaten
* Spielkoordinaten wie bei Strats: **x nach Osten, y nach oben, z = Rasterzeile (positiv,
"Süden")**, 1 Einheit = 1 Rasterzelle. Raster des Dorfes: 96×96 → alles muss in x,z ∈ [0,96) liegen.
* Winkel in **Grad**, 0° = Blick nach +z, 90° = Blick nach +x (im Spiel geprüft).
* Intern wird in den Exportraum (z gespiegelt) umgerechnet; Umlaufsinn wird automatisch
korrigiert (`_orient`), man gibt nur die gewünschte Außennormale an.
### Coordinates
* Game coordinates as for strats: **x towards east, y up, z = grid row (positive,
"south")**, 1 unit = 1 grid cell. Village grid: 96×96 → everything must lie within x,z ∈ [0,96).
* Angles in **degrees**, 0° = facing +z, 90° = facing +x (checked in game).
* Internally converted into export space (z mirrored); winding order is corrected
automatically (`_orient`), you only specify the desired outward normal.
### Geometrie-Bausteine
| Methode | Beschreibung |
### Geometry building blocks
| Method | Description |
|---|---|
| `floor(x0,z0,x1,z1,y, tex, rot='random', coll=True)` | waagerechte Fläche (in 1×1-Kacheln unterteilt) |
| `box(x0,z0,x1,z1,y0,y1, top, side, side_top=None, bottom=None, sides='nesw', side_coll=None)` | Quader; `side_top` = Textur der obersten Wandreihe (z. B. `'graskante'`), `side_coll=False` = Wände ohne Kollision (niedrige Sockel begehbar) |
| `platform(x,z,w,d,y, top, side, thick=0.5, side_top='graskante', bottom='erde')` | schwebende Plattform, Oberkante y |
| `pillar(x,z,y0,y1,size, side, top)` | Säule um Mittelpunkt |
| `ramp(x0,z0,x1,z1, ya, yb, '+x', top, side, base)` | Rampe, steigt in Richtung `+x/-x/+z/-z` |
| `stairs(x0,z0,x1,z1, y0, y1, n, '+x', top, side, base, walkable=True)` | Treppe; `walkable`: unsichtbare Schräge als Kollision |
| `fence(pts, y, height)` | Zaun (Pfosten + Latte) entlang Punktliste |
| `tree(x,z,y, height, crown)` | Klotz-Baum |
| `sea(x0,z0,x1,z1, y, tex='meer', kill=(...))` | Wasserfläche ohne Kollision + Todesebene |
| `quad([BL,BR,TR,TL], tex, coll, rot)` | einzelnes Viereck (Ecken von vorne gesehen) |
| `collider(pts, normal)` | unsichtbare Kollisionsfläche |
| `obj(path, offset, scale, tex)` | OBJ-Datei (Spielkoordinaten), `usemtl tex_N` oder Texturname |
| `height_at(x,z)` | Höhe der obersten begehbaren Fläche |
| `floor(x0,z0,x1,z1,y, tex, rot='random', coll=True)` | horizontal surface (subdivided into 1×1 tiles) |
| `box(x0,z0,x1,z1,y0,y1, top, side, side_top=None, bottom=None, sides='nesw', side_coll=None)` | cuboid; `side_top` = texture of the topmost wall row (e.g. `'graskante'`), `side_coll=False` = walls without collision (low plinths walkable) |
| `platform(x,z,w,d,y, top, side, thick=0.5, side_top='graskante', bottom='erde')` | floating platform, top edge y |
| `pillar(x,z,y0,y1,size, side, top)` | pillar around a centre point |
| `ramp(x0,z0,x1,z1, ya, yb, '+x', top, side, base)` | ramp, rises in direction `+x/-x/+z/-z` |
| `stairs(x0,z0,x1,z1, y0, y1, n, '+x', top, side, base, walkable=True)` | stairs; `walkable`: invisible slope as collision |
| `fence(pts, y, height)` | fence (posts + rail) along a point list |
| `tree(x,z,y, height, crown)` | blocky tree |
| `sea(x0,z0,x1,z1, y, tex='meer', kill=(...))` | water surface without collision + death plane |
| `quad([BL,BR,TR,TL], tex, coll, rot)` | single quad (corners seen from the front) |
| `collider(pts, normal)` | invisible collision surface |
| `obj(path, offset, scale, tex)` | OBJ file (game coordinates), `usemtl tex_N` or texture name |
| `height_at(x,z)` | height of the topmost walkable surface |
Texturen: Name aus `TEX` (Dorf: `gras, gras2, gras_dunkel, blumen, sand, erde, graskante,
Textures: name from `TEX` (village: `gras, gras2, gras_dunkel, blumen, sand, erde, graskante,
stein, stein_kante, stein_moos, platte, holz_dunkel, holz_maser, holzbalken, rinde, kiste,
inka, muster_lila, …`), direkter Index (int) oder Flachfarbe (`'meer'`, `'rot'`, … oder
`(r,g,b)`; wird als Flachfarb-Eintrag (Flags 0x105, Seite −1) an die Texturtabelle angehängt).
Kontaktabzug mit Indizes: `notes/img/texturen_dorf.png`.
**Jedes Dreieck zeigt eine ganze Texturkachel** (die Engine hat keine freien UVs) – daher wird
alles in 1×1-Stücke zerlegt; schmale/flache Wände zeigen die Textur gestaucht.
inka, muster_lila, …`), a direct index (int) or a flat colour (`'meer'`, `'rot'`, … or
`(r,g,b)`; appended to the texture table as a flat-colour entry (flags 0x105, page −1)).
Contact sheet with indices: `notes/img/texturen_dorf.png`.
**Every triangle shows a whole texture tile** (the engine has no free UVs) – so
everything is split into 1×1 pieces; narrow/flat walls show the texture squashed.
### Beleuchtung (Vertexfarben)
### Lighting (vertex colours)
`L.sun = dict(azimuth=215, elevation=50, color=(1,.93,.8), strength=.62, sky=(.5,.55,.66),
ground=(.3,.28,.24), ao=.55, ao_radius=1.3, shadow=.35)` – je Vertex:
Himmel/Boden-Halbkugellicht + Sonne (Lambert) mit **Schatten** (Strahl zur Sonne gegen alle
Quader) + **Umgebungsverdeckung** (24 Strahlen, Reichweite `ao_radius`). Quader, Säulen,
Treppen, Plattformen melden sich als Schattenwerfer an (`solids`). 1.0 = Texturfarbe
(Vertexfarbe 0x80), max. 2.0. `tint=(r,g,b)` je Baustein multipliziert.
ground=(.3,.28,.24), ao=.55, ao_radius=1.3, shadow=.35)` – per vertex:
sky/ground hemisphere light + sun (Lambert) with **shadows** (ray towards the sun against all
cuboids) + **ambient occlusion** (24 rays, range `ao_radius`). Cuboids, pillars,
stairs, platforms register as shadow casters (`solids`). 1.0 = texture colour
(vertex colour 0x80), max. 2.0. `tint=(r,g,b)` multiplies per building block.
### Objekte (Strats)
### Objects (strats)
`L.strat(name, x, z, y=None, rot=None, params=None, wp='relative', variant=0, hidden=False)`
* `y=None`: auf die oberste begehbare Fläche bei (x,z) stellen.
* Typfelder (nvars, Slots, Flags, Zusatzspeicher) und Parameter werden von der **ersten
Original-Instanz** des Typs kopiert (`params=[…]` überschreibt; Anzahl muss passen).
* `variant`: Typen mit mehreren Code-Einstiegen (z. B. SmashPot: 2 Spawn-Stubs).
* **Wegpunkte** (`wp`): `'relative'` = Wegpunktkette des Originals mitverschieben (Versatz zum
Objekt bleibt, y wird auf den Boden gesetzt, wenn dort einer ist), `'here'` = alle Punkte auf
das Objekt, `'none'` = keine (−1), oder eigene Liste `[(x,z[,y[,grad]]), …]` (Zusatzfelder
der Original-Wegpunkte werden übernommen). Ketten werden neu nummeriert (keine Verweise ins
alte Level).
* `player(x,z,rot)` (Strat `Llama`), `checkpoint(x,z)` (`CheckPointPost`), `exit(x,z)`
* `y=None`: place on the topmost walkable surface at (x,z).
* Type fields (nvars, slots, flags, extra memory) and parameters are copied from the **first
original instance** of the type (`params=[…]` overrides; count must match).
* `variant`: types with several code entry points (e.g. SmashPot: 2 spawn stubs).
* **Waypoints** (`wp`): `'relative'` = move the original's waypoint chain along (offset to the
object is kept, y is set to the floor if there is one), `'here'` = all points on
the object, `'none'` = none (−1), or your own list `[(x,z[,y[,deg]]), …]` (extra fields
of the original waypoints are taken over). Chains are renumbered (no references into the
old level).
* `player(x,z,rot)` (strat `Llama`), `checkpoint(x,z)` (`CheckPointPost`), `exit(x,z)`
(`LevelDoor`), `coins([(x,z),…])`.
* Typen, die im Dorf-Code stehen, aber dort nicht platziert sind, über `Strats.BORROWED` mit Typfeldern
aus einem anderen Level: derzeit `RubyIdol` (aus t1l1m002).
* `poolExtra` wird auf max(Original, Anzahl Strats + 80) gesetzt; der Parameterpool des
Originals bleibt erhalten, neue Parameter werden angehängt.
* Types that are in the village code but not placed there, via `Strats.BORROWED` with type fields
from another level: currently `RubyIdol` (from t1l1m002).
* `poolExtra` is set to max(original, number of strats + 80); the original's parameter pool
is kept, new parameters are appended.
### Was `build()` erzeugt
1. Alle Dreiecke werden nach (Rasterzelle, Höhenband 10 Einheiten ab y = −3) auf Kacheln
verteilt; Kachelursprung = Zellmitte/Bandmitte (Kollisions-d muss in ±8 liegen).
2. **Kollision**: jedes Viereck mit `coll=True` wird ein Kollisionspolygon; Böden nur in der
eigenen Zelle, Wände/Schrägen zusätzlich in Nachbarzellen bis 0,3 Abstand (die Engine prüft
nur die Kacheln der Zelle, in der der Spieler steht). Unbenutzte Hilfsvertices sorgen dafür,
dass die Kachel-Box die Kollisionspolygone umfasst.
3. Identische Kachelmodelle werden zusammengefasst (Vertexfarben stehen in MAP je Kachel).
4. **Todesebene**: Original-Modell 0 des Dorfes (unsichtbar, Material 0x1e) in jeder Zelle
des `kill`-Bereichs, die nicht vollständig von Boden bedeckt ist.
5. MAP: Zonenraster = 0, Zone 0 sieht alles (Maske 0xffffffff), keine Kachel-Wechsel (Alt-Block
leer), Kachelblöcke A/B/Modelle/Vertexfarben konsistent, Strats + Wegpunkte neu, Licht-/
Umgebungszonen (90-Byte-Block) und Rest unverändert.
6. TEXT: angehängte Flachfarben. NAME: `LEVEL_ARGS['name']`.
7. Gegenprobe: MAP wird nach dem Schreiben wieder vollständig geparst.
### What `build()` generates
1. All triangles are distributed onto tiles by (grid cell, height band of 10 units from y = −3);
tile origin = cell centre/band centre (collision d must be within ±8).
2. **Collision**: every quad with `coll=True` becomes a collision polygon; floors only in their
own cell, walls/slopes additionally in neighbouring cells up to 0.3 distance (the engine only checks
the tiles of the cell the player stands in). Unused helper vertices make sure
the tile box encloses the collision polygons.
3. Identical tile models are merged (vertex colours are stored per tile in MAP).
4. **Death plane**: original model 0 of the village (invisible, material 0x1e) in every cell
of the `kill` area that is not completely covered by floor.
5. MAP: zone grid = 0, zone 0 sees everything (mask 0xffffffff), no tile swaps (alt block
empty), tile blocks A/B/models/vertex colours consistent, strats + waypoints new, light/
environment zones (90-byte block) and rest unchanged.
6. TEXT: appended flat colours. NAME: `LEVEL_ARGS['name']`.
7. Cross-check: MAP is fully parsed again after writing.
## Erkenntnisse aus den Spieltests (lamatest, Xvfb)
* **Maßstab**: Lama ≈ 0,6 Einheiten hoch, läuft ≈ 1,1 Einheiten/s (Münz-Messbahn, s. u.; die
früher notierten 0,85 waren zu niedrig), braucht ~1 s zum Beschleunigen. Sprung (X) überwindet
0,35 hohe Kanten, 0,5 nicht. **Mit Anlauf fliegt es ~1,3 weit** (horizontal gleich schnell wie
beim Laufen), auf +0,2 landet es nach ~1,05; aus dem Stand deutlich kürzer (0,5-Lücke nicht
geschafft). → Parkour: Lücken 0,3 mit +0,2, Plattformen davor/dahinter ≥ 1,5 lang (nach der
Landung muss Platz für den nächsten Absprung sein), größere Höhen über Treppen/Rampen.
Messlevel: `levels/sprunglabor.py` (`--set spur=a..g|m`; `m` = Münz-Messbahn: Münze alle 0,25 →
Münzzähler × 0,25 = gelaufene Strecke; `f` = Weitsprung über Wasser).
* **Keine Stufen-Logik**: schon eine 0,12 hohe Kante *mit Wandkollision* blockiert das Lama.
Ohne Wandkollision (`side_coll=False`) läuft es auf niedrige Sockel hinauf; Treppen mit
unsichtbarer Schräge (`stairs(walkable=True)`) und Rampen sind begehbar (geprüft).
* **Steuerung ist kamerarelativ** (Hoch = Kamerablickrichtung). Die Startkamera steht hinter
dem Lama; steht direkt dahinter ein Hindernis (z. B. Pfosten), weicht sie seitlich aus →
Startpunkte hinten freihalten.
* Rot 0° = Blick +z, 90° = Blick +x.
* **Todesebene funktioniert**: Sturz ins Meer → Lebensenergie weg, Respawn am letzten
Checkpoint (bzw. Start).
* Spieler-Wegpunktkette (14 Punkte beim Dorf-Lama, Bedeutung unklar): `player()` legt den
ersten 8 Einheiten hinter das Lama, den Rest auf das Lama, Zusatzfelder vom Original.
* **LevelDoor** (Ausgang) – gelöst, siehe Abschnitt „Ausgang (LevelDoor)“ unten.
* Größen, die laufen: TRAK 0,6–1,0 MB, bis 1100 Modelle, 5200 Kacheln, WAD bis 6,9 MB
(Originale: TRAK ≤ 1 MB, ≤ 5800 Kacheln, WAD ≤ 6,85 MB). Testwelt: 628 Modelle,
2474 Kacheln, TRAK 646 KB, WAD 6,3 MB.
## Findings from the game tests (lamatest, Xvfb)
* **Scale**: llama ≈ 0.6 units tall, walks ≈ 1.1 units/s (coin measuring track, see below; the
0.85 noted earlier was too low), needs ~1 s to accelerate. A jump (X) clears
0.35 high edges, 0.5 not. **With a run-up it flies ~1.3 far** (horizontally as fast as
when walking), onto +0.2 it lands after ~1.05; from standing much shorter (0.5 gap not
cleared). → Parkour: gaps 0.3 with +0.2, platforms before/after ≥ 1.5 long (after
landing there must be room for the next take-off), larger heights via stairs/ramps.
Measuring level: `levels/sprunglabor.py` (`--set spur=a..g|m`; `m` = coin measuring track: a coin every 0.25 →
coin counter × 0.25 = distance walked; `f` = long jump over water).
* **No step logic**: even a 0.12 high edge *with wall collision* blocks the llama.
Without wall collision (`side_coll=False`) it walks up onto low plinths; stairs with
an invisible slope (`stairs(walkable=True)`) and ramps are walkable (checked).
* **Controls are camera-relative** (Up = camera view direction). The start camera is behind
the llama; if there is an obstacle right behind it (e.g. a post), it moves aside →
keep the area behind start points clear.
* Rot 0° = facing +z, 90° = facing +x.
* **Death plane works**: falling into the sea → health gone, respawn at the last
checkpoint (or start).
* Player waypoint chain (14 points for the village llama, meaning unclear): `player()` puts the
first one 8 units behind the llama, the rest on the llama, extra fields from the original.
* **LevelDoor** (exit) – solved, see section "Exit (LevelDoor)" below.
* Sizes that work: TRAK 0.6–1.0 MB, up to 1100 models, 5200 tiles, WAD up to 6.9 MB
(originals: TRAK ≤ 1 MB, ≤ 5800 tiles, WAD ≤ 6.85 MB). Test world: 628 models,
2474 tiles, TRAK 646 KB, WAD 6.3 MB.
## Ausgang (LevelDoor) – wie er funktioniert
## Exit (LevelDoor) – how it works
Strat-Bytecode (Dorf, STPC 0x430332, 3 Parameter, unsichtbar, kein Modell), dekodiert über die
VM `FUN_0054d180` (Opcode < 0x45: Tabelle 0x5796bc mit 16-Bit-Operand, sonst 0x578ff4+op·4):
Strat bytecode (village, STPC 0x430332, 3 parameters, invisible, no model), decoded via the
VM `FUN_0054d180` (opcode < 0x45: table 0x5796bc with 16-bit operand, otherwise 0x578ff4+op·4):
```
g65,g66,g67 = Param 0,1,2 ; Stamm, Welt, Kapitel des nächsten Levels (20.12)
loop: if g21 == 3 ; Spielzustand "läuft"
and Spieler.Boden.Material == 16 ; Eigenschaft 0x11 = *(strat+0xc0)+4 = Material-Byte des
; Kollisionspolygons unter dem Spieler (FUN_00403020)
and floor(Spieler.x) == floor(Tür.x) and floor(Spieler.z) == floor(Tür.z) ; gleiche Rasterzelle
-> op 0x192 (FUN_0054d620): Levelwechsel(Param), g1 += 1, Ende
g65,g66,g67 = param 0,1,2 ; tribe, world, chapter of the next level (20.12)
loop: if g21 == 3 ; game state "running"
and player.floor.material == 16 ; property 0x11 = *(strat+0xc0)+4 = material byte of the
; collision polygon below the player (FUN_00403020)
and floor(player.x) == floor(door.x) and floor(player.z) == floor(door.z) ; same grid cell
-> op 0x192 (FUN_0054d620): level change(param), g1 += 1, end
yield
```
* Die Tür ist also nur ein Auslöser; im Dorf liegt im Türdurchgang ein Bodenpolygon mit
**Material 16** (8 Stück im Dorf, auch in allen anderen Levels). Unsere Testwelt hatte überall
Material 0 → nichts passierte. **Fix:** Mosaik im Tor mit `mat=16` (`box(..., mat=TUER_MAT)`),
Tür in dieselbe Rasterzelle, Spielerweg führt mitten hindurch.
* **Nur Kacheln der Türzelle zählen** (Zellliste, FUN_004036d0), die Höhe nicht (t1l3m002–4: Boden
3 Einheiten unter der Tür). **Dorf 1 & Co.:** In der Türzelle liegt zunächst *kein* Material 16 –
es steckt im **Wechselmodell** der Kachel (MAP-Kachel-Wechsel, WFPC 0x10000): die RubyIdolDoor in
derselben Zelle tauscht beim Öffnen (Op 0x11a/0x11b) Kachel 420 von Modell 48 auf 49, und erst
Modell 49 hat das Ausgangspolster (42,25–42,75 / 49,23–49,50, y 3,25). Im Spiel geprüft
(2026-11-07): Modell 49 eingesetzt + Tür entfernt, Lama bei 42,5/3,5/49,37 → sofort Bilanz
„Dorf Kapitel 1“; Original (Tür zu) → nichts. Betrifft 8 der 31 Original-Türen (Ruby-,
PressurePad-, Lab-Türen); das Polster bei 43,5/48,45 (Zelle 43,48) ist dagegen ein Hauseingang
(Zonen-Teleport). t1l3m003: 2 der 4 Türen (Zellen 71,48/71,49) haben gar kein Polster.
* Zweiter Weg (Strat-Code bei 0x4252aa, Spielerlogik [wahrsch.]): Boden-Material 16 und Zelle =
g104/g105 (setzt die LevelDoor beim Start, Llama setzt sie auf −1) → Levelwechsel(g101..g103).
* Parameter in den Originalen: (1, Welt, Kapitel+1) = nächstes Kapitel; im letzten Kapitel einer
Welt **(7,7,7)** = „Welt geschafft“ → zurück zur Auswahlkarte, nächste Welt freischalten.
* `FUN_0054d620`: merkt aktuelles Level (0x585008..14), setzt nächstes (0x584eb4/b8/bc), zeigt die
Bilanz (Münzen x/55, Geheimnisse) mit „EINGABETASTE = speichern / ESC = weiter“. Fortschritt
0x585018/19 (max. Welt/Kapitel, im KSV +0x42/+0x43): ist man im höchsten freien Kapitel, wird
das nächste Kapitel freigeschaltet; bei (7,7,7) die nächste Welt.
* **(7,7,7) in Welt 9 stürzt ab**: schaltet „Welt 10“ frei → nach dem Speichern steht der
Kartencursor auf Welt 10, Weltnamen-Zeiger 0x57bd40[9] = NULL → Page Fault bei 0x41B5CC.
* **Lösung Testwelt: Parameter (1,9,1)** – nach der Bilanz wird Kapitel 1 neu geladen (kein
Absturz, getestet mit Speichern). Nebenwirkung: Kapitel 2 wird freigeschaltet (und von
Kapitel n aus Kapitel n+1). Fehlt dessen WAD, stürzt die Auswahl ab („Couldnt load wad“).
Deshalb legt `levelbuild.py --install` (Funktion `kapitel_links()`) in der Mod
**Symlinks T1L9M002..049.wad → T1L9M001.wad und T192..T1949.COR → T191.COR** an (Welt 9 hat
49 Kapitel-Plätze, Tabelle 0x572420) und ruft `lama-mod an` auf. `lama-mod` übernimmt dafür jetzt
auch Symlinks aus Mod-Ordnern (`find … -type f -o -type l`). Geprüft: Kapitel 2 auf der Karte
wählbar, lädt die Testwelt.
* Die Auswahlkarte zeigt für Welt 9 keinen Hintergrund (nur Text) und 772 als Münzziel
(Münztabelle 0x5722f4 hat nur Welten 1–8) – kosmetisch.
* So the door is only a trigger; in the village there is a floor polygon with
**material 16** in the doorway (8 of them in the village, also in all other levels). Our test world had
material 0 everywhere → nothing happened. **Fix:** mosaic in the gate with `mat=16` (`box(..., mat=TUER_MAT)`),
door in the same grid cell, the player's path leads right through it.
* **Only tiles of the door cell count** (cell list, FUN_004036d0), not the height (t1l3m002–4: floor
3 units below the door). **Village 1 & co.:** the door cell initially contains *no* material 16 –
it is in the **swap model** of the tile (MAP tile swap, WFPC 0x10000): the RubyIdolDoor in
the same cell swaps tile 420 from model 48 to 49 when opening (op 0x11a/0x11b), and only
model 49 has the exit pad (42.25–42.75 / 49.23–49.50, y 3.25). Checked in game
(2026-11-07): model 49 inserted + door removed, llama at 42.5/3.5/49.37 → immediately the summary
"Dorf Kapitel 1"; original (door closed) → nothing. Affects 8 of the 31 original doors (Ruby,
PressurePad, Lab doors); the pad at 43.5/48.45 (cell 43,48), on the other hand, is a house entrance
(zone teleport). t1l3m003: 2 of the 4 doors (cells 71,48/71,49) have no pad at all.
* Second path (strat code at 0x4252aa, player logic [likely]): floor material 16 and cell =
g104/g105 (set by the LevelDoor at start, Llama sets them to −1) → level change(g101..g103).
* Parameters in the originals: (1, world, chapter+1) = next chapter; in the last chapter of a
world **(7,7,7)** = "world completed" → back to the selection map, unlock the next world.
* `FUN_0054d620`: remembers the current level (0x585008..14), sets the next (0x584eb4/b8/bc), shows the
summary (coins x/55, secrets) with "EINGABETASTE = speichern / ESC = weiter" (Enter = save / Esc = continue). Progress
0x585018/19 (max. world/chapter, in KSV +0x42/+0x43): if you are in the highest unlocked chapter,
the next chapter is unlocked; with (7,7,7) the next world.
* **(7,7,7) in world 9 crashes**: unlocks "world 10" → after saving, the
map cursor is on world 10, world name pointer 0x57bd40[9] = NULL → page fault at 0x41B5CC.
* **Solution for the test world: parameters (1,9,1)** – after the summary chapter 1 is reloaded (no
crash, tested with saving). Side effect: chapter 2 is unlocked (and from
chapter n, chapter n+1). If its WAD is missing, the selection crashes ("Couldnt load wad").
That is why `levelbuild.py --install` (function `kapitel_links()`) creates in the mod
**symlinks T1L9M002..049.wad → T1L9M001.wad and T192..T1949.COR → T191.COR** (world 9 has
49 chapter slots, table 0x572420) and calls `lama-mod an`. For this, `lama-mod` now also takes
symlinks from mod folders (`find … -type f -o -type l`). Checked: chapter 2 on the map is
selectable and loads the test world.
* The selection map shows no background for world 9 (only text) and 772 as the coin target
(coin table 0x5722f4 only has worlds 1–8) – cosmetic.
## Die Testwelt (`levels/testwelt.py` → `out/testwelt.wad`, installiert als T1L9M001.wad)
**2026-11-06 kompakt umgebaut** („alles näher zusammen“): L-förmige Insel, Hauptteil 32,5×23,5
(x 11..43,5, z 9,5..33) + Streifen unter dem Parkour-Ende (x 43,5..49, z 9,5..20,5), vorher
42×34 (x 10..52, z 8..42). Galerie, Gehege und Parkour grenzen direkt aneinander, keine leeren
Wiesen mehr. NAME-Chunk "Testwelt" (wird im Spiel nirgends angezeigt).
Vorschaubilder (Handy, 1280 px): `notes/img/vorschau/` (01–03 Blender-Renderings mit
Beschriftung, 04–11 Spielfotos, 12 Montage). Ältere Bilder `notes/img/testwelt_*.jpg` zeigen
noch das alte, weitläufige Layout.
## The test world (`levels/testwelt.py` → `out/testwelt.wad`, installed as T1L9M001.wad)
**Rebuilt compactly on 2026-11-06** ("everything closer together"): L-shaped island, main part 32.5×23.5
(x 11..43.5, z 9.5..33) + strip below the parkour end (x 43.5..49, z 9.5..20.5), previously
42×34 (x 10..52, z 8..42). Gallery, enclosure and parkour adjoin each other directly, no empty
meadows any more. NAME chunk "Testwelt" (not shown anywhere in the game).
Preview images (phone, 1280 px): `notes/img/vorschau/` (01–03 Blender renderings with
labels, 04–11 game shots, 12 montage). Older images `notes/img/testwelt_*.jpg` still show
the old, sprawling layout.
| Bereich | alt | neu |
| Area | old | new |
|---|---|---|
| Startplatz | x 14..22, z 11..19 (8×8) | x 15..21, z 12..19 (6×7) |
| Galerie | z 22..38, Spalten 2,5, Reihen z 25/28,5/32/35,5 | x 14,5..32,5, z 20,5..32, Spalten 1,9, Reihen z 22,5/25,2/27,9/30,6 (Abstand 2,7) |
| Gehege | x 39..49, z 24..35 | x 33..42, z 20,8..30 (direkt östlich der Galerie) |
| Parkour | x 22..53,6 | x 21..52,6 (nur um −1 in x verschoben, Maße gleich) |
| Ausgang | LevelDoor (51,5/15,5) | LevelDoor (50,5/15,5), Zelle 50/15, Mosaik mit Material 16 |
| Start square | x 14..22, z 11..19 (8×8) | x 15..21, z 12..19 (6×7) |
| Gallery | z 22..38, columns 2.5, rows z 25/28.5/32/35.5 | x 14.5..32.5, z 20.5..32, columns 1.9, rows z 22.5/25.2/27.9/30.6 (spacing 2.7) |
| Enclosure | x 39..49, z 24..35 | x 33..42, z 20.8..30 (directly east of the gallery) |
| Parkour | x 22..53.6 | x 21..52.6 (only shifted by −1 in x, same dimensions) |
| Exit | LevelDoor (51.5/15.5) | LevelDoor (50.5/15.5), cell 50/15, mosaic with material 16 |
* **Startplatz** (x 15..21, z 12..19): Steinplatz mit Inka-Mosaik, 4 Inka-Säulen, Blumenbeete,
Bäume, 2 Münzen, Checkpoint (16,1/17,4). Lama startet bei (18, 14,6) mit Blick nach Süden
(Galerie direkt dahinter, Parkour-Treppe links im Bild).
* **Galerie** (z 20,5..32): 4 Reihen, je Objekt ein flacher Sockel (0,9 breit, 0,08 hoch,
begehbar), Lücke 1,0 zwischen Sockeln, Sandweg (0,6) vor jeder Reihe, farbiger Pfosten am
Reihenanfang (gelb/grau/blau/rot), Hauptweg Nord–Süd bei x 15,6..16,3:
1. Sammeln: Coin, ChargeBooster, RubyIdol, HealthFull, HealthGenerator, ChacaChargePotion, Secret
2. Kisten/Töpfe: SmashPot (2 Varianten), Jar, ObjectDontDropInBox, WaterReboundPoints, CheatTeleport
3. Figuren: TipoVillage, ChacaVillage, PachaGivingInfo, KidOnLlama, Wompy
4. Mechanik: PressurePadDoor, RubyIdolDoor, SwitchPlatformPadVillage, AppearingPlatformsVillage,
* **Start square** (x 15..21, z 12..19): stone square with Inca mosaic, 4 Inca pillars, flower beds,
trees, 2 coins, checkpoint (16.1/17.4). The llama starts at (18, 14.6) facing south
(gallery directly behind it, parkour stairs on the left of the picture).
* **Gallery** (z 20.5..32): 4 rows, each object on a flat plinth (0.9 wide, 0.08 high,
walkable), gap 1.0 between plinths, sand path (0.6) in front of each row, coloured post at the
start of the row (yellow/grey/blue/red), main path north–south at x 15.6..16.3:
1. Collectables: Coin, ChargeBooster, RubyIdol, HealthFull, HealthGenerator, ChacaChargePotion, Secret
2. Crates/pots: SmashPot (2 variants), Jar, ObjectDontDropInBox, WaterReboundPoints, CheatTeleport
3. Characters: TipoVillage, ChacaVillage, PachaGivingInfo, KidOnLlama, Wompy
4. Mechanics: PressurePadDoor, RubyIdolDoor, SwitchPlatformPadVillage, AppearingPlatformsVillage,
RisingColumns, SinkingStatue, FallingTree, CheckPointPost
* **Gehege** (x 33..42, z 20,8..30, Zaun, Eingang Westseite z 20,8..24): Bunny ×2, Ladybird,
FlockOfBirds, VillagerHerder; Aussichtspodest (0,6) mit Treppe an der Westseite zwischen
Galerie-Reihe 2 und 3.
* **Parkour** (beginnt an der Ostkante des Platzes, **alles auf der Linie z = 15,5 nach Osten**,
damit man ohne Kameradrehen durchkommt – Drehen per A/S oder Links/Rechts war im Skript nicht
reproduzierbar). Geometrie im Skript als „alte x + PX“ (PX = −1) geschrieben:
Treppe 0→0,8 · **Anschlag-Stufe** +0,3 (ohne Lücke: Lama läuft dagegen und steht still) ·
2 Lücken 0,3 mit je +0,2 auf 1,5 lange Holzklötze · Balken 0,5 breit · Mittelplattform (1,5) mit
Checkpoint · Sandrampe 1,5→2,4 · **Anschlag-Stufe** +0,3 · 3 Trittsteine (1,5 lang, Lücken 0,3,
je +0,2) · schmaler Balken 0,45 · Treppe 3,3→4,0 · Ziel-Plattform (4,0; x 47,6..52,6, ragt über
die Küste) mit Säulen, Brustwehr und Tor; darunter Mosaik mit Material 16, LevelDoor bei
(50,5 / 15,5). 18 Münzen entlang. Unter dem Parkour liegt Wiese (Herunterfallen ist harmlos).
* **Skript-Durchlauf** (Start → Ziel → Ausgang → Bilanz), alte Fassung 4/4, kompakte Fassung 2/2
Läufe erfolgreich (Bilanz 18/55 Münzen); die .seq ist zeitbasiert/relativ und blieb unverändert:
* **Enclosure** (x 33..42, z 20.8..30, fence, entrance on the west side z 20.8..24): Bunny ×2, Ladybird,
FlockOfBirds, VillagerHerder; viewing pedestal (0.6) with stairs on the west side between
gallery rows 2 and 3.
* **Parkour** (starts at the east edge of the square, **everything on the line z = 15.5 going east**,
so it can be done without rotating the camera – rotating via A/S or Left/Right was not
reproducible in scripts). Geometry is written in the script as "old x + PX" (PX = −1):
stairs 0→0.8 · **stop step** +0.3 (no gap: the llama runs into it and stands still) ·
2 gaps of 0.3 with +0.2 each onto 1.5 long wooden blocks · beam 0.5 wide · middle platform (1.5) with
checkpoint · sand ramp 1.5→2.4 · **stop step** +0.3 · 3 stepping stones (1.5 long, gaps 0.3,
+0.2 each) · narrow beam 0.45 · stairs 3.3→4.0 · goal platform (4.0; x 47.6..52.6, juts out over
the coast) with pillars, parapet and gate; below it a mosaic with material 16, LevelDoor at
(50.5 / 15.5). 18 coins along the way. Below the parkour there is meadow (falling down is harmless).
* **Scripted run** (start → goal → exit → summary), old version 4/4, compact version 2/2
runs successful (summary 18/55 coins); the .seq is time-based/relative and stayed unchanged:
```
python3 tools/levelbuild.py levels/testwelt.py out/testwelt_parkour.wad --set start=parkour
lamatest.py run out/testwelt_parkour.wad AUSGABE --seq "$(cat levels/testwelt_parkour.seq)"
lamatest.py run out/testwelt_parkour.wad OUTDIR --seq "$(cat levels/testwelt_parkour.seq)"
```
(`run:T:x@t,…` hält Up T s und drückt X zu festen Zeiten; die Anschlag-Stufen setzen den
Zeitfehler zurück. Fotos (`shot@`) blockieren ~0,4 s → nie kurz vor einem Sprung.)
* Im Spiel geprüft: Laden ohne Absturz (viele Läufe), alle 31 Galerie-Objekte erscheinen
(Münze/Secret/… reagieren: Münzzähler, "GEHEIMNIS!"), Lama läuft Galerie/Gehege ab,
Treppe hoch, Sprung auf Plattform B und auf Trittstein 1, Rampe hoch, Treppe zur
Ziel-Plattform, Meer = Respawn, kompletter Parkour per Skript, Ausgang → Bilanz → Kapitel 1 neu.
Bilder: aktuell `notes/img/vorschau/` (kompaktes Layout); `notes/img/testwelt_*.jpg` =
altes weitläufiges Layout (1–6 + Montage sogar noch mit altem Parkour).
* Startpunkt für Tests umschaltbar: `--set start=platz|reihe1..4|weg1..4|gehege|podest|parkour|pk1..pk5|ziel|ziel2`
(`podest` = auf dem Aussichtspodest, Blick nach Westen über die Galerie);
Ausgangsparameter `--set tuer=1,9,1`.
(`run:T:x@t,…` holds Up for T s and presses X at fixed times; the stop steps reset the
timing error. Shots (`shot@`) block ~0.4 s → never shortly before a jump.)
* Checked in game: loading without a crash (many runs), all 31 gallery objects appear
(coin/secret/… react: coin counter, "GEHEIMNIS!"), the llama walks through gallery/enclosure,
up the stairs, jumps onto platform B and onto stepping stone 1, up the ramp, stairs to the
goal platform, sea = respawn, complete parkour by script, exit → summary → chapter 1 again.
Images: current `notes/img/vorschau/` (compact layout); `notes/img/testwelt_*.jpg` =
old sprawling layout (1–6 + montage even still with the old parkour).
* Start point for tests is switchable: `--set start=platz|reihe1..4|weg1..4|gehege|podest|parkour|pk1..pk5|ziel|ziel2`
(`podest` = on the viewing pedestal, looking west over the gallery);
exit parameters `--set tuer=1,9,1`.
### Nicht platzierte Dorf-Typen
| Typ | Grund |
### Village types not placed
| Type | Reason |
|---|---|
| Llama | ist der Spieler (Start) |
| LevelDoor | Ausgang am Parkour-Ende (braucht Boden mit Material 16, s. o.) |
| CameraSide | Kamera-Zone, unsichtbar |
| CutSceneCameraDirectional | Zwischensequenz-Kamera, unsichtbar |
| CutSceneTrigger | startet Zwischensequenz |
Kein platzierbarer Typ hat das Spiel zum Absturz gebracht (keine Bisektion nötig).
Hinweis: Mechanik-Objekte (Türen, Säulen, Statue) gehören im Dorf zu bestimmter Geometrie /
Kachel-Wechseln; in der Galerie stehen sie "nackt" (z. B. schwebendes RubyIdolDoor-Schild).
Typen, die nur im Code stehen (BouncingCoin, Sparkles, RubyIdolStatue …), haben keine
Original-Instanz mit Typfeldern und werden nicht angeboten (Ausnahme RubyIdol über `Strats.BORROWED`).
| Llama | is the player (start) |
| LevelDoor | exit at the end of the parkour (needs a floor with material 16, see above) |
| CameraSide | camera zone, invisible |
| CutSceneCameraDirectional | cutscene camera, invisible |
| CutSceneTrigger | starts a cutscene |
No placeable type crashed the game (no bisection needed).
Note: mechanics objects (doors, pillars, statue) belong to specific geometry /
tile swaps in the village; in the gallery they stand "bare" (e.g. a floating RubyIdolDoor sign).
Types that only exist in the code (BouncingCoin, Sparkles, RubyIdolStatue …) have no
original instance with type fields and are not offered (exception: RubyIdol via `Strats.BORROWED`).
## Vorschau-Renderings (Blender)
`/usr/bin/blender` (Debian 3.0.1): **Cycles rendert nur Schwarz** (auch die Standardszene), OCIO
fehlt („Color management: fallback“, nur View „Standard“). **Eevee geht**, braucht aber ein
X-Display → eigenes `Xvfb :7` (nie :0, :5 gehört lamatest). Ablauf (Skripte in
`tools/vorschau/`: `strats_json.py` (aus levelbau/ aufrufen), `render_level.py`, `label.py`):
## Preview renderings (Blender)
`/usr/bin/blender` (Debian 3.0.1): **Cycles renders only black** (even the default scene), OCIO
is missing ("Color management: fallback", only the "Standard" view). **Eevee works**, but needs an
X display → own `Xvfb :7` (never :0, :5 belongs to lamatest). Procedure (scripts in
`tools/vorschau/`: `strats_json.py` (call from levelbau/), `render_level.py`, `label.py`):
1. `geom_export.py out/testwelt.wad GE` → level.obj/mtl + page_NN.png
2. Strat-Positionen: Levelskript mit `levelbuild.Level` ausführen (ohne `L.build`), `L.strats` als JSON
2. Strat positions: run the level script with `levelbuild.Level` (without `L.build`), `L.strats` as JSON
3. `DISPLAY=:7 STRATS=strats.json VIEWS=schraeg,schraeg2,oben blender -b -P render_level.py -- GE r 64`:
eigener OBJ-Leser (der 3.0-Importer kennt keine Vertexfarben), Spiegelung OBJ→Spiel (y_blender =
−Spiel-z, Umlaufsinn drehen), Material = Textur (Closest) × Vertexfarbe, etwas Emission,
Sonne + Himmel, Marker für Strats (Münzen gold, Lama-Start oranger Kegel, Galerie-Objekte
in Reihenfarbe, Tiere weiß, Ausgang lila Ring); schreibt je Ansicht PNG + JSON mit
Bildkoordinaten der Bereichsnamen.
4. PIL: auf 1280 px skalieren, Beschriftungen (Start/Galerie/Gehege/Parkour/Ziel), JPG q85.
own OBJ reader (the 3.0 importer does not know vertex colours), mirroring OBJ→game (y_blender =
−game z, flip winding), material = texture (closest) × vertex colour, a little emission,
sun + sky, markers for strats (coins gold, llama start orange cone, gallery objects
in row colour, animals white, exit purple ring); writes PNG + JSON per view with
image coordinates of the area names.
4. PIL: scale to 1280 px, labels (Start/Galerie/Gehege/Parkour/Ziel), JPG q85.
+115 -115
View File
@@ -1,135 +1,135 @@
# MAP-Chunk & Objekte (Strats) – "Ein Königreich für ein Lama" (PC)
# MAP chunk & objects (strats) – "Ein Königreich für ein Lama" (PC)
Quelle: groove.exe, MAP-Lader `FUN_0042ac50`, Strat-Spawn `FUN_0054cfc0` → `FUN_0054bfc0`,
Strat-VM `FUN_0054d180`, Chunk-Dispatcher bei 0x558960. Werkzeuge in `../tools/`.
Alle 34 Original-MAPs werden von `mapchunk.parse()` vollständig gelesen (Rest 0 Byte) und
von `mapchunk.build()` **byte-identisch** zurückgeschrieben.
Source: groove.exe, MAP loader `FUN_0042ac50`, strat spawn `FUN_0054cfc0` → `FUN_0054bfc0`,
strat VM `FUN_0054d180`, chunk dispatcher at 0x558960. Tools in `../tools/`.
All 34 original MAPs are read completely by `mapchunk.parse()` (0 bytes left over) and
written back **byte-identically** by `mapchunk.build()`.
## Begriffe
* **Tile** = platziertes Geometrie-Modell aus **TRAK** (Level-"Baukasten", 1x1-Raster).
* **Strat** = Objekt/Script-Instanz (Gegner, Münze, Tür, Spieler …). Der Strat-Code
(Bytecode, u32-Wörter) und die Objektmodelle liegen in **STPC**; MAP enthält nur Instanzen
mit Offset in STPC. Es gibt **keine Typ-ID** – der Typ *ist* die Code-Adresse.
Typnamen stehen im Code: Opcode `0xd4, 0xea, s32 rel` → Name = C-String bei
(Adresse hinter rel) + rel. `strats.py` löst das auf.
* Koordinaten: Welt-y nach **oben**. Tiles speichern float x, y, z mit **z negativ**;
Strats/fixed-point (20.12, 4096 = 1.0) speichern z **positiv** (= −tile.z).
Winkel: 4096 = 360°. Boden-Tiles (flaches Modell, Vertex-y = +0.25) → Objekte stehen auf
`tile.y + 0.25` (bestätigt an Spieler/Checkpoints/Münzen).
## Terms
* **Tile** = placed geometry model from **TRAK** (the level "construction kit", 1x1 grid).
* **Strat** = object/script instance (enemy, coin, door, player …). The strat code
(bytecode, u32 words) and the object models live in **STPC**; MAP only contains instances
with an offset into STPC. There is **no type ID** – the type *is* the code address.
Type names are in the code: opcode `0xd4, 0xea, s32 rel` → name = C string at
(address after rel) + rel. `strats.py` resolves this.
* Coordinates: world y points **up**. Tiles store float x, y, z with **z negative**;
strats/fixed point (20.12, 4096 = 1.0) store z **positive** (= −tile.z).
Angles: 4096 = 360°. Floor tiles (flat model, vertex y = +0.25) → objects stand at
`tile.y + 0.25` (confirmed on player/checkpoints/coins).
## Layout MAP (alles little endian) — Konfidenz: hoch (aus Lader + Roundtrip)
WFPC (u32 Flags, globale 0x6da330) steuert optionale Blöcke.
## MAP layout (all little endian) — confidence: high (from loader + roundtrip)
WFPC (u32 flags, global 0x6da330) controls optional blocks.
| # | Block | Format |
|---|---|---|
| 1 | Kopf | u32 nTiles, u32 W, u32 H (Rastergröße in Zellen) |
| 2 | Tiles | nTiles × {f32 x, f32 y, f32 z, u32 rotY(0/1024/2048/3072), u32 modell, u32 hatAlt} |
| 3 | Parameterpool | u32 n, n × u32 (Strat-Parameter, von Strats per Index referenziert) |
| 4 | Zonen ("Environment") | u32 n, n × 90 Byte (s.u.) |
| 5 | Wegpunkte | u32 n, n × 34 Byte {u32 weiter(≠0: nächster Eintrag gehört zur selben Kette, 0 = Kettenende), u32, u16 rx, u16 ry, u16 rz, s32 x, y, z (fixed, z positiv), u32, u32} |
| 6 | Regionstabelle | 32 × {u32 flags, u32 wert} (→ 0x6da350) |
| 7 | Regionsraster | W×H × u32 Regionsindex (0..31) je Zelle, Zeile = z-Zelle |
| 8 | Tiles fixed | nTiles × 6 u32 {rx=0, rotY, rz=0, x·4096, y·4096, −z·4096} – **exakt aus Block 2 ableitbar** |
| 9 | Tile-Modelle | nTiles × u32 TRAK-Modellindex (= Feld 5 aus Block 2) |
| 10 | Alt-Modelle (WFPC&0x10000) | u32 n, n × 5 u32; n = Anzahl Tiles mit hatAlt≠0, in Tile-Reihenfolge; Feld 4 = alternatives TRAK-Modell (z. B. Tür offen/zu, stMapreplace) |
| 11 | Vertexfarben | je Tile: nv(TRAK-Modell) × u32 RGBA (Byte 3 = Frameanzahl F); wenn max F>1: (F−1)×nv weitere u32; danach dasselbe für das Alt-Modell, falls vorhanden. Farbe wird ×2 (Clamp 255) |
| 12 | Strats | u32 nStrats, u32 poolExtra, nStrats × 58 Byte (s.u.) |
| 13 | Pfade (WFPC&0x200000) | u32 n; je: u32 nsub; je sub: u16 cnt, u16 flags; cnt × {u32,u32,[u32 wenn WFPC&0x4000000],u16 typ,u16} – in keinem Original-Level benutzt |
| 14 | (WFPC&0x10) | u32 (Default 200; Größe eines 0x48-Puffers) |
| 15 | | u16 (→ mapobj+0xc, Bedeutung offen) |
| 1 | Header | u32 nTiles, u32 W, u32 H (grid size in cells) |
| 2 | Tiles | nTiles × {f32 x, f32 y, f32 z, u32 rotY(0/1024/2048/3072), u32 model, u32 hasAlt} |
| 3 | Parameter pool | u32 n, n × u32 (strat parameters, referenced by strats via index) |
| 4 | Zones ("Environment") | u32 n, n × 90 bytes (see below) |
| 5 | Waypoints | u32 n, n × 34 bytes {u32 next(≠0: next entry belongs to the same chain, 0 = end of chain), u32, u16 rx, u16 ry, u16 rz, s32 x, y, z (fixed, z positive), u32, u32} |
| 6 | Region table | 32 × {u32 flags, u32 value} (→ 0x6da350) |
| 7 | Region grid | W×H × u32 region index (0..31) per cell, row = z cell |
| 8 | Tiles fixed | nTiles × 6 u32 {rx=0, rotY, rz=0, x·4096, y·4096, −z·4096} – **exactly derivable from block 2** |
| 9 | Tile models | nTiles × u32 TRAK model index (= field 5 of block 2) |
| 10 | Alt models (WFPC&0x10000) | u32 n, n × 5 u32; n = number of tiles with hasAlt≠0, in tile order; field 4 = alternative TRAK model (e.g. door open/closed, stMapreplace) |
| 11 | Vertex colours | per tile: nv(TRAK model) × u32 RGBA (byte 3 = frame count F); if max F>1: (F−1)×nv more u32; then the same for the alt model, if present. Colour is ×2 (clamped to 255) |
| 12 | Strats | u32 nStrats, u32 poolExtra, nStrats × 58 bytes (see below) |
| 13 | Paths (WFPC&0x200000) | u32 n; each: u32 nsub; each sub: u16 cnt, u16 flags; cnt × {u32,u32,[u32 if WFPC&0x4000000],u16 type,u16} – not used in any original level |
| 14 | (WFPC&0x10) | u32 (default 200; size of a 0x48 buffer) |
| 15 | | u16 (→ mapobj+0xc, meaning open) |
Zellenzuordnung Tile: `col = int(x−0.5)`, `row = −int(z+0.5)`, Index `col + row·W` (verkettete
Liste je Zelle). **Tiles außerhalb 0..W−1 / 0..H−1 schreiben in fremden Speicher.**
Cell assignment of a tile: `col = int(x−0.5)`, `row = −int(z+0.5)`, index `col + row·W` (linked
list per cell). **Tiles outside 0..W−1 / 0..H−1 write into foreign memory.**
### Strat-Eintrag (58 Byte) — `mapchunk.STRAT = '3H3iIIi4Iiih'+'H'`
| Off | Typ | Feld | Konfidenz |
### Strat entry (58 bytes) — `mapchunk.STRAT = '3H3iIIi4Iiih'+'H'`
| Off | Type | Field | Confidence |
|---|---|---|---|
| 0 | u16 ×3 | Rotation x, y, z (4096=360°; x/z fast immer 0) | hoch |
| 6 | s32 ×3 | Position x, y, z fixed 20.12 (z positiv) | hoch |
| 18 | u32 | **Code-Offset in STPC** (= Objekttyp) | hoch |
| 22 | u32 | Anzahl Parameter n | hoch |
| 26 | s32 | Index in Parameterpool (−1 wenn n=0); Parameter werden auf den Strat-Stack gelegt | hoch |
| 30 | u32 | Anzahl Strat-Variablen (vom Compiler, **typkonstant**) | hoch |
| 34 | u32 | Anzahl 16-Byte-Slots (Kollision/Unterobjekte; typkonstant) | mittel |
| 38 | u32 | Typflags (0x8000/0x80000/0x40000 = Kollisions-/Physikblock; typkonstant) | mittel |
| 42 | u32 | Zusatzspeicher (&0x3fffffff, meist 0..3; typkonstant) | mittel |
| 46 | s32 | Wegpunkt-Index (Block 5) oder −1 | hoch |
| 50 | u32 | immer 0 | – |
| 54 | u16 | Flags; Bit 1 (2) = beim Levelstart nicht erzeugen | hoch |
| 56 | u16 | immer 0 | – |
| 0 | u16 ×3 | rotation x, y, z (4096=360°; x/z almost always 0) | high |
| 6 | s32 ×3 | position x, y, z fixed 20.12 (z positive) | high |
| 18 | u32 | **code offset in STPC** (= object type) | high |
| 22 | u32 | number of parameters n | high |
| 26 | s32 | index into parameter pool (−1 if n=0); parameters are pushed onto the strat stack | high |
| 30 | u32 | number of strat variables (from the compiler, **constant per type**) | high |
| 34 | u32 | number of 16-byte slots (collision/sub-objects; constant per type) | medium |
| 38 | u32 | type flags (0x8000/0x80000/0x40000 = collision/physics block; constant per type) | medium |
| 42 | u32 | extra memory (&0x3fffffff, mostly 0..3; constant per type) | medium |
| 46 | s32 | waypoint index (block 5) or −1 | high |
| 50 | u32 | always 0 | – |
| 54 | u16 | flags; bit 1 (2) = do not create at level start | high |
| 56 | u16 | always 0 | – |
`poolExtra` (zweites u32 im Strat-Kopf) = max. gleichzeitige Strats − 30 (Engine legt
poolExtra+30 Slots à 0x154 Byte an). **Muss ≥ Anzahl Strats + dynamisch gespawnte sein.**
`poolExtra` (second u32 in the strat header) = max. simultaneous strats − 30 (the engine allocates
poolExtra+30 slots of 0x154 bytes). **Must be ≥ number of strats + dynamically spawned ones.**
### Zonen (90 Byte, im Speicher 0x5c) — Konfidenz mittel
u32 cellX, cellY, cellZ, s32 x,y,z (fixed), u32 ?, u32 rot?, u32 flags(+0x20; **Bit 8 = Startzone**
→ DAT_00584644), u32, u32 ptr-Offset in STPC (0=keiner), u32, u32, u32, u16, u32 Nebel nah/fern
(+0x3c/+0x40), u32 ×2 (+0x44/+0x48), u32 Farbe (z. B. 0x404040), u32 Farbe, u16, u16, u32 Musik-ID
(+0x58; FUN_00546b70). Kamera-/Nebel-/Musikbereiche; nicht Lichtquellen (die stehen in LGHT).
### Zones (90 bytes, 0x5c in memory) — confidence medium
u32 cellX, cellY, cellZ, s32 x,y,z (fixed), u32 ?, u32 rot?, u32 flags(+0x20; **bit 8 = start zone**
→ DAT_00584644), u32, u32 ptr offset in STPC (0=none), u32, u32, u32, u16, u32 fog near/far
(+0x3c/+0x40), u32 ×2 (+0x44/+0x48), u32 colour (e.g. 0x404040), u32 colour, u16, u16, u32 music ID
(+0x58; FUN_00546b70). Camera/fog/music areas; not light sources (those are in LGHT).
## Spieler, Checkpoints, Ausgang
* **Spielerstart** = Position/Rotation der Strat-Instanz **`Llama`** (genau 1× in jedem
Lauf-Level; Typfelder nvars 78, flags 0x248000). Sonderlevel: Fluss (t1l3*) =
`KuscoAndPachaTogetherOnATree`, Achterbahn (t1l7m001/3/5) = `RollerCoaster`,
## Player, checkpoints, exit
* **Player start** = position/rotation of the strat instance **`Llama`** (exactly 1× in every
running level; type fields nvars 78, flags 0x248000). Special levels: river (t1l3*) =
`KuscoAndPachaTogetherOnATree`, roller coaster (t1l7m001/3/5) = `RollerCoaster`,
t1l2m003 = `LlamaChase`.
* **Checkpoints** = `CheckPointPost` (spawnt `CheckPointHead`), Variante `CheckPointBox`.
* **Ausgang** = `LevelDoor` (1×/Level), Sonderfälle `VillageEndDoor`, `EndGameDoor`.
* Immer vorhanden: `CheatTeleport`, `Wompy` (Funktion nicht geprüft), `CutSceneTrigger`.
* **Checkpoints** = `CheckPointPost` (spawns `CheckPointHead`), variant `CheckPointBox`.
* **Exit** = `LevelDoor` (1×/level), special cases `VillageEndDoor`, `EndGameDoor`.
* Always present: `CheatTeleport`, `Wompy` (function not checked), `CutSceneTrigger`.
## Objekttypen
Vollständige Tabelle Typ × Level (Instanzzahlen): `strat_types_table.md`
(erzeugt mit `tools/strat_table.py <wads>`). Kurz:
* in praktisch allen Levels: Coin, CutSceneTrigger, CutSceneCameraDirectional, LevelDoor,
## Object types
Complete table type × level (instance counts): `strat_types_table.md`
(generated with `tools/strat_table.py <wads>`). Summary:
* in practically all levels: Coin, CutSceneTrigger, CutSceneCameraDirectional, LevelDoor,
Secret, Llama, CheatTeleport, ChargeBooster, Wompy, CheckPointPost, Coin5, CameraSide, Health,
HealthFull, SmashPot, RubyIdol, Grapes, HealthGenerator, RubyIdolDoor …
* ~300 Typen insgesamt, ~200 weitere nur intern gespawnt (Splitter, Seile, Effekte).
* **Jeder Level hat seinen eigenen kompilierten Strat-Code + Modelle in STPC.** Ein Typ ist
nur in Levels platzierbar, deren STPC ihn enthält (`strats.py level.wad --all` listet alle
benannten Strats im Code, auch nie direkt platzierte). Der Code enthält **absolute
STPC-Offsets** (z. B. Spawn-Stubs `b2 <abs> e0 …`), STPC ist also nicht verschiebbar,
ohne zu relozieren. Typen aus anderen Levels zu übernehmen = STPC (+ TEXT-Indizes)
zusammenführen → aufwendig, vorerst nicht.
* "Spawn-Stub"-Einträge (z. B. SmashPot): MAP zeigt auf einen kleinen Code
`push X; spawn(abs Ziel, nvars, …); end` → `strats.py` meldet `Name(spawn)`.
* ~300 types in total, ~200 more only spawned internally (splinters, ropes, effects).
* **Every level has its own compiled strat code + models in STPC.** A type can only be
placed in levels whose STPC contains it (`strats.py level.wad --all` lists all
named strats in the code, including ones never placed directly). The code contains **absolute
STPC offsets** (e.g. spawn stubs `b2 <abs> e0 …`), so STPC cannot be moved
without relocating. Taking types from other levels = merging STPC (+ TEXT indices)
→ laborious, not for now.
* "Spawn stub" entries (e.g. SmashPot): MAP points to a small piece of code
`push X; spawn(abs target, nvars, …); end` → `strats.py` reports `Name(spawn)`.
## Regeln beim Schreiben einer neuen MAP
1. Tile-Blöcke 2/8/9/11 parallel halten (gleiche Anzahl/Reihenfolge); Block 8 aus 2 ableiten;
Block 9 = Feld 5 aus 2; Vertexfarben-Länge exakt = Σ nv(Modell)·Frames (+Alt).
2. Alt-Block: Anzahl = Tiles mit hatAlt≠0, gleiche Reihenfolge. Neue Tiles einfach hinten
anhängen (Strats können Tiles per Index ersetzen – `stMapadd/stMapreplace` – deshalb
bestehende Indizes nicht umsortieren).
3. Alle Tiles in 0..W × 0..H (z negativ!). Regionsraster W·H Einträge.
4. Strat: Code-Offset muss Strat-Anfang in **diesem** STPC sein; nvars/slots/flags/extra
**vom Original-Typ kopieren**; Parameter in Pool anhängen, Index/Anzahl setzen; Wegpunkt
−1 oder gültiger Index (manche Typen brauchen Wegpunkte, sonst "removing strat").
5. poolExtra erhöhen, wenn mehr Strats.
6. WFPC-Flags bestimmen, welche optionalen Blöcke vorhanden sein müssen – nicht ändern.
## Rules for writing a new MAP
1. Keep tile blocks 2/8/9/11 in parallel (same count/order); derive block 8 from 2;
block 9 = field 5 of 2; vertex colour length exactly = Σ nv(model)·frames (+alt).
2. Alt block: count = tiles with hasAlt≠0, same order. Simply append new tiles
at the end (strats can replace tiles by index – `stMapadd/stMapreplace` – so
do not reorder existing indices).
3. All tiles within 0..W × 0..H (z negative!). Region grid has W·H entries.
4. Strat: code offset must be a strat start in **this** STPC; **copy** nvars/slots/flags/extra
**from the original type**; append parameters to the pool, set index/count; waypoint
−1 or a valid index (some types need waypoints, otherwise "removing strat").
5. Increase poolExtra if there are more strats.
6. WFPC flags determine which optional blocks must be present – do not change them.
## Werkzeuge
* `mapchunk.py` – parse/build (Roundtrip identisch für alle 34 MAPs), `vcol_split` je Tile.
## Tools
* `mapchunk.py` – parse/build (identical roundtrip for all 34 MAPs), `vcol_split` per tile.
* `map_dump.py level.wad [--strats --tiles --lights]`
* `strats.py level.wad [--all]` – Typen mit Instanzzahlen, Code-Offsets, Typfeldern.
* `strat_table.py wads_dir` – Typ×Level-Tabelle.
* `stpcwalk.py` – STPC grob ablaufen (Modelle → Animsets → [WFPC&0x100-Block] → Strat-Code).
* `make_testlevel.py quelle.wad ziel.wad [--name …] [--spacing 3] [--floor-model N] [--keep-original]`
– sucht freien Rasterbereich, legt Bodenplatte aus Kopien des häufigsten flachen
Boden-Tiles (t1l1m001: TRAK-Modell 12) an, stellt je Typ eine Instanz (Parameter vom
Original) im 6er-Raster auf, Spieler an den Rand, Parkour (6 Trittplatten +0.5 mit
Lücken, Münzen) zu einer Zielplattform mit LevelDoor. Ausgelassen: Camera*/CutScene*/
Force*/Trigger. Test (nicht im Spiel geprüft): T1L9M001 → 1753 Tiles, 36 Strats, 27 Typen.
* `strats.py level.wad [--all]` – types with instance counts, code offsets, type fields.
* `strat_table.py wads_dir` – type×level table.
* `stpcwalk.py` – roughly walks STPC (models → animsets → [WFPC&0x100 block] → strat code).
* `make_testlevel.py src.wad dst.wad [--name …] [--spacing 3] [--floor-model N] [--keep-original]`
– finds a free grid area, lays a floor plate from copies of the most common flat
floor tile (t1l1m001: TRAK model 12), places one instance per type (parameters from the
original) on a 6-unit grid, the player at the edge, and a parkour (6 stepping plates +0.5 with
gaps, coins) leading to a goal platform with a LevelDoor. Skipped: Camera*/CutScene*/
Force*/Trigger. Test (not checked in game): T1L9M001 → 1753 tiles, 36 strats, 27 types.
## Andere Chunks (Dispatcher 0x558960)
* WFPC: u32 Feature-Flags (→ 0x6da330), z. B. 0x18410690.
* INFO/VERS: je u32 1 (ignoriert).
* NAME: u32 + Levelname.
* TRAK: Tile-Modelle (u32 n, n×0x84-Header, dann Daten; Header+0x6c u16 nVert, +0x6e nFace(0x1c),
+0x78/7a/7c Anzahl 0x20-Primitive). Face+8 u16 = Index in TEXT-Texturliste.
* STPC: u32 n Modelle (je 0x8c-Header direkt + Daten), u32 n Animsets (0x20-Header + Daten),
[WFPC&0x100: Zusatzmodelle], dann Strat-Code bis Chunkende.
* LGHT: u32 n; je Licht u8 typ, u8 r, u8 g, u8 b (×2), Typ 1/2/…: weitere u32 (Richtung/Position,
z negiert). FUN_0042c180.
* LNFO: 2 u32 (→ 0x5ff070, 0x5ff060), z. B. 55, 2.
* LGPC: u32 n(+1), u32 m, u32; dann (n+1)·m × u32 Größen, danach die Blobs. FUN_00558db0.
* FONT: 256 × 4 u16 (Glyphen-Rechtecke). SPRT: u32 (bei WFPC&0x100000 Liste).
* SMPC / AMPC / SRPC: Soundbänke ("cvg"-Samples / "ambi" Ambient / Referenzen), FUN_00545350.
* 'GRIM' (nicht in Level-WADs gesehen): FUN_00437b40.
## Other chunks (dispatcher 0x558960)
* WFPC: u32 feature flags (→ 0x6da330), e.g. 0x18410690.
* INFO/VERS: u32 1 each (ignored).
* NAME: u32 + level name.
* TRAK: tile models (u32 n, n×0x84 header, then data; header+0x6c u16 nVert, +0x6e nFace(0x1c),
+0x78/7a/7c count of 0x20 primitives). Face+8 u16 = index into the TEXT texture list.
* STPC: u32 n models (each 0x8c header directly + data), u32 n animsets (0x20 header + data),
[WFPC&0x100: extra models], then strat code up to the end of the chunk.
* LGHT: u32 n; per light u8 type, u8 r, u8 g, u8 b (×2), type 1/2/…: more u32 (direction/position,
z negated). FUN_0042c180.
* LNFO: 2 u32 (→ 0x5ff070, 0x5ff060), e.g. 55, 2.
* LGPC: u32 n(+1), u32 m, u32; then (n+1)·m × u32 sizes, followed by the blobs. FUN_00558db0.
* FONT: 256 × 4 u16 (glyph rectangles). SPRT: u32 (list if WFPC&0x100000).
* SMPC / AMPC / SRPC: sound banks ("cvg" samples / "ambi" ambient / references), FUN_00545350.
* 'GRIM' (not seen in level WADs): FUN_00437b40.
+2 -2
View File
@@ -1,4 +1,4 @@
| Typ | Level (MAP-Instanzen) | Σ | im Code von n Levels |
| Type | Level (MAP instances) | Σ | in code of n levels |
|---|---|---|---|
| Coin | 1l1m001:36 1l1m002:29 1l1m003:11 1l1m004:6 1l2m001:12 1l2m002:26 1l2m003:23 1l3m001:45 1l3m002:55 1l3m003:25 1l3m004:20 1l4m001:54 1l4m002:28 1l4m003:41 1l5m001:32 1l5m002:22 1l5m003:29 1l6m001:19 1l6m002:33 1l6m003:1 1l7m002:39 1l7m004:46 1l8m001:27 1l8m002:45 1l8m003:10 1l8m004:46 1l8m005:27 | 787 | 27 |
| CutSceneTrigger | 1l1m001:1 1l1m002:1 1l1m003:1 1l1m004:1 1l2m001:1 1l2m002:1 1l2m003:1 1l3m001:1 1l3m002:1 1l3m003:1 1l3m004:1 1l4m001:1 1l4m002:1 1l4m003:1 1l5m001:1 1l5m002:1 1l5m003:1 1l6m001:5 1l6m002:5 1l6m003:3 1l7m002:1 1l7m004:2 1l8m001:1 1l8m002:1 1l8m003:1 1l8m004:7 1l8m005:2 | 45 | 27 |
@@ -339,4 +339,4 @@
| YzmaThrone | 1l6m003:1 | 1 | 1 |
| fat_bird | 1l3m003:1 | 1 | 1 |
Nur im Code (nie direkt in MAP): AppearedPlatform, AppleHole, AppleSeedFX, AppleSeedPad, AxeGuardAxe, BalanceStatue, BallShrapnel, Balloon, BalloonFish, BalloonFishPlatform, BalloonPacha, BalloonPlatform, Bat, BeetleBlueBodyPart, BeetleBlueShell, BeetleBodyPart, BeetleShell, Bird, BlobOnTopCity, BlobOnTopJungle, BlobOnTopJungleDay, BlobShell, BlockShadow, BlockheadColl, BossBomb, BouncingCoin, BouncingHealth, BridgePiece, BridgeSmashStatue, BunnyPotion, BunnyRotors, Camera, CameraCollisionInFront, CameraPlayCutSceneAutomatically, ChacaGrapes, ChangePlayerTo, ChargingDoorPad, CheatCharge, CheckExitFinalBit, CheckPointHead, CircleDoorPlatform, CityBarrelHalf, CityGeneralBarrel, CityGeneralFinalAxe, CityMurderBunny, CityWallShrapnel, CoinForBat, Column, ColumnChild, CondorSleeping, CowGuardSpawned, CutSceneDirectionalWithWPs, DropObject, DumbGuardLab, EndDoorPad, EndDoorWeight, EndGameChacaSkip, EndGameChacaSkipShadow, EndGameGuardsGorrila, EndGameGuardsLizard, EndGameGuardsOstrich, EndGameGuardsWartHog, EndGameKronkRope, EndGameTipoSkip, EndGameTipoSkipShadow, EnemyCoin, EnemyCoin5, FallingPitLab, Feather, FishSpit, Flame, FrogPotion, FrogPotionFromPacha, GoldIdolStatue, Groove, GuardLookAhead, HammerForYzma, JengaSpiralLab, JungleDart, JungleDaySlidingDoorPad, JungleFallingRubyIdol, JunglePillarHelper, JunglePressurePad, KidOnLlamaCircle, KidsRubyIdol, KronkIdol, KronkLabNet, KronkOnIcePickup, KronkPotionLlama, KronkRubyIdol, KuscoBunny, KuscoFrog, KuscoTurtle, KuzcoStatueDoor, KuzcoStatuePart, LabFinalPotion, LabLiftPad, LabSlidingDoorBox, LabSlidingDoorBoxJamie, LabSlidingDoorPad, LabSlidingDoorPadJamie, LabSlidingDoorTile, LabSlidingDoorTileJamie, LabSlidingDoorTrigger, LabSlidingDoorTriggerJamie, LabSlidingDoorWhizzJamie, LabWallShrapnel, LabWaterfallPad, LlamaSpit, Lobster, LobsterPotShrapnel, LobsterStars, LobsterStarsChild, Lobsters, Lobsters1, MerryGoRoundCar, MerryGoRoundOracleHead, MurderBunny, NestForBridge, NestForHorizontalLift, New, NewBossBlockBit, OracleDoor, OracleHead, PachaLookAhead, PinataShrapnel, PlayerFallenOff, PressurePad, PressurePadCata, PressurePadPalace, PumpBlock, PumpLift, PumpLiftPad, RealAppleHole, RisingColumnForStatue, RiverBat, RiverCrocDam, RiverCrocMinGame, RiverDamShrapnel, RiverJumpingMonkeyItem, RiverJumpingMonkeyRedItem, RiverSwingingMonkeyItem, RiverSwingingMonkeyRedItem, RockPad, RockShrapnel, RollingBoulderForMountain, RollingBoulderForMountainNoWalls, RopeForBalloon, RopeForBalloonFish, RopeForBalloonPacha, RopeForHelpGirlSkip, RopeForRiverFallingSpider, RopeForRiverHangingSpider, RopeForSpider, RopeForTargetPlatform, RopeForTargetPlatformCity, RopeForTargetPlatformJungleDay, RopeForTargetSpider, RubyIdolReward, RubyIdolStatue, SalamanderNew, SandTimerDoorJungle, SandTimerPad, SandTimerPadCity, SandTimerPadJungle, ScorpionBlack, ScorpionBrown, ScorpionBrownStars, ScorpionBrownStarsChild, ScorpionSalamander, Scorpions, Scorpions2, SecretEffect, SinkingStatuePad, SlidingBlockheadPad2, SmartFlyDoor, SmashLobsterCrate, SmashPot2, SmashPotShrapnel, SmashStatueBloke, SmashWallShrapnel, SmokeOnPlayer, SmokeOnYzma, Sparkles, SparklesRGB, SpawnableNeedleBall, SpawnableNeedleBallRev, SpawnablePop, SpawnableRing, SpawnableSmoker, SpawnedAlligator, SpiderIceBlock, SpitPlatform, Squasher, StatueShrapnel, StunnedStars, StunnedStarsChild, SwitchForBridge, SwitchForHorizontalLift, SwitchForMouthPlatform, SwitchForSummonPlatform, SwitchForVerticalLift, SwitchPlatform, SwitchPlatformLab, SwitchPlatformVillage, TheNewGrooveCamera, ThreePartHead, ThrowAcorn, TickTockSound, TrampolineDoor, TreadmillForBridge, TreadmillForVerticalLift, TurtlePotion, TurtlePotionFromPacha, WaterArcDot, WaterFallSound, WaterSplash, WaterTrail, WaterfallBit, WompyBeingUsed, YzmaBigCutscenePotion, YzmaCatIntroFake, YzmaCatIntroPotion, YzmaDamMist, YzmaDamShrapnel, YzmaInVillageProjectile, YzmaInVillageProjectileCity, YzmaRiverItem, YzmaRubyIdol
Only in code (never placed directly in MAP): AppearedPlatform, AppleHole, AppleSeedFX, AppleSeedPad, AxeGuardAxe, BalanceStatue, BallShrapnel, Balloon, BalloonFish, BalloonFishPlatform, BalloonPacha, BalloonPlatform, Bat, BeetleBlueBodyPart, BeetleBlueShell, BeetleBodyPart, BeetleShell, Bird, BlobOnTopCity, BlobOnTopJungle, BlobOnTopJungleDay, BlobShell, BlockShadow, BlockheadColl, BossBomb, BouncingCoin, BouncingHealth, BridgePiece, BridgeSmashStatue, BunnyPotion, BunnyRotors, Camera, CameraCollisionInFront, CameraPlayCutSceneAutomatically, ChacaGrapes, ChangePlayerTo, ChargingDoorPad, CheatCharge, CheckExitFinalBit, CheckPointHead, CircleDoorPlatform, CityBarrelHalf, CityGeneralBarrel, CityGeneralFinalAxe, CityMurderBunny, CityWallShrapnel, CoinForBat, Column, ColumnChild, CondorSleeping, CowGuardSpawned, CutSceneDirectionalWithWPs, DropObject, DumbGuardLab, EndDoorPad, EndDoorWeight, EndGameChacaSkip, EndGameChacaSkipShadow, EndGameGuardsGorrila, EndGameGuardsLizard, EndGameGuardsOstrich, EndGameGuardsWartHog, EndGameKronkRope, EndGameTipoSkip, EndGameTipoSkipShadow, EnemyCoin, EnemyCoin5, FallingPitLab, Feather, FishSpit, Flame, FrogPotion, FrogPotionFromPacha, GoldIdolStatue, Groove, GuardLookAhead, HammerForYzma, JengaSpiralLab, JungleDart, JungleDaySlidingDoorPad, JungleFallingRubyIdol, JunglePillarHelper, JunglePressurePad, KidOnLlamaCircle, KidsRubyIdol, KronkIdol, KronkLabNet, KronkOnIcePickup, KronkPotionLlama, KronkRubyIdol, KuscoBunny, KuscoFrog, KuscoTurtle, KuzcoStatueDoor, KuzcoStatuePart, LabFinalPotion, LabLiftPad, LabSlidingDoorBox, LabSlidingDoorBoxJamie, LabSlidingDoorPad, LabSlidingDoorPadJamie, LabSlidingDoorTile, LabSlidingDoorTileJamie, LabSlidingDoorTrigger, LabSlidingDoorTriggerJamie, LabSlidingDoorWhizzJamie, LabWallShrapnel, LabWaterfallPad, LlamaSpit, Lobster, LobsterPotShrapnel, LobsterStars, LobsterStarsChild, Lobsters, Lobsters1, MerryGoRoundCar, MerryGoRoundOracleHead, MurderBunny, NestForBridge, NestForHorizontalLift, New, NewBossBlockBit, OracleDoor, OracleHead, PachaLookAhead, PinataShrapnel, PlayerFallenOff, PressurePad, PressurePadCata, PressurePadPalace, PumpBlock, PumpLift, PumpLiftPad, RealAppleHole, RisingColumnForStatue, RiverBat, RiverCrocDam, RiverCrocMinGame, RiverDamShrapnel, RiverJumpingMonkeyItem, RiverJumpingMonkeyRedItem, RiverSwingingMonkeyItem, RiverSwingingMonkeyRedItem, RockPad, RockShrapnel, RollingBoulderForMountain, RollingBoulderForMountainNoWalls, RopeForBalloon, RopeForBalloonFish, RopeForBalloonPacha, RopeForHelpGirlSkip, RopeForRiverFallingSpider, RopeForRiverHangingSpider, RopeForSpider, RopeForTargetPlatform, RopeForTargetPlatformCity, RopeForTargetPlatformJungleDay, RopeForTargetSpider, RubyIdolReward, RubyIdolStatue, SalamanderNew, SandTimerDoorJungle, SandTimerPad, SandTimerPadCity, SandTimerPadJungle, ScorpionBlack, ScorpionBrown, ScorpionBrownStars, ScorpionBrownStarsChild, ScorpionSalamander, Scorpions, Scorpions2, SecretEffect, SinkingStatuePad, SlidingBlockheadPad2, SmartFlyDoor, SmashLobsterCrate, SmashPot2, SmashPotShrapnel, SmashStatueBloke, SmashWallShrapnel, SmokeOnPlayer, SmokeOnYzma, Sparkles, SparklesRGB, SpawnableNeedleBall, SpawnableNeedleBallRev, SpawnablePop, SpawnableRing, SpawnableSmoker, SpawnedAlligator, SpiderIceBlock, SpitPlatform, Squasher, StatueShrapnel, StunnedStars, StunnedStarsChild, SwitchForBridge, SwitchForHorizontalLift, SwitchForMouthPlatform, SwitchForSummonPlatform, SwitchForVerticalLift, SwitchPlatform, SwitchPlatformLab, SwitchPlatformVillage, TheNewGrooveCamera, ThreePartHead, ThrowAcorn, TickTockSound, TrampolineDoor, TreadmillForBridge, TreadmillForVerticalLift, TurtlePotion, TurtlePotionFromPacha, WaterArcDot, WaterFallSound, WaterSplash, WaterTrail, WaterfallBit, WompyBeingUsed, YzmaBigCutscenePotion, YzmaCatIntroFake, YzmaCatIntroPotion, YzmaDamMist, YzmaDamShrapnel, YzmaInVillageProjectile, YzmaInVillageProjectileCity, YzmaRiverItem, YzmaRubyIdol
+110 -110
View File
@@ -1,146 +1,146 @@
# Test-Harness `tools/lamatest.py`
# Test harness `tools/lamatest.py`
Startet "Ein Königreich für ein Lama" unbeaufsichtigt und unsichtbar (Xvfb :5,
Ton in Null-Sink), lädt TestLevel Kapitel 1 (T1L9M001.wad) und macht Screenshots.
Getestet am 2026-11-06 mit Wine 6.0.3, llvmpipe.
Starts "Ein Königreich für ein Lama" unattended and invisibly (Xvfb :5,
sound into a null sink), loads TestLevel chapter 1 (T1L9M001.wad) and takes screenshots.
Tested on 2026-11-06 with Wine 6.0.3, llvmpipe.
## Befehle
## Commands
```
lamatest.py run <wad> <outdir> [n] # alles in einem: installieren, start, tolevel, tour, stop
lamatest.py start # Xvfb :5 + Null-Sink lama_stumm + groove.exe
lamatest.py tolevel # Menü -> Slot mit TestLevel -> Kapitel 1 -> Gameplay
lamatest.py tour <outdir> [n] # n Kamera-Fotos (Rundumblick) + n Lauf-Fotos + Lama-Sicht
lamatest.py shot out.png # Vollbild 1920x1080
lamatest.py key <xdotool-key> [s] # Taste s Sekunden halten (Default 0.12)
lamatest.py keys Up:1.5 a:0.5 x # Folge, Name[:Haltezeit], auch sleep=2
lamatest.py seq <outdir> "TOKENS" # --seq-Tokens (shot:, run:, rj: …) im laufenden Spiel
lamatest.py run <wad> <outdir> [n] # all in one: install, start, tolevel, tour, stop
lamatest.py start # Xvfb :5 + null sink lama_stumm + groove.exe
lamatest.py tolevel # menu -> slot with TestLevel -> chapter 1 -> gameplay
lamatest.py tour <outdir> [n] # n camera shots (look around) + n walking shots + llama view
lamatest.py shot out.png # full screen 1920x1080
lamatest.py key <xdotool-key> [s] # hold key for s seconds (default 0.12)
lamatest.py keys Up:1.5 a:0.5 x # sequence, name[:hold time], also sleep=2
lamatest.py seq <outdir> "TOKENS" # --seq tokens (shot:, run:, rj: …) in the running game
lamatest.py status | stop | restore-wad
```
### `run`-Optionen (2026-11-06 ergänzt)
### `run` options (added 2026-11-06)
```
lamatest.py run <wad> <outdir> [n] [--seq "TOKENS"] [--tour] [--keep]
```
* **WAD-Wiederherstellung**: `run` merkt sich die *vorher installierte*
`T1L9M001.wad` (`$LAMATEST_DIR/vorher_T1L9M001.wad`) und spielt sie nach dem Lauf zurück –
also nicht `.orig`. Mit `--keep` bleibt die getestete WAD installiert.
(`restore-wad` stellt weiterhin explizit `.orig` her.)
* `--seq` ersetzt die Standard-Tour durch eine Tastenfolge (mit `--tour` zusätzlich Tour):
* `Up:1.5`, `a:0.5`, `x` … Taste[:Haltezeit] wie bei `keys`; `sleep=1`
* `shot:NAME` → `seq_NN_NAME.jpg`, `fp:NAME` → Foto in Lama-Sicht (Return gehalten)
* `dialog` → Dialog/Letterbox wegklicken
* `Up+x:0.8` → Tasten gleichzeitig halten
* `rj:T1:T2` → Anlauf-Sprung (Up halten, nach T1 s X, Up noch T2 s)
* `dj:T` → Doppel-X mit Anlauf, `hop:N:T` → Up halten, N× alle T s springen
* `run:T:x@1.3,x@2.9,shot@5.0,Left@3.0-3.4` → Up T s halten, nach Zeitplan (s ab Beginn)
X tippen / fotografieren (`seq_NN_runT.jpg`) / Taste von–bis halten; `run/Left:T:…` hält
statt Up eine andere Taste. Echtzeit: ein Foto blockiert ~0,4 s (verzögert folgende Ereignisse
und bremst das Spiel) → keine Fotos kurz vor Sprüngen.
* Steuerung ist **kamerarelativ** (Up = in Kamerablickrichtung, Left/Right seitlich).
A/S drehen die Kamera, S:0.45 ≈ 90° nach links schauen.
* Messwerte (Lama): ~1,1 Einheiten/s Laufen, Sprung 0,35 hoch, mit Anlauf ~1,3 weit – siehe
levelbau.md. Zeitgesteuerte Läufe streuen um ~0,3 Einheiten (Startverzögerung, Bildrate);
längere Strecken brauchen „Anschläge“ (Wand/Stufe, an der das Lama stehen bleibt).
* Kamera: nach Laufen gegen eine Wand oder einem Sturz dreht sich die Kamera unvorhersehbar →
Up läuft danach in eine andere Richtung. A/S-Drehung (S:0.45) ist nur ungefähr 90°.
* Kompletter Testwelt-Parkour per Skript: `levels/testwelt_parkour.seq` (levelbau.md).
* **WAD restore**: `run` remembers the *previously installed*
`T1L9M001.wad` (`$LAMATEST_DIR/vorher_T1L9M001.wad`) and restores it after the run –
i.e. not `.orig`. With `--keep` the tested WAD stays installed.
(`restore-wad` still explicitly restores `.orig`.)
* `--seq` replaces the default tour with a key sequence (with `--tour` the tour runs as well):
* `Up:1.5`, `a:0.5`, `x` … key[:hold time] as with `keys`; `sleep=1`
* `shot:NAME` → `seq_NN_NAME.jpg`, `fp:NAME` → shot in llama view (Return held)
* `dialog` → click away dialog/letterbox
* `Up+x:0.8` → hold keys simultaneously
* `rj:T1:T2` → running jump (hold Up, X after T1 s, Up for another T2 s)
* `dj:T` → double X with run-up, `hop:N:T` → hold Up, jump N× every T s
* `run:T:x@1.3,x@2.9,shot@5.0,Left@3.0-3.4` → hold Up for T s, following a schedule (s from start)
tap X / take a shot (`seq_NN_runT.jpg`) / hold a key from–to; `run/Left:T:…` holds
another key instead of Up. Real time: a shot blocks ~0.4 s (delays subsequent events
and slows the game down) → no shots shortly before jumps.
* Controls are **camera-relative** (Up = in the camera's view direction, Left/Right sideways).
A/S rotate the camera, S:0.45 ≈ look 90° to the left.
* Measurements (llama): ~1.1 units/s walking, jump 0.35 high, ~1.3 far with run-up – see
levelbau.md. Timed runs scatter by ~0.3 units (start delay, frame rate);
longer distances need "stops" (a wall/step where the llama comes to a halt).
* Camera: after running into a wall or a fall the camera rotates unpredictably →
Up then walks in a different direction. A/S rotation (S:0.45) is only roughly 90°.
* Complete test world parkour as a script: `levels/testwelt_parkour.seq` (levelbau.md).
* `LAMATEST_SHOT=1280` (Umgebung): `shot:`/`fp:`-Fotos 1280×720, Qualität 85 statt 960×540 (Vorschaubilder).
* „EINGEFROREN?“ bei einer `--seq` ohne Bewegung (z. B. nur `sleep=3 shot:x`) ist ein Fehlalarm –
das stehende Lama ändert das Bild nicht; mit Kameradrehung/Laufen kommt „OK“.
* Gute Blickwinkel im Spiel sind schwer: die Kamera hängt tief hinter dem Lama; erhöhte
Startpunkte (Podest, Parkour-Checkpoint `pk2` + `A:0.6`) zeigen mehrere Bereiche.
* `LAMATEST_SHOT=1280` (environment): `shot:`/`fp:` shots at 1280×720, quality 85 instead of 960×540 (preview images).
* "EINGEFROREN?" (frozen?) for a `--seq` without movement (e.g. only `sleep=3 shot:x`) is a false alarm –
the standing llama does not change the image; with camera rotation/walking you get "OK".
* Good viewing angles in the game are hard: the camera hangs low behind the llama; elevated
start points (pedestal, parkour checkpoint `pk2` + `A:0.6`) show several areas.
Hilfsskript für Serien: `$CLAUDE_JOB_DIR/tmp/lb_run.sh WAD NAME "SEQ"` (Montage `lb_mNAME.jpg`,
WAD-Pfad absolut!), `rs.sh NAME "SEQ"` (Neustart mit installierter WAD + `seq`, Montage `i/NAME.jpg`).
**Achtung**: kein `pgrep -f groove.exe` in Warteschleifen – das trifft die eigene Shell
(Kommandozeile enthält "groove.exe") und lamatest hält sie für eine fremde Instanz.
Helper script for series: `$CLAUDE_JOB_DIR/tmp/lb_run.sh WAD NAME "SEQ"` (montage `lb_mNAME.jpg`,
WAD path absolute!), `rs.sh NAME "SEQ"` (restart with installed WAD + `seq`, montage `i/NAME.jpg`).
**Caution**: no `pgrep -f groove.exe` in wait loops – it matches your own shell
(the command line contains "groove.exe") and lamatest takes it for a foreign instance.
Zustand/PIDs/wine.log: `$LAMATEST_DIR` (Default `/tmp/lamatest-$USER`).
State/PIDs/wine.log: `$LAMATEST_DIR` (default `/tmp/lamatest-$USER`).
`run` Exit-Codes: 0 OK, 3 Absturz, 4 Timeout/hängt, 5 Bild eingefroren.
Ausgabe in `<outdir>`: `report.txt`, `wine.log` (stderr, auf 2 MB gekürzt),
`00_geladen.png` (voll) + `.jpg`, `tour_*.jpg` (960x540, ~30 KB), bei Fehler
`zz_absturz.jpg` / `zz_timeout.jpg`. Ein Lauf ≈ 75 s, ≈ 1,2 MB.
`run` exit codes: 0 OK, 3 crash, 4 timeout/hang, 5 image frozen.
Output in `<outdir>`: `report.txt`, `wine.log` (stderr, truncated to 2 MB),
`00_geladen.png` (full) + `.jpg`, `tour_*.jpg` (960x540, ~30 KB), on failure
`zz_absturz.jpg` / `zz_timeout.jpg`. One run ≈ 75 s, ≈ 1.2 MB.
## Sicherheitsregeln (im Skript umgesetzt)
## Safety rules (implemented in the script)
- Bricht ab, wenn eine groove.exe mit anderem DISPLAY läuft (Elias' Instanz).
- xdotool nur mit DISPLAY=:5.
- Ton: eigener Null-Sink, App wird nie stummgeschaltet; `stop` entlädt den Sink.
- Spielstände (`*.ksv` im Spielordner) werden bei `start` gesichert und bei `stop`
bei Änderung zurückgespielt (neu entstandene gelöscht).
- `stop`: `wineserver -k` nur, wenn keine fremde groove.exe läuft, sonst nur eigene PIDs.
- Der Ausgang der Testwelt speichert Fortschritt (Kapitel 2 frei) – wird bei `stop` mit den
Spielständen zurückgespielt.
- WAD: erstes `run` sichert das Original als `mods/testlevel/wads/T1L9M001.wad.orig`;
`restore-wad` spielt es zurück. Das Overlay (`lama-mod an`) besteht aus
**Symlinks** auf die Mod-Dateien → Überschreiben von
`~/.wine-lama/mods/testlevel/wads/T1L9M001.wad` wirkt sofort; `lama-mod an` wird
nur aufgerufen, wenn der Overlay-Link fehlt.
- Aborts if a groove.exe with a different DISPLAY is running (Elias' instance).
- xdotool only with DISPLAY=:5.
- Sound: own null sink, the app is never muted; `stop` unloads the sink.
- Save games (`*.ksv` in the game folder) are backed up on `start` and restored
on `stop` if changed (newly created ones are deleted).
- `stop`: `wineserver -k` only if no foreign groove.exe is running, otherwise only own PIDs.
- The test world exit saves progress (chapter 2 unlocked) – restored on `stop` together with the
save games.
- WAD: the first `run` backs up the original as `mods/testlevel/wads/T1L9M001.wad.orig`;
`restore-wad` restores it. The overlay (`lama-mod an`) consists of
**symlinks** to the mod files → overwriting
`~/.wine-lama/mods/testlevel/wads/T1L9M001.wad` takes effect immediately; `lama-mod an` is
only called if the overlay link is missing.
## Ablauf `tolevel` (gemessen)
## `tolevel` sequence (measured)
| Schritt | Taste | Erkennung | t (ab Start) |
| Step | Key | Detection | t (from start) |
|---|---|---|---|
| Logo → Hauptmenü | – | OCR Ausschnitt 700x300+1150+650, Schwelle 75 % → nur markierter Punkt "Spiel starten" lesbar | ~6 s |
| Spiel starten | Return | – | |
| Spielstandauswahl (Karussell, startet bei Slot 2, **kein** Wrap) | Left ×3, dann Right bis "TestLevel" | OCR Ausschnitt 1600x280+160+740 (fuzzy, 2 Schwellen) | ~20 s |
| Slot 3 laden | Return | Auswahlkarte = >75 % beige Pixel | ~25 s |
| Kapitel 1 aktivieren (Cursor aus KSV +4/+5) | Return | | |
| Kamera-Flug (Letterbox: oberste+unterste Zeile schwarz) | Return überspringt | | +1,5 s |
| Gameplay | – | hell, kein Letterbox, goldenes HUD oben links (oder >25 s) 3× hintereinander | ~29 s gesamt, Laden ~4 s |
| Logo → main menu | – | OCR crop 700x300+1150+650, threshold 75 % → only the highlighted item "Spiel starten" is readable | ~6 s |
| "Spiel starten" (start game) | Return | – | |
| Save slot selection (carousel, starts at slot 2, **no** wrap) | Left ×3, then Right until "TestLevel" | OCR crop 1600x280+160+740 (fuzzy, 2 thresholds) | ~20 s |
| Load slot 3 | Return | selection map = >75 % beige pixels | ~25 s |
| Activate chapter 1 (cursor from KSV +4/+5) | Return | | |
| Camera fly-through (letterbox: top+bottom row black) | Return skips | | +1.5 s |
| Gameplay | – | bright, no letterbox, golden HUD top left (or >25 s) 3× in a row | ~29 s total, loading ~4 s |
Ohne Überspringen dauert der Kamera-Flug im Dorf ~20 s.
Ladebild = T191.COR ("TestLevel Kapitel 1 … LADEVORGANG").
Without skipping, the camera fly-through in the village takes ~20 s.
Loading screen = T191.COR ("TestLevel Kapitel 1 … LADEVORGANG").
Fallback: wird kein Menü erkannt, nach 40 s Return (überspringt Demo). Achtung:
Return auf einem falsch erkannten Bildschirm kann ein anderes Spiel starten –
Spielstände werden aber am Ende zurückgespielt.
Fallback: if no menu is detected, Return after 40 s (skips the demo). Caution:
Return on a misdetected screen can start a different game –
but save games are restored at the end.
## Steuerung PC (Tastatur, aus "Optionen → Tastatur konfigurieren", verifiziert)
## PC controls (keyboard, from "Optionen → Tastatur konfigurieren", verified)
| Aktion | Taste |
| Action | Key |
|---|---|
| Auf / Ab (vorwärts / rückwärts laufen) | Up / Down |
| Links / Rechts (drehen) | Left / Right |
| Sprung (auch Dialog weiter) | X |
| Up / Down (walk forward / backward) | Up / Down |
| Left / Right (turn) | Left / Right |
| Jump (also advances dialog) | X |
| Action | Z |
| Power | Space |
| Lama-Sicht (Ego-Perspektive, halten) | Return |
| Kamera nach links / rechts drehen | A / S (0,5 s halten ≈ 90°) |
| Pausenmenü | Escape |
| Llama view (first person, hold) | Return |
| Rotate camera left / right | A / S (hold 0.5 s ≈ 90°) |
| Pause menu | Escape |
Registry `HKCU\Software\Disney Interactive\Emperors New Groove\1.0`,
`"InputDevice Wine Keyboard"` = 14 DIK-Codes (je 4 Hex):
`"InputDevice Wine Keyboard"` = 14 DIK codes (4 hex digits each):
CB Left, CD Right, C8 Up, D0 Down, 1E A, 1F S, 39 Space, 2D X, 1C Return,
19 P, C9 PgUp, D1 PgDn, 2C Z, 18 O. Die Reihenfolge der Registry ≠ Menüreihenfolge;
P/PgUp/PgDn/O vermutlich Pause/Inventar (laut exe-Strings: PAUSE GAME, CYCLE
INVENTORY UP/DOWN, USE AN ITEM, TURN 180, SELECT) – nicht verifiziert.
Weitere Werte dort: CameraMode, ControlType, ScreenW/H (1920x1142), CDPath=L:\.
19 P, C9 PgUp, D1 PgDn, 2C Z, 18 O. The registry order ≠ menu order;
P/PgUp/PgDn/O probably pause/inventory (according to exe strings: PAUSE GAME, CYCLE
INVENTORY UP/DOWN, USE AN ITEM, TURN 180, SELECT) – not verified.
Other values there: CameraMode, ControlType, ScreenW/H (1920x1142), CDPath=L:\.
Pausenmenü (Escape): Grafik-/Tonoptionen, Tastatur/Joystick konfigurieren,
Weitere Optionen, Weiter, "Beenden und zur Auswahlkarte wechseln" (Bestätigung
Ja/Nein, Default Nein → Up, Return), Spiel beenden. Hauptmenü-Untermenüs: Escape = zurück.
Pause menu (Escape): graphics/sound options, configure keyboard/joystick,
more options, continue, "Beenden und zur Auswahlkarte wechseln" (quit to selection map; confirmation
Ja/Nein, default Nein → Up, Return), quit game. Main menu submenus: Escape = back.
## Beobachtungen Dorf-1-Kopie
## Observations on the village 1 copy
- Läuft man im Dorf vorwärts, startet sofort der Pacha-Dialog (Letterbox mit
Untertiteln, ~9× X zum Durchklicken). `tour` erkennt das, fotografiert den
Dialog einmal und klickt ihn weg.
- Lama kann in der Tour von der Klippe fallen (Respawn) – egal für Sichtprüfung.
- Walking forward in the village immediately starts the Pacha dialog (letterbox with
subtitles, ~9× X to click through). `tour` detects this, photographs the
dialog once and clicks it away.
- The llama can fall off the cliff during the tour (respawn) – irrelevant for visual checks.
## Absturzerkennung
## Crash detection
Pro Poll: groove.exe-Prozess auf :5 weg / Fenster "Groove" weg / Fenster mit
"Programmfehler", "error", "exception" … (Wine-Crashdialog). Im Report steht die
Zeile `wine: Unhandled page fault … at address XXXXXXXX` (Adresse in Ghidra
nachschlagen). Getestet: auf 300 KB gekürzte WAD und WAD mit 2000 zufälligen
Bytes → beide Absturz während des Ladebilds, erkannt nach ~5 s (Exit 3).
Harmloses Log-Rauschen (gstreamer/MPEG-1 fehlt, ole, ChangeDisplaySettings,
MIDIMAP) wird im Report ausgefiltert.
Per poll: groove.exe process on :5 gone / window "Groove" gone / window with
"Programmfehler", "error", "exception" … (Wine crash dialog). The report contains the
line `wine: Unhandled page fault … at address XXXXXXXX` (look up the address in Ghidra).
Tested: WAD truncated to 300 KB and WAD with 2000 random
bytes → both crash during the loading screen, detected after ~5 s (exit 3).
Harmless log noise (gstreamer/MPEG-1 missing, ole, ChangeDisplaySettings,
MIDIMAP) is filtered out of the report.
## Abhängigkeiten
## Dependencies
Xvfb, xdotool, ImageMagick (import/convert), tesseract mit `deu`, pactl, wine.
Xvfb, xdotool, ImageMagick (import/convert), tesseract with `deu`, pactl, wine.
+39 -39
View File
@@ -1,51 +1,51 @@
# TEXT-Chunk (Texturen)
# TEXT chunk (textures)
Quelle: groove.exe `FUN_004067b0` (Größenlauf), `FUN_004071d0` → `FUN_00406880` (Paletten),
`FUN_00406ab0` (Seiten), `FUN_00406a10` (RLE), `FUN_00406d30` (Texturliste),
`FUN_00406e40` (Texturanimationen), `FUN_00407240`/`FUN_00408470` (Upload/Konvertierung).
Werkzeug: `tools/textchunk.py`, `tools/text_export.py`. Parse + Neuaufbau ist für **alle
Level-WADs byte-identisch** (auch der RLE-Encoder erzeugt exakt die Originaldaten).
Source: groove.exe `FUN_004067b0` (size pass), `FUN_004071d0` → `FUN_00406880` (palettes),
`FUN_00406ab0` (pages), `FUN_00406a10` (RLE), `FUN_00406d30` (texture list),
`FUN_00406e40` (texture animations), `FUN_00407240`/`FUN_00408470` (upload/conversion).
Tools: `tools/textchunk.py`, `tools/text_export.py`. Parse + rebuild is **byte-identical
for all level WADs** (the RLE encoder also reproduces the original data exactly).
## Aufbau
## Layout
```
u32 nPal (in allen WADs 0)
nPal × { u32 id, u32 n, u16 farbe[n], u8 extra[n] }
u32 nSeiten (13–22 je Level, alle 256×256)
nSeiten × { u32 flags, u32 w, u32 h, Daten }
flags == 0 : w·h Byte (8 Bit, Palette) – unbenutzt
flags & 0x80 : u32 Länge, RLE16 – alle Originalseiten
sonst : w·h u16 roh
u32 nPal (0 in all WADs)
nPal × { u32 id, u32 n, u16 colour[n], u8 extra[n] }
u32 nPages (13–22 per level, all 256×256)
nPages × { u32 flags, u32 w, u32 h, data }
flags == 0 : w·h bytes (8 bit, palette) – unused
flags & 0x80 : u32 length, RLE16 – all original pages
otherwise : w·h u16 raw
u32 nTex (~180–1260)
nTex × 8 Byte { u16 flags, u8 seite, u8 clut(0xff), u8 x0, u8 x1, u8 y0, u8 y1 } (Kanten inklusive)
nTex × 8 bytes { u16 flags, u8 page, u8 clut(0xff), u8 x0, u8 x1, u8 y0, u8 y1 } (edges inclusive)
u32 nAnim
nAnim × { u16 texIndex, u8 typ, Nutzdaten je typ }
typ 1,2 : 3 Byte typ 7,9 : u16,u16,u8,u8
typ 8 : u8, u8 nFrames, u8, nFrames × u16 texIndex (Daumenkino)
typ 10,11: u8,u8,u8,u8 n, n × u16 texIndex
nAnim × { u16 texIndex, u8 type, payload per type }
type 1,2 : 3 bytes type 7,9 : u16,u16,u8,u8
type 8 : u8, u8 nFrames, u8, nFrames × u16 texIndex (flipbook)
type 10,11: u8,u8,u8,u8 n, n × u16 texIndex
```
RLE16: Folge von s16 n; n>0 → n Literal-u16; n<0 → nächstes u16 (−n)-mal.
RLE16: sequence of s16 n; n>0 → n literal u16; n<0 → next u16 repeated (−n) times.
## Pixel
16 Bit **PS1-Reihenfolge**: Bits 0–4 R, 5–9 G, 10–14 B, Bit 15 = STP.
`0x0000` = durchsichtig (Konverter schreibt 0 für Pixel 0, alles andere wird umgerechnet).
STP=0 bei Farbe≠0 tritt auf Seiten mit Blendmodus auf (halbtransparente Pixel).
Export: Alpha 0 = 0x0000, Alpha 128 = STP 0, Alpha 255 = STP 1.
## Pixels
16 bit **PS1 order**: bits 0–4 R, 5–9 G, 10–14 B, bit 15 = STP.
`0x0000` = transparent (the converter writes 0 for pixel 0, everything else is converted).
STP=0 with colour≠0 occurs on pages with a blend mode (semi-transparent pixels).
Export: alpha 0 = 0x0000, alpha 128 = STP 0, alpha 255 = STP 1.
Seiten-flags & 0x7f = Format/Blendklasse (Tabelle 0x570370, je 0x20 Byte):
1 = opak, 2 = eigener Konverter (0x409640), 3 = opak+Flag, 4/7 = Alpha (Blend 1),
5 = Blend 3, 6 = wie 1. (Modi 1/2/3 ≈ PS1-Halbtransparenz-Modi; Details offen.)
Page flags & 0x7f = format/blend class (table 0x570370, 0x20 bytes each):
1 = opaque, 2 = own converter (0x409640), 3 = opaque+flag, 4/7 = alpha (blend 1),
5 = blend 3, 6 = same as 1. (Modes 1/2/3 ≈ PS1 semi-transparency modes; details open.)
## Texturliste
Faces der Modelle (TRAK/STPC, Face+8 u16) verweisen auf den Index in der Texturliste;
Eintrag = Rechteck auf einer Seite. `flags`-Lowbyte 4/5/6/0xc6, Highbyte 1/4/17/20/36/…
(Bedeutung offen; vermutl. Blend/Größenklasse). Fürs Ersetzen: Liste unverändert lassen
und nur Pixel in den Seiten tauschen → keine Querverweise betroffen.
## Texture list
Model faces (TRAK/STPC, face+8 u16) reference an index into the texture list;
entry = rectangle on a page. `flags` low byte 4/5/6/0xc6, high byte 1/4/17/20/36/…
(meaning open; probably blend/size class). For replacing: leave the list unchanged
and only swap pixels in the pages → no cross references affected.
## Export / Ersetzen
## Export / replace
```
text_export.py export level.wad ordner # pageNN_fXX.png + texinfo.txt (≈370 KB je Level)
text_export.py import level.wad ordner neu.wad
text_export.py export level.wad folder # pageNN_fXX.png + texinfo.txt (≈370 KB per level)
text_export.py import level.wad folder new.wad
```
Import ändert nur Pixel, deren RGBA vom Export abweicht → unveränderte Seiten bleiben
bitgenau. Größe muss 256×256 bleiben. Neue Seiten/Texturen anhängen ist möglich
(nTex/nSeiten erhöhen), dann müssen aber Faces in TRAK/STPC die neuen Indizes verwenden.
Import only changes pixels whose RGBA differs from the export → unchanged pages stay
bit-exact. Size must stay 256×256. Appending new pages/textures is possible
(increase nTex/nPages), but then faces in TRAK/STPC must use the new indices.