Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,53 @@ Exported as `TriggerInstance` (TR4, TRNG and TR5), inverse of the trigger part o
- The camera / sink sets are unchanged and PrjDiag is unchanged (97.01%).

**Not recovered.** Action types 27 and 31 (zoo 15, Venice 1, Burial Chambers 1) are skipped with a warning (see 2.38). A Moveable-target action whose number is a static's script ID (NG) is indistinguishable from an item index; numbers beyond the item count are skipped with a warning. When one CameraInstance is used by several triggers with different move timers the last one wins. In non-NG levels an action 11 (clear bodies) is mapped to ActionNg, which has no counterpart there; no sample level contains one.
### 2.41 -- TombEngine levels (.ten): container and data layout

Reader: `Core/TenLevel.cs` (not yet wired into the exporter). Source of truth: TombEngine `Specific/level.cpp` (`LoadLevel` and the `Load*` functions); the writer side is TombLib `Compilers/TombEngine` (`Structs.cs`, `FloorData.cs`).

**Container.** 16-byte header (`TEN`, compiler version[4], system hash, level hash), then three blocks: media (texture atlases, samples), geometry, dynamic data. Each block is `[int64 uncompressed][int64 compressed]` followed by `[uint32 chunks]{[uint32 uncompressed][uint32 compressed][LZ4 block]}` (K4os.Compression.LZ4). Primitive types are little-endian; strings are LEB128-prefixed UTF-8 (same as `BinaryReader.ReadString`); bools are one byte.

**Geometry block.** Rooms (position x/z, bottom/top height, vertex positions / colours / effects, texture buckets of quads and triangles with UVs, portals, size, sectors, lights), then the floordata words, then moveable models, sprites, boxes, mirrors, animated textures and material definitions (the last group is not read). Sectors carry trigger index, pathfinding box, material, stopper, and for floor and ceiling: split angle, two portal room numbers and two "fake planes" (x, y = tilt per block, z = height at the sector centre), plus the side portal and nine flags (death, monkey, four climbs, mark triggerer, mark beetle).

**Dynamic block.** Per room: name, tags, ambient, flipped room, flags, mesh effect, reverb, flip number, statics (position, rotation, scale, flags, colour, slot, hit points, name) and trigger volumes; then items, AI objects, cameras, flyby cameras (with DOF), sinks, sound sources, materials, event sets (global, volume) and the property blob.

**Verification.** palace.ten and venice.ten (The Emerald Vault, compiler 2.0.0) load in about 0.3 s each; the dynamic block is consumed to the last byte, which validates every field read there (palace: 13 rooms, 18 items, 49 statics; venice: 68 rooms, 147 items, 13 flybys, 231 statics). The geometry block cannot be checked to its end because only the room part is read. Both levels use floordata triggers (`TriggerIndex` + floordata) and no volumes.
### 2.42 -- TombEngine levels to PRJ2: rooms, sectors, portals, objects, triggers

Code: `Core/TenExporter.cs` (rooms, portals, flip), `Core/TenSectors.cs` (sector surfaces), `Core/TenObjects.cs` (objects), triggers through `TriggerExporter.ExportCore` (shared with the TR path, `tenFormat` switch). Not wired into the UI yet and no textures yet (see below). Ground truth: the Tomb Editor sources of The Emerald Vault levels (`palace.prj2`, `venice.prj2`) loaded with `Prj2Loader`, compared with what is rebuilt from `palace.ten` / `venice.ten`.

**Rooms.** Position X/Z = file X/Z / 1024. `Position.Y` is NOT `-BottomHeight` (that is only the lowest corner): every complete wall has all four planes at Z = -Position.Y, so the exact value comes from any wall sector (the border always has some). The file stores half of the editor's colour values (ambient, statics, moveables, lights): multiply by 2. Room flags: 0x1 water, 0x4 quicksand, 0x8 horizon, 0x20 outside, 0x80 no lensflare, 0x800 damage, 0x1000 cold (0x40 is automatic). Flip: `FlippedRoom` names the alternate room, `FlipNumber` is the alternate group.

