translated the remaining german notes, python comments, help and messages to english (in-game strings, parsed report lines and cli values stay german)
This commit is contained in:
1 parent
86fe26d786
commit
526f6072f6
36 files changed
+1553
-1544
No files matched your search
+185
-184
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
Reference in new issue
Block a user