Files
llamamod/notes/debugviz.md
T

21 KiB
Raw Permalink Blame History

Debug view: collision & invisible objects in game (tools/debugviz.py)

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.

Usage

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

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.

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]

Run time ≈ 10–40 s per level.

Colour legend

All colours are RGB (0–255). Lines = wireframe (opaque), fill = semi-transparent (alpha 96/255, engine blend mode 1).

Collision surfaces (frame around every collision polygon of the world tiles)

Edge colour by orientation, as long as the material is 0 (normal):

Colour RGB Meaning
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.

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 Colour RGB Meaning
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)

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).

Colour RGB Category Types
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)

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).

How it is built (decisions)

  • 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).

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).

Allowance areas (LlamaMod option debug-wireframe.allowance, 2026-11-08)

Choice off (default) / tips ("Floating spots") / all ("All edges"). Shows the floor test's edge allowance: the engine accepts a floor polygon up to tol = −0x40 − ((0x1000 − ny·32) >> 6) (≈0.016–0.023) outside each edge plane; at sharp corners this becomes a sliver up to tol / sin(θ/2) long (geometry.md "Floor test: inclusion rule …"). Colour pink 255,60,200.

  • Code: mods/debug/FloorTest = bit-exact port of FUN_004036d0's floor branch (same as tools/floortest.py; the sine table is rint(sin(2πi/4096)·4096), identical to DAT_00574318), mods/debug/AllowanceAreas. All geometry uses the engine's plane scale (n·p = d/32 with |n| = 127), not Collision.decode's d/4096, so the bands sit ~0.8 % further from the model origin than the green wireframe.
  • tips: for every placed tile and every up-facing collision polygon (not material 17 = skipped for the llama, not 30 = no floor) and every corner whose tolerance corner lies ≥ MIN_REACH (0.05) beyond the true corner, the area beyond the corner (tolerance region ∩ half-plane past the corner) is sampled in up to 40 rows × 7 points. A sample counts when the exact engine test, with the feet on that polygon's plane, picks this polygon, the point is outside the polygon without tolerance, and no other passing floor of the cell is less than MIN_GAP (0.05) below (material-30 floors count as none). Samples outside the tile's cell are dropped (the engine only tests the cell's tiles). Kept when the winning samples reach ≥ 0.05 outside the polygon: translucent strip (alpha 150) through the winning part of each row + an opaque centre line (4 mm), at the plane height + 0.012, in the tile's marker cell model (no collision). Strat object collision (crates as strats, platforms) is not considered.
  • all: per TRAK model, every up-facing floor polygon (same material filter) gets an opaque ring from its true corners to its tolerance corners (2n vertices, 2n triangles), appended to the model overlay like the wireframe (tile transform, alt models free). Tolerance corners further than 1.5 out are clamped.
  • Budget: if TEXT+TRAK+MAP+STPC+LGPC would exceed 12 MB (16 MB level arena) the level is rebuilt without allowance geometry and the apply log says "allowance areas left out: level too large". Not reached in any original level; a model whose overlay would pass 7900 vertices keeps its wireframe without bands.
  • Numbers (all 33 levels): tips 0–1454 slivers per level (river t1l3m00x most, t1l4m002 394), WAD +0–2.0 MB, ≤ 2 s per level; all: +0.02–2.9 MB, arena max 11.1 MB (t1l3m003). t1l4m002 crate (tile 443): poly 15 corner 2 (reach 0.18, contains Kuzco's spot 57.204/43.733), poly 11 corner 2 (0.175), polys 10/14 (0.11/0.13) on the other end, 12/13 barely (0.05).
  • Tests: AllowanceAreasTest (exact port vs. 44 floortest.py points, sine table vs. groove.exe, slivers of t1l4m002, no slivers on the flat start ground, band counts, process in each mode).

Statistics per level (build of 2026-11-06)

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 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

(Collision polygons counted per TRAK model, not per tile instance.)

Tests (2026-11-06)

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).