testlevel chapters 2-31 overwrite game memory (coin counter and flag rows end after world 8: progress, language, options): level projects, play-tests and the arena use chapters 1 and 32-49, old settings migrate; level tally counts the level's own coins and secrets
This commit is contained in:
1 parent
987b0104fa
commit
d6b852a1b6
35 files changed
+1015
-175
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
@@ -68,6 +68,31 @@ stein, stein_kante, stein_moos, platte, holz_dunkel, holz_maser, holzbalken, rin
|
||||
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`.
|
||||
|
||||
**Texture names on other bases** (2026-11-07). The `TEX` indices are village (t1l1m001) indices; on another
|
||||
base the same index is a different picture (village 2: index 1 = dark red floor). Names are therefore
|
||||
resolved per base by picture: `levelbuild.BaseTextures` holds a fingerprint (FNV-1a over page format,
|
||||
size and pixels of the entry's rectangle) of every named village texture; a name maps to the village index
|
||||
if the base shows that picture there (village 1: always → outputs unchanged), else to the first base entry
|
||||
with the same picture (flags 0x104 preferred). Coverage of the 47 names: t1l1m002 38 (missing: the three
|
||||
flower textures, `wood_grain*`, `wood_brown*`, `wood_rings`, `wood_edge`), t1l1m003 29, t1l1m004 28,
|
||||
t1l2m001 9, t1l3m001/t1l5m001 0–2.
|
||||
* Java builder (`LevelBuilder`, test world/arena/jump lab, `fromCd`): a missing picture is **copied from the
|
||||
village** (CD file loader `wads/t1l1m001.wad`): packed onto a new 256×256 page (same page flags, 1 px
|
||||
clamped border) and appended as an entry after the base table; flat colours and copies share the
|
||||
appended range in order of first use. Without a loader (editor brushes: `BrushCompiler` passes none)
|
||||
the name is rejected: "texture 'flowers' is a village texture this base level does not have …"
|
||||
(the editor shows that as a brush warning and only offers the names the scene's base has).
|
||||
`LevelBuilder.baseTextureNames()` lists the names a base has.
|
||||
* Python `tools/levelbuild.py --base`: same matching (md5 of RGBA rectangle); missing names raise a
|
||||
KeyError (no copying).
|
||||
* In game (apptest, display :22, 2026-11-07): Java-built level on t1l1m002 (grass box, stone box, sand,
|
||||
trees, plus copied `flowers`, `wood_rings`, `wood_grain`), `--editor-import` → level mod, Village
|
||||
chapter 2: grass green, stone grey, trees bark + leaves, copied textures correct
|
||||
(`notes/img/editor/village2_levelbuild_textures.jpg`, `…_textures2.jpg`). Before: grass = dark red
|
||||
(`village2_levelbuild_start_camera.jpg`).
|
||||
* Editor (display :22): on a t1l1m002 scene the brush swatches and the texture grid show the base's own
|
||||
pictures for the names, without the names it lacks (`notes/img/editor/village2_texture_picker.jpg`).
|
||||
**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.
|
||||
|
||||
@@ -190,6 +215,9 @@ loop: if g21 == 3 ; game state "running"
|
||||
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.
|
||||
* **Only chapters 1 and 32–49 are safe** for world 9: the per-chapter coin counter and flag byte of
|
||||
the other chapters overwrite game globals (chapter 9 destroys the furthest progress), see
|
||||
[testlevel-chapters.md](testlevel-chapters.md).
|
||||
* 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.
|
||||
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
# Level building with `tools/levelbuild.py`
|
||||
|
||||
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).
|
||||
|
||||
## Invocation
|
||||
|
||||
```
|
||||
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` 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 script
|
||||
|
||||
```python
|
||||
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)) # 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)
|
||||
L.checkpoint(17, 18.5)
|
||||
L.exit(70.5, 12.5)
|
||||
L.strat('Bunny', 66, 34)
|
||||
L.coins([(27, 20), (28, 20)])
|
||||
|
||||
def report(L): # optional, after building
|
||||
...
|
||||
```
|
||||
`L.opts` contains the `--set` values (strings).
|
||||
|
||||
### 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.
|
||||
|
||||
### Geometry building blocks
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `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 |
|
||||
|
||||
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, …`), 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`.
|
||||
|
||||
**Texture names on other bases** (2026-11-07). The `TEX` indices are village (t1l1m001) indices; on another
|
||||
base the same index is a different picture (village 2: index 1 = dark red floor). Names are therefore
|
||||
resolved per base by picture: `levelbuild.BaseTextures` holds a fingerprint (FNV-1a over page format,
|
||||
size and pixels of the entry's rectangle) of every named village texture; a name maps to the village index
|
||||
if the base shows that picture there (village 1: always → outputs unchanged), else to the first base entry
|
||||
with the same picture (flags 0x104 preferred). Coverage of the 47 names: t1l1m002 38 (missing: the three
|
||||
flower textures, `wood_grain*`, `wood_brown*`, `wood_rings`, `wood_edge`), t1l1m003 29, t1l1m004 28,
|
||||
t1l2m001 9, t1l3m001/t1l5m001 0–2.
|
||||
* Java builder (`LevelBuilder`, test world/arena/jump lab, `fromCd`): a missing picture is **copied from the
|
||||
village** (CD file loader `wads/t1l1m001.wad`): packed onto a new 256×256 page (same page flags, 1 px
|
||||
clamped border) and appended as an entry after the base table; flat colours and copies share the
|
||||
appended range in order of first use. Without a loader (editor brushes: `BrushCompiler` passes none)
|
||||
the name is rejected: "texture 'flowers' is a village texture this base level does not have …"
|
||||
(the editor shows that as a brush warning and only offers the names the scene's base has).
|
||||
`LevelBuilder.baseTextureNames()` lists the names a base has.
|
||||
* Python `tools/levelbuild.py --base`: same matching (md5 of RGBA rectangle); missing names raise a
|
||||
KeyError (no copying).
|
||||
* In game (apptest, display :22, 2026-11-07): Java-built level on t1l1m002 (grass box, stone box, sand,
|
||||
trees, plus copied `flowers`, `wood_rings`, `wood_grain`), `--editor-import` → level mod, Village
|
||||
chapter 2: grass green, stone grey, trees bark + leaves, copied textures correct
|
||||
(`notes/img/editor/village2_levelbuild_textures.jpg`, `…_textures2.jpg`). Before: grass = dark red
|
||||
(`village2_levelbuild_start_camera.jpg`).
|
||||
* Editor (display :22): on a t1l1m002 scene the brush swatches and the texture grid show the base's own
|
||||
pictures for the names, without the names it lacks (`notes/img/editor/village2_texture_picker.jpg`).
|
||||
**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.
|
||||
|
||||
### 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)` – 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.
|
||||
|
||||
### Objects (strats)
|
||||
`L.strat(name, x, z, y=None, rot=None, params=None, wp='relative', variant=0, hidden=False)`
|
||||
* `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[,unk2]]]), …]` (other 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),…])`.
|
||||
* 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.
|
||||
|
||||
### 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.
|
||||
|
||||
## 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 = start camera positions (the game takes the first node within 2.5 cells
|
||||
of the llama, see `editor.model.cmd.PlayerStart`): `player()` gives the llama ONE node 2 cells
|
||||
behind it, 0.5 above, unk2 = 0 (default camera distance/height) – like the editor's player start.
|
||||
Until 2026-11-07 it copied the prototype's 14-node chain (first node 8 behind = out of range, the
|
||||
rest on the llama): on village 2 bases (node 2 has unk2 ≠ 0) that put the camera into the llama's
|
||||
head. Waypoint lists take an optional 5th value unk2: `[(x,z,y,deg,unk2)]`.
|
||||
In game (test world, apptest 2026-11-07): the start camera is behind the llama at the default
|
||||
distance (`notes/img/editor/testworld_start_camera.jpg`). Village 2 base (levelbuild floor + player on
|
||||
t1l1m002, `--editor-import` as an edit project, boot-to-level 1/2): camera behind the llama, not in its
|
||||
head (`notes/img/editor/village2_levelbuild_start_camera.jpg`).
|
||||
* **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.
|
||||
|
||||
## Exit (LevelDoor) – how it works
|
||||
|
||||
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 ; 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
|
||||
```
|
||||
* 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.
|
||||
|
||||
## 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.
|
||||
|
||||
| Area | old | new |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
|
||||
* **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
|
||||
* **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 OUTDIR --seq "$(cat levels/testwelt_parkour.seq)"
|
||||
```
|
||||
(`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`.
|
||||
|
||||
### Village types not placed
|
||||
| Type | Reason |
|
||||
|---|---|
|
||||
| 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`).
|
||||
|
||||
## 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 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`:
|
||||
own OBJ reader (the 3.0 importer does not know vertex colours), OBJ (= game space since
|
||||
2026-11-07) → Blender z up (y_blender = −game z, winding kept), 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.
|
||||
@@ -313,7 +313,9 @@ Decoded from FUN_0041c640 (load) / FUN_0041c820 (save); details in `save/Ksv.jav
|
||||
| 0xD0–0xD9 | options: effects volume, music volume, 0x80, speaker mode, 6 more option bytes |
|
||||
|
||||
Chapters per world (table 0x572420): 4, 3, 4, 3, 3, 3, 5, 5, 49. World 9 has no row in the
|
||||
per-chapter arrays. The KSV files shipped on the CD are empty (0x00 then 0xCC). Selecting an empty
|
||||
per-chapter arrays. Its coin counter/flag byte for chapters 2–31 land on other globals (chapter 9 =
|
||||
furthest progress): mods use only chapters 1 and 32–49 (`save/TestLevelChapters`,
|
||||
[testlevel-chapters.md](testlevel-chapters.md)). The KSV files shipped on the CD are empty (0x00 then 0xCC). Selecting an empty
|
||||
slot and "new game" immediately writes a zeroed file.
|
||||
|
||||
## Tests done (2026-11-06)
|
||||
|
||||
+10
-8
@@ -11,11 +11,12 @@ Registered in `ModRegistry` (one line). Test-only: `ArenaTestMod` (src/test, scr
|
||||
| `waves` | 3 (1–5) | wave n has min(n, 2) alligators (3 waves = 5 alligators) |
|
||||
| `energy` | 4 (1–5) | llama energy cap (5 = game normal) |
|
||||
| `coins` | 1 (0–5) | EnemyCoins per hit (+5 on defeat, +5 bonus at the exit) |
|
||||
| `chapter` | 8 (2–8) | TestLevel chapter; a chapter already written by another mod → apply error |
|
||||
| `chapter` | 49 (32–49) | TestLevel chapter; a chapter already written by another mod → apply error; a stored 2–8 falls back to 49 |
|
||||
|
||||
Chapter ≤ 8 on purpose: `levelRecord` (coin counter) lives at `0x584F1C[world·6 + chapter]`; for
|
||||
world 9 chapter 9 that is 0x585018 = furthest progress (unverified risk for level projects on
|
||||
chapters ≥ 9, see "Issues").
|
||||
Only chapters 32–49: the coin counter `0x584F1C[world·6 + chapter]` and the flag byte
|
||||
`0x584EC8[…]` of world-9 chapters 2–31 overwrite other globals (chapter 8 = last played map,
|
||||
chapter 9 = furthest progress – verified, see [testlevel-chapters.md](testlevel-chapters.md)).
|
||||
The tally total (LNFO) is 2 placed coins + alligators · ((lives − 1) · coins + 5) + 5 bonus.
|
||||
|
||||
Script options (`Arena`, tests/`ArenaTestMod`): `probe=true` (event probe 1.6 ahead of the llama),
|
||||
`master=false` (no alligators), `start=x,z,rot`, `debug=true` (HUD coin counter shows
|
||||
@@ -72,7 +73,9 @@ per type (nVars/slots/typeFlags/extra), then `LevelBuilder` builds the level.
|
||||
* `spawn` copies the strat's matrix/translation block, not `x/y/z`: after `self.x = …` the child
|
||||
appears at the old place unless the spawner yields first.
|
||||
* Energy 0: the game's "Weiter / Beenden" screen; Weiter restarts the level (all strats fresh).
|
||||
* The level tally after the exit shows "Münzen gesammelt 0 / 55" (not our coin counter).
|
||||
* The level tally after the exit showed "Münzen gesammelt 0 / 55" on chapter 8: 0 = the coin slot
|
||||
after endLevel wrote the last played map into it, 55 = LNFO of the Village base. Fixed: chapter
|
||||
32–49 and recounted LNFO ("2 / 12" for 1 alligator with 1 life).
|
||||
|
||||
## Verified in game (apptest :24, Wine 6, load 3–9, 2026-11-07)
|
||||
|
||||
@@ -101,7 +104,6 @@ mod's chapter → IOException; probe option.
|
||||
|
||||
## Issues / limits
|
||||
|
||||
* Loading picture and tally use the TestLevel texts ("TestLevel Kapitel 8").
|
||||
* Loading picture and tally use the TestLevel texts ("TestLevel Kapitel 49").
|
||||
* The exit target is the arena itself (1, 9, chapter): it restarts after the tally.
|
||||
* Level projects on TestLevel chapters ≥ 9 may write the coin counter over furthest progress
|
||||
(0x585018) – not verified.
|
||||
* Exiting chapter 49 unlocks "chapter 50" (progress 9/50); the map caps the cursor at 49 (checked).
|
||||
+16
-6
@@ -35,6 +35,16 @@ properties). Language/syntax: `notes/strat-language.md`. Disassembler/assembler:
|
||||
getter FUN_00550e60 and setter FUN_005509f0 share the numbering; ids without a setter are read-only
|
||||
(a write would go to address 0).
|
||||
|
||||
## Spawning and collision (engine facts, found by the arena, 2026-11-07)
|
||||
|
||||
* `spawn<params, vars, slots, typeFlags, extra>` (0xe0, FUN_0054bfc0) takes the same fields as a MAP
|
||||
strat record: params = MAP parameter count, vars = local variables, slots, typeFlags = MAP type flags
|
||||
(collision setup), `extra` = words of the collision-bone area.
|
||||
* `spawn` copies the spawner's matrix/translation block (+0x20…), not its `x/y/z` properties: after
|
||||
`self.x = …` the child still appears at the old place. Set the position, **yield**, then spawn.
|
||||
* A MAP-placed type that uses `collisionOn` needs collision type flags in its MAP record (0x28000 like
|
||||
Coin); with typeFlags 0 the game crashes (page fault in the heap allocator FUN_0041ae20).
|
||||
|
||||
## Sources
|
||||
|
||||
* groove.exe handler tables, decompiled with Ghidra 12.2 (headless, all 0x1b2 + 0x45 entries and
|
||||
@@ -642,7 +652,7 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 143 | `camScale` | yes | yes | low | DAT_006D9D30, camera value initialised to 1.0 (zoom/fov factor?). |
|
||||
| 144 | `levelHeaderC` | yes | no | low | Level header (DAT_00584648)+0x0C ushort << 12. |
|
||||
| 145 | `slopeSteepness` | yes | no | medium | 1.0 - floor normal y (DAT_0057DDA4) << 12: 0 on flat ground (FUN_004054c0). |
|
||||
| 147 | `levelTableByte` | yes | no | low | Byte table 0x585024[map + (chapter + tribe*10)*4] << 12 (per-level constant). |
|
||||
| 147 | `levelTableByte` | yes | no | low | Byte table 0x585024[map + (world + tribe*10)*4] << 12 (FUN_0041af40(tribe, world, map)); bss, never written by the game (always 0) – but world-9 coin counters of TestLevel chapters 22–31 land in its tribe-1 part (see testlevel-chapters.md). |
|
||||
| 149 | `flagEC_1` | yes | no | low | Flag +0xEC & 0x1. |
|
||||
| 150 | `flagEC_8000000` | yes | no | low | Flag +0xEC & 0x8000000. |
|
||||
| 151 | `cheat5` | yes | no | medium | Cheat toggle bits DAT_00584710 & 0x20 (button-sequence cheats, FUN_0041a0xx). |
|
||||
@@ -660,10 +670,10 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 165 | `instanceData1` | yes | yes | low | Map-instance record (+0xF0)+0x28 -> word 1. |
|
||||
| 166 | `floorCollisionOn` | yes | no | medium | Flag +0xEC & 0x8000 (floor collision, ops 0xa5/0xa6). |
|
||||
| 169 | `lives` | yes | yes | low | DAT_00584E6C (fixed, starts at 5.0, saved per slot) - lives or max health. |
|
||||
| 171 | `saveProgressA` | yes | no | low | DAT_00585008 << 12 (saved progress byte). |
|
||||
| 172 | `progressWorld` | yes | no | low | DAT_0058500C << 12 (saved; world of the furthest/selected level, 8 = finale). |
|
||||
| 173 | `progressChapter` | yes | no | low | DAT_00585010 << 12 (saved; chapter, 6 with world 8 = last level). |
|
||||
| 174 | `saveProgressB` | yes | no | low | DAT_00585014 << 12 (saved progress byte). |
|
||||
| 171 | `saveProgressA` | yes | no | low | DAT_00585008 << 12: last played tribe (KSV +0x3E, set by endLevel). |
|
||||
| 172 | `progressWorld` | yes | no | low | DAT_0058500C << 12: last played world (KSV +0x3F, set by endLevel; 8 = finale). |
|
||||
| 173 | `progressChapter` | yes | no | low | DAT_00585010 << 12: last played chapter (KSV +0x40; 6 with world 8 = last level). |
|
||||
| 174 | `saveProgressB` | yes | no | low | DAT_00585014 << 12: last played map (KSV +0x41). |
|
||||
| 175 | `unk_6da308` | yes | no | low | DAT_006DA308, no other reference. |
|
||||
| 178 | `cheat4` | yes | no | medium | DAT_00584710 & 0x10 (cheat toggle). |
|
||||
| 179 | `hudFlags` | yes | yes | medium | DAT_006DA2E0 HUD element enable mask (default 0x3F000; 0x2000 timer, 0x8000/0x10000/0x20000/0x40000 counters). |
|
||||
@@ -676,7 +686,7 @@ ops = operands (inline words for long ops, the 16-bit argument for short ops); p
|
||||
| 187 | `hitDamage` | yes | yes | medium | +0x140: damage dealt on contact (copied into the other strat's +0x144). |
|
||||
| 188 | `damageTaken` | yes | no | medium | +0x144: damage received from the last contact (player cleared to 0 each frame). |
|
||||
| 189 | `health2` | yes | yes | medium | DAT_00584E70 = health (same as 92, settable). |
|
||||
| 190 | `levelRecord` | yes | yes | low | Per-level saved word DAT_00584F1C[chapter + world*6] (best/collected value for the current level; HUD shows it). |
|
||||
| 190 | `levelRecord` | yes | yes | medium | Coin counter of the current level: u32 DAT_00584F1C[chapter + world*6] (coins << 12; HUD and tally show it, zeroed on level start, KSV keeps worlds 0–8 as u16). World 9 has no row: TestLevel chapter c hits 0x584FF4 + 4c (chapter 9 = furthest progress 0x585018) – only chapters 1 and 32–49 are free (testlevel-chapters.md). |
|
||||
| 191 | `counterC` | yes | yes | low | DAT_00584FFC: persistent HUD counter (icon 0x150), saved. |
|
||||
| 192 | `counterA` | yes | yes | low | DAT_00584FF4: persistent HUD counter (icon 0x184, displayed up to 999) - probably coins. |
|
||||
| 201 | `distToPlayerSq` | yes | no | high | +0x148: cached squared distance to the player ((d>>6)^2 sum), updated each frame when tracking (op 0x30). |
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# TestLevel chapters: which ones are safe (2026-11-07)
|
||||
|
||||
World 9 ("TestLevel") has 49 chapter slots (table 0x572420), but the game's per-chapter arrays only
|
||||
have rows for worlds 0–8. Indexing them with world 9 runs into other globals. Code:
|
||||
`save/TestLevelChapters` (usable = chapter **1** and **32–49**).
|
||||
|
||||
## The arrays (index world·6 + chapter)
|
||||
|
||||
| Array | Where it is used |
|
||||
|---|---|
|
||||
| u32 coins 0x584F1C (coins << 12) | `levelRecord` (strat var 0xBE) get/set, HUD (FUN_00426bf0), tally (FUN_0040dd60), endLevel (FUN_0054d620: "all coins" check), **zeroed on every level start** (FUN_0041cba0), KSV keeps 54 entries as u16 |
|
||||
| u8 flags 0x584EC8 (bit 0 collectible, bit 1 gold coin) | collectible picked up (HUD, strat var 0xFA rising) → \|=1; endLevel → \|=2 when coins == static total 0x5722F4[world·5 + chapter] (garbage for world 9, often 0) or cheat 0x584710&8 → \|=3; gold count FUN_0041cd70 (worlds 1–8 only) |
|
||||
|
||||
Coin total table 0x5722F4 and the selection map only know worlds 1–8 (map shows garbage targets
|
||||
for world 9 – cosmetic).
|
||||
|
||||
## What world-9 chapter c hits
|
||||
|
||||
Coin slot 0x584FF4 + 4c, flag byte 0x584EFE + c (Ghidra references 0x584EF0–0x585240):
|
||||
|
||||
| c | coin slot | flag byte |
|
||||
|---|---|---|
|
||||
| 0 | 0x584FF4 counter var 0xC0 (HUD) | free |
|
||||
| **1** | 0x584FF8 free (only cleared by new game) | free |
|
||||
| 2 | 0x584FFC counter var 0xBF (HUD icon 0x150) | free |
|
||||
| 3 | 0x585000 HUD flag (FUN_00427c40) | free |
|
||||
| 4 | 0x585004 var 0x80 | free |
|
||||
| 5–8 | 0x585008–14 last played tribe/world/chapter/map (strat vars 0xAB–0xAE, KSV +0x3E–41) | free |
|
||||
| 6–9 | | 0x584F04 **language index** (u32; \|=2 → text table out of range) |
|
||||
| 9 | 0x585018 **furthest progress** (world, chapter bytes; KSV +0x42/43) | |
|
||||
| 10–13 | 0x58501C/20 free | 0x584F08 option (KSV +0xD6) |
|
||||
| 12 | 0x585024 start of byte table (var 0x93, tribe 0) | |
|
||||
| 14–21 | 0x585028–48 free | 0x584F0C / 0x584F10 (var 0x5B) |
|
||||
| 22–25 | 0x58504C–58 table 0x585024, tribe 1 (read by var 0x93 in original levels) | free |
|
||||
| 26–29 | table, tribe 1 | 0x584F18 option (KSV +0xD8) |
|
||||
| 30–31 | table, tribe 1 (world 8/9) | coin slots of world 0 (never read) |
|
||||
| **32–49** | table, tribes 2/3 (never used: tribe is 1, 4 only in the attract demo) | coin slots of world 0 |
|
||||
|
||||
## Verified in game (apptest :24, 2026-11-07)
|
||||
|
||||
* **Chapter 9** (arena test mod, 1 alligator, 2 coins collected): pause → "Beenden und zur
|
||||
Auswahlkarte" → the selection map shows world "°p", "Kapitel 0", coin target 14384: furthest
|
||||
progress 0x585018 was replaced by the coin counter (2 << 12 → world 0, chapter 32).
|
||||

|
||||
* **Chapter 49** (same run): tally "Münzen gesammelt 2 / 12, Geheimnisse 0 / 0" (LNFO recounted),
|
||||
save written with progress (9, 50) – the map caps the cursor at chapter 49, no crash; map fine.
|
||||

|
||||
* Before (arena on chapter 8, arena agent): tally "0 / 55": the collected value is the coin slot
|
||||
after endLevel copied the level ids into 0x585008–14 (chapter 8 = last played map = 0), the total
|
||||
is LNFO of the Village base.
|
||||
|
||||
## Tally totals (LNFO)
|
||||
|
||||
Chunk LNFO = u32 coins, u32 secrets → 0x5FF070 / 0x5FF060 (FUN_0042AB10; the tally shows the values
|
||||
of the level just finished, kept in 0x5FF064 / 0x5FF06C). Originals: e.g. t1l3m004 40 = 20 Coin +
|
||||
4 Coin5·5; levels with CoinLine/CoinCircle/enemies count those too. `mods/levels/LevelTally` recounts
|
||||
Coin (1) + Coin5 (5) and Secret strats for new level projects and the Test World (20 coins, 0
|
||||
secrets); the arena adds its dynamic coins (alligator hit/death coins + 5 exit bonus).
|
||||
|
||||
## Migration
|
||||
|
||||
* Level projects: stored chapter 1 or 32–49 kept (first file wins), others get the first free of
|
||||
32–49; more than 18 projects share chapters (only one of them can be on – apply error otherwise).
|
||||
Play-tests/exports of an old project: `migrate` (2..19 → 32..49, 20..31 → 32..43).
|
||||
* The chapter options are choices (level: 1, 32–49; arena: 32–49, default 49); a stored old value is
|
||||
invalid and falls back to the default (ModOptions).
|
||||
* Editor: New level / Level settings spinners only offer usable chapters.
|
||||
|
||||

|
||||
|
||||
## Not fixed
|
||||
|
||||
* The map lets the player pick any unlocked world-9 chapter; empty ones are linked to an installed
|
||||
world-9 WAD (ModManager), so playing e.g. chapter 9 from the map still clobbers progress. Unlocking
|
||||
chapter 32+ unlocks 2–31 on the map too.
|
||||
Reference in new issue
Block a user