**Sectors.** The file keeps two "fake planes" per surface (x, y = tilt over one sector, z = height at the centre: engine Y = z + x * xl + y * zl, xl / zl in -512..512 from the centre) instead of corner heights; TombLib height = -Y - Position.Y. A complete wall is flat floor = ceiling = -Position.Y with no split (wall heights are lost); a wall triangle is marked by Z = -float.MaxValue. Diagonal splits are inverted from the compiler's `RoomSectorShape` rules: split angle pi/4 allows XnZp / XpZn, 3pi/4 allows XnZn / XpZp, a step of 0 means no split; a wall triangle gives the diagonal wall type (pi/4: first = XnZp, second = XpZn; 3pi/4: first = XpZp, second = XnZn) with the unknown corner copied. Flags: death, monkey, four climbs, beetle, triggerer.

**Portals.** The room's portal polygons give direction (normal (0,0,1) = WallNegativeZ, (0,0,-1) = WallPositiveZ, (1,0,0) = WallNegativeX, (-1,0,0) = WallPositiveX, (0,1,0) = Ceiling, (0,-1,0) = Floor) and area (wall portals sit one unit inside their sector: index = v / 1024; the other axis spans [min, max) in sectors). Only the lower-indexed room adds the portal, AddObject creates the mirror.

**Objects.** Local position = (x - roomX, -y - Position.Y, z - roomZ). Statics: slot, rotation Y, scale, colour, name. Moveables: slot, rotations X/Y and roll = -Z, OCB = trigger flags, flags = code bits << 9 | 0x80 clear body | 0x100 invisible, colour, name. AI objects are moveables (code bits = flags >> 1). Lights: type 0 sun, 1 point, 2 spot, 3 shadow, 4 fog bulb; ranges / 1024, spot direction y negated. Cameras: flags 1 locked, 2 glide out, speed = move timer. Flybys: the file stores a point one sector ahead instead of the rotation; speed / 655, roll = 65536 - roll. Sinks and sound sources have no room index: the room whose box contains them is taken. Sound source flags: 0x8000 always, 0x4000 base only, 0x2000 alternate only; the combinations Automatic writes for the room's flip state are read back as Automatic.

**Triggers.** Floordata at the sector's TriggerIndex: `[type << 8][setup][actions]`, no function word; an action is `target type << 10 | parameter`; camera, flyby and event actions (target types 16 / 17) have an extra word; bit 15 of the last word ends the list. Type 12 = Monkey, 13 = TightRope, 14 = Crawl, 15 = Climb. Targets: item index, camera / sink index in the file's own lists, flyby sequence. In editor-made levels only the first (setup) action carries the sector's type, timer, one-shot and code bits; the other actions are plain Triggers with defaults. TR levels keep the legacy convention (all actions share the setup), which matches TombLib's own PRJ import and the reference prj2 of alexhub2 (51 of 51).

**Results.**
- Sectors: palace 1151, venice 5681. Type, flags, floor and ceiling heights, diagonal split and (when the surface is not planar) split direction are identical on all of them; the only differences are 5 palace sectors that carry a ghost block in the source (the file holds the total height). Room properties, flip links and room geometry (name, position, size) are identical.
- Portals: palace 20 of 24, venice 160 of 172 identical; the others differ in area (the compiler leaves out sectors whose neighbour is a wall) and a few are not in the file.
- Objects: venice 231 / 231 statics, 149 / 149 moveables, 37 / 37 lights, 8 / 8 cameras, 13 / 13 flybys, 1 / 1 sound source identical; palace 49 / 49 statics, 4 of 5 lights (one shadow light with negative intensity is not in the file), 16 of 18 moveables (two doors are placed differently in the source than in the released level).
- Triggers: venice 108 of 109 (one auxiliary PlayAudio keeps a timer in the source that the file does not store), palace 10 of 14 (the same two doors, swapped between source and release; the indices are consistent inside the .ten).

