Files

5.4 KiB
Raw Permalink Blame History

Mod "Mirror mode" (mods.mirror.MirrorMod, id mirror, GAMEPLAY)

Mirrors every playable level left-right (x → W − x on the W×H tile grid), like the mirror mode of racing games. Controls are unchanged (the world is mirrored, not the input). Code: mods/mirror/Mirror.java (pure byte transform), MirrorMod.java; test MirrorTest.

Options

Key Default Meaning
worlds all all or world numbers, e.g. 1,3 (9 = TestLevel / Test World)
readableTextures false on: face flag 0x20 (mirror U) is toggled so writing reads normally, but textures that continue across faces get seams (fountain water rim in the village); off: true mirror image
objects false also mirror rigid STPC object models (verts, face planes, collision); skinned characters never

order() = 75: after testworld (50, so the Test World is mirrored too) and before debug-wireframe (100, so the overlay is built from the mirrored collision and lines up). Files: wads/t1l<w>m<c>.wad only (front end t0*, map t6l6* stay).

What is transformed (all chunk sizes unchanged → STPC absolute offsets stay valid)

Data Transform
TRAK models (all, shared by tiles) vertex x, normal x, face plane nx negated; bbox centre x negated, corner pairs (−x/+x) swapped + x negated; collision: i8 nx of all 5 planes negated (d unchanged; −128 → 127)
Face winding kept. groove.exe sets D3DRENDERSTATE_CULLMODE = D3DCULL_NONE once (decomp ~line 31587, Device3 vtable +0x58) and culls in software with the stored face plane. Reordering vertices would break the fixed UV corner patterns (pattern 0x800+0x10 has no odd permutation that keeps the UVs).
MAP tiles (float A) x → W − x, rot → (4096 − rot) & 0xfff
MAP tiles (fixed B) x → W·4096 − x, rot as above (A and B stay exactly consistent)
Region grid, tile-swap block (cell x) columns mirrored (cell → W − 1 − cell)
Strats x → W·4096 − 1 − x (keeps floor(x) in the mirrored cell: LevelDoor compares cells), ry and rz → −angle, rx kept
Waypoints / camera nodes same as strats (x, ry, rz)
90-byte "env zones" really teleport/respawn entries (FUN_0054d560 / FUN_0054de00): f0..2 = trigger cell (x,y,z), f3..5 = target position (fixed), f7 bits 4..11 = facing angle (bits 0/1 = y compare flags). Mirrored: cell x, position x, angle. Default entry (pos 1,1,1) untouched. f11 (+0x2c) = sky model yaw offset, kept.
LGHT (FUN_0042c180) u32 n; per light u8 type, r, g, b; type 1 directional: 3 f32 → x negated; types 2/4 point: 3 f32 + 2 f32 + u8 → x → W − x
STPC (option) rigid models (nBones = 0) incl. WFPC&0x100 extra models, same as TRAK models
Vertex colours unchanged (vertex order unchanged)

Rotation rule: mirroring x conjugates rotations; about y and z they become −angle, about x unchanged. 0 = facing +z, so facing (sin a, cos a) → (−sin a, cos a) = angle −a.

Mirroring twice gives the original WAD byte for byte (except LGHT float rounding and −128 normals) – checked in MirrorTest, together with: every render vertex and collision corner of every tile of t1l1m001 lands exactly at the mirrored world position, region per tile cell preserved.

Not mirrored / known limits

  • Strat bytecode is untouched: hard-coded offsets/directions in scripts (if any) are not mirrored. Parameter-pool values are not interpreted (could contain coordinates/angles for some types).
  • Object models stay unmirrored by default (placed and turned mirrored, so their collision matches their own look). Skinned/animated characters are never mirrored (bone matrices and compressed vertex deltas of the anim sets would need conjugation).
  • Sky/background models (STPC, drawn camera-relative) are not mirrored.
  • Attract-mode demos (*.DEM, recorded input) desync in mirrored levels (harmless).
  • Textures are mirrored by default (true mirror); readableTextures flips them back per face, which breaks textures that are laid out across several faces.
  • River intro fly-by (t1l3m001): the camera partly shows an empty grey view and the intro dialog starts earlier than in the original. No crash; after the intro the river plays normally. Most likely a hard-coded lateral camera offset in strat code (camera ends up in a cell whose visibility zone hides the river). The village fly-by is correct.

In-game verification (apptest, display :7, 2026-11-06)

  • Village t1l1m001: same key sequence on original and mirrored – a perfect mirror image (house/fountain swapped sides), llama stands on the ground, walls/fountain rim block, coins collect (counter matches the original run), Pacha dialog triggers, camera rotate (A) works, intro fly-by (not skipped) correct. With debug-wireframe + objects=true: overlay lines up with the mirrored geometry, no crash.
  • River t1l3m001: loads, raft/tree floats on the mirrored river, auto-scroll and steering work, coins collected, wireframe lines up; fly-by see limits.
  • City t1l6m001: loads, intro cutscene with Pacha (cutscene cameras) fine, walls and crates block, wireframe lines up.
  • Test World (testworld,mirror, world 9): mirrored island (parkour stairs on the right), walking/jumping fine.
  • Images: notes/img/mods/mirror_01_village.jpg (original vs. mirror), mirror_02_wireframe.jpg (village + city with the debug wireframe), mirror_03_river_testworld.jpg.