**Not done.** Face textures (atlases of the media block + polygon UVs), trigger volumes and event sets (neither level has any), UI wiring (`LevelSession` is built around `TrLevel`). Lost by design of the format: wall heights, ghost blocks, per-action timers of non-setup triggers, the exact area of portals next to walls, static OCB / flags, shadow lights with no intensity. A trigger Object action whose number is a static's script ID is indistinguishable from an item index.
### 2.43 -- TombEngine levels: face textures

Code: `Core/TenTextures.cs`, `Core/TenWalls.cs`, atlases read by `TenLevel.ReadAtlases`. Verified against the Tomb Editor sources of palace and venice, comparing pixels.

**Atlases.** The media block holds five texture groups (room, moveables, statics, animated, sprites), each `[count]{width, height, size, colour map, 3 x (flag, blob)}`; the colour map is PNG or DDS (BC1 / BC3). Room and animated atlases are kept; PNG ones are written as they are to `<name>_textures\room00.png` ... and registered as level textures (with 512-wide pages not converted). DDS atlases are not decoded yet (warning, faces stay untextured). Animated buckets are used as a still image (first frame), the animation sequences are not exported.

**Polygons to faces.** A room polygon stores its vertices and one UV per vertex (normalised, atlas relative). TombLib's own `RoomGeometry` of the rebuilt room says which sector face owns which vertices (`VertexRangeLookup`, vertex positions = file position with Y = -(y + Position.Y)), so a polygon is matched to its face by the set of its vertex positions and its UVs are written back as the face's `TextureArea` (UV x atlas size, pixel coordinates). Corner order, inverse of the compiler (`Rooms.cs`) and `UpdateFaceTexture`: quad TexCoord0..3 = vertices 3, 2, 0, 1 (ceiling: 5, 0, 2, 1); triangle = vertices 0, 1, 2 (ceiling: 2, 1, 0 with TexCoord3 = TexCoord2). Blend mode is the bucket's; a second polygon on the same vertices means double sided. Two faces back to back (both sides of a thin wall) have the same vertices; the polygon whose normal agrees with the face's winding belongs to it. A face the compiler cut into several polygons (a wall face with a sloped top becomes a quad plus a triangle) is rebuilt from the polygons lying inside it, taking each corner's UV from whichever polygon has a vertex there.

**Wall heights.** The file keeps no heights for wall sectors, but TombLib builds the wall faces (QA / middle / WS) from them, so without them most wall polygons match nothing. At each end of a wall edge the polygon vertex heights are the open neighbour's floor / ceiling at that corner plus the wall's own floor / ceiling; whatever does not equal the neighbour's corner heights is the wall's. Corners are inferred one by one (sloped walls work), voted by the edges touching them, and a corner with no information takes its wall's most common value. With only one extra boundary the ceiling is assumed to coincide with the neighbour's.

**Portals.** A portal whose plane carries textured polygons gets opacity TraversableFaces (also on the mirrored portal in the other room), because TombLib builds no faces under a portal otherwise.

**Results (venice, palace).** Polygons that match no face: 13 of 8300 and 9 of 1931. Pixel comparison against the sources (six sample points per face): palace 1864 of 1890 faces textured on both sides are close; venice 7513 of 7925 close, 64 near, 348 different (ceiling quads 225 of 1218, floors 20 of 2918, walls 103 of 3525). Faces textured in the sources but with no polygon in the file are not an error (faces TombLib does not compile).

**Open.** The different ceiling quads share geometry and vertex order with the good ones, and their atlas content is the transposed or flipped version of the source tile, so it looks like an orientation difference between the editor and the compiled level rather than a mapping bug (not proven). A wall face higher than one tile is stored as several polygons that each carry their own atlas region; the rebuilt face takes the corner UVs from different polygons and looks wrong in about 3% of walls. DDS atlases, animated texture sequences and materials (the bucket's material index) are not handled.
## Key structural lessons (apply to future work on this codebase)

- **TombLib local source is authoritative.** For any format question, find what TombLib's *compiler* writes (PRJ2→TR4) and invert it, rather than guessing from TRosettaStone/trview alone (though both are good secondary checks, and TRosettaStone caught the real bump-mapping mechanism in 2.5).
Expand Down
Loading
Loading