Tomb Raider is driven by various sets of files — level files, script files, FMVs, audio tracks and sound files. In TR4 and TR5, there is also specific file type which contains cutscene data — cutseq pack.
The script file structure differs from version to version.
In TR1, all script info was embedded into executable file (TOMB.EXE
), and thus is hardcoded. TR2 and TR3 had unified TOMBPC.DAT
file, which contains all the text strings describing the various elements in the game (e.g. the game engine knows about “Key 1”; it looks in TOMBPC.DAT
to determine the name to be displayed in Lara’s inventory, such as “Rusty Key” or “Taste rostige” or “Clé Rouillée”), the level and cut-scene filenames (e.g. WALL.TR2
, CUT3.TR2
), the order in which they are to be played, and various per-level and per-game configuration options (e.g. what weapons and objects Lara starts the level with, whether or not the “cheat” codes work, etc.).
TR4 and TR5 introduced a new script format, where the actual script defining the gameflow was separated from text strings used in game — hence, both TR4 and TR5 have two .DAT
files — SCRIPT.DAT
and LANGUAGE.DAT
, where LANGUAGE
differs depending on regional origin of the game — US.DAT
, FRENCH.DAT
, JAPANESE.DAT
, and so on.
The level files, {level-name}.PHD/TUB/TR2/TR4/TRC
, contain everything about the level, including the geographical geometry, the geometry (meshes) of all animate and inanimate objects in the level, all the textures and colour data, all animation data, index information (and, in TR1, TR4 and TR5 — also the actual sound sample data) for all sounds, accessibility maps — everything necessary to run the game. For whatever reason, Core has included everything in one file instead of breaking it up into logical groupings; this means that every level contains all the meshes, textures, sound information, and animation data for Lara and all of her weapons. There are a fair number of other redundancies, too.
Since TR4, the level file is divided into several chunks, each of them being compressed with zlib. Usually, each chunk of compressed data is preceded by two 32-bit unsigned integers defining the uncompressed size of the chunk and the compressed size of the chunk. Therefore, the engine allocates an empty buffer equal to the uncompressed size of a specific chunk, and another buffer equal to the compressed size. The compressed data is loaded directly within it based on the compressed size. The compressed data is then decompressed into the result buffer and the buffer containing the compressed data is destroyed. In TR5, those chunks aren’t compressed anymore.
.PHD
is actually the initials of the Lead Programmer for Tomb Raider 1: Paul Howard Douglas. Looks like this programmer contributed a lot of the code during early development stages of Tomb Raider. This is suggested because the phd initials also became a prefix for several helper functions in the original source code, for instance: phd_sin
, phd_cos
etc. Most likely, he was also responsible for developing the level file structure for Tomb Raider.
TR1-3 shared the same proprietary Eidos codec for videos, called Escape. The extension for such files is .RPL
, that’s why they occasionally (and mistakingly) called Replay codec. Signature feature of RPL videos is that they are always interlaced with black stripes; most likely, this was used to conserve disk space (however, PlayStation videos were in .STR
format, which is basic MPEG compression, and they had no interlacing — but suffered from blocking issues). In TR1 and TR2, framerate was limited to 15 FPS, while in TR3 it was doubled to 30 FPS.
For a long time, Escape codec was largely unexplored and barely reverse-engineered; there was only an abandoned open source Mplayer implementation for some Escape codec versions, but recent ffmpeg revisions feature fully functional decoder for Escape videos.
Since TR4, all FMVs are in Bink Video format, which is much more common and easy to rip, convert and explore.
These are long sound files which occasionally play either on some in-game events (e.g. approaching certain important checkpoint in game, like big hall with ladder and two wolves in “Caves” — it triggers danger music theme) or in looped manner as background ambience. Audio tracks are stored differently across TR game versions — CD-Audio in TR1-TR2, single merged file CDAUDIO.WAD
in TR3, and separate audio files in TR4 and TR5.
TR2 and TR3 also featured external sound sample files, which allowed to share samples between all level files. This sound file is called MAIN.SFX
, and usually placed in DATA
subfolder. Hence, engine loads sound samples not from level files (as it’s done in TR1, TR4 and TR5 — see above), but rather from this MAIN.SFX
file.
TR4 and TR5 featured special data type containing all the necessary information to play in-game cutscenes. While in earlier games such info was embedded into the level file itself, and generally, cutscenes themselves were separate level files (easily distinguished by their filenames, e.g. CUT1.TR2
etc.), TR4 changed this approach, and cutscenes could be loaded and played right inside level files at runtime.
The data for such cutscene setup was packed into single file titled CUTSEQ.PAK
in TR4 or CUTSEQ.BIN
in TR5. There will be a special section describing whole cutseq file format.
For the purposes of further discussion, the following are assumed:
int8_t | specifies an 8-bit signed integer (range -128..127) |
uint8_t | specifies an 8-bit unsigned integer (range 0..255) |
int16_t | specifies a 16-bit signed integer (range -32768..32767) |
uint16_t | specifies a 16-bit unsigned integer (range 0..65535) |
int32_t | specifies a 32-bit signed integer (range -2147483648..2147483647) |
uint32_t | specifies a 32-bit unsigned integer (range 0..4294967295) |
float | specifies a 32-bit IEEE-754 floating-point number |
fixed | specifies a 32-bit non-trivial 16.16 fixed point value — see further |
ufixed16 | specifies a 16-bit non-trivial 8.8 fixed point value — see further |
All multi-byte integers ({u}int16_t
, {u}int32_t
) are stored in little-endian (Intel-x86, etc.) format, with the least significant byte stored first and the most significant byte stored last. When using this data in platforms with big-endian (PowerPC, etc.) number format, be sure to reverse the order of bytes.
These very specific data types mimic floating-point behaviour, while remaining integer. It is done by splitting floating-point value into whole and fractional parts, and keeping each part as int16_t
and uint16_t
correspondingly for `fixed` type and as uint8_t
and uint8_t
for ufixed16
type. Whole part is kept as it is, while fractional part is multiplied by 65536 (for fixed
) or by 255 (for ufixed16
), and then kept as unsigned integer. So, the formula to calculate floating-point from fixed
is:
$F_{real} = P_{whole} + ( P_{frac} \div 65536 )$
Formula to calculate floating-point from ufixed16
is:
$F_{real} = P_{whole} + ( P_{frac} \div 255 )$
…where $P_{whole}$ is whole part of mixed float (signed for fixed
, unsigned for ufixed16
), and $P_{frac}$ is fractional part (unsigned).
However, some internal variables and constants (like drawing distance, fog distance constants and some light properties) are PC-specific and stored in floating point numbers. Also, last game in series, TR5, extensively used floating-point numbers for certain data types – like colours, vertices and coordinates.
Data alignment is something one has to be careful about. When some entity gets an address that is a multiple of $n$, it is said to be $n$-byte aligned. The reason it is important here is that some systems prefer multibyte alignment for multibyte quantities, and compilers for such systems may pad the data to get the “correct” alignments, thus making the in-memory structures out of sync with their file counterparts. However, a compiler may be commanded to use a lower level of alignment, one that will not cause padding. And for TR’s data structures, 2-byte alignment should be successful in nearly all cases, with exceptions noted below.
To set single-byte alignment in any recent compiler, use the following compiler directive:
#pragma pack(push, 1)
To return to the project’s default alignment, use the following directive:
#pragma pack(pop)
The world coordinate system is oriented with the $X-Z$ plane horizontal and $Y$ vertical, with $-Y$ being “up” (e.g. decreasing $Y$ values indicate increasing altitude). The world coordinate system is specified using int32_t
values; however, the geography is limited to the $+X$/$+Z$ quadrant for reasons that are explained below. Mesh coordinates are relative and are specified using int16_t
.
There are some additional coordinate values used, such as “the number of 1024-unit blocks between points A and B”; these are simply scaled versions of more conventional coordinates.
All colours in TR are specified either explicitly (using either the [tr_colour] structure, described below, 16-bit structures or 32-bit structures) or implicitly, by indexing one of the palettes. However, it is only applicable to TR1-3 — there is no palette in TR4 and TR5.
In TR1-3, mesh surfaces could be either coloured or textured. Coloured surfaces are “painted” with a single colour that is either specified explicitly or using an index into the palette.
Beginning from TR4, coloured faces feature was removed, so each face must have a texture attached to it.
Textured surfaces map textures from the texture atlases (textiles) to each point on the mesh surface. This is done using conventional UV mapping, which is specified in “Object Textures” below; each object texture specifies a mapping from a set of vertices to locations in an atlas, and these texture vertices are associated with position vertices specified here. Each atlas has a size of 256×256 pixels.
The 16-bit atlas array, which contains [tr_image16] structures, specifies colours using 16-bit 1-5-5-5 ARGB encoding.
If, for some reason, 16-bit textures are turned off, all colours and textures use an 8-bit palette that is stored in the level file. This palette consists of a 256-element array of [tr_colour] structures, each designating some colour; textures and other elements that need to reference a colour specify an index (0..255) into the Palette[]
array. There is also a 16-bit palette, which is used for identifying colours of solid polygons. The 16-bit palette contains up to 256 four-byte entries; the first three bytes are a [tr_colour], while the last byte is ignored (set to 0).
The 32-bit texture atlas array, which contains [tr4_image32] structures, specifies colours using 32-bit ARGB, where the alpha channel is unused. The 16-bit and 32-bit texture atlas arrays depict the same graphics data, but of course the 32-bit array has a better colour resolution. It’s the one used if you select a 32-bit A8R8G8B8 texture format in the setup menu from TR4 and TR5.
There are two basic types of “visible objects” in TR2 — meshes and sprites.
Meshes are collections of textured or coloured polygons that are assembled to form a three-dimensional object (such as a tree, a tiger, or Lara herself). The “rooms” themselves are also composed of meshes. Mesh objects may contain more than one mesh; though these meshes are moved relative to each other, each mesh is rigid.
Sprites are two-dimensional images that are inserted into three-dimensional space, such as the “secret” dragons, ammunition, medi-packs, etc. There are also animated sprite sequences, such as the fire at the end of “The Great Wall.” Core had presumably used this method to reduce CPU utilization on the PlayStation and/or the earlier PCs. Sprites become less and less abundant; TR2 has very few scenery sprites, and TR3’s pickups are models instead of sprites.
Each Tomb Raider game has an internal hardcoded set of entity types, each of them linked to specific model (hence, entity type and model can be considered equal). Entity is an individual object with its own specific function and purpose. Almost every “moving” or “acting” thing you see is an entity — like enemies, doors, pick-up items, and even Lara herself.
A level can contain numerous instances of the same entity type, e.g. ten crocodiles, five similar doors and switches, and so on.
Entities are referenced in one of two ways — as an offset into an array (e.g. Entities[i]
) or internally, using an unique index . In the latter case, the related array (Entities[]
) is searched until a matching index is found. Each entity also refers to its entity type
by TypeID
to select behaviour and model to draw. In this case, Models[]
array is searched for matching TypeID
until one found.
There are three basic types of animations in TR, two corresponding with textures — sprite animations and animated textures — and one corresponding directly with meshes.
Sprite animation (sprite sequences) consists simply of a series of sprites that are to be displayed one after another, e.g. grenade explosions. Sprite animations were quite common in earlier games (TR1 and TR2), while in TR3 onwards there are almost no sprite animations — only notable example is fire particle sprites and water splash effect.
These are either a list of textures cycled through in endless loop, or (in TR4-5) a single texture with shifting coordinates, creating an illusion of “rolling” image.
Mesh animations are much more complex than sprite and texture animations, and done by what is essentially a skeletal-modeling scheme. These involve some arrays (Frames[] and MeshTree[]) of offsets and rotations for each element of a composite mesh. Frames are then grouped into an array (Animations[]) that describes discrete “movements”, e.g. Lara taking a step or a tiger striking with its paw. The animations are “sewn together” by a state change array and an animation dispatch array, which, together with state information about the character, ensure that the animation is fluid (e.g. if Lara is running and the player releases the RUN key, she will stop; depending upon which of her feet was down at the time, either her left or right foot will strike the floor as part of the “stop” animation. The correct animation (left foot stop vs. right foot stop) is selected using these structures and the state information).
There are two main types of lighting in Tomb Raider, constant and vertex. Constant lighting means that all parts of an object have the same illumination, while in vertex lighting, each polygon vertex has its own light value, and the illumination of the polygon interiors is interpolated from the vertex values.
Furthermore, lighting can be either internal or external. Internal lighting is specified in an object’s data, external lighting is calculated using the room’s light sources (ambient light, point light sources, spotlights (TR4-5), dynamic lights).
When available, external lighting also uses the vertex normals to calculate the incoming light at each vertex. Light intensities are described either with a single value or with a 16 bits color value (you can see it more like a “color filter”), depending mainly on the TR version.
Light intensities are described with a single value in TR1 and a pair of values in TR2 and TR3; the paired values are almost always equal, and the pairing may reflect some feature that was only imperfectly implemented, such as off/on or minimum/maximum values. In TR1 and TR2, the light values go from 0 (maximum light) to 8192 (minimum light), while in TR3, the light values go from 0 (minimum light) to 32767 (maximum light).
There are two ways for sound samples to play.
First one is basically sound emitter sitting at a static global position in level, and continuously emitting specified sound (such as waterfalls — these are in SoundSources[]
). Second one is triggered sounds — these are sounds played when some event happens, such as at certain animation frames (footsteps and other Lara sounds), when doors open and close, and when weapons are fired.
Either way, each played sound is referred to using a three-layer indexing scheme, to provide a maximum amount of abstraction. An internal sound index references SoundMap[]
, which points to a SoundDetails[]
record, which in turn points to a SampleIndices[]
entry, which in turn points to a sound sample. SoundDetails[]
, contains such features as sound intensity, how many sound samples to choose from, among others. The sound samples themselves are in Microsoft WAVE format, and, as already mentioned, they are embedded either in the data files (TR1, TR4 and TR5) or in a separate file (MAIN.SFX
) in TR2 and TR3.
Much of the .TR2 file is comprised of structures based on a few fundamental data structures, described below.
This is how most colours are specified.
struct tr_colour // 3 bytes { uint8_t Red; // Red component (0 -- darkest, 255 -- brightest) uint8_t Green; // Green component (0 -- darkest, 255 -- brightest) uint8_t Blue; // Blue component (0 -- darkest, 255 -- brightest) };
(Some compilers will pad this structure to make 4 bytes; one must either read and write 3 bytes explicitly, or else use a simple array of bytes instead of this structure.)
And as mentioned earlier, the 16-bit palette uses a similar structure:
struct tr_colour4 // 4 bytes { uint8_t Red; uint8_t Green; uint8_t Blue; uint8_t Unused; };
In TR5, there is new additional colour type composed of floating-point numbers. This type is primarily used in light structures.
struct tr5_colour // 16 bytes { float Red; float Green; float Blue; float Unused; // Usually filler value = 0xCDCDCDCD };
This is how vertices are specified, using relative coordinates. They are generally formed into lists, such that other entities (such as quads or triangles) can refer to them by simply using their index in the list.
struct tr_vertex // 6 bytes { int16_t x; int16_t y; int16_t z; };
As with colours, TR5 introduced additional vertex type comprised of floating-point numbers:
struct tr5_vertex // 12 bytes { float x; float y; float z; };
Four vertices (the values are indices into the appropriate vertex list) and a texture (an index into the object-texture list) or colour (index into 8-bit palette or 16-bit palette). If the rectangle is a coloured polygon (not textured), the .Texture element contains two indices: the low byte (Texture & 0xFF
) is an index into the 256-colour palette, while the high byte (Texture >> 8
) is in index into the 16-bit palette, when present. A textured rectangle will have its vertices mapped onto all 4 vertices of an object texture, in appropriate correspondence.
struct tr_face4 // 12 bytes { uint16_t Vertices[4]; uint16_t Texture; };
Texture
field can have the bit 15 set: when it is, the face is double-sided (i.e. visible from both sides).
If the rectangle is a coloured polygon (not textured), the .Texture element contains two indices: the low byte (Texture & 0xFF
) is an index into the 256-colour palette, while the high byte (Texture >> 8
) is in index into the 16-bit palette, when present.
TR4 and later introduced an extended version only used for meshes, not for triangles and quads making rooms:
struct tr4_mesh_face4 // 12 bytes { uint16_t Vertices[4]; uint16_t Texture; uint16_t Effects; };
The only difference is the extra field Effects
. It has this layout:
Attribute
field of [tr_object_texture] is 2, but this flag overrides it).Bit 0 set, blending enabled | Bit 0 not set, blending disabled |
Shiny effect at max | No shiny effect |
These structures has the same layout than the quad face definitions, except a textured triangle will have its vertices mapped onto the first 3 vertices of an object texture, in appropriate correspondence. Moreover, a triangle has only 3 vertices, not 4.
struct tr_face3 // 8 bytes { uint16_t Vertices[3]; uint16_t Texture; };
struct tr4_mesh_face3 // 10 bytes { uint16_t Vertices[3]; uint16_t Texture; uint16_t Effects; // TR4-5 ONLY: alpha blending and environment mapping strength };
All the info about Texture
and Effects
fields is also similar to same info from [tr_face4] and [tr4_mesh_face4] respectively.
Each uint8_t
represents a pixel whose colour is in the 8-bit palette.
struct tr_image8 // 65536 bytes { uint8_t pixels[256 * 256]; };
Each uint16_t
represents a pixel, encoded as 1-5-5-5 ARGB.
struct tr_image16 // 131072 bytes { uint16_t pixels[256 * 256]; };
A room in TR2 is simply a rectangular three-dimensional area. A room may be “indoors” or “outdoors,” may or may not be enclosed, may be accessible or inaccessible to Lara, may or may not contain doors or objects.
All rooms have “portals,” called “doors” in some documentation, which are pathways to adjacent rooms. There are two kinds of portals — visibility portals and collisional portals. Visibility portals are for determining how much of a room (if any) is visible from another room, while collisional portals are for enabling an object to travel from one room to another.
The visibility portals are most likely for doing “portal rendering”, which is a visibility-calculation scheme that goes as follows: the viewpoint is a member of some room, which is then listed as visible from it. This room’s portals are checked for visibility from that viewpoint, and visible portals have their opposite-side rooms marked as visible. These rooms are then checked for portals that are visible from the viewpoint through the viewpoint’s room’s portals, and visible ones have their opposite-side rooms marked as visible. This operation is repeated, with viewing through intermediate portals, until all visible portals have been found. The result is a tree of rooms, starting from the viewpoint’s room; only those rooms and their contents need to be rendered.
It is clear that both visibility and collision calculations require that objects have room memberships given for them, and indeed we shall find that most map objects have room memberships.
Rooms may overlap; as we shall see, this is involved in how horizontal collisional portals are implemented. However, different rooms may overlap without either being directly accessible from the other; there are several inadvertent examples of such “5D space” in the Tomb Raider series. The only possibly deliberate example I know of is the flying saucer in “Area 51” in TR3, whose interior is bigger than its exterior.
A room can have an “alternate room” specified for it; that means that that room can be replaced by that alternate as the game is running. This trick is used to produce such tricks as empty rooms vs. rooms full of water, scenery rearrangements (for example, the dynamited house in “Bartoli’s Hideout” in TR2), and so forth. An empty room is first created, and then a full room is created at its location from a copy of it. The empty room then has that full room set as its alternate, and when that room is made to alternate, one sees a full room rather than an empty one.
The rooms are stored sequentially in an array, and “Room Numbers” are simply indices into this array (e.g. “Room Number 5” is simply Rooms[5]
; the first room is Rooms[0]
).
Rooms are divided into sectors (or squares), which are 1024×1024 unit squares that form a grid on the $X-Z$ plane. Sectors are the defining area for floor/ceiling heights and triggers (e.g. a tiger appears and attacks when Lara steps on a given square); the various attributes of each sector are stored in the Sector Data (described in this section) and the [FloorData]. As an aside, Sectors correspond to the “squares,” easily visible in all of the Tomb Raider games, that experienced players count when gauging jumps; they also account for some of the game’s less-appealing graphic artifacts. Careful tiling and texture construction can make these “squares” almost invisible.
While this distinctive feature was never used in originals (collisional room “meshes” fully resembled visible room “meshes”), it is now extensively used by level editing community with the help of a program called meta2tr. This utility allows level builder to replace visible geometry generated by TRLE with any custom geometry, usually modelled in Metasequoia 3D editor (hence the name of meta2tr utility).
Rooms are defined with a complex structure, which is described below “inside-out,” meaning that the smaller component structures are described first, followed by the larger structures that are built using the smaller structures.
$X / Z$ indicate the base position of the room mesh in world coordinates ($Y$ is always zero-relative)
struct tr_room_info // 16 bytes { int32_t x; // X-offset of room (world coordinates) int32_t z; // Z-offset of room (world coordinates) int32_t yBottom; int32_t yTop; };
yBottom
is actually largest value, but indicates lowest point in the room. yTop
is actually smallest value, but indicates highest point in the room.
TR5 uses an extended version of this structure:
struct tr5_room_info // 20 bytes { int32_t x; // X-offset of room (world coordinates) int32_t y; // Y-offset of room (world coordinates) - only in TR5 int32_t z; // Z-offset of room (world coordinates) int32_t yBottom; int32_t yTop; };
The additional y
value is usually 0.
These portals, sometimes called “doors”, define the view from a room into another room. This can be through a “real” door, a window, or even some open area that makes the rooms look like one big room. Note that “rooms” here are really just areas; they aren’t necessarily enclosed. The portal structure below defines only visibility portals, not an actual door model, texture, or action (if any). And if the portal is not properly oriented, the camera cannot “see” through it.
struct tr_room_portal // 32 bytes { uint16_t AdjoiningRoom; // Which room this portal leads to tr_vertex Normal; tr_vertex Vertices[4]; };
Normal
field tells which way the portal faces (the normal points away from the adjacent room; to be seen through, it must point toward the viewpoint).
Vertices
are the corners of this portal (the right-hand rule applies with respect to the normal). If the right-hand-rule is not followed, the portal will contain visual artifacts instead of a viewport to AdjoiningRoom
.
Core: FLOOR_INFO
All the geometry specified here is collisional geometry.
struct tr_room_sector // 8 bytes { uint16_t FDindex; // Index into FloorData[] uint16_t BoxIndex; // Index into Boxes[] (-1 if none) uint8_t RoomBelow; // 255 is none int8_t Floor; // Absolute height of floor uint8_t RoomAbove; // 255 if none int8_t Ceiling; // Absolute height of ceiling };
Floor
and Ceiling
are signed numbers of 256 units of height (relative to 0) — e.g. Floor 0x04
corresponds to $Y = 1024$ in world coordinates. Therefore, 256 units is a minimum vertical stride of collisional geometry. However, this rule could be broken by specific entities, which Lara can stand on. But horizontal sector dimensions, which, as mentioned earlier, are 1024 x 1024 (in world coordinates), could not. Therefore, minimal horizontal platform dimensions, on which Lara can stand and grab, are 1024 x 1024 as well.
Floor
and Ceiling
value of 0x81
is a magic number used to indicate impenetrable walls around the sector. Floor
values are used by the game engine to determine what objects Lara can traverse and how. Relative steps of 1 (-256) can be walked up; steps of 2..7 (-512..-1792) can/must be jumped up; steps larger than 7 (-2048..-32768) cannot be jumped up (too tall).
RoomAbove
and RoomBelow
values indicate what neighboring rooms are in these directions — the number of the room below this one and the number of the room above this one. If RoomAbove
is not none, then the ceiling is a collisional portal to that room, while if RoomBelow
is not none, then the floor is a collisional portal to that room.
Also, RoomBelow
value is extensively used by engine to determine actual sector data and triggers in so-called stacked room setups, when one room is placed above another through collisional portal. The thing is, engine uses sector data and triggers only for the lowest sector of the stacked room setup, so it recursively scans for a lowest room to determine which sector to use.
FDindex
is a pointer to specific entry in [FloorData] array, which keeps all the information about sector flags, triggers and other parameters. While it is implied that one FDindex
entry may be shared between several sectors, it is usually not the case with original Tomb Raider levels built with TRLE. However, Dxtre3d takes advantage of this feature and may optimize similar sectors to share same FDindex pointer.
BoxIndex
is a pointer to special [Boxes] array entry, which is basically a subset of sectors with same height configuration. It is primarily used for AI pathfinding (see the Non-player character behaviour chapter for more details).
In these games, BoxIndex
field is more complicated:
struct tr3_room_sector // 8 bytes { uint16_t FDindex; // Index into FloorData[] uint16_t Material : 4; // Material index, used uint16_t Box : 11; // Actual box index uint16_t Stopper : 1; uint8_t RoomBelow; // 255 is none int8_t Floor; // Absolute height of floor uint8_t RoomAbove; // 255 if none int8_t Ceiling; // Absolute height of ceiling };
Hex value | 0x8000 | 0x7FF0 | 0x000F |
|||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bit | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Field | Stopper | Box | Material |
Material
is the material index, which is used to produce specific footstep sound, when Lara is walking or running in this sector. On PlayStation game versions, this index was also used to determine if footprint textures should be applied to this particular place. Both procedures are invoked via FOOTPRINT_FX
flipeffect, which is described in corresponding section.
Majority of material index values are the same across game versions, but some of them exist only in particular game. Here is the description:
Mud, snow, sand, grass and maybe some other materials produce footprints in PlayStation version.
Furthermore, in TR3-5, actual box index may contain special value 2047 (full 1s), which is most likely indicates that this sector is a slope on which Lara can slide (and, therefore, possibly impassable by most NPCs).
There are four different types of room light structures. First one is used in TR1-2, second is used in TR3, third is used in TR4, and fourth is used in TR5. Here is the description of each:
struct tr_room_light // 18 bytes { int32_t x, y, z; // Position of light, in world coordinates uint16_t Intensity1; // Light intensity uint32_t Fade1; // Falloff value };
X/Y/Z
are in world coordinates. Intensity1/Intensity2
are almost always equal. This lighting only affects externally-lit objects. Tomb Raider 1 has only the first of the paired Intensity
and Fade
values.
Intensity1
ranges from 0
(dark) to 0x1FFF
(bright). However, some rooms occasionally have some lights with intensity greater than 0x1FFF
(for example, look at room #9, 2nd light in level1.phd
). Fade1
is the maximum distance the light shines on, and ranges from 0
to 0x7FFF
.
TR2 uses an extended version of TR1 light structure:
struct tr2_room_light // 24 bytes { int32_t x, y, z; // Position of light, in world coordinates uint16_t Intensity1; // Light intensity uint16_t Intensity2; // Only in TR2 uint32_t Fade1; // Falloff value uint32_t Fade2; // Only in TR2 };
Intensity2
and Fade2
values are seemingly not used. Intensity1
can go very well beyond 0x1FFF
, right to 0x7FFF
(ultra bright light). Above 0x7FFF
, it is always black, so the number is pseudo-signed (negative values are always treated as zero).
TR3 introduced a rudimentary “light type” concept. Although only possible types are sun (type 0) and point light (type 1). It is not clear how sun affetcts room lighting, but somehow it interpolates with all other light objects in a given room. Both light types are kept using the same structure, with two last 8 bytes used as union.
struct tr3_room_light // 24 bytes { int32_t x, y, z; // Position of light, in world coordinates tr_colour Colour; // Colour of the light uint8_t LightType; // Only 2 types - sun and point lights if (LightType == 0) // Sun { int16_t nx, ny, nz; // Most likely a normal int16_t Unused; } else if (LightType == 1) // Point { int32_t Intensity; int32_t Fade; // Falloff value } };
Intensity
is the power of the light and ranges mainly from 0
(low power) to 0x1FFF
(high power). Fade
is the distance max the light can shine on. Range is mainly from 0
to 0x7FFF
.
struct tr4_room_light // 46 bytes { int32_t x, y, z; // Position of light, in world coordinates tr_colour Colour; // Colour of the light uint8_t LightType; uint8_t Unknown; // Always 0xFF? uint8_t Intensity; float In; // Also called hotspot in TRLE manual float Out; // Also called falloff in TRLE manual float Length; float CutOff; float dx, dy, dz; // Direction - used only by sun and spot lights };
LightType
was extended and is now somewhat similar to D3D light type, but there are some differences.
Fog bulbs don’t use Colour
field to define its colour. However, Red
field of a Colour
structure is used to define fog density. Colour itself can be only changed with special flipeffect #28 trigger function, which takes a value from the timer field of the trigger to index into hardcoded RGB table of pre-defined colours. The table consists of 28 RGB values:
0 = 0,0,0 | 7 = 0,64,192 | 14 = 111,255,223 | 21 = 0,30,16 |
||||
1 = 245,200,60 | 8 = 0,128,0 | 15 = 244,216,152 | 22 = 250,222,167 |
||||
2 = 120,196,112 | 9 = 150,172,157 | 16 = 248,192,60 | 23 = 218,175,117 |
||||
3 = 202,204,230 | 10 = 128,128,128 | 17 = 252,0,0 | 24 = 225,191,78 |
||||
4 = 128,64,0 | 11 = 204,163,123 | 18 = 198,95,87 | 25 = 77,140,141 |
||||
5 = 64,64,64 | 12 = 177,162,140 | 19 = 226,151,118 | 26 = 4,181,154 |
||||
6 = 243,232,236 | 13 = 0,223,191 | 20 = 248,235,206 | 27 = 255,174,0 |
struct tr5_room_light // 88 bytes { float x, y, z; // Position of light, in world coordinates float r, g, b; // Colour of the light uint32_t shadow; // Dummy value = 0xCDCDCDCD float In; // Cosine of the IN value for light / size of IN value float Out; // Cosine of the OUT value for light / size of OUT value float RadIn; // (IN radians) * 2 float RadOut; // (OUT radians) * 2 float Range; // Range of light float dx, dy, dz; // Direction - used only by sun and spot lights int32_t x2, y2, z2; // Same as position, only in integer. int32_t dx2, dy2, dz2; // Same as direction, only in integer. uint8_t LightType; uint8_t Filler[3]; // Dummy values = 3 x 0xCD };
struct tr5_fog_bulb // 36 bytes { float x, y, z; // Position of light, in world coordinates float rad, sqrad; float den; float r, g, b; };
x,y,z
values shouldn’t be used by sun type light, but sun seems to have a large x
value (9 million, give or take), a zero y
value, and a small z
value (4..20) in the original TR5 levels.
In
and Out
values aren’t used by sun type. For the spot type, these are the hotspot and falloff angle cosines. For the light and shadow types, these are the TR units for the hotspot / falloff (1024 = 1 sector).
RadIn
, RadOut
and Range
are only used by the spot light type.
dx
, dy
and dz
values are used only by the sun and spot type lights. They describe the directional vector of the light. This can be obtained by:
x
and y
$\text{LightDirectionVectorX} = \cos(X) \cdot \sin(Y)$
x2
, y2
, z2
, dx2
, dy2
and dz2
values repeat previous corresponding information in long data types instead of floats.
This defines the vertices within a room. As mentioned above, room lighting is internal vertex lighting, except for necessarily external sources like flares, flame emitters and gunflashes. Room ambient lights and point sources are ignored.
As TR3 introduced colored lighting, room vertex structure drastically changed. It changed once again in TR5, when floating-point numbers were introduced. So we’ll define vertex structure for TR1-2, TR3-4 and TR5 independently.
struct tr_room_vertex // 8 bytes { tr_vertex Vertex; int16_t Lighting; };
Vertex
is the coordinates of the vertex, relative to [tr_room_info] x
and z
values.
Lighting
ranges from 0
(bright) to 0x1FFF
(dark). This value is ignored by TR2, and Lighting2
is used instead with the same brightness range.
TR2 uses an extended version of the structure:
struct tr2_room_vertex // 12 bytes { tr_vertex Vertex; int16_t Lighting; uint16_t Attributes; // A set of flags for special rendering effects int16_t Lighting2; // Almost always equal to Lighting1 };
Attributes
field is a set of flags, and their meaning is:
LightMode
field of the [tr_room] structure. See below.
Lighting
field is ignored by TR2, and Lighting2
is used instead with the same brightness range — from 0 (bright) to 0x1FFF (dark).
struct tr3_room_vertex // 12 bytes { tr_vertex Vertex; int16_t Lighting; // Value is ignored! uint16_t Attributes; // A set of flags for special rendering effects uint16_t Colour; // 15-bit colour };
Lighting
value is ignored by the engine, as now each vertex has its own defined 15-bit colour (see below).
Attributes
bit flags were extended. Also, old bits 0..4 aren't working properly anymore, because effect lighting was broken in TR3. Here is the list of new flags:
Colour
value specifies vertex colour in 15-bit format (each colour occupies 5 bits). Therefore, each colour value’s maximum is 31. You can use this code to get each colour:
((Colour & 0x7C00) >> 10)
((Colour & 0x03E0) >> 5)
(Colour & 0x001F)
In TR5, room vertex structure was almost completely changed. Coordinates were converted to floats, and normal was added:
struct tr5_room_vertex // 28 bytes { tr5_vertex Vertex; // Vertex is now floating-point tr5_vertex Normal; uint32_t Colour; // 32-bit colour };
There is no more Attributes
field in room vertex structure for TR5.
struct tr_room_sprite // 4 bytes { int16_t Vertex; // Offset into vertex list int16_t Texture; // Offset into sprite texture list };
Vertex
indicates an index into room vertex list (Room.Vertices[room_sprite.Vertex]
), which acts as a point in space where to display a sprite.
Texture
is an index into the sprite texture list.
This is the whole geometry of the “room,” including walls, floors, ceilings, and other embedded landscape. It does not include objects that Lara can interact with (keyholes, moveable blocks, moveable doors, etc.), neither does it include static meshes (mentioned below in the next section).
The surfaces specified here are rendered surfaces.
NumXXX
elements that precede them. Also [tr_room_vertex] could be replaced by any other version-specific room vertex type ([tr3_room_vertex], etc.).
virtual struct tr_room_data // (variable length) { int16_t NumVertices; // Number of vertices in the following list tr2_room_vertex Vertices[NumVertices]; // List of vertices (relative coordinates) int16_t NumRectangles; // Number of textured rectangles tr_face4 Rectangles[NumRectangles]; // List of textured rectangles int16_t NumTriangles; // Number of textured triangles tr_face3 Triangles[NumTriangles]; // List of textured triangles int16_t NumSprites; // Number of sprites tr_room_sprite Sprites[NumSprites]; // List of sprites };
Core: MESH_INFO
Positions and IDs of static meshes (e.g. skeletons, spiderwebs, furniture, trees). This is comparable to the [tr_entity] structure, except that static meshes have no animations and are confined to a single room.
struct tr_room_staticmesh // 18 bytes { uint32_t x, y, z; // Absolute position in world coordinates uint16_t Rotation; uint16_t Intensity1; uint16_t MeshID; // Which StaticMesh item to draw };
Intensity1
ranges from 0
(bright) to 0x1FFF
(dark).
In Rotation
field, high two bits (0xC000
) indicate steps of 90 degrees (e.g. (Rotation >> 14) * 90
). However, when parsing this value, no extra bitshifting is needed, as you can simply interpret it using this formula:
$RealRotation = \frac{Rotation}{16384} \cdot -90$
TR2 again uses an extended version:
struct tr2_room_staticmesh // 20 bytes { uint32_t x, y, z; // Absolute position in world coordinates uint16_t Rotation; uint16_t Intensity1; uint16_t Intensity2; // Absent in TR1 uint16_t MeshID; // Which StaticMesh item to draw };
Intensity2
is seemingly not used, as changing this value does nothing.
virtual struct tr3_room_staticmesh // 20 bytes { uint32_t x, y, z; // Absolute position in world coordinates uint16_t Rotation; uint16_t Colour; // 15-bit colour uint16_t Unused; // Not used! uint16_t MeshID; // Which StaticMesh item to draw };
Colour
value specifies vertex colour in 15-bit format (each colour occupies 5 bits): 0b0RRRRRGGGGGBBBBB. Therefore, each colour value’s maximum is 31. You can use this code to get each colour:
((Colour & 0x7C00) >> 10)
((Colour & 0x03E0) >> 5)
(Colour & 0x001F)
In TR5 the room format was drastically changed. The room itself is made up of sections. These sections encompass a 3×3 sector grid (actually 3069×3069 pixels). Historically, these sections are referred as layers, however, more proper name for them is volumes. Layers are organized in a quadtree-like structure, and their purpose was presumably optimizing rendering by some kind of space partitioning and culling invisible volumes.
Another thing to note is that some rooms in TR5 do not actually contain visible mesh data. If concerned, we will refer to these rooms as null rooms.
struct tr5_room_layer // 56 bytes { uint16_t NumLayerVertices; // Number of vertices in this layer (2 bytes) uint16_t NumLayerVerticesWater; // Number of underwater vertices in this layer (2 bytes) uint16_t NumLayerVerticesShore; uint16_t NumLayerRectangles; // Number of rectangles in this layer (2 bytes) uint16_t NumLayerTriangles; // Number of triangles in this layer (2 bytes) uint16_t NumLayerRectanglesWater; // Number of rectangles containing water vertices uint16_t NumLayerTrianglesWater; // Number of trianglescontaining water vertices uint16_t Filler; // Always 0 // The following 6 floats define the bounding box for the layer float LayerBoundingBoxX1; float LayerBoundingBoxY1; float LayerBoundingBoxZ1; float LayerBoundingBoxX2; float LayerBoundingBoxY2; float LayerBoundingBoxZ2; uint32_t Filler3; // Always 0 (4 bytes), internal pointer uint32_t VerticesOffset; // Those fields are overwritten at level loading uint32_t PolyOffset; // by the ones present in the tr5_room struct + an offset uint32_t PrelightOffset; // i.e. the values are not read, the fields are there for storage purposes }
NumLayerVerticesShore
appears to be the number of double sided textures in this layer, however is sometimes 1 off (2 bytes).
Here’s where all the room data come together.
Room structure differs drastically across different game versions (especially in TR5). For this reason, we will define each version of Room structure independently, to avoid confusion. Also, version-specific fields will be described in each version’s section in a “backwards-compatible” manner, while common fields with version-specific variations, such as Flags
, will be described afterwards in separate section.
These are not “real” C/C++ structures, in that the arrays are sized by the NumXXX
elements that precede them.
As it’s stored in the file, the [tr_room_info] structure comes first, followed by a uint32_t NumDataWords
, which specifies the number of 16-bit words to follow. Those data words must be parsed in order to interpret and construct the variable-length arrays of vertices, meshes, doors, and sectors. Such setup is also applicable to all variations of room structures, except [tr5_room], which will be described independently.
virtual struct tr_room // (variable length) { tr_room_info info; // Where the room exists, in world coordinates uint32_t NumDataWords; // Number of data words (uint16_t's) uint16_t Data[NumDataWords]; // The raw data from which the rest of this is derived tr_room_data RoomData; // The room mesh uint16_t NumPortals; // Number of visibility portals to other rooms tr_room_portal Portals[NumPortals]; // List of visibility portals uint16_t NumZsectors; // ``Width'' of sector list uint16_t NumXsectors; // ``Height'' of sector list tr_room_sector SectorList[NumXsectors * NumZsectors]; // List of sectors in this room int16_t AmbientIntensity; uint16_t NumLights; // Number of lights in this room tr_room_light Lights[NumLights]; // List of lights uint16_t NumStaticMeshes; // Number of static meshes tr_room_staticmesh StaticMeshes[NumStaticMeshes]; // List of static meshes int16_t AlternateRoom; int16_t Flags; };
AmbientIntensity
is a brightness value which affects only externally-lit objects. It ranges from 0
(bright) to 0x1FFF
(dark).
AlternateRoom
(or, as it is called in TRLE terms, flipped room) is the number of the room that this room can flip with. In the terms of the gameplay, flipped room is a state change of the same room — for example, empty or flooded with water, filled with sand or debris. Alternate room usually has the same boundaries as original room, but altered geometry and/or texturing. Detailed description of alternate rooms will be provided in a separate section.
virtual struct tr2_room // (variable length) { tr_room_info info; // Where the room exists, in world coordinates uint32_t NumDataWords; // Number of data words (uint16_t's) uint16_t Data[NumDataWords]; // The raw data from which the rest of this is derived tr_room_data RoomData; // The room mesh uint16_t NumPortals; // Number of visibility portals to other rooms tr_room_portal Portals[NumPortals]; // List of visibility portals uint16_t NumZsectors; // "Width" of sector list uint16_t NumXsectors; // "Height" of sector list tr_room_sector SectorList[NumXsectors * NumZsectors]; // List of sectors in this room int16_t AmbientIntensity; int16_t AmbientIntensity2; // Usually the same as AmbientIntensity int16_t LightMode; uint16_t NumLights; // Number of point lights in this room tr_room_light Lights[NumLights]; // List of point lights uint16_t NumStaticMeshes; // Number of static meshes tr2_room_staticmesh StaticMeshes[NumStaticMeshes]; // List of static meshes int16_t AlternateRoom; int16_t Flags; };
AmbientIntensity2
value is usually equal to AmbientIntensity
value. Seems it’s not used.
LightMode
specifies lighting mode special effect, which is applied to all room vertices in conjunction with 5 lowest bits of Attributes
field belonging to [tr_room_vertex] structure. Here we will refer these 5 bits value to as effect_value
:
effect_value
acting the same way — as intensity multiplier.effect_value
is in 1-15 range, then vertex lighting is cyclically fading to more bright value. The lower the value is, the deeper the fade to full vertex lighting is. If effect_value
is in 17-30 range (not 31!), then vertex lighting is cyclically fading to more dark value. The higher the value is, the deeper the fade to black is. If effect_value
is 16 or 0, no effect is produced. So practically, effect_value
serves as a multiplier to overall effect brightness.sunset
gameflow script opcode is present, rooms with this light type will gradually dim all lights for 20 minutes. This happens in Bartoli's Hideout. Sunset state isn't saved in savegames and will be reset on reload.virtual struct tr3_room // (variable length) { tr_room_info info; // Where the room exists, in world coordinates uint32_t NumDataWords; // Number of data words (uint16_t's) uint16_t Data[NumDataWords]; // The raw data from which the rest of this is derived tr_room_data RoomData; // The room mesh uint16_t NumPortals; // Number of visibility portals to other rooms tr_room_portal Portals[NumPortals]; // List of visibility portals uint16_t NumZsectors; // ``Width'' of sector list uint16_t NumXsectors; // ``Height'' of sector list tr3_room_sector SectorList[NumXsectors * NumZsectors]; // List of sectors in this room int16_t AmbientIntensity; // Affects externally-lit objects int16_t LightMode; // Broken in this game version uint16_t NumLights; // Number of point lights in this room tr3_room_light Lights[NumLights]; // List of point lights uint16_t NumStaticMeshes; // Number of static meshes tr3_room_staticmesh StaticMeshes[NumStaticMeshes]; // List of static meshes int16_t AlternateRoom; int16_t Flags; uint8_t WaterScheme; uint8_t ReverbInfo; uint8_t Filler; // Unused. };
LightMode
types are broken and produce aburpt “bumpy ceiling” effect. It happens because internally same data field was reused for vertex waving effects (seen in quicksand and water rooms). Type 3 (sunset) works the same as in TR2.
virtual struct tr4_room // (variable length) { tr_room_info info; // Where the room exists, in world coordinates uint32_t NumDataWords; // Number of data words (uint16_t's) uint16_t Data[NumDataWords]; // The raw data from which the rest of this is derived tr_room_data RoomData; // The room mesh uint16_t NumPortals; // Number of visibility portals to other rooms tr_room_portal Portals[NumPortals]; // List of visibility portals uint16_t NumZsectors; // ``Width'' of sector list uint16_t NumXsectors; // ``Height'' of sector list tr3_room_sector SectorList[NumXsectors * NumZsectors]; // List of sectors in this room uint32_t RoomColour; // In ARGB format! uint16_t NumLights; // Number of point lights in this room tr4_room_light Lights[NumLights]; // List of point lights uint16_t NumStaticMeshes; // Number of static meshes tr3_room_staticmesh StaticMeshes[NumStaticMeshes]; // List of static meshes int16_t AlternateRoom; int16_t Flags; uint8_t WaterScheme; uint8_t ReverbInfo; uint8_t AlternateGroup; // Replaces Filler from TR3 };
RoomColour
replaces AmbientIntensity
and AmbientIntensity2
values from [tr2_room] structure. Note it’s not in [tr_colour4] format, because colour order is reversed. It should be treated as ARGB, where A is unused.
AlternateGroup
was introduced in TR4 to solve long-existing engine limitation, which flipped all alternate rooms at once (see flipmap trigger action description in Trigger actions section). Since TR4, engine only flips rooms which have similar index in room’s AlternateGroup
field and trigger operand.
As it was mentioned before, TR5 room structure was almost completely changed, when compared to previous versions. For example, TR5 shuffles numerous values and structures in almost chaotic manner, and introduces a bunch of completely new parameters (mostly to deal with layers). Also, there is vast amount of fillers and separators, which contain no specific data.
tr5_room
structure, i.e. to the beginning of the tr5_room_data
structure.
struct tr5_room // 216 bytes { char XELA[4]; // So-called "XELA landmark" uint32_t RoomDataSize; // size of following fields (208 bytes) + tr5_room_data (dynamic) char* RoomData; // is filled by the game at run time. Always 0xCDCDCDCD in level files (4 bytes), uint16_t* NumPortals; // points to tr5_room_data.NumPortals tr3_room_sector* SectorList; // points to tr5_room_data.SectorList tr5_room_light* Lights; // points to tr5_room_data.Lights tr3_room_staticmesh* StaticMeshes; // points to tr5_room_data.StaticMeshes tr5_room_info info; uint16_t NumZSectors; uint16_t NumXSectors; uint32_t RoomColour; // In ARGB format! uint16_t NumLights; uint16_t NumStaticMeshes; uint8_t ReverbInfo; uint8_t AlternateGroup; int8_t MeshEffect; int8_t bound_active; int16_t left; // always 0x7FFF int16_t right; // always 0 int16_t top; // always 0x7FFF int16_t bottom; // always 0 int16_t test_left; // always 0xCDCD int16_t test_right; // always 0xCDCD int16_t test_top; // always 0xCDCD int16_t test_bottom; // always 0xCDCD int16_t item_number; // always 0xFFFF (-1) int16_t fx_number; // always 0xFFFF (-1) uint16_t AlternateRoom; uint16_t Flags; uint32_t nVerts; uint32_t nWaterVerts; // Always 0 uint32_t nShoreVerts; // Always 0 uint32_t Separator; // 0xCDCDCDCD, internally a pointer uint32_t Separator; // internally a pointer to the face data float RoomX; float RoomY; float RoomZ; uint32_t vnormals; // internal pointer uint32_t fnormals; // internal pointer uint32_t prelight; // internal pointer uint32_t prelightwater; // internal pointer uint32_t watercalc; // internal pointer, 0 for normal rooms and 0xCDCDCDCD for null rooms uint32_t verts; // internal pointer uint32_t NumRoomTriangles; uint32_t NumRoomRectangles; tr5_room_light* RoomLights; // points to tr5_room_data.RoomLights tr5_fog_bulb* FogBulbs; // points to tr5_room_data.FogBulbs uint32_t NumLights2; // Always same as NumLights uint32_t NumFogBulbs; int32_t RoomYTop; int32_t RoomYBottom; uint32_t NumLayers; tr5_room_layer* Layers; // points to tr5_room_data.Layers tr5_room_vertex* Vertices; // points to tr5_room_data.Vertices void* PolyOffset; // points to tr5_room_data.Faces void* PrelightOffset; // points to tr5_room_data.Faces, internal? uint32_t NumVertices; // Always 0xCDCDCDCD float fLeft; float fRight; float fTop; float fBottom; } // immediately after virtual struct tr5_room_data { tr5_room_light Lights[NumLights]; // Data for the lights (88 bytes * NumRoomLights) tr5_fog_bulb FogBulbs[NumFogBulbs]; // Data for the fog bulbs (36 bytes * NumFogBulbs) tr3_room_sector SectorList[NumXSectors * NumZSectors]; // List of sectors in this room uint16_t NumPortals; // Number of visibility portals to other rooms tr_room_portal Portals[NumPortals]; // List of visibility portals uint16_t Separator; // Always 0xCDCD tr3_room_staticmesh StaticMeshes[NumStaticMeshes]; // List of static meshes tr5_room_layer Layers[NumLayers]; // Data for the room layers (volumes) (56 bytes * NumLayers) uint8_t Faces[(NumRoomRectangles * sizeof(tr_face4) + NumRoomTriangles * sizeof(tr_face3)]; tr5_room_vertex Vertices[NumVertices]; }
XELA
landmark seemingly serves as a header for room structure. It is clear that XELA is a reversed ALEX, which is most likely the name of TR5 programmer, Alex Davis. It probably indicates that Alex Davis is responsible for changes in room structures.
RoomDataSize
is a handy value determining the size of the following data. You can use this value to quickly parse thru to the next room.
RoomX
, RoomY
and RoomZ
values are positions of room in world coordinates. NOTE: If room is null room, then each of these values will be 0xCDCDCDCD
.
NumRoomTriangles
and NumRoomRectangles
are respectively the numbers of triangular and rectangular faces in a given room. NOTE: If room is null room, each of these values will be 0xCDCDCDCD
.
LightDataSize
is the size of the light data in bytes (not in [tr5_room_light] units).
RoomYTop
and RoomYBottom
are equal to yTop
and yBottom
values in [tr_room_info] structure. If room is a null room, both of these values are 0xCDCDCDCD
.
NumLayers
is a number of layers (volumes) in this room.
VerticesSize
is the size of vertex data block in bytes. Therefore, it must be a multiple of [tr5_room_vertex] size, else it means the block size is wrong.
Faces
is a sequential data array for the room polygons (both [tr_face4] and [tr_face3]),
Faces
array is strictly linked with NumLayers
value. The data is sequentially structured for each layer — at first it lists first layer’s rectangles then triangles, followed by the second layer’s rectangles and triangles, and so on, until all layers are done.
Flags
is an array of various flag bits, which meaning is as follows:
SFX_WATER
(0x01
) will not play.water_scheme
byte).water_scheme
byte). When the flag is set for normal room and there is water room below it, game engine creates “reflection effect” above the water surface — effectively it means that all the vertices at the bottom of the room receive caustics effect described well above.
WaterScheme
is used for different purposes. If room is a water room, then it specifies underwater caustics patterns. If it is set for normal room placed above the water room, then it controls wave strength effect applied to the faces adjoining water room. Maximum value in both cases is 15.
ReverbInfo
defines room reverberation type. It affects sound postprocessing, if listener position belongs to that room. This feature was present only in PlayStation versions of the game, but not on PC. Nevertheless, the info is preserved in PC level files. Here are the types of reverberation:
The FloorData is the key part of the level structure, which defines almost everything related to “physical” world — geometry, interaction and response of a level. While room geometry (see above) may be considered as a “face” of the level, FloorData is its “heart” and “brain”.
Distinctive feature of the FloorData is its serialized nature. While in room geometry you can easily jump through structures using data sizes and pointers to get the needed part, FloorData require sequential parsing, one unit by one.
The FloorData defines special sector attributes such as individual floor and ceiling corner heights (slopes), collisional portals to other rooms, climbability of walls, and, most important of all, the various types of triggering. Each room sector (see [tr_room_sector] structure) points to the FloorData using FDIndex
variable. It is referenced as an array of 16-bit unsigned integers (uint16
s).
Therefore, the current [tr_room_sector] offset (not yet the FloorData pointer itself!) is calculated using this formula:
$S_{Offset} = \frac{X_{current} - X_{room}}{1024} \cdot n_{Zsectors} + \frac{Z_{current} - Z_{room}}{1024}$
…where $X_{current}$ and $Z_{current}$ are current player positions, $X_{room}$ and $Z_{room}$ are corresponding tr_room_info.x and tr_room_info.z fields, and $n_{Zsectors}$ is tr_room.NumZsectors value.
Then, the current FloorData pointer is derived from calculated [tr_room_sector] structure’s FDIndex
field. In other words, FDindex
is an offset into the FloorData[]
array.
As mentioned above, The FloorData consists of solely uint16_t
entries without general structure — the way engine treats specific entry depends on the sequence order and type of previously parsed entries. While it’s a bit difficult to understand it at first, you should get used to it. Main thing to remember is the FloorData should be read sequentially.
First order of FloorData entries has a common “bitwise” structure, which we will call FDSetup
. The structure could be divided into three fields:
Hex value | 0x8000 | 0x7F00 | 0x001F |
|||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bit | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Field | EndData | SubFunction | Function |
Function
defines the type of action that must be done with current FloorData entry, and SubFunction
is usually used in that action’s conditions and case switches (if there are any). If there are no any special conditions for a given Function
, then SubFunction
is not used.
FDSetup
structure, but actually they belong to Function
value, but only in TR1 and TR2. When parsing FDSetup
for TR3+, you should use only the lower 5 bits (0..4) to find the Function
value, because some of TR3 triangulation functions use the upper 3 bits of the lower byte for other purpose. However, this will also work correctly in TR1 and TR2, since maximum possible function value is way below 5-bit limitation.
If EndData
is set, there should be no more similar FloorData entries (after the current one) in the FloorData[]
array — so further parsing must be stopped. Otherwise, the following uint16_t
should be interpreted after the current one in the same manner.
EndData
is set, it doesn’t specifically mean that there are no more uint16_t
following the current one at all. As we will see, some FloorData functions and subfunctions require to parse additional entries with their own rules. In programming terms, EndData
just indicates that parsing loop must be broken — however, there may be following code which reads additional entries.
FloorData
index 0 means the sector does not use floordata, there is still a “dummy” entry for index 0. This dummy entry doesn’t contain any useful information.
00
, 01
, 10
, and 11
, where the first digit is the corner’s X coordinate and the second digit is the corner’s Z coordinate, with both given as multiples of 1024.
SubFunction is not used
The next FloorData
entry is the number of the room that this sector is a collisional portal to. An entity that arrives in a sector with this function present will gets its room membership changed to provided room number, without any change in position.
To understand what exactly happens when room membership is changed, you must understand how collisional portals work in Tomb Raider’s 4D space. When two rooms are connected with portal, it means that they also overlap within a distance of two sectors (because these sectors contain portal in each of the connected rooms). This way, when room is changed, it remains unnoticed by the player, cause portal sectors are interconnected:
Collisional portal layout. Blue sectors are walls around each room. Green sector is Room 2’s collisional portal to Room 1, and dark blue sector is Room 1’s collisional portal to Room 2 |
SubFunction is not used
The next FloorData
entry contains two uint8_t
slant values for the floor of this sector. Slant values are specified in increments of 256 units (so-called clicks in TRLE terms). The high byte is the Z slope, while the low byte is the X slope. If the X slope is greater than zero, then its value is added to the floor heights of corners 00
and 01
. If it is less than zero, then its value is subtracted from the floor heights of corners 10
and 11
. If the Z slope is greater than zero, then its value is added to the floor heights of corners 00
and 10
. If it is less than zero, then its value is subtracted from the floor heights of corners 01
and 11
.
SubFunction is not used
The next FloorData
entry contains two uint8_t
slant values for the ceiling of this sector. Slant values are specified in increments of 256 units. The high byte is the Z slope, while the low byte is the X slope. If the X slope is greater than zero, then its value is subtracted from the ceiling heights of corners 10
and 11
. If it is less than zero, then its value is added to the ceiling heights of corners 00
and 01
. If the Z slope is greater than zero, then its value is subtracted from the ceiling heights of corners 00
and 10
. If it is less than zero, then its value is added to the ceiling heights of corners 01
and 11
.
The uint16_t
immediately following current entry is called TriggerSetup
, and contains general trigger properties stored in a “bitwise” manner:
Hex value | 0x3E00 | 0x0100 | 0x00FF |
|||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bit | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Field | Mask | Oneshot | Timer |
Timer
is a value generally used for making timed triggers of certain entities — for example, the door which opens only for a few seconds and then closes, or a fire which extinguishes and then burns again. In such case, engine copies timer value in corresponding field of each triggered entity. Then each entity’s timer begins to count time back, and when it reaches zero, entity deactivates.
However, it’s not the only purpose of Timer
field. As trigger may not specifically activate entities but do some other actions, Timer
field may be re-used as a general-purpose numerical field to specify particular trigger behaviour. We will mention it separately for such trigger actions.
Timer
field became signed, i.e. it may contain negative values. Effectively, it means that entities activated with such trigger won’t be immediately activated and then deactivated after given amount of time, but wait for a given time before being activated. Most prominent example is timed spike pit in the beginning of “Burial Chambers”.
Mask
: The five bits at 0x3E00
are the so-called Trigger Mask. The purpose of trigger mask is to create puzzle set-ups which require a combination of activated triggers to achieve certain result. A good example of trigger mask use is the multiple-switch room of “Palace Midas” in TR1.
XOR
operation (for switch trigger types — namely, switch and heavy switch — see further) or bitwise OR
operation on activation mask using trigger mask. Trigger action purposed for deactivation (namely, antitrigger, antipad and heavy antitrigger types) don’t take its trigger mask into consideration, and instead just reset target entity’s activation mask to zero.
Whenever entity’s activation mask is changed to anything but 0x1F (all bits set), entity is automatically deactivated, excluding the cases when OneShot
flag was previously set for a given entity — see further.
OneShot
flag is used only for activation of entities (it is also copied to entity’s own flag field with same name), and indicates that after activation, entity state is locked. It means that even if entity’s own activation mask is unset (as with switch trigger type — see further), entity will remain activated. However, it doesn’t mean that entity couldn’t be deactivated at all — because antitrigger trigger type (see further) ignores and resets this flag.
In these versions, any antitriggers for locked entity have no effect, and engine completely bypasses antitrigger processing for such entities. It means that once entity was activated by trigger with OneShot
flag, it couldn’t be deactivated at all.
There’s a bit different OneShot
flag behaviour for TR3 — entity state will be locked even if activation mask isn’t set yet. For example, if there are two complementary triggers for certain entity (let’s say, with activation masks 0x0F
and 0x10
), and each of them has OneShot
flag, effectively target entity won’t ever be activated, cause each trigger immediately locks entity state, bypassing further trigger processing.
Trigger types and trigger actions will be described separately right after listing all FloorData functions.
SubFunction not used
Instantly kills Lara. Usually she is simply set on fire, however, there is one special case in TR3. If current level index is 7 (“Madubu Gorge”), then instead of catching fire, Lara will play drowning animation.
The SubFunction
indicates climbability of walls; its value is the bitwise OR
of the values associated with all the climbable-wall directions (0x01
= +Z, 0x02
= +X, 0x04
= -Z, 0x08
= -X), e.g. SubFunction 0x09
indicates that the walls on both the +Z and -X sides of this sector are climbable.
Beginning with TR3, geometry layout of each sector was significantly changed. Engine introduced triangles as a minimal collisional unit, compared with rectangles only in TR1 and TR2. This advantage allowed to create much more organic and natural terrain (albeit still limited by minimal sector width and depth of 1024 units), but also complicated things a lot, introducing the whole new set of different FloorData collisional functions.
Similarly to slant functions, triangulation functions define the floor and ceiling corner heights, but besides, they also specify dividing up the floors and ceilings into triangles along either of the two diagonals. Also, one of the triangles may be a collisional portal to the room above (if in the ceiling) or to the room below (if in the floor).
Each triangulation function uint16_t
must be parsed differently, not like ordinary FDSetup
entry:
Hex value | 0x8000 | 0x7C00 | 0x03E0 | 0x001F |
||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bit | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Field | EndData | $H_{\triangle2}$ | $H_{\triangle1}$ | Function |
$H_{\triangle1}$ and $H_{\triangle2}$ are signed values, and replace FDSetup
's SubFunction
field.
Own triangulation function’s uint16_t
is followed by one extra uint16_t
to be parsed as follows:
Hex value | 0xF000 | 0x0F00 | 0x00F0 | 0x000F |
||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bit | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Field | $\Delta C_{11}$ | $\Delta C_{01}$ | $\Delta C_{00}$ | $\Delta C_{10}$ |
All four values here are unsigned.
The idea behind this set up is dividing each sector rectangle into two independent triangles, and adjust each triangle height by combination of corner and triangle heights. To get each triangle’s individual corner height, you should use this formula:
$H_{\angle} = H_{floor} + (\max(\Delta C_{10}, \Delta C_{00}, \Delta C_{01}, \Delta C_{11}) - \Delta C_{n} \cdot 1024 )$
…where $H_{\angle}$ is absolute floor height specified in [tr_room_sector]'s Floor
field, and $\Delta C_{n}$ is triangle’s individual corner height.
While four corner height values are shared by both triangles, triangle height values specify additional overall height of individual triangle. Therefore, sector corner heights may or may not be shared between two triangles:
Corner heights are not shared | Corner heights are shared |
The way engine interprets triangle height values $H_{\triangle1}$ and $H_{\triangle2}$ is not exactly known — however, meta2tr understands them and uses them to create so-called diagonal steps, example of which is pictured on the left side. There is no case of diagonal steps in original games, but they may exist in levels edited with meta2tr.
Overall, there are 12 different triangulation functions, which can be divided into two pairs of groups — one pair of groups is for floor, and another pair is for ceiling. Each pair is categorized by split direction, and each group is categorized if it’s floor or ceiling. In each group, there are three functions — first function denotes that both triangles in sector are solid, second and third functions denote that one of triangles is a collisional vertical portal. When function denotes a vertical portal, target room of a portal is taken from [tr_room_sector] structure — RoomBelow
for floor functions, and RoomAbove
for ceiling functions.
Here is an example illustration depicting sectors with all possible floor triangulation functions. Ceiling triangulation happens in similar manner.
Floor sector triangulation types. Black triangles depict vertical collisional portal to different room. |
X
axis in world coordinates also may be considered north for more simple reference (because you can always check compass direction in actual game engines, at least in TR1 and TR4).
These functions define floor triangles split in the northwest-southeast direction.
0x07
— Both triangles are solid.0x0B
— Triangle pointing its right angle to the southwest is a collisional portal.0x0C
— Triangle pointing its right angle to the northeast is a collisional portal.These functions define floor triangles split in the northeast-southwest direction.
0x08
— Both triangles are solid.0x0D
— Triangle pointing its right angle to the southwest is a collisional portal.0x0E
— Triangle pointing its right angle to the northwest is a collisional portal.These functions define ceiling triangles split in the northwest direction.
0x09
— Both triangles are solid.0x0F
— Triangle pointing its right angle to the southwest is a collisional portal.0x10
— Triangle pointing its right angle to the northeast is a collisional portal.These functions define ceiling triangles split in the northeast direction.
0x0A
— Both triangles are solid.0x11
— Triangle pointing its right angle to the northwest is a collisional portal.0x12
— Triangle pointing its right angle to the southeast is a collisional portal.SubFunction is not used
Sets monkey-swingability of the ceiling in specified sector.
This function has a different meaning in TR3 and TR4/5.
0x15
functions are set for a given sector, minecart will stop there.0x14
, trigger won’t be activated until there’s an activated Trigger Triggerer object in the same sector. This allows to create setups where player can cross trigger sector without activating it, until some other event occurs later in level.This function has a different meaning in TR3 and TR4.
0x14
functions are set for a given sector, minecart will stop there.0x15
and inactive Mapper entity, it rotates in the same direction Mapper is pointing to, activates it, and then rolls forward, until next sector with function 0x15
is reached. Then it waits until Lara picks it up.A trigger type specifies the condition of a given trigger function to be activated. Condition may be a type of activator (Lara or some other entity), a specific state of activator, specific trigger action (activate or deactivate), and so on.
Trigger type is placed in SubFunction
field of FDSetup
structure, so we will refer to trigger types as SubFunctions.
Activated by Lara whenever she enters a given sector — either steps, climbs, jumps over it, and so on.
Activated by Lara only if she steps or lands on a given sector.
This particular type of trigger takes first ActionList
entry’s Parameter
field as a reference to specific switch entity in level. It activates every time the switch state is changed. For Object trigger actions, activation means performing XOR
operation on these object’s (entities) activation masks. (See next section for description of Object trigger action and Parameter
field.)
Please note that this trigger type (as well as any other trigger types) always perform all trigger actions except Object in the same manner! Meaning, if there is a Camera or Flipeffect trigger action, it will be performed every time the switch is flipped on or off.
Similar to previous trigger type, it works only if there is a keyhole entity listed in the first ActionList
entry’s Parameter
field. It activates only if a key was inserted into that particular keyhole.
As above, this type of trigger works only if there is a pick-up entity listed in the first ActionList
entry’s Parameter
field. It activates only if this item was picked up by Lara.
Activated by specific entity type (activator) wherever it enters a specified sector. Entity types which are able to activate heavytriggers are hardcoded, and usually include NPCs (enemies), rolling balls and pushable objects. Since TR4, heavytriggers may also be activated by destroying shatter static mesh which is placed in a given sector.
Note that heavytrigger does not perform deactivation action, if activator leaves trigger sector.
Same as Pad — activates only if Lara has landed or stepped onto a given sector. The difference is, Antipad performs deactivation for each case of Object trigger action. What deactivation specifically means is it resets entity activation mask to zero (trigger mask is ignored), thus flipping entity activation procedure.
As it was mentioned for Switch trigger type, any other trigger actions beside Object will perform exactly in the same manner as with normal trigger types. So you shouldn’t expect soundtrack to stop, if you have placed PlayTrack trigger action for antipad.
Activated by Lara whenever she enters a given sector with her weapons drawn. This trigger type was (presumably) never used in original games.
This type doesn’t perform any trigger action listed for it except Object type — for these trigger actions, it applies standable collision for Lara on a given entities, if such entities are in this trigger sector. For particular entity types, it works even if entity is deactivated (e.g. collapsing floor), but for other types it works only if entity was activated (e.g. trapdoors). Selected behaviour is most likely hardcoded.
It’s worth noting that any trigger type will apply standable collision on such entity types, if they are in the same sector. It’s not a bug, rather a way TR engines process FloorData.
Same as Trigger, but performs deactivation for each case of Object trigger action.
Antitrigger type also copies its parent trigger’s OneShot
flag into any entities activated by Object trigger action, meaning that after any further entity activation it will be locked.
Don’t be fooled by the name of this trigger type. It is not literally a switch, as only similarity between it and switch type is XOR operation with activation mask. In fact, this trigger performs action when specific entity type (activator) enters a given trigger sector, but only if trigger mask is equal to activator’s activation mask.
The best example of heavy switch setup is Planetarium in “The Lost Library”. Trigger mask is only applied to raising block if pushable in trigger sector has a similar activation mask.
Same as Antitrigger, but performs deactivation for each case of Object trigger action.
Activated by Lara whenever she enters a given sector in monkeyswing state. Best example is locust swarm attacking Lara when she monkeyswings across the street in “Trenches”.
This trigger type temporarily replaces Lara model with a combination of models #25 (Lara skeleton), #26 (see-through body) and #27 (see-through joints). See-through body and joints are applied on top of the skeleton model with additive blending.
Activated by Lara whenever she enters a given sector walking on a tightrope.
Activated by Lara whenever she enters a given sector crawling or crouching.
Activated by Lara whenever she enters a given sector climbing on a wall.
This concludes the description of Trigger FloorData function trigger types.
Trigger function references an additional list of FloorData entries called ActionList
, which is a “chain” of entries that immediately follows TriggerSetup
entry. As you maybe already guessed, the ActionList
contains the list of actions to be performed for a specified trigger.
ActionList
entry format is:
Hex value | 0x8000 | 0x7C00 | 0x03FF |
|||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bit | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Field | ContBit | TrigAction | Parameter — used bytes may vary |
TrigAction
is a type of action to be performed. These will be listed seperately.
Parameter
is used with certain trigger actions which need a certain numerical argument provided to them.
ContBit
flag meaning is similar to EndData
flag described for FDSetup
structure. It indicates if there is another ActionList
entry after current one. If ContBit
is not set, it means we have reached the end of ActionList
, and there’s nothing more to do for a given trigger.
ActionList
's parent trigger type is either Switch or Key, first entry of ActionList
is used to get reference entity (switch or keyhole) index. Hence, it is ignored here, as by the time engine reaches ActionList
offset, its first entry is already parsed by preceding code.
ContBit
flag is not the same as EndData
flag! When writing a parser, do not overwrite one with another.
Activate or deactivate entity (object) with index specified in Parameter
.
Switches to camera. Parameter
(bits 0..6 used) serves as index into Cameras[]
array.
uint16_t
entry after its own entry! Its format is:
Hex value | 0x8000 | 0x3E00 | 0x0100 | 0x0FF |
||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Bit | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Field | ContBit | MoveTimer | Once | Timer |
Timer
is a number of seconds to wait before automatically switching back to the normal camera. If 0, it never switches back to normal camera, as long as trigger is active.
Once
: If set, only switch to camera once; otherwise, switch to camera every time trigger is active.
MoveTimer
: Specifies time which is used to smoothly move camera from Lara to desired camera viewpoint. Movement is done via spline function. The larger value is, the more time it takes to move camera away from Lara. Therefore, if MoveTimer
is zero, then camera immediately cuts to new viewpoint.
ContBit
flag overwrites the same flag from the preceding ActionList
entry.
Continuously moves Lara to specifed sink. Parameter
serves as index into Cameras[]
array. If sink is placed lower than current sector absolute floor height or upper than current sector absolute ceiling height, $Y$ coordinate will be ignored when dragging Lara to sink. Since TR3, sink also prevents Lara from surfacing the water.
Cameras[]
array was mentioned here by mistake, it is not. TR engines share the same structure for both cameras and sinks. The way engine treats it in either case will be discussed in corresponding section.
FlipMap is an internal engine array of uint8_t
s which is used to determine if alternate rooms should be turned on or off (in TRLE terms, flipped). It uses trigger mask in the same manner as for Object activation and deactivation, but in this case, alternate rooms are activated if given FlipMap
entry mask is set (0x1F
), and deactivated, if FlipMap
entry is not set (not 0x1F
).
This trigger action at first applies trigger mask to a given FlipMap
entry using OR
bitwise operation and then immediately checks if it’s already set or not. If FlipMap entry is set, then it immediately switches rooms to alternate mode.
Parameter
defines which FlipMap
entry engine should refer to decide should it switch alternate rooms on or off. The size of FlipMap
array is around 10 (judging by the number of unused FLIP_MAPn
flipeffect entries), but in original levels, number usually never tops 2 or 3.
FlipMap
array was merely used as a “hint table” to tell the engine if it should flip all rooms at once. That is, to check and apply another FlipMap entry, alternate rooms should have been reverted to previous state before — that’s the purpose of next two listed trigger actions. However, in TR4 algorithm was changed — each “flippable” room now bears additional parameter called “alternate group”, which strictly tells an engine to flip it only when room alternate group is equal to FlipMap Parameter
value. This change in algorithm made next two trigger actions unnecessary in TR4-5 (however, they are still available).
Tries to turn alternate rooms on, judging on current value of a given FlipMap
entry (entry index is specified by Parameter
). If corresponding FlipMap is not set (i.e. the value is not 0x1F
), rooms won’t be flipped. Parameter
defines a FlipMap
entry to work with.
Tries to turn alternate rooms off, judging on current value of a given FlipMap
entry (entry index is specified by Parameter
). If corresponding FlipMap is not set (i.e. the value is not 0x1F
), rooms won’t be flipped. Parameter
defines a FlipMap
entry to work with.
Specifies an entity which current camera should look at. If current camera is “ordinary” one following Lara, then it will also rotate Lara model in a target direction, creating an illusion of Lara looking at it. If current camera is changed to “triggered” one (by trigger action 0x01
— see above), then this camera’s orientation will be changed to a given entity. Note that if such camera change is desired, this action should come first, not the Camera one.
Parameter
specifies an entity index to look at.
Immediately loads next level. In TR1-3 and TR5, Parameter
field is not used, i.e. engine just loads next level specified in script.
In TR4, so called “hub system” was implemented, which allows Lara to jump between levels back and forth. For this reason, Parameter
field must also explicitly specify level index to jump.
Also, since TR4 Lara can have multiple start positions for each level, so Timer
field in TriggerSetup
entry specifies an index of lara start posision AI object in AI objects array to warp Lara to. That is, if there’s an end level trigger with value 3
in Timer
field, it means that Lara will be warped to third start position AI object in AI objects array. For more info on AI objects, refer to this section.
Triggers a playback of a soundtrack specified in Parameter
field. Type of soundtrack (looped or one-shot) is hardcoded and assigned automatically.
This trigger action makes use of trigger mask and one-shot trigger flag to mark if this track was already played with a given trigger mask or not in a special internal soundtrack map. That is, if it is called with trigger mask set to, say, 0x01
, then all further calls from triggers with same trigger mask will be ignored. However, if same track playback is called with trigger mask value of 0x02
, it will play again, as it’s another byte in trigger mask. Effectively, it allows to play specified track six times (five bits of activation mask plus one bit of one-shot flag). Comparison is done via bitwise AND
operation, so if playback is called with trigger mask + one-shot value of (0x1F + 0x20 = 0x3F)
, then any other playback call to that track will be blocked.
To overcome this issue and enable complete soundtrack functionality, several patches were created by the community. However, PC version is missing soundtrack map structure, which potentially produces bugs when single track could be played independently by both triggers in the same level, although mostly this bug comes unnoticed, as majority of TR1 soundtracks are engaged only once in a level.
By the name of “flipeffect” comes any non-trivial or special trigger action which should be seperately defined. This workaround was implemented because TR engines lack any scripting language to program arbitrary trigger, so you can consider a flipeffect as a call to some “pre-compiled” scripted function.
For example, in TR2 “Lara’s Home”, there is a need to control assault course timer, like restarting it while reaching start point or stopping it when Lara is off the course. This task is accomplished via several different flipeffects.
Plays “secret” soundtrack theme and marks a secret number specified in Parameter
field as found. For finding each secret, another Parameter
value must be specified, or else secret won’t be counted as found.
Removes dead bodies of enemies from a level to conserve memory usage. This action has effect only on entities which had clear body flag specified in their parameters (see further). Parameter
field is unused.
Engages a flyby camera sequence specified in Parameter
field. The feature was added in TR4 and enables to play cinematographic interludes with camera continuously “flying” from one point to another. Such sequences, their points, properties and order are defined in a level editor, and engine moves camera across them using spline function.
uint16_t
immediately following flyby’s own entry contains one-shot flag at 0x0100
. If this flag is not set, flyby will infinitely loop. As with Camera TrigAction, flag at 0x8000
is a continuation bit, which overrides previous entry’s continuation bit.
Engages a cutscene pre-defined in script file. Parameter
is taken as cutscene ID. In turn, script file refers to CUTSEQ.PAK
(TR4) or CUTSEQ.BIN
(TR5) file to get all the data for a cutscene, such as actor positions and animations, camera movement, soundtrack and many more. There will be a special section describing particular CUTSEQ.PAK
file format.
This concludes the description of Trigger FloorData function action types.
Nearly all of the non-geographic visual elements in TR (as well as a few parts of the landscape) consist of as meshes. A mesh is simply a list of vertices and how they’re arranged. The mesh structure includes a list of vertices as relative coordinates (which allows meshes to easily be placed anywhere in the world geometry), a list of normals (to indicate which side of each face is visible), and lists of Rectangles and Triangles, both textured and coloured. The elements of each [tr_face4] or [tr_face3] (or same version-specific) structure (Rectangles and Triangles) contain an offset into the Vertices[]
array for the mesh. Other arrays (Entities[]
, StaticMeshes[]
) do not reference the array Meshes[]
directly, but instead reference the array MeshPointers[]
, which points to locations inside of Meshes[]
, inside of which the meshes are stored in packed fashion.
While it may be not obvious, but every time you see mesh look is changed, it means that meshswap happened. There was never any other way to modify mesh looks in classic TRs.
The sign of the number of normals specifies which sort of lighting to use. If the sign is positive, then external vertex lighting is used, with the lighting calculated from the room’s ambient and point-source lighting values. The latter appears to use a simple Lambert law for directionality: intensity is proportional to $\max(\langle \vec n, \vec o - \vec p \rangle, 0)$, with $\vec n$ being the normal, $\vec o$ the lit position, and $\vec p$ the light's position. If the sign is negative, then internal vertex lighting is used, using the data included with the mesh.
NumXXX
elements that precede them.
virtual struct tr_mesh // (variable length) { tr_vertex Centre; int16_t CollRadius; uint16_t Flags; // 0x01 for Flat Shaded, i.e. Normals should not affect lighting int16_t NumVertices; // Number of vertices in this mesh tr_vertex Vertices[NumVertices]; // List of vertices (relative coordinates) int16_t NumNormals; if(NumNormals > 0) tr_vertex Normals[NumNormals]; else int16_t Lights[abs(NumNormals)]; int16_t NumTexturedRectangles; // number of textured rectangles in this mesh tr_face4 TexturedRectangles[NumTexturedRectangles]; // list of textured rectangles int16_t NumTexturedTriangles; // number of textured triangles in this mesh tr_face3 TexturedTriangles[NumTexturedTriangles]; // list of textured triangles int16_t NumColouredRectangles; // number of coloured rectangles in this mesh tr_face4 ColouredRectangles[NumColouredRectangles]; // list of coloured rectangles int16_t NumColouredTriangles; // number of coloured triangles in this mesh tr_face3 ColouredTriangles[NumColouredTriangles]; // list of coloured triangles };
Centre
is usually close to the mesh’s centroid, and is the center of a sphere used for certain kinds of collision testing.
CollRadius
is the radius of that aforementioned collisional sphere.
NumNormals
: If positive, it is a number of normals in this mesh. If negative, it is a number of vertex lighting elements (abs
value).
Depending on a value of NumNormals
, next data block is interpreted either as Normals[]
array (in [tr_vertex] format) or Lights
array (just standard int16_t
values).
NumTexturedTriangles
and NumTexturedRectangles
are respectively the number of triangular and rectangular faces in this mesh. Corresponding TexturedTriangles
and TexturedRectangles
array contain textured triangles and rectangles themselves.
NumColoredTriangles
and NumColoredRectangles
are respectively the number of triangular and rectangular faces in this mesh. Corresponding ColoredTriangles
and ColoredRectangles
array contain colored triangles and rectangles themselves.
As coloured faces feature was removed since TR4, [tr_mesh] structure was changed, and contain no data for coloured faces anymore:
virtual struct tr4_mesh // (variable length) { tr_vertex Centre; int32_t CollRadius; int16_t NumVertices; // Number of vertices in this mesh tr_vertex Vertices[NumVertices]; // List of vertices (relative coordinates) int16_t NumNormals; if(NumNormals > 0) tr_vertex Normals[NumNormals]; else int16_t Lights[abs(NumNormals)]; int16_t NumTexturedRectangles; // number of textured rectangles in this mesh tr_face4 TexturedRectangles[NumTexturedRectangles]; // list of textured rectangles int16_t NumTexturedTriangles; // number of textured triangles in this mesh tr_face3 TexturedTriangles[NumTexturedTriangles]; // list of textured triangles };
As the name tells, static meshes are meshes that don’t move (e.g. skeletons lying on the floor, spiderwebs, trees, statues, etc.) Usually it implies that static mesh is completely non-interactive, i.e. all it does is sitting there in place serving as an ornament.
StaticMeshes have two bounding boxes. First one serves as visibililty box, and other is the collisional box. The former is being used for visibility testing, and the latter is used for collision testing.
struct tr_staticmesh // 32 bytes { uint32_t ID; // Static Mesh Identifier uint16_t Mesh; // Mesh (offset into MeshPointers[]) tr_bounding_box VisibilityBox; tr_bounding_box CollisionBox; uint16_t Flags; };
struct tr_bounding_box // 12 bytes { int16_t MinX, MaxX, MinY, MaxY, MinZ, MaxZ; };
VisibilityBox
and CollisionBox
boundaries is always stay axis aligned even after applying tr_room_staticmesh::Rotation (always have 90 degrees step). Additionally, the test whether to rotate the box or not relies on the mesh’s rotation being an exact multiple of 0x4000 (aka 90 degrees). If this is not the case, the box is not rotated, which results in wrong collision checks.
Flags
:
0x0001
): no collision0x0002
): is visible
This defines a list of contiguous meshes that comprise one object, which is called a model. This structure also points to the hierarchy and offsets of the meshes (MeshTree
), and also to the animations used (Animation
); these will be described in detail below. If the Animation index is -1, that means that there are no predefined animations, and entity’s movement is all generated by the engine; an example is Lara’s ponytail or rolling balls from TR4 and TR5.
Some entities are really stationary, such as locks and the skybox, and some are not rendered at all, such as “camera target” points to aim the camera at, flame emitters, AI objects and other service entities. Such invisible models are frequently called nullmeshes, because usually they have null mesh index specified for them, and never actually use it.
Sometimes, model may refer to sprite or sprite sequence to draw itself (for example, pick-up items and flame emitters). In this case, model is replaced with sprite in run-time. This behaviour is hardcoded for specific model IDs.
Sometimes, model may have two different versions defined in level files — one is normal, and another is low-detailed one, with the latter used when camera position gets too far from them. These are called MIP models, and mostly exist for NPCs (enemies). Usually, their type IDs are one off their normal counterparts (for example, skeleton type ID in TR4 is 35, and its MIP variation is 36).
struct tr_model // 18 bytes { uint32_t ID; // Type Identifier (matched in Entities[]) uint16_t NumMeshes; // Number of meshes in this object uint16_t StartingMesh; // Stating mesh (offset into MeshPointers[]) uint32_t MeshTree; // Offset into MeshTree[] uint32_t FrameOffset; // Byte offset into Frames[] (divide by 2 for Frames[i]) uint16_t Animation; // Offset into Animations[] };
There is an extra uint16_t
at the end of [tr_model] structure, which is always 0xFFEF
and used for alignment. Consider it while parsing.
Entities are the actual instances of entity types, consisting either of models or sprites (with the latter existing in TR1-2 only). For an entity to appear in a level, it must be referenced in the Models[]
array. Multiple instances of the same model are possible (e.g. two identical tigers in different rooms are represented using two entries in Entities[]
, one for each).
Entity structure has gone through different variations across game versions, so we’ll list them all.
struct tr_entity // 22 bytes { int16_t TypeID; // Entity type ID (matched in Models[]) int16_t Room; int32_t x; // Item position in world coordinates int32_t y; int32_t z; int16_t Angle; int16_t Intensity1; uint16_t Flags; };
struct tr2_entity // 24 bytes { int16_t TypeID; int16_t Room; int32_t x; int32_t y; int32_t z; int16_t Angle; int16_t Intensity1; int16_t Intensity2; // Like Intensity1, and almost always with the same value. uint16_t Flags; };
struct tr4_entity // 24 bytes { int16_t TypeID; int16_t Room; int32_t x; int32_t y; int32_t z; int16_t Angle; int16_t Intensity1; int16_t OCB; // Replaces Intensity2, see further for explanations. uint16_t Flags; };
TypeID
is used to assign appropriate action for this entity and/or locate the appropriate sprite sequence or model to draw. If TypeID
is zero, it means it’s playable character (i.e. Lara).
Room
is a room ID to which this particular entity belongs to. If room value was modified incorrectly, entity will glitch and, most likely, won’t appear in engine. That is, you can’t change entity position without complementary edit or Room
field.
Angle
is an Euler Yaw angle (i.e. “horizontal” rotation) stored in a special manner. To convert it to ordinary degrees, use this formula:
$\angle^\circ = \frac{Angle}{16384} \cdot -90$
Intensity2
field is missing in this game version, so the structure size is 2 bytes less.
Intensity1
: If not -1, it is a value of constant lighting. -1 means “use mesh lighting”.
Flags
value contain packed list of several parameters:
0x0080
) — Clear Body flag. It is used together with Clear Bodies trigger action to remove the body of dead enemy from the level to conserve resources.0x0100
) — Invisible flag. If entity has this flag set, it will be invisible on start-up. However, it only works for specific types of entities. It is primarily used with pick-ups or other entities which should appear at certain point only after activation, but are visible by default.0x3E00
) — Activation Mask for this entity. As you already learned in Trigger Actions chapter, entity is only activated when activation mask is all set (i.e. all 5 bits are set, and value is 0x1F
). However, activation mask doesn’t strictly required to be set by trigger — level editor allows to pre-define activation mask, so entity will bear specific activation mask layout on level start-up.
If activation mask was pre-set to 0x1F
(all set), entity will activate immediately after level loading, and engine will also reset activation mask to zero and mark entity as inactive, effectively swapping “inactive” state with “active”. That is, when player will activate such pre-activated entity with a trigger, it will actually “deactivate”, et cetera. Most prominent example of this behaviour is pre-opened grated door in Tomb of Qualopec.
In TR4 and TR5, Intensity2
field was replaced with completely new one, called Object Code Bit (or OCB
). OCB allows to alter entity behaviour based on its value, thus providing very basic “script-like” functionality. For example, flame emitter entities have a case switch for OCB value, and each valid OCB value produces different result — flame emttier acts either as a static flame, as a directional flame, as a lightning, and so on.
More detailed description of OCB is provided in this section.
These are “billboard” objects that are always rendered perpendicular to the view direction. These are used for text and explosion effects and similar things; they are also used for some scenery objects and pickup items, though this use gets less as one goes from TR1 to TR3. The various “Sides” below are the positions of the sprite sides relative to the sprite’s overall position, measured in TR’s world-coordinate units.
struct tr_sprite_texture // 16 bytes { uint16_t Atlas; uint8_t x; uint8_t y; uint16_t Width; // (ActualWidth * 256) + 255 uint16_t Height; // (ActualHeight * 256) + 255 int16_t LeftSide; int16_t TopSide; int16_t RightSide; int16_t BottomSide; };
x
and y
values are not used in this version. Additionally, formula for Width
and Height
is changed: now it’s $(\text{ActualWidth} - 1) \cdot 256$ and $(\text{ActualHeight} - 1) \cdot 256$ respectively.
These are collections of sprites that are referred to as a group. The members of this group can be cycled through (animated sprites such as flames, blood splats or explosions) or selected in other ways (text). Some sequences have only one member; this is done so as to access all the sprites in the same way.
struct tr_sprite_sequence // 8 bytes { int32_t SpriteID; // Sprite identifier int16_t NegativeLength; // Negative of ``how many sprites are in this sequence'' int16_t Offset; // Where (in sprite texture list) this sequence starts };
The animated mesh objects in the Tomb Raider series are sets of meshes that are moved relative to each other, as defined by Entities[]
entries. Each entry describes which meshes to be used (a contiguous set of them referred to in MeshPointers[]
), what hierarchy and relative offsets they have (contents of MeshTree[]
pointed to), and what animations are to be used (contents of Animations[]
pointed to).
The hierarchy used is a branching one, with the meshes being at the nodes, and with the first mesh being the root node. The MeshTree[]
values are applied to each of the child meshes in sequence; they are sets of four int32_t
s, the first being a hierarchy operator, and the remaining three being the coordinates in the parent mesh’s system. A hierarchy example is that for the Lara meshes:
Top-down hierarchy of Lara’s MeshTree. Hips is a root mesh. Ponytail is not listed, as it is a separate object. |
This is implemented by using a stack of meshes and “push” and “pop” operations in MeshTree[]
. Normally, each mesh’s parent is the previous mesh in series. But such meshes can be “remembered” by adding them to a stack of meshes with a “push” operation. This remembered mesh can then be used as the parent mesh with a “pop” operation. It is not clear what the maximum stack depth is; most TR mesh stacks do not extend beyond 2 or 3 meshes.
The animations for each mesh object are selected with some ingenious techniques. Which animations to use are not hardcoded; instead, each entity has some states it can be in, and these states are used to select which animation. For example, locks have only one state (they just sit there), doors have two states (open and closed), and Lara has numerous states, such as standing, walking, running, jumping, falling, being hurt, dying, etc. Each animation has a state ID, which can be used to select it; however, state transitions might seem to require a large number of intermediate states (opening, closing, starting to jump, landing, etc.). The alternative used in the Tomb Raider engine is for each animation to have bridge animations to other states' animations, which are selected using the ID of which state to change to. These bridge animations then lead to the animation with the appropriate state. Thus, a closed door will run a looped closed-door animation as long as its state stays “closed”, but when its state becomes “open”, it will change to an opening-door bridge animation, which will end in a looped open-door animation. Likewise, closing a door will make it use a closing-door bridge animation. Some bridge animations are chosen with a finer grain of selectivity, however, such as using one for left foot forward and one for right foot forward.
Thus, each animation references a set of StateChange
structures, each one of which references an AnimDispatch
structure (called a “range” in some documentation). Each StateChange
structure contains a new state and which AnimDispatch
structures to use. When an entity goes into a new state, the StateChange
structures are scanned for that state’s ID, and if one matches, then that StateChange
's AnimDispatch
es are then scanned for a range of frames that contains the ID of the current frame. If such an AnimDispatch
is found, the animation and the frame are changed to those listed in it.
The ultimate unit of animation is, of course, the frame, and each frame consists of a bounding box, the offset of the root mesh, and rotation angles for all the meshes with respect to their parent meshes. The root mesh is also rotated, but relative to the object’s overall coordinates. All rotations are performed around the meshes' origins, and are in order Y, X, Z (yaw, pitch, roll). The reason for the root mesh’s displacement is because entities traveling on solid surfaces are likely tracked by having their locations be at ground level, and Lara’s hips, for example, are well above the ground. Finally, some of the angles are not specified explicitly, when they are not, they are zero.
Frames are referenced in two ways, either by an offset into the Frames[]
array that contains them, or by frame index. The values of the latter appear to be unique to each kind of entity, but not between entities; the first frame for each kind is numbered 0. This is likely a convenience when constructing the animations, since the list of animation frames for each entity can be constructed separately. However, using these indices is fairly simple. Each Animation structure has a first-frame index; this index is subtracted from the index of the desired frame in order to find out its index relative to the animation’s first frame.
There are also some special AnimCommands for doing various additional things. Some of them are for moving entities in absolute coordinates, for example to position Lara at climb location, or specifying jump momentum. Some others define actions per frame, like playing sounds, emitting bubbles, and so forth.
Finally, some entities appear to have incomplete set of animations; their complete animations are “borrowed” from similar entities. Such setup is mostly used in TR2’s Venice levels — some of Venice goons them have a full set of animations, while some others have only the standing animation. The ones with only the standing animation borrow their other animations from the fully-animated ones.
struct tr_meshtree_node // 4 bytes { uint32_t Flags; int32_t Offset_X; int32_t Offset_Y; int32_t Offset_Z; };
MeshTree[]
array consists of meshtree nodes.
In Flags
field, two bits are used:
0x0001
) indicates “take the top mesh off of the mesh stack and use as the parent mesh” when set, otherwise “use the previous mesh as the parent mesh”.0x0002
) indicates “put the parent mesh on the mesh stack”.When both bits are set, the Bit 0 operation is always done before the Bit 1 operation. In effect, read the stack but do not change it.
Offset_X
, Offset_Y
and Offset_Z
are offsets of the mesh’s origin from the parent mesh’s origin.
This describes each individual animation. These may be looped by specifying the next animation to be itself. In TR2 and TR3, one must be careful when parsing frames using the FrameSize value as the size of each frame, since an animation’s frame range may extend into the next animation’s frame range, and that may have a different FrameSize value.
struct tr_animation // 32 bytes { uint32_t FrameOffset; // Byte offset into Frames[] (divide by 2 for Frames[i]) uint8_t FrameRate; // Engine ticks per frame uint8_t FrameSize; // Number of int16_t's in Frames[] used by this animation uint16_t State_ID; fixed Speed; fixed Accel; uint16_t FrameStart; // First frame in this animation uint16_t FrameEnd; // Last frame in this animation uint16_t NextAnimation; uint16_t NextFrame; uint16_t NumStateChanges; uint16_t StateChangeOffset; // Offset into StateChanges[] uint16_t NumAnimCommands; // How many of them to use. uint16_t AnimCommand; // Offset into AnimCommand[] };
FrameOffset
is a byte offset into Frames[] (divide by 2 for Frames[i]).
FrameRate
is a multiplier value which defines how many game frames will be spent for each actual animation frame. For example, if value is 1, then each animation frame belongs to single game frame. If value is 2, then each animation frame belongs to two game frames, and so on. In latter case, animation frames will be interpolated between game frames using slerp function.
State_ID
identifies current state type to be used with this animation. Engine uses current State_ID
not only to solve state changes, but also to define current Lara behaviour — like collisional routines to be used, controls to be checked, health/air/sprint points to be drained, and so on.
Speed
and Accel
values are used to set a specific momentum to a given entity. That is, entity will be accelerated with Accel
value, until Speed
value is reached. If Accel
is negative, speed will be decreased to fit Speed
value. The direction in which entity is moved using speed value is hardcoded, and mostly is forward.
NextAnimation
defines which animation should be played after current one is finished. When current animation ends, engine will switch it to NextAnimation
, not regarding current State_ID
value. If NextAnimation
value is the same as animation number itself, it means animation will be looped until loop is broken by state change.
NextFrame
specifies the frame number to be used when switching to next animation. That is, if NextFrame
is 5 and NextAnimation
is 20, it basically means that at the end of current animation engine will switch right to frame 5 of animation 20. If animation is looped, NextFrame
defines to which frame animation should be rewound. It allows to “eat up” certain start-up frames of some animations and re-use them as looped.
Core: ANIM_STRUCT
For TR4 and TR5, extended version of [tr_animation] is used:
struct tr4_animation // 40 bytes { uint32_t FrameOffset; uint8_t FrameRate; uint8_t FrameSize; uint16_t State_ID; fixed Speed; fixed Accel; fixed SpeedLateral; // New field fixed AccelLateral; // New field uint16_t FrameStart; uint16_t FrameEnd; uint16_t NextAnimation; uint16_t NextFrame; uint16_t NumStateChanges; uint16_t StateChangeOffset; uint16_t NumAnimCommands; uint16_t AnimCommand; };
In addition to Speed
and Accel
values, TR4 introduced LateralSpeed
and LateralAccel
values, which are used to move entity to the sides, rather than forward or backward. However, these values are only used for any entity but Lara — engine ignores them in such case.
Lateral speed and acceleration primarily used for “start-up” animations of NPCs — for example, armed baddies in TR4 can roll or jump aside.
Core: CHANGE_STRUCT
Each state change entry contains the state to change to and which animation dispatches to use; there may be more than one, with each separate one covering a different range of frames.
struct tr_state_change // 6 bytes { uint16_t StateID; uint16_t NumAnimDispatches; // number of ranges (seems to always be 1..5) uint16_t AnimDispatch; // Offset into AnimDispatches[] };
Core: RANGE_STRUCT
This specifies the next animation and frame to use; these are associated with some range of frames. This makes possible such specificity as one animation for left foot forward and another animation for right foot forward.
struct tr_anim_dispatch // 8 bytes { int16_t Low; // Lowest frame that uses this range int16_t High; // Highest frame that uses this range int16_t NextAnimation; // Animation to dispatch to int16_t NextFrame; // Frame offset to dispatch to };
These are various commands associated with each animation. They are varying numbers of int16_t
s packed into an array. As the FloorData, AnimCommands must be parsed sequentially, one by one.
The first AnimCommand entry is the type, which also determines how many int16_t
arguments (operands) follow it (i.e. how many int16_t
values must be parsed after current one without switching to next AnimCommand). For a given animation, AnimCommands are parsed until NumAnimCommands
value is reached.
Some of commands refer to the whole animation (jump speed, position change, kill and empty hands commands), while others of them are associated with specific frames (sound, bubbles, etc.). When command refers whole animation, it means that actual command execution will occur on animation transition, i.e. last animation frame.
struct tr_anim_command // 2 bytes { int16_t Value; };
Here are all the AnimCommand
types and their arguments.
0x4000
): play this sound when on dry land (example: footsteps)0x8000
): play this sound when in water (example: running through shallow water)FOOTPRINT_FX
flipeffect (ID 32) is used, second argument also may contain one of two “packed” bit flags, similar to Play Sound animcommand. However, meaning is different:0x4000
): apply FOOTPRINT_FX
in relation to left foot0x8000
): apply FOOTPRINT_FX
in relation to right footFrames indicate how composite meshes are positioned and rotated. They work in conjunction with Animations[] and MeshTree[]. A given frame has the following format:
struct tr_anim_frame // Variable size { tr_bounding_box box; // Bounding box int16_t OffsetX, OffsetY, OffsetZ; // Starting offset for this model int16_t NumValues; uint16_t AngleSets[]; // Variable size }
NumValues
: Number of angle sets to follow; these start with the first mesh, and meshes without angles get zero angles. NumValues is implicitly NumMeshes (from model).
AngleSets
are sets of rotation angles for all the meshes with respect to their parent meshes. In TR2/3, an angle set can specify either one or three axes of rotation.
If either of the high two bits (0xC000
) of the first angle uint16_t
are set, it’s one axis: only one uint16_t
, low 10 bits (0x03FF
), scale is 0x0100
— 90 degrees; the high two bits are interpreted as follows: 0x4000
— X only, 0x8000
— Y only, 0xC000
— Z only.
If neither of the high bits are set, it’s a three-axis rotation. The next 10 bits (0x3FF0
) are the X rotation, the next 10 (including the following uint16_t
) (0x000F
, 0xFC00
) are the Y rotation, the next 10 (0x03FF
) are the Z rotation, same scale as before (0x0100
— 90 degrees).
Rotations are performed in Y, X, Z order.
All angle sets are two words and interpreted like the two-word sets in TR2/3, except that the word order is reversed.
All the Tomb Raider game physics and entity behaviour is hardcoded, with each type ID being associated with some specific sort of behaviour (as Lara, as a boat, as a tiger, as a door, as a boulder, as a lock, etc.). That is, each model refer two internal engine routines — collisional one and control one. For static entities (like flame emitters), collisional routine may contain no functional code, while control routine is present. On contrary, “decorative” entities (usually called animatings in TRLE) may lack control code, while retaining collisional code.
Several entity types may share the same collisional and/or control routines — for example, there is one generic collisional routine for almost all enemies, another generic routine for doors, and another one for standable entities, like bridge or platform objects.
Lara is unique player character, so she has a large set of both control and collisional routines, which are switched depending on her current state.
lara_col_STATE
and lara_as_STATE
, where STATE
is the name of the state, like walk, run, reach, and so on.
For other entity types, there is more generic scheme: collisional routines are called NAMECollision
, where NAME
is entity type name, like CreatureCollision, and control routines are called NAMEControl
, where NAME
is entity type name. E.g., bear will have a pair of routines linked to it, named CreatureCollision and BearControl.
Despite the existence of script files, here is no any scripting for entity behaviour, like in most contemporary games. This hardcoding makes it difficult to port the earlier Tomb Raider scenarios to the engines of the later games, which could be desirable with their improved hardware support. While textures, models, and animations can be ported, behaviour cannot be.
However, there is a small change in TR4 and TR5 which indicates that specific entity behaviour can be altered — it’s called OCB. It was briefly described in this section. OCB is a special value defined for each entity instance, based on which entity can switch the way it acts (most prominent examples are flame emitters, which change their size and emit direction based on OCB, and teeth spikes, which change their orientation in space).
Sometimes OCB is interpreted as a “packed” field with several values incorporated — like teeth spike OCB contain information about their horizontal and vertical orientation, and also about their “physical” behaviour (stick out constantly, pop-retract in looped manner, or pop-retract just once).
As for TR5, no proper OCB list exists for its entity types, so it may be considered a big unknown.
However, OCB can’t be seriously called “scripting”, as it also operates with pre-defined hardcoded behaviour.
Despite the lack of scripting, the Tomb Raider series does have navigation hints for the Non-Player Characters; those entities that move freely across the maps under the command of the game AI. NPCs find their way in a level by checking “FloorData” collisional functions in the same way Lara does, and also with the help of special data structures which are used for proper pathfinding.
TR engines use three different structures to assist pathfinding. These are boxes, overlaps, and zones. Most sectors point to some box, the main exceptions being horizontal-portal sectors. Several neighbour sectors may point to the same box. A box is a horizontal rectangle, with corners and height specified; each box also has a pointer into the list of overlaps. Each segment in that list is the list of accessible neighbouring boxes for some box; the NPCs apparently select from this list to decide where to go next. Several neighbour sectors may point to the same box. Each box also has a pointer into the list of overlaps. Each segment in that list is the list of accessible neighbouring boxes for some box; the NPCs apparently select from this list to decide where to go next.
This selection is done with the help of the zones. These structures of 6 (TR1) or 10 (TR2-TR5) int16_t
s that act as zone IDs; their overall indexing is the same as the boxes, meaning that each box will have an associated set of zone IDs. An NPC will select one of this set to use, and will prefer to go only into the overlaps-list boxes that have the same zone value as the box it is currently in. For example, one can create guard paths by making chains of zone-ID-sharing boxes, with their overlaps pointing to the next boxes in those chains.
A box is a horizontal rectangle, with corners and height specified.
This is presumably the way TRLE creates boxes for each level:
There are two variations of box structure — one for TR1 and another for TR2 and any other game version.
struct tr_box // 20 bytes { uint32_t Zmin; // Horizontal dimensions in global units uint32_t Zmax; uint32_t Xmin; uint32_t Xmax; int16_t TrueFloor; // Height value in global units uint16_t OverlapIndex; // Bits 0-13 is the index into Overlaps[]. };
struct tr2_box // 8 bytes { uint8_t Zmin; // Horizontal dimensions in sectors uint8_t Zmax; uint8_t Xmin; uint8_t Xmax; int16_t TrueFloor; // Height value in global units int16_t OverlapIndex; // Bits 0-13 is the index into Overlaps[] };
The OverlapIndex
contains a block mask for path finding by enemies in two highest bits: Bit 15 (blockable) and bit 14 (blocked). The first one marks it as unpassable by large enemies, like the T-Rex (ID 18), the Mutant (ID 20) or the Centaur (ID 23) and is always set behind doors. The second one marks it unpassable for other enemies and is set for movable blocks (if blockable bit is set), for closed doors and for some flip maps (set at start).
This is a set of lists of neighbouring boxes for each box, each member being a uint16_t
. NPCs apparently use this list to decide where to go next.
Overlaps must be parsed in serial manner, as with FloorData functions: the highest bit (0x8000
) is used to terminate overlap list iteration for a single box.
This is a set of int16_t
s, 6 for TR1 and 10 for TR2-5. NPCs prefer to travel to a box with the same zone ID as the one they are currently at. Which of these zone IDs it uses depends on the kind of the NPC and its current state. The first half of the Zones structure is for the normal room state, and the second half is for the alternate (flipped) room state. TR1 has 2 sets of ground zones and 1 set of fly zones:
struct tr_zone // 12 bytes { uint16_t GroundZone1_Normal; uint16_t GroundZone2_Normal; uint16_t FlyZone_Normal; uint16_t GroundZone1_Alternate; uint16_t GroundZone2_Alternate; uint16_t FlyZone_Alternate; };
TR2-5 have similar breakdowns, though they have 4 ground zones:
struct tr2_zone // 20 bytes { uint16_t GroundZone1_Normal; uint16_t GroundZone2_Normal; uint16_t GroundZone3_Normal; uint16_t GroundZone4_Normal; uint16_t FlyZone_Normal; uint16_t GroundZone1_Alternate; uint16_t GroundZone2_Alternate; uint16_t GroundZone3_Alternate; uint16_t GroundZone4_Alternate; uint16_t FlyZone_Alternate; };
The ground zones are for NPCs that travel on the ground, while the fly zones are for flying or swimming NPCs.
Each mobile NPC in TR may be in a certain mood. A mood defines the way creature behaves. The way NPCs change their mood is based on several conditions, mainly on certain enemy implementation.
Most obvious example of mood change is wolf, which can sleep, walk slowly, chase Lara and flee. This change of mood is based on Lara’s position — if Lara is found in any of the box that NPC can reach, mood is changed to chase, which means that pathfinding algorithm is in effect (see further).
There are another examples of mood changes. In TR3, monkeys are calm unless Lara shoots them. However, this is not the case for level 2 (Temple Ruins), where monkeys are hardcoded to chase Lara on start-up.
For most of TR NPCs, when they are in chase mood, pathfinding (and eventual attack) may be broken down to several steps:
Since TR3, in addition to pathfinding data structures, there are now special AI objects, which are used in a node-like manner, defining specific action, like wandering between two points, guarding specific point or running to specific place in case Lara is around. For example, MP Guards in TR3’s “Area 51” may patrol specific area when they are limited by special AI_PATROL object.
Specific set of AI objects and their respective entity type IDs are different across game versions, but types themselves largely remained unchanged from TR3 to TR5. Here are they:
TR4 introduced three additional AI objects, AI_X1, AI_X2 and LARA_START_POS. First two are used, for example, with SAS Guards in Cairo story arc. When AI_X1 object is placed in the same sector with SAS Guard, he will prefer to shoot grenades instead of bullets. If another SAS Guard with AI_X2 is activated nearby, then first one will stop shooting grenades, and second one will shoot them instead.
As for LARA_START_POS AI object, it is used to modify Lara’s level starting position, according to prior end level trigger action configuration. That is, at start Lara will be teleported to any existing LARA_START_POS AI object with same OCB value as in Timer
field of end level trigger in previous level.
Here are all AI Object type IDs in each TR version which has them:
TR3 | TR4 | TR5 | |
---|---|---|---|
AI_GUARD | 74 | 398 | 378 |
AI_AMBUSH | 75 | 399 | 379 |
AI_PATROL1 | 76 | 400 | 380 |
AI_PATROL2 | 79 | 403 | 383 |
AI_MODIFY | 77 | 401 | 381 |
AI_FOLLOW | 78 | 402 | 382 |
AI_X1 | 404 | 384 |
|
AI_X2 | 405 | 385 |
|
LARA_START_POS | 406 | 386 |
Beginning with TR4, AI objects are not kept along with other entities. Instead, they have their own structure, which is basically simplified [tr4_entity] structure, and moved to separate data block. This seems reasonable, as the only purpose of AI objects is to serve as “waypoints”, and they have neither collisional nor control code attached to them.
The format of AI object structure as follows:
struct tr4_ai_object // 24 bytes { uint16_t TypeID // Object type ID (same meaning as with tr4_entity) uint16_t Room; // Room where AI object is placed int32_t x, y, z; // Coordinates int16_t OCB; // Same meaning as with tr4_entity uint16_t Flags; // Activation mask, bitwise-shifted left by 1 int32_t Angle; };
As it was described in the beginning, all sound in TR engines can be separated into two distinctive sections — audio tracks and sounds.
Audio tracks are long separate files which are loaded by streaming, and usually they contain background ambience, music or voiceovers for cutscenes.
Sounds are short audio samples, which are frequently played on eventual basis. While engine can usually play one audio track at a time, it can play lots of sounds in the same time, and also play numerous copies of the same sound (for example, when two similar enemies are roaming around).
Audio tracks can be looped or one-shot. Looped tracks are usually contain background ambience (these creepy sounds heard in the beginning of “Caves” and so on), but occasionally they can use music (e. g., “Jeep theme” from TR4). One-shot tracks are used for musical pieces which are usually triggered on certain event, and also for “voice chatting” (e. g. approaching the monk from “Diving Area” in TR2).
As both looped and one-shot tracks use the same audio playing routine, there’s no chance both looped and one-shot tracks could be played simultaneously. This is the reason why background ambience stops and restarts every time another (one-shot) track is triggered. However, this limitation was lifted in TREP and TRNG.
The audio tracks are stored in different fashions in the various versions of the TR series:
TR1 and TR2 used CD-Audio tracks for in-game music, and therefore, they needed auxiliary CD-audio fed into the soundcard. That’s the reason why most contemporary PCs have issues with audiotrack playback in these games — such setup is no longer supported in modern CD/DVD/BD drives, and digital pipeline is not always giving the same result. Currently, various modernized game repacks (such as Steam or GOG releases) officially features no-CD cracks with embedded MP3 player, which takes place of deprecated CD-Audio player.
In TR3, we have somewhat special audiotrack setup. Audio format was changed to simple WAV (MS-ADPCM codec), but all tracks were embedded into CDAUDIO.WAD
file, which also contained a header with a list of all tracks and their durations. So, when game requests an audiotrack to play, it takes info on needed track from CDAUDIO.WAD
header, and then goes straight to an offset for this track into it. The format of CDAUDIO.WAD
header entry is:
struct tr3_cdaudio_entry // 0x10C bytes { char Name[260]; // C string with track name uint32_t WavLength; // Wave file size uint32_t WavOffset; // Absolute offset in CDAUDIO.WAD };
The number of header entries is always 130 (meaning the whole size of header should be 0x10C * 130
). Header is immediately followed by embedded audio files in WAV format.
CDAudio.db
, which contains the names of all the track files as 32-byte zero-padded C strings with no extra contents.
In TR4-5, track format remained the same (MS-ADPCM), but tracks were no longer embedded into CDAUDIO.WAD
. Instead, each track was saved as simple .WAV
file, and file names themselves were embedded into executable. Hence, when TR4-5 plays an audiotracks, it refers to internal filename table, and then loads an audiotrack with corresponding name.
In TR engines, sounds appear in a variety of contexts.
They can be either continuous or triggered. Continuous ones are usually produced by sound source object, which make sound in a range around some specific point (range appears to be hardcoded, and is equal to around 8 sectors). Likewise, triggered ones can be triggered by a variety of events. The triggering can be hardcoded in the engine (for example, gunshots) or by reaching some animation frame (footsteps, Lara’s somewhat unladylike sounds). Flipeffects can also be used to play certain sound sample in specific circumstances, and either called by triggers or from animations.
Sounds may be looped or one shot, with looped ones playing until specifically untriggered by some in-game event. For example, Uzi and M16 sounds are looped and will be played until Lara stops firing these weapons — in such case, engine sends a command to stop this particular sound.
Sounds may be global or local. Global sounds have no 3D positioning in world space, they always have constant volume and usually not linked to any object in level. These are primarily menu sounds. Local sounds are usually produced by entities or sound source objects. They have certain coordinates in space, and could be affected by their position and environment. Local sounds, when emitted by entities, may travel along with these entities.
Sounds are referred to by an internal sound index; this is translated into which sound sample with the help of three layers of indexing, to allow for a suitable degree of abstraction. Internal sound indices for various sounds are consistent across all the level files in a game; a gunshot or a passport opening in one level file will have the same internal sound index as in all the others. The highest level of these is the SoundMap[]
array, which translates the internal sound index into an index into SoundDetails[]
. Each SoundDetails
record contains such details as the sound volume, pitch and volume randomization, looping configuration, how many samples to select from, and an index into SampleIndices[]
. This allows for selecting among multiple samples to produce variety; that index is the index to the SampleIndices[]
value of first of these, with the rest of them being having the next indices in series of that array. Thus, if the number of samples is 4, then the TR engine looks in SampleIndices[]
locations Index, Index+1, Index+2, and Index+3. Finally, the SampleIndices[]
array references some arrays of sound samples.
Additionally, TR4 and TR5 introduced usage of MS-ADPCM codec to store sample data, which was compressed and loaded into buffers on level loading. However, TR4 engine version bundled with TRLE used uncompressed wave data, as in TR1-TR3.
In TR1, TR4 and TR5 samples themselves are embedded in the level files. In TR1, SampleIndices[]
array contains the displacements of each sample in bytes from the beginning of that embedded block. In TR4 and TR5, way to access sample data was changed — each sample data is preceded by uncompressed size and compressed size uint32_t
values, which are used to extract given sample and load it into DirectSound buffer.
struct tr4_sample // (variable length) { uint32_t UncompSize; uint32_t CompSize; uint8_t SoundData[CompSize]; // zlib-compressed sound data (CompSize bytes) };
.WAV
file, uncompressed size defines the size of raw PCM data size in 16-bit, 22050 kHz, mono format. However, uncompressed size does not necessarily equal to a value derived from compressed size by wave type conversion, because MS-ADPCM codec tends to leave a bit of silence in the end of the file (which produces audible interruption in looped samples).
In TR2 and TR3, these samples are concatenated in the file MAIN.SFX
with no additional information; SampleIndices[]
contains sequence numbers (0, 1, 2, 3, …) in MAIN.SFX
. Finally, the samples themselves are all in Microsoft WAVE format.
This structure contains the details of continuous-sound sources. Although a SoundSource object has a position, it has no room membership; the sound seems to propagate omnidirectionally for about 8 horizontal-grid sizes without regard for the presence of walls.
struct tr_sound_source // 16 bytes { int32_t x; // absolute X position of sound source (world coordinates) int32_t y; // absolute Y position of sound source (world coordinates) int32_t z; // absolute Z position of sound source (world coordinates) uint16_t SoundID; // internal sound index uint16_t Flags; // 0x40, 0x80, or 0xC0 };
Flags
field defines sound source behaviour, if it was placed in room which can be flipped to alternate room:
0x40
: Play sound if room is in alternate state.0x80
: Play sound if room is in original state.Note that these flags are not mutually exclusive.
SoundMap
is used for mapping from internal-sound index to SoundDetails
index; it is 256 int16_t
in TR1, 370 int16_t
in TR2, TR3 and TR4, and 450 int16_t
in TR5. A value of -1 (0xFFFF)
indicates “none”, meaning the sample is not used in current level.
Each SoundDetails
entry can be described as such:
struct tr_sound_details // 8 bytes { uint16_t Sample; // (index into SampleIndices) uint16_t Volume; uint16_t Chance; // If !=0 and ((rand()&0x7fff) > Chance), this sound is not played uint16_t Characteristics; };
Characteristics
is a packed field containing various options for this particular sound detail:
Bit | Hex | Flag | Description |
---|---|---|---|
0 | 0x0003 | Looping behaviour | either normal playback (value 00 ), one-shot rewound (01 in TR1/TR2, 10 in other games), meaning the sound will be rewound if triggered again, or looped (value 10 in TR1, 11 in other games), meaning the sound will be looped until strictly stopped by an engine event. Since TR3, one-shot wait mode is introduced (value 10 ), meaning the same sound will be ignored until current one stops |
1 | |||
2 | 0x00FC | Number of sound samples in this group | If there are more than one samples, then engine will select one to play based on randomizer (for example, listen to Lara footstep sounds) |
3 | |||
4 | |||
5 | |||
6 | |||
7 | |||
8 | |||
9 | |||
10 | |||
11 | |||
12 | 0x1000 | No panoramic | Disables panoramic calculation of a given sample (always plays at center). |
13 | 0x2000 | Randomize pitch | When this flag is set, sound pitch will be slightly varied with each playback event |
14 | 0x4000 | Randomize gain | When this flag is set, sound volume (gain) will be slightly varied with each playback event |
15 |
In TR3 onwards, [tr_sound_details] structure was rearranged:
struct tr3_sound_details // 8 bytes { uint16_t Sample; // (index into SampleIndices) uint8_t Volume; uint8_t Range; uint8_t Chance; uint8_t Pitch; int16_t Characteristics; };
Range
now defines radius (in sectors), on which this sound can be heard. Previously (in TR1 and TR2), each sound had a predefined range about 8 sectors.
Pitch
specifies absolute pitch volume for this sound (may be also varied by bit 13 of Characteristics). Mainly, this value was used to “speed-up” certain samples, thus allowing to keep high-quality samples with lower sample rate, or on contrary, “slow down” sample, making it sound longer than its native sample rate, thus conserving some memory.
In TR1, this is an offset value array, each offset of which points into the embedded sound-samples block, which follows this array in the level file. In TR2 and TR3, this is a list of indices into the file ‘MAIN.SFX` file; the indices are the index numbers of that file’s embedded sound samples, rather than the samples’ starting locations. That file itself is a set of concatenated sound files with no catalogue info present.
These are various odds and ends that do not fit into the earlier categories.
Every level file begins with a uint32_t
version number. This seems to be used by the engine to guarantee compatibility between various level editor versions and the game engine version. More generally, it can be used to determine what sort of level is being read.
Here are the known (observed) values for the version header:
0x00000020
— Tomb Raider 1, Gold, Unfinished Business0x0000002D
— Tomb Raider 2, Gold 0xFF180038
— Tomb Raider 3, Gold, Gold Title level0xFF080038
— Tomb Raider 3 (Title level on Normal)0xFF180034
— Tomb Raider 3 (VICT.TR2
that only contains palette and atlases)0x00345254
(TR4
) — Tomb Raider 4 and Tomb Raider 50x63345254
(TR4c
) — Tomb Raider 4 (demo versions)This consists of 256 [tr_colour] structs, one for each palette entry. However, the individual colour values range from 0 to 63; they must be multiplied by 4 (or bitshifted by 2 to the left) to get the correct values. Palette is used for all 8-bit colour, such as 8-bit textures.
First entry in palette is treated as transparent colour used for textures with alpha testing. In later games, transparent colour was replaced by so-called “magenta transparency”, meaning that any pixel with red and blue values at maximum and green value at zero, is treated as completely transparent.
An object texture (or texture details in TRLE terms) keeps detailed information about each texture independently used in the game. While it’s not a texture image itself (these are kept inside texture atlases), it’s rather a reference to a particular texture in an atlas, kept with all other necessary information to display this texture.
This sub-structure used by object textures specifies a vertex location in texture tile coordinates. The Xcoordinate and Ycoordinate are the actual coordinates of the vertex's pixel. If the object texture is used to specify a triangle, then the fourth vertex's values will all be zero.
struct tr_object_texture_vert // 4 bytes { ufixed16 Xcoordinate; ufixed16 Ycoordinate; };
It’s object texture structure itself. These, thee contents of ObjectTextures[]
, are used for specifying texture mapping for the world geometry and for mesh objects.
struct tr_object_texture // 20 bytes { uint16_t Attribute; uint16_t AtlasAndFlag; tr_object_texture_vert Vertices[4]; // The four corners of the texture };
AtlasAndFlag
is a combined field:
Attribute
specifies transparency mode (i.e. blending mode) used for face with this texture applied. There are several ones available:
The structure introduced many new fields in TR4, partly related to new bump mapping feature.
For bump mapping, TR4 used fairly simple approach, which was actually not a true bump mapping, but multitexturing with additive operation. Therefore, bump maps were not normal maps mainly used for bump-mapping nowadays, but simple monochrome heightmaps automatically generated by level editor. This is a test screenshot comparison demonstrating same scene with and without bump mapping:
Bump mapping turned off | Bump mapping turned on |
Assignment of bump maps happened inside level editor, where each texture piece could be marked as either level 1 or level 2 of the bump map effect. When the level was converted, all texture pieces with bumpmaps were placed into separate texture atlases after all other texture atlases, followed by the same amount of texture atlases with auto-generated bump maps arranged in the same manner as the original texture atlases. The number of bump map atlases was kept in a separate variable as well (see TR4 Level Format section).
So, when the engine rendered a face with a texture marked as bump-mapped, it rendered the original texture first, then it jumped to the texture atlas plus the number of bump-mapped texture atlases, and rendered one more texture pass on this face using the texture from the resulting texture atlas and the same UV coordinates.
struct tr4_object_texture // 38 bytes { uint16_t Attribute; uint16_t AtlasAndFlag; uint16_t NewFlags; tr_object_texture_vert Vertices[4]; // The four corners of the texture uint32_t OriginalU; uint32_t OriginalV; uint32_t Width; // Actually width-1 uint32_t Height; // Actually height-1 };
NewFlags
is a bit field with flags:
AdjustUV
function which crops the texture in specific way to prevent border bleeding issue happening because of texture atlas packing. Value meaning depends on texture face type (triangle or quad). For quads, only types 0
and 1
are actually used (0
being normal and 1
being mirrored quad texture), while other types (2-7) produce same result as 0. For triangles, all possible values (0-7) are used for each possible right triangle type (including mirrored coordinates):Triangle mapping correction types. Orange shapes indicate normal (non-mirrored) texture coordinates, while blue shapes indicate mirrored ones. Mirrored coordinates mean that they are placed in counterclockwise order. |
00 = 0
(no bump mapping), 01 = 1
(level 1) or 10 = 2
(level 2).
Width
and Height
are helper values which specify width and height for a given object texture.
OriginalU
and OriginalV
are unused values, which seem to identify original UV coordinates of object texture in TRLE texture page listings. These coordinates are getting messed up when level is compiled, so one shouldn’t bother about parsing them correctly.
There is also null uint16_t
filler in the end of each [tr4_object_texture].
Animated textures describe sets of object textures that are cycled through to produce texture animations; they are a set of int16_t’s with the following format (not a “real” C/C++ structure):
int16_t NumAnimatedTextures virtual struct { int16_t NumTextureIDs; // Actually, this is the number of texture ID's - 1. int16_t TextureIDs[NumTextureIDs + 1]; // offsets into ObjectTextures[], in animation order. } AnimatedTextures[NumAnimatedTextures];
If a texture belongs to an animated-texture group, it will automatically be animated by the engine.
There are two types of animated textures — classic frames and UVRotate:
angkor1.tr4
, room #76:In foreground, you can see alpha-blended waterfall object animated with UVRotate. In background, UVRotate animation is also applied to room mesh. |
UVRotate mode is engaged by specifying UVRotate
command in level script entry, which takes rotation speed as an argument, where speed is amount of pixel shift per each frame, considering fixed 30 FPS framerate (speed value may be negative, and in such cases pixel shift occurs in reverse direction). If such command is found (and argument is not zero — for example, UVRotate = 4
), engine uses special variable value kept in level file, NumUVRotates
, to determine if animation range belongs to UVRotate mode or classic frames mode. Then, if it belongs to UVRotate mode, each frame of this range is treated as individual rotating texture.
The speed of animation for waterfall objects is not affected by UVRotate
script command. Instead, it is hardcoded value of 7.
This data block serves for two different purposes, albeit keeping the same structure for both. First purpose is to provide positions to switch the camera to using Camera trigger action, and the second purpose is to move Lara to specified position when she is underwater, and Underwater Current trigger action was used.
struct tr_camera // 16 bytes { int32_t x; int32_t y; int32_t z; int16_t Room; uint16_t Flag; };
X
, Y
and Z
values are coordinates of a given camera or sink. When used with camera, it is an origin point of a camera. When used with sink, it is a point, towards which Lara is pushed.
Room
value specifies the room where camera is placed. For sink cases, this value is used to define strength of the current which moves Lara underwater.
Flag
value for cameras specifies whether it’s possible to breakout from fixed camera angle. If first bit of this field is set, camera becomes persistent, and it’s not possible to bypass it with look or draw weapon buttons. For sinks, Flag
value contains Box index — which is used as pathfinding reference when Lara is pushed towards sink via underwater current trigger action.
Flyby cameras are cinematic interludes, in which camera flies from one point to another using spline trajectory. Each point in such sequence is a single flyby camera, and current camera properties (position, direction, roll, FOV, speed, and some more) are calculated by interpolating corresponding values from such flyby camera points — for example, if camera 0 has speed value of 10, and camera 1 has speed value of 5, then speed will gradually change from 10 to 5 when moving from one to another.
struct tr4_flyby_camera // 40 bytes { int32_t x; // Camera position int32_t y; int32_t z; int32_t dx; // Camera angles (so called "look at" vector) int32_t dy; int32_t dz; uint8_t Sequence; uint8_t Index; uint16_t FOV; int16_t Roll; uint16_t Timer; uint16_t Speed; uint16_t Flags; uint32_t Room_ID; };
Sequence
is a number of flyby camera “chain” this particular camera belongs to. Maximum amount of flyby sequences in single level is 8 (however, this limit was raised to 64 in TREP).
Index
specifies order of the cameras in this particular sequence. Camera with index
0 will be first one in sequence, index
1 means camera will be second in sequence, and so on.
Room_ID
should be valid for a given flyby camera, so it will display properly, as well as have the ability to activate heavy triggers.
FOV
changes this particular camera’s field of view. The value is 182 times higher than the value entered in the TRLE.
Roll
changes roll factor of a particular camera. When this parameter is not zero, camera will rotate either left or right along roll axis, creating so-called “dutch angle”. The value is 182 times higher than the value entered in the TRLE.
Timer
field mainly used to stop camera movement for a given time (in game frames). As this parameter is temporal, it won’t be interpolated between two cameras.
Speed
specifies movement speed for this particular camera. The value is 655 times higher than the value entered in the TRLE.
Flags
is an array of bit flags specifying different camera options:
Hex | Bit | Description |
---|---|---|
0x0001 | 0 | Make a cut to flyby from Lara camera position. Without it, it’ll pan smoothly. |
0x0002 | 1 | Tracks specified entity position (from Entities[] array). |
Creates a vignette around the picture, giving impression of “subjective” camera. | ||
0x0004 | 2 | Infinitely loop sequence. |
0x0008 | 3 | Used only with first camera in a sequence: whole sequence is treated merely as a camera “rails”, and camera itself focuses on Lara, thus creating “tracking” camera. Best example is “tracking” view in ALEXHUB2.TR4 , rooms #23 and #31. |
0x0010 | 4 | Camera focuses on Lara’s last head position. |
For TR5, this flag is now used to hide Lara for this camera. | ||
0x0020 | 5 | Camera continuously focuses on Lara’s head, overriding own angle. |
0x0040 | 6 | Used only with last camera in a sequence: camera smoothly pans back to Lara camera position. |
0x0080 | 7 | When flyby arrives to this position, cuts to specific camera in same sequence. Next camera number is specified in Timer field of this camera. |
0x0100 | 8 | Stops camera movement for a given time (see Timer field). |
0x0200 | 9 | Disables look keypress breakout. |
0x0400 | 10 | Disables all Lara controls for all next camera points. Also engages widescreen bars to create cinematic feel. |
0x0800 | 11 | Overrides Bit 10 controls lock, enabling them back. Widescreen bars remain unaffected. |
0x1000 | 12 | Make screen fade-in. |
0x2000 | 13 | Make screen fade-out. |
0x4000 | 14 | Camera can activate heavy triggers, just like particular kinds of entities (boulders, pushables, etc.). When camera is moving right above heavy trigger sector, it will be activated. |
0x8000 | 15 | TRLE for TR5 says this flag is used to make camera one-shot, but it’s not true. Actual one-shot flag is placed in extra uint16_t field at 0x0100 for flyby camera TrigAction. |
These are camera positionings and properties for cutscene frames. All the entity animations are specified separately, and they are not synced with actual camera positions.
switch (CutLevelID) { case 0: // CUT1.PHD levelPosX = 36668; // X pivot position levelPosZ = 63180; // Z pivot position levelRotY = -23312; // Y rotation trackID = 23; // Audio track index break; case 1: // CUT2.PHD levelPosX = 51962; levelPosZ = 53760; levelRotY = 16380; trackID = 25; break; case 2: // CUT3.PHD levelRotY = 0x4000; // Default rotation, 90 degrees FlipMap(); // Do a flipmap trackID = 24; break; case 3: // CUT4.PHD levelRotY = 0x4000; trackID = 22; break; }
levelPosX
, levelPosZ
and levelRotY
parameters are used, if defined, to set-up initial camera pivot position and Y axis rotation. If some of these parameters are not defined (which is the case for CUT3.PHD and CUT4.PHD), they are borrowed from X and Z position and Y rotation of entity with ID #77 (slot used for main cutscene actor, usually Lara). Also, same parameters are used as master model matrix (i. e. multiplied by them instead of using their own position and rotation) for models with IDs #77-79 (which are slots for cutscene actors).
struct tr_cinematic_frame // 16 bytes { int16_t targetX; // Camera look at position about X axis, int16_t targetY; // Camera look at position about Y axis int16_t target2; // Camera look at position about Z axis int16_t posZ; // Camera position about Z axis int16_t posY; // Camera position relative to something (see posZ) int16_t posX; // Camera position relative to something (see posZ) int16_t fov; int16_t roll; // Rotation about X axis };
All target
and pos
parameters, as well as roll
parameter, are encoded in the same manner as Angle
parameter for [tr_entity] structure.
A 32*256 array of uint8_t
which is apparently for applying light to 8-bit colour, in some documentation called ColourMap
. The current palette index and lighting value are used to calcuate an index to this table, which is a table of palette indices.
The Tomb Raider series' software rendering, like that of most real-time-3D games, uses 8-bit colour for speed and low bulk; however, there is the serious problem of how to do lighting with 8-bit colour, because doing it directly is computationally expensive. The usual solution is to arrange the palettes' colours in ramps, which the engine then follows in the appropriate directions. However, the TR series' palettes generally lack such neat ramps.
But the TR series has a more general solution, one that does not require palettes to have colour ramps. It uses precalculated lighting tables, the ColourMap
objects. These contain translations of a colour value and a lighting value, listed by palette index. The translation goes as follows:
$n = ColourMap[256 \cdot k + i]$
where $i$ is the original palette index, $k$ is determined from the lighting value, and $n$ is the new palette index. The lighting index $k$ varies from $0$ to $31$, and the corresponding lighting value is $2 - k / 16$ for TR1 and $2 - (k + 1) / 16$ for TR2 and TR3.
This may be associated with the curious fact of the lighting values in the data files increasing in the “wrong” direction in TR1 and TR2, with $0$ being full brightness and greater values being darker.
As it was briefly mentioned earlier, flipeffect is a special pre-compiled routine which is called when some non-trivial event occurs.
The concept of flipeffect is somewhat similar to task, i.e. when some flipeffect is engaged, it could be flagged by engine to call every game frame (however, there are primarily one-shot flipeffects present). Such setup is needed for some complex flipeffect events, like flickering lights in TR1 Atlantis (see FLICKER_FX
description), which stops automatically after some time.
If flipeffect is flagged to execute every game frame, this flag can only be unset by own current flipeffect code (when its task is done — for this purpose, special internal flip timer is used to count how much time have passed since flipeffect activation) or replaced by any other flipeffect call (however, newly called flipeffect doesn’t necessarily overwrite current flipeffect flag, so you can have one-shot flipeffect executed with another one queued for continuous execution). Ergo, it’s not possible to queue more than one _continuous flipeffect at a time, but it’s possible to have one one-shot and another continuous flipeffect executed every game frame.
In this chapter, we’ll try to describe each flipeffect for every TR engine version. Given the fact that flipeffect listing changed from version to version, yet retaining common ones, the easiest way to lay them down is to create a table with flipeffect indexes corresponding to each game version.
There are some guidelines to flipeffect table:
On contrary, some flipeffects may require certain trigger action and/or certain trigger type to be called at the moment. In such case, if flipeffect is called via AnimCommand, resulting outcome is undefined in original engine.
Index | TR1 | TR2 | TR3 | TR4 | TR5 |
---|---|---|---|---|---|
0 | TURN180 | TURN180 | TURN180 | ROTATE_180 | ROTATE_180 |
1 | DINO_STOMP | FLOOR_SHAKE | FLOOR_SHAKE | FLOOR_SHAKE | FLOOR_SHAKE |
2 | LARA_NORMAL | LARA_NORMAL | LARA_NORMAL | FLOOD_FX | FLOOD_FX |
3 | LARA_BUBBLES | LARA_BUBBLES | LARA_BUBBLES | LARA_BUBBLES | LARA_BUBBLES |
4 | FINISH_LEVEL | FINISH_LEVEL | FINISH_LEVEL | FINISH_LEVEL | FINISH_LEVEL |
5 | EARTHQUAKE_FX | FLOOD_FX | FLOOD_FX | ACTIVATE_CAMERA | ACTIVATE_CAMERA |
6 | FLOOD_FX | CHANDELIER_FX | CHANDELIER_FX | ACTIVATE_KEY | ACTIVATE_KEY |
7 | RAISINGBLOCK_FX | RUBBLE_FX | RUBBLE_FX | RUBBLE_FX | RUBBLE_FX |
8 | STAIRS2SLOPE_FX | PISTON_FX | PISTON_FX | SWAP_CROWBAR | SWAP_CROWBAR |
9 | SAND_FX | CURTAIN_FX | CURTAIN_FX | - | - |
10 | POWERUP_FX | SETCHANGE_FX | SETCHANGE_FX | TIMER_FIELD_FX | TIMER_FIELD_FX |
11 | EXPLOSION_FX | EXPLOSION_FX | EXPLOSION_FX | EXPLOSION_FX | EXPLOSION_FX |
12 | LARA_HANDSFREE | LARA_HANDSFREE | LARA_HANDSFREE | LARA_HANDSFREE | LARA_HANDSFREE |
13 | FLIP_MAP | FLIP_MAP | FLIP_MAP | - | - |
14 | DRAW_RIGHTGUN | DRAW_RIGHTGUN | DRAW_RIGHTGUN | DRAW_RIGHTGUN | - |
15 | CHAINBLOCK_FX | DRAW_LEFTGUN | DRAW_LEFTGUN | DRAW_LEFTGUN | - |
16 | FLICKER_FX | - | SHOOT_RIGHTGUN | SHOOT_RIGHTGUN | SHOOT_RIGHTGUN |
17 | - | SHOOT_LEFTGUN | SHOOT_LEFTGUN | SHOOT_LEFTGUN |
|
18 | MESH_SWAP1 | MESH_SWAP1 | MESH_SWAP1 | - |
|
19 | MESH_SWAP2 | MESH_SWAP2 | MESH_SWAP2 | - |
|
20 | MESH_SWAP3 | MESH_SWAP3 | MESH_SWAP3 | - |
|
21 | INV_ON | INV_ON | INV_ON | INV_ON |
|
22 | INV_OFF | INV_OFF | INV_OFF | INV_OFF |
|
23 | DYN_ON | DYN_ON | - | - |
|
24 | DYN_OFF | DYN_OFF | - | - |
|
25 | STATUE_FX | STATUE_FX | - | - |
|
26 | RESET_HAIR | RESET_HAIR | RESET_HAIR | RESET_HAIR |
|
27 | BOILER_FX | BOILER_FX | - | - |
|
28 | ASSAULT_RESET | ASSAULT_RESET | SETFOG | SETFOG |
|
29 | ASSAULT_STOP | ASSAULT_STOP | GHOSTTRAP | - |
|
30 | ASSAULT_START | ASSAULT_START | LARALOCATION | LARALOCATION |
|
31 | ASSAULT_FINISHED | ASSAULT_FINISHED | CLEARSCARABS | RESET_TEST (?) |
|
32 | FOOTPRINT_FX | FOOTPRINT_FX | FOOTPRINT_FX |
||
33 | ASSAULT_PENALTY_8 | - | CLEAR_SPIDERS_PATCH (?) |
||
34 | RACETRACK_START | - | - |
||
35 | RACETRACK_RESET | - | - |
||
36 | RACETRACK_FINISHED | - | - |
||
37 | ASSAULT_PENALTY_30 | - | - |
||
38 | GYM_HINT_1 | - | - |
||
39 | GYM_HINT_2 | - | - |
||
40 | GYM_HINT_3 | - | - |
||
41 | GYM_HINT_4 | - | - |
||
42 | GYM_HINT_5 | - | - |
||
43 | GYM_HINT_6 | POURSWAP_ON | - |
||
44 | GYM_HINT_7 | POURSWAP_OFF | - |
||
45 | GYM_HINT_8 | LARALOCATIONPAD | LARALOCATIONPAD |
||
46 | GYM_HINT_9 | KILLACTIVEBADDIES | KILLACTIVEBADDIES |
||
47 | GYM_HINT_10 | TUT_HINT_1 |
|||
48 | GYM_HINT_11 | TUT_HINT_2 |
|||
49 | GYM_HINT_12 | TUT_HINT_3 |
|||
50 | GYM_HINT_13 | TUT_HINT_4 |
|||
51 | GYM_HINT_14 | TUT_HINT_5 |
|||
52 | GYM_HINT_15 | TUT_HINT_6 |
|||
53 | GYM_HINT_16 | TUT_HINT_7 |
|||
54 | GYM_HINT_17 | TUT_HINT_8 |
|||
55 | GYM_HINT_18 | TUT_HINT_9 |
|||
56 | GYM_HINT_19 | TUT_HINT_10 |
|||
57 | GYM_HINT_RESET | TUT_HINT_11 |
|||
58 | TUT_HINT_12 |
LARA_
prefix automatically take Lara character as an entity to work with. Also, most flipeffects with _FX
postfix are simple sound effect events.
ROTATE_180
— Rotates an entity 180 degrees around yaw axis and also around pitch axis for underwater cases. Mostly used in Lara roll animations. This flipeffect needs special approach if original animation frames are interpolated, because usually rotation is done on animation transition (e.g., frame 5 of Lara animation 48, which is second and final part of her roll movement). To prevent stray misaligned interpolated frames, this flipeffect must be performed only in the end of frame-to-frame interpolated sequence.TURN180
— Same as ROTATE_180
.LARA_NORMAL
— Resets certain internal Lara parameters to default ones, including movement modes, FOV and camera position.FLOOR_SHAKE
— If entity producing this effect is in less than 8 sector range, send shake effect to camera. Shake effect is a variable which is inversely proportional to entity distance, and, when sent to camera, makes it shake with corresponding amplitude gradually fading out. If there are multiple FLOOR_SHAKE events constantly occuring nearby camera, shake effect won’t accumulate, but rather overwrite previous value.DINO_STOMP
— Same as FLOOR_SHAKE
.LARA_BUBBLES
— When underwater, emit bubble sound (ID #37) and produce bubble particle for Lara. Position of bubble is linked to model’s last mesh (which is headmesh in case of Lara).FINISH_LEVEL
— Same effect as TrigAction 0x07
— immediately loads next level. For TR4, (which requires explicit level index to jump), current level index is increased and passed as level index to jump to.ACTIVATE_CAMERA
— If there is a trigger type Key (SubFunction 0x03
) being queued at the moment, and there are any Camera TrigActions (0x01
) present in ActionList
, these TrigActions will be forced to activate at a given frame of keyhole entity current animation, rather than at the ending frame of it. Works only for keyhole entities which have complex activation animations, not single-frame ones. It can be used to change camera POV before keyhole animation is finished.ACTIVATE_KEY
— Same as above, but works for Object TrigAction. That is, any entities to be activated from ActionList
will be activated at a given frame of keyhole entity current animation, rather than at the ending frame of it. Can be used to activate entities before actual keyhole animation is finished.LARA_HANDSFREE
— Functionally removes any weapon from Lara’s hands. If called during holstering or unholstering operation, immediately aborts it. Note that holstering animation won’t be automatically performed, and weapon model meshswaps won’t be swapped back to normal hands.DRAW_RIGHTGUN
— Swaps given entity’s mesh #10 index with same mesh’s index from PISTOLS_ANIM model (model ID #1 in all TR versions). Calling this effect again swaps mesh #10 back to native. Used primarily in cutscenes to create an illusion of Lara getting pistol in her right hand.DRAW_LEFTGUN
— Swaps given entity’s mesh #13 index with same mesh’s index from PISTOLS_ANIM model (model ID #1 in all TR versions). Calling this effect again swaps mesh #13 back to native. Used primarily in cutscenes to create an illusion of Lara getting pistol in her left hand.SHOOT_RIGHTGUN
— Activates given entity’s muzzle flash effect and dynamic light near mesh #10. Muzzle flash position and orientation, as well as effect duration and intensity is hardcoded. Used primarily in cutscenes.SHOOT_LEFTGUN
— Activates given entity’s muzzle flash effect and dynamic light near mesh #13. Muzzle flash position and orientation, as well as effect duration and intensity is hardcoded. Used primarily in cutscenes.MESH_SWAP1
— Swaps all given entity meshes with MESH_SWAP1 model meshes (model ID varies across TR versions). Each mesh is swapped only if source meshswap model mesh is not null, otherwise swap is ignored for a given mesh. Calling this flipeffect again swaps all meshes back to native. Used primarily in cutscenes.MESH_SWAP2
— Swaps all given entity meshes with MESH_SWAP2 model meshes (model ID varies across TR versions). Each mesh is swapped only if source meshswap model mesh is not null, otherwise swap is ignored for a given mesh. Calling this flipeffect again swaps all meshes back to native. Used primarily in cutscenes.MESH_SWAP3
— Swaps all given entity meshes with MESH_SWAP3 model meshes (model ID varies across TR versions). Each mesh is swapped only if source meshswap model mesh is not null, otherwise swap is ignored for a given mesh. Calling this flipeffect again swaps all meshes back to native. Used primarily in cutscenes.SWAP_CROWBAR
— Swaps given entity’s mesh #10 index with same mesh’s index from CROWBAR_ANIM model (either model ID #246 in TR4, or model ID #240 in TR5). Calling this flipeffect again swaps mesh #10 back to native. Used primarily in cutscenes to create an illusion of Lara getting crowbar in her hand.POURSWAP_ON
— Swaps given entity’s mesh #10 index with same mesh’s index from LARA_WATER_MESH model (TR4, modei ID #25). Used in Lara’s waterskin animations used in late TR4 levels with waterskin puzzle.POURSWAP_OFF
— Swaps given entity’s mesh #10 back to native. Used in Lara’s waterskin animations used in late TR4 levels with waterskin puzzle.INV_ON
— Hides given entity.INV_OFF
— Shows given entity, if it was hidden.DYN_ON
— Turns dynamic lights on for a given entity. Actual result is unclear.DYN_OFF
— Turns dynamic lights off for a given entity. Actual result is unclear.RESET_HAIR
— Presumably used to save Lara’s ponytail from potential stuck during cutscenes by resetting all hair parameters to “identity”.SETFOG
— When called by trigger action, changes global colour for volumetric fog effect. Takes TriggerSetup
Timer field as an index into hardcoded RGB table of colours (see this section for more info). If specified index is 100, engine temporarily turns off volumetric fog effect (possibly, this was used for debug purposes).GHOSTTRAP
— Kills all the living WRAITH3 entities (model ID #88 in TR4) this way: the wraith starts falling towards given entity. Reaching it or not, the wraith will die if it hits the floor of the room.CLEARSCARABS
— Removes all swarms of scarabs currently wandering in level.KILLACTIVEBADDIES
— Disable and remove all active NPCs from level.CLEAR_SPIDERS_PATCH
— Present only in TR5. It seems it’s same as KILLACTIVEBADDIES
, but some other processing is done. Never used in actual levels.RESET_TEST
— Present only in TR5. No visible or significant effect on gameplay. If there are any NPCs in level, then this flipeffect will fill certain memory zone with zero bytes. This flipeffect seems like last-minute fix-up for some memory leak bug. Used in RICH1.TRC level (The 13th Floor)LARALOCATION
— When activated, makes Guide NPC (TR4, model ID #37) or Von Croy NPC (TR4, model ID #39) to move to specific AI_FOLLOW object. Takes TriggerSetup
Timer field as an index to search for such OCB within AI objects array. When AI_FOLLOW AI object with same OCB index is found, NPC is then directed to this AI_FOLLOW object. This flipeffect also stores this index in additional global variable which is used to prevent NPC to get back to AI_FOLLOW objects with lower OCB indexes that were already passed — for example, if NPC already passed AI_FOLLOW with OCB 2, he won’t return to AI_FOLLOW with OCB 1, even if he hasn’t been there before.LARALOCATIONPAD
— Same action as LARALOCATION
, but with one difference - Timer field is checked for certain values to engage either specific soundtrack and/or cinematic dialogue with Von Croy NPC (for demonstration, look for Angkor Wat level walkthrough). This additional behaviour is hardcoded for TR4’s first level index only.ASSAULT_RESET
— Resets assault course clock (for ex., when Lara stepped out of assault course).ASSAULT_STOP
— Stops assault course clock.ASSAULT_START
— Starts assault course clock.ASSAULT_FINISHED
— Finishes assault course clock and fixes the record. Depending on record time, plays either unbeat record (“I’m sure you can do better”, track ID #24 in TR2) soundtrack or best record (“Gosh, that was my best time yet”, track ID #22 in TR2, #95 in TR3) soundtrack. Record time is hardcoded to 100 seconds in TR2 and to 180 seconds in TR3. In TR3, flipeffect also checks if all targets in shooting range were hit by Lara, and if not, applies penalty of 10 seconds for each unhit target. Also, TR3 lacks “unbeat record” soundtrack.ASSAULT_PENALTY_8
— 8-second penalty for losing track on assault course.ASSAULT_PENALTY_30
— 30-second penalty for losing track on assault course.RACETRACK_START
— Prepare racetrack timer for counting lap time. Only works when Lara is on a quadbike! As soon as quadbike leaves sector with this flipeffect, timer will start counting.RACETRACK_RESET
— Resets current lap time. Only works when Lara is on a quadbike!RACETRACK_FINISHED
— Finishes racetrack timer and fixes the record. Only works when Lara is on a quadbike!GYM_HINT_1-19
— Sequence of Lara’s voice hints on how to complete gym training. Reason why these are activated via flipeffects rather than normal soundtrack is they must be engaged in predefined order, e.g. voice hint #8 can’t play before #7 was played, and so on.GYM_HINT_RESET
— Resets gym training progress, so all voice hints will be played once again.TUT_HINT_1-12
— Sequence of Lara’s voice hints on how to complete tutorial on Streets of Rome (TR5). Setup is similar to GYM_HINT
flipeffects, but seems that there’s no reset flipeffect to restart tutorial.RAISINGBLOCK_FX
— Plays global sound with ID 117. Used in TR1, Palace Midas.CHAINBLOCK_FX
— Plays global sounds with ID 173 and ID 33 with predefined interval. Used in TR1, Tomb of Tihocan.EARTHQUAKE_FX
— Shakes screen violently and plays sounds with ID 99 and 70 globally with predefined intervals. Used in TR1, Palace Midas.STAIRS2SLOPE_FX
— Plays global sound with ID 119 with predefined delay. Used in TR1, City of Khamoon.SAND_FX
— Plays global sounds with ID 161, 118 and 155 with predefined intervals. Used in TR1, City of Khamoon.POWERUP_FX
— Plays global sound with ID 155 for 1 second. Presumably used in TR1, one of the Atlantis levels, but never appears on map.FLICKER_FX
— Flips alternate rooms back and forth several times with predefined intervals, creating illusion of flickering light. Used in TR1, first room of Atlantis.CHANDELIER_FX
— Plays global sound with ID 278 for 1 second. Used in TR2, Bartoli’s Hideout.BOILER_FX
— Plays global sound with ID 338. Used in TR2, Wreck of the Maria Doria.PISTON_FX
— Plays global sound with ID 190. Used in TR2, Living Quarters.CURTAIN_FX
— Plays global sound with ID 191. Used in TR2, Living Quarters.SET_CHANGE_FX
— Plays global sound with ID 330. Used in TR2, Opera House and Temple of Xian.STATUE_FX
— Plays global sound with ID 331. Used in TR2, Barkhang Monastery.RUBBLE_FX
— Plays global rumble sound FX and holds camera shake effect for some time, then finishes it with “shutting” sound. If there are any earthquake type objects in a level, engine engages same behaviour locally for these objects.TIMER_FIELD_FX
— If this flipeffect is called by trigger action, play global sound FX, taking TriggerSetup
Timer field as a sound ID.EXPLOSION_FX
— Plays global explosion sound (ID #105) and produce full-screen flash graphical FX (TR3-5) or camera shake effect (TR1-2).FLOOD_FX
— Plays global flooding sound (TR1 — ID #81, TR2 — ID #79, TR3 — ID #163, TR4 — ID #238). Implementation differs from version to version — in TR1 and TR2 looped waterfall sound is used (which is then stopped by an engine after 1 second), while in TR3 and TR4 one-shot sound is engaged.FOOTPRINT_FX
— Plays random footprint sound effect, taking current block's material index into consideration. On PlayStation, also applies footprint sprite under left or right Lara foot (target foot is selected based on packed flag which is stored in animcommand argument — look here for details).
What follows is the physical .PHD
and .TUB
file layout, byte for byte.
uint32_t Version; // version (4 bytes) uint32_t NumImages; // number of texture images (4 bytes) tr_image8 Images8[NumImages]; // 8-bit (palettized) images (NumImages * 65536 bytes) uint32_t Unused; // supposed to be the level number but TOM2PC didn't allow changing it, always 0 (4 bytes) uint16_t NumRooms; // number of rooms (2 bytes) tr_room Rooms[NumRooms]; // room list (variable length) uint32_t NumFloorData; // number of floor data uint16_t's to follow (4 bytes) uint16_t FloorData[NumFloorData]; // floor data (NumFloorData * 2 bytes) uint32_t NumMeshData; // number of uint16_t's of mesh data to follow (=Meshes[]) (4 bytes) tr_mesh Meshes[NumMeshPointers]; // note that NumMeshPointers comes AFTER Meshes[] uint32_t NumMeshPointers; // number of mesh pointers to follow (4 bytes) uint32_t MeshPointers[NumMeshPointers]; // mesh pointer list (NumMeshPointers * 4 bytes) uint32_t NumAnimations; // number of animations to follow (4 bytes) tr_animation Animations[NumAnimations]; // animation list (NumAnimations * 32 bytes) uint32_t NumStateChanges; // number of state changes to follow (4 bytes) tr_state_change StateChanges[NumStateChanges]; // state-change list (NumStructures * 6 bytes) uint32_t NumAnimDispatches; // number of animation dispatches to follow (4 bytes) tr_anim_dispatch AnimDispatches[NumAnimDispatches]; // animation-dispatch list list (NumAnimDispatches * 8 bytes) uint32_t NumAnimCommands; // number of animation commands to follow (4 bytes) tr_anim_command AnimCommands[NumAnimCommands]; // animation-command list (NumAnimCommands * 2 bytes) uint32_t NumMeshTrees; // number of MeshTrees to follow (4 bytes) tr_meshtree_node MeshTrees[NumMeshTrees]; // MeshTree list (NumMeshTrees * 4 bytes) uint32_t NumFrames; // number of words of frame data to follow (4 bytes) uint16_t Frames[NumFrames]; // frame data (NumFrames * 2 bytes) uint32_t NumModels; // number of models to follow (4 bytes) tr_model Models[NumModels]; // model list (NumModels * 18 bytes) uint32_t NumStaticMeshes; // number of StaticMesh data records to follow (4 bytes) tr_staticmesh StaticMeshes[NumStaticMeshes]; // StaticMesh data (NumStaticMesh * 32 bytes) uint32_t NumObjectTextures; // number of object textures to follow (4 bytes) (after AnimatedTextures in TR3) tr_object_texture ObjectTextures[NumObjectTextures]; // object texture list (NumObjectTextures * 20 bytes) (after AnimatedTextures in TR3) uint32_t NumSpriteTextures; // number of sprite textures to follow (4 bytes) tr_sprite_texture SpriteTextures[NumSpriteTextures]; // sprite texture list (NumSpriteTextures * 16 bytes) uint32_t NumSpriteSequences; // number of sprite sequences records to follow (4 bytes) tr_sprite_sequence SpriteSequences[NumSpriteSequences]; // sprite sequence data (NumSpriteSequences * 8 bytes) uint32_t NumCameras; // number of camera data records to follow (4 bytes) tr_camera Cameras[NumCameras]; // camera data (NumCameras * 16 bytes) uint32_t NumSoundSources; // number of sound source data records to follow (4 bytes) tr_sound_source SoundSources[NumSoundSources]; // sound source data (NumSoundSources * 16 bytes) uint32_t NumBoxes; // number of box data records to follow (4 bytes) tr_box Boxes[NumBoxes]; // box data (NumBoxes * 20 bytes [TR1 version]) uint32_t NumOverlaps; // number of overlap records to follow (4 bytes) uint16_t Overlaps[NumOverlaps]; // overlap data (NumOverlaps * 2 bytes) uint16_t GroundZone[2*NumBoxes]; // ground zone data uint16_t GroundZone2[2*NumBoxes]; // ground zone 2 data uint16_t FlyZone[2*NumBoxes]; // fly zone data uint16_t GroundZoneAlt[2*NumBoxes]; // ground zone data (alternate rooms?) uint16_t GroundZoneAlt2[2*NumBoxes]; // ground zone 2 data (alternate rooms?) uint16_t FlyZoneAlt[2*NumBoxes]; // fly zone data (alternate rooms?) uint32_t NumAnimatedTextures; // number of animated texture records to follow (4 bytes) uint16_t AnimatedTextures[NumAnimatedTextures]; // animated texture data (NumAnimatedTextures * 2 bytes) uint32_t NumEntities; // number of entities to follow (4 bytes) tr_entity Entities[NumEntities]; // entity list (NumEntities * 22 bytes [TR1 version]) uint8_t LightMap[32 * 256]; // light map (8192 bytes) tr_colour Palette[256]; // 8-bit palette (768 bytes) uint16_t NumCinematicFrames; // number of cinematic frame records to follow (2 bytes) tr_cinematic_frame CinematicFrames[NumCinematicFrames]; // (NumCinematicFrames * 16 bytes) uint16_t NumDemoData; // number of demo data records to follow (2 bytes) uint8_t DemoData[NumDemoData]; // demo data (NumDemoData bytes) int16_t SoundMap[256]; // sound map (512 bytes) uint32_t NumSoundDetails; // number of sound-detail records to follow (4 bytes) tr_sound_details SoundDetails[NumSoundDetails]; // sound-detail list (NumSoundDetails * 8 bytes) uint32_t NumSamples; // number of uint8_t's in Samples (4 bytes) uint8_t Samples[NumSamples]; // array of uint8_t's -- embedded sound samples in Microsoft WAVE format (NumSamples bytes) uint32_t NumSampleIndices; // number of sample indices to follow (4 bytes) uint32_t SampleIndices[NumSampleIndices]; // sample indices (NumSampleIndices * 4 bytes)
What follows is the physical .TR2
file layout, byte for byte.
uint32_t Version; // version (4 bytes) tr_colour Palette[256]; // 8-bit palette (768 bytes) tr_colour4 Palette16[256]; // (1024 bytes) uint32_t NumImages; // number of texture images (4 bytes) tr_teximage8 Images8[NumImages]; // 8-bit (palettized) images (NumImages * 65536 bytes) tr_teximage16 Images16[NumImages]; // 16-bit (ARGB) images (NumImages * 131072 bytes) uint32_t Unused; // supposed to be the level number but TOM2PC didn't allow changing it, always 0 (4 bytes) uint16_t NumRooms; // number of rooms (2 bytes) tr2_room Rooms[NumRooms]; // room list (variable length) uint32_t NumFloorData; // number of floor data uint16_t's to follow (4 bytes) uint16_t FloorData[NumFloorData]; // floor data (NumFloorData * 2 bytes) uint32_t NumMeshData; // number of uint16_t's of mesh data to follow (=Meshes[]) (4 bytes) tr_mesh Meshes[NumMeshPointers]; // note that NumMeshPointers comes AFTER Meshes[] uint32_t NumMeshPointers; // number of mesh pointers to follow (4 bytes) uint32_t MeshPointers[NumMeshPointers]; // mesh pointer list (NumMeshPointers * 4 bytes) uint32_t NumAnimations; // number of animations to follow (4 bytes) tr_animation Animations[NumAnimations]; // animation list (NumAnimations * 32 bytes) uint32_t NumStateChanges; // number of state changes to follow (4 bytes) tr_state_change StateChanges[NumStateChanges]; // state-change list (NumStructures * 6 bytes) uint32_t NumAnimDispatches; // number of animation dispatches to follow (4 bytes) tr_anim_dispatch AnimDispatches[NumAnimDispatches]; // animation-dispatch list list (NumAnimDispatches * 8 bytes) uint32_t NumAnimCommands; // number of animation commands to follow (4 bytes) tr_anim_command AnimCommands[NumAnimCommands]; // animation-command list (NumAnimCommands * 2 bytes) uint32_t NumMeshTrees; // number of MeshTrees to follow (4 bytes) tr_meshtree_node MeshTrees[NumMeshTrees]; // MeshTree list (NumMeshTrees * 4 bytes) uint32_t NumFrames; // number of words of frame data to follow (4 bytes) uint16_t Frames[NumFrames]; // frame data (NumFrames * 2 bytes) uint32_t NumModels; // number of models to follow (4 bytes) tr_model Models[NumModels]; // model list (NumModels * 18 bytes) uint32_t NumStaticMeshes; // number of StaticMesh data records to follow (4 bytes) tr_staticmesh StaticMeshes[NumStaticMeshes]; // StaticMesh data (NumStaticMesh * 32 bytes) uint32_t NumObjectTextures; // number of object textures to follow (4 bytes) tr_object_texture ObjectTextures[NumObjectTextures]; // object texture list (NumObjectTextures * 20 bytes) (after AnimatedTextures in TR3) uint32_t NumSpriteTextures; // number of sprite textures to follow (4 bytes) tr_sprite_texture SpriteTextures[NumSpriteTextures]; // sprite texture list (NumSpriteTextures * 16 bytes) uint32_t NumSpriteSequences; // number of sprite sequences records to follow (4 bytes) tr_sprite_sequence SpriteSequences[NumSpriteSequences]; // sprite sequence data (NumSpriteSequences * 8 bytes) uint32_t NumCameras; // number of camera data records to follow (4 bytes) tr_camera Cameras[NumCameras]; // camera data (NumCameras * 16 bytes) uint32_t NumSoundSources; // number of sound source data records to follow (4 bytes) tr_sound_source SoundSources[NumSoundSources]; // sound source data (NumSoundSources * 16 bytes) uint32_t NumBoxes; // number of box data records to follow (4 bytes) tr2_box Boxes[NumBoxes]; // box data (NumBoxes * 8 bytes) uint32_t NumOverlaps; // number of overlap records to follow (4 bytes) uint16_t Overlaps[NumOverlaps]; // overlap data (NumOverlaps * 2 bytes) int16_t Zones[10*NumBoxes]; // zone data (NumBoxes * 20 bytes) uint32_t NumAnimatedTextures; // number of animated texture records to follow (4 bytes) uint16_t AnimatedTextures[NumAnimatedTextures]; // animated texture data (NumAnimatedTextures * 2 bytes) uint32_t NumEntities; // number of entities to follow (4 bytes) tr2_entity Entities[NumEntities]; // entity list (NumEntities * 24 bytes) uint8_t LightMap[32 * 256]; // light map (8192 bytes) uint16_t NumCinematicFrames; // number of cinematic frame records to follow (2 bytes) tr_cinematic_frame CinematicFrames[NumCinematicFrames]; // (NumCinematicFrames * 16 bytes) uint16_t NumDemoData; // number of demo data records to follow (2 bytes) uint8_t DemoData[NumDemoData]; // demo data (NumDemoData bytes) int16_t SoundMap[370]; // sound map (740 bytes) uint32_t NumSoundDetails; // number of sound-detail records to follow (4 bytes) tr_sound_details SoundDetails[NumSoundDetails]; // sound-detail list (NumSoundDetails * 8 bytes) uint32_t NumSampleIndices; // number of sample indices to follow (4 bytes) uint32_t SampleIndices[NumSampleIndices]; // sample indices (NumSampleIndices * 4 bytes)
What follows is the physical Tomb Raider III .TR2
file layout, byte for byte.
uint32_t Version; // version (4 bytes) tr_colour Palette[256]; // 8-bit palette (768 bytes) tr_colour4 Palette16[256]; // (1024 bytes) uint32_t NumImages; // number of texture images (4 bytes) tr_image8 Images8[NumImages]; // 8-bit (palettized) images (NumImages * 65536 bytes) tr_image16 Images16[NumImages]; // 16-bit (ARGB) images (NumImages * 131072 bytes) (absent from TR1) // "VICT.TR2" stops here, nothing after uint32_t Unused; // supposed to be the level number but TOM2PC didn't allow changing it, always 0 (4 bytes) uint16_t NumRooms; // number of rooms (2 bytes) tr3_room Rooms[NumRooms]; // room list (variable length) uint32_t NumFloorData; // number of floor data uint16_t's to follow (4 bytes) uint16_t FloorData[NumFloorData]; // floor data (NumFloorData * 2 bytes) uint32_t NumMeshData; // number of uint16_t's of mesh data to follow (=Meshes[]) (4 bytes) tr_mesh Meshes[NumMeshPointers]; // note that NumMeshPointers comes AFTER Meshes[] uint32_t NumMeshPointers; // number of mesh pointers to follow (4 bytes) uint32_t MeshPointers[NumMeshPointers]; // mesh pointer list (NumMeshPointers * 4 bytes) uint32_t NumAnimations; // number of animations to follow (4 bytes) tr_animation Animations[NumAnimations]; // animation list (NumAnimations * 32 bytes) uint32_t NumStateChanges; // number of state changes to follow (4 bytes) tr_state_change StateChanges[NumStateChanges]; // state-change list (NumStructures * 6 bytes) uint32_t NumAnimDispatches; // number of animation dispatches to follow (4 bytes) tr_anim_dispatch AnimDispatches[NumAnimDispatches]; // animation-dispatch list list (NumAnimDispatches * 8 bytes) uint32_t NumAnimCommands; // number of animation commands to follow (4 bytes) tr_anim_command AnimCommands[NumAnimCommands]; // animation-command list (NumAnimCommands * 2 bytes) uint32_t NumMeshTrees; // number of MeshTrees to follow (4 bytes) tr_meshtree_node MeshTrees[NumMeshTrees]; // MeshTree list (NumMeshTrees * 4 bytes) uint32_t NumFrames; // number of words of frame data to follow (4 bytes) uint16_t Frames[NumFrames]; // frame data (NumFrames * 2 bytes) uint32_t NumModels; // number of models to follow (4 bytes) tr_model Models[NumModels]; // model list (NumModels * 18 bytes) uint32_t NumStaticMeshes; // number of StaticMesh data records to follow (4 bytes) tr_staticmesh StaticMeshes[NumStaticMeshes]; // StaticMesh data (NumStaticMesh * 32 bytes) uint32_t NumSpriteTextures; // number of sprite textures to follow (4 bytes) tr_sprite_texture SpriteTextures[NumSpriteTextures]; // sprite texture list (NumSpriteTextures * 16 bytes) uint32_t NumSpriteSequences; // number of sprite sequences records to follow (4 bytes) tr_sprite_sequence SpriteSequences[NumSpriteSequences]; // sprite sequence data (NumSpriteSequences * 8 bytes) uint32_t NumCameras; // number of camera data records to follow (4 bytes) tr_camera Cameras[NumCameras]; // camera data (NumCameras * 16 bytes) uint32_t NumSoundSources; // number of sound source data records to follow (4 bytes) tr_sound_source SoundSources[NumSoundSources]; // sound source data (NumSoundSources * 16 bytes) uint32_t NumBoxes; // number of box data records to follow (4 bytes) tr2_box Boxes[NumBoxes]; // box data (NumBoxes * 8 bytes) uint32_t NumOverlaps; // number of overlap records to follow (4 bytes) uint16_t Overlaps[NumOverlaps]; // overlap data (NumOverlaps * 2 bytes) int16_t Zones[10*NumBoxes]; // zone data (NumBoxes * 20 bytes) uint32_t NumAnimatedTextures; // number of animated texture records to follow (4 bytes) uint16_t AnimatedTextures[NumAnimatedTextures]; // animated texture data (NumAnimatedTextures * 2 bytes) uint32_t NumObjectTextures; // number of object textures to follow (4 bytes) (after AnimatedTextures in TR3) tr_object_texture ObjectTextures[NumObjectTextures]; // object texture list (NumObjectTextures * 20 bytes) uint32_t NumEntities; // number of entities to follow (4 bytes) tr2_entity Entities[NumEntities]; // entity list (NumEntities * 24 bytes) uint8_t LightMap[32 * 256]; // light map (8192 bytes) uint16_t NumCinematicFrames; // number of cinematic frame records to follow (2 bytes) tr_cinematic_frame CinematicFrames[NumCinematicFrames]; // (NumCinematicFrames * 16 bytes) uint16_t NumDemoData; // number of demo data records to follow (2 bytes) uint8_t DemoData[NumDemoData]; // demo data (NumDemoData bytes) int16_t SoundMap[370]; // sound map (740 bytes) uint32_t NumSoundDetails; // number of sound-detail records to follow (4 bytes) tr3_sound_details SoundDetails[NumSoundDetails]; // sound-detail list (NumSoundDetails * 8 bytes) uint32_t NumSampleIndices; // number of sample indices to follow (4 bytes) + uint32_t SampleIndices[NumSampleIndices]; // sample indices (NumSampleIndices * 4 bytes)
What follows is the physical Tomb Raider IV .TR4
file layout, byte for byte.
uint32_t Version; // version (4 bytes) uint16_t NumRoomImages; // number of non.bumpmapped room images (2 bytes) uint16_t NumObjImages; // number of object images (2 bytes) uint16_t NumBumpmaps; // number of bump-mapped room images (2 bytes) uint32_t Image32_UncompSize; // uncompressed size (in bytes) of the 32-bit textures chunk (4 bytes) uint32_t Image32_CompSize; // compressed size (in bytes) of the 32-bit textures chunk (4 bytes) uint8_t Image32_Compressed[Image32_CompSize]; // zlib-compressed 32-bit textures chunk (Image32_CompSize bytes) { tr4_image32 Image32[NumRoomImages + NumObjImages + NumBumpmaps]; } uint32_t Image16_UncompSize; // uncompressed size (in bytes) of the 16-bit textures chunk (4 bytes) uint32_t Image16_CompSize; // compressed size (in bytes) of the 16-bit textures chunk (4 bytes) uint8_t Image16_Compressed[Image32_CompSize]; // zlib-compressed 16-bit textures chunk (Image16_CompSize bytes) { tr_image16 Image16[NumRoomImages + NumObjImages + NumBumpmaps]; } uint32_t Image32Misc_UncompSize; // uncompressed size (in bytes) of the 32-bit misc textures chunk (4 bytes), should always be 524288 uint32_t Image32Misc_CompSize; // compressed size (in bytes) of the 32-bit misc textures chunk (4 bytes) uint8_t Image32Misc_Compressed[Image32Misc_CompSize]; // zlib-compressed 32-bit misc textures chunk (Image32Misc_CompSize bytes) { tr4_image32 Image32Misc[2]; } uint32_t LevelData_UncompSize; // uncompressed size (in bytes) of the level data chunk (4 bytes) uint32_t LevelData_CompSize; // compressed size (in bytes) of the level data chunk (4 bytes) uint8_t LevelData_Compressed[LevelData_CompSize]; // zlib-compressed level data chunk (LevelData_CompSize bytes) { uint32_t Unused; // supposed to be the level number but TOM2PC didn't allow changing it, always 0 (4 bytes) uint16_t NumRooms; // number of rooms (2 bytes) tr4_room Rooms[NumRooms]; // room list (variable length) uint32_t NumFloorData; // number of floor data uint16_t's to follow (4 bytes) uint16_t FloorData[NumFloorData]; // floor data (NumFloorData * 2 bytes) uint32_t NumMeshData; // number of uint16_t's of mesh data to follow (=Meshes[]) (4 bytes) tr4_mesh Meshes[NumMeshPointers]; // note that NumMeshPointers comes AFTER Meshes[] uint32_t NumMeshPointers; // number of mesh pointers to follow (4 bytes) uint32_t MeshPointers[NumMeshPointers]; // mesh pointer list (NumMeshPointers * 4 bytes) uint32_t NumAnimations; // number of animations to follow (4 bytes) tr4_animation Animations[NumAnimations]; // animation list (NumAnimations * 40 bytes) uint32_t NumStateChanges; // number of state changes to follow (4 bytes) tr_state_change StateChanges[NumStateChanges]; // state-change list (NumStructures * 6 bytes) uint32_t NumAnimDispatches; // number of animation dispatches to follow (4 bytes) tr_anim_dispatch AnimDispatches[NumAnimDispatches]; // animation-dispatch list list (NumAnimDispatches * 8 bytes) uint32_t NumAnimCommands; // number of animation commands to follow (4 bytes) tr_anim_command AnimCommands[NumAnimCommands]; // animation-command list (NumAnimCommands * 2 bytes) uint32_t NumMeshTrees; // number of MeshTrees to follow (4 bytes) tr_meshtree_node MeshTrees[NumMeshTrees]; // MeshTree list (NumMeshTrees * 4 bytes) uint32_t NumFrames; // number of words of frame data to follow (4 bytes) uint16_t Frames[NumFrames]; // frame data (NumFrames * 2 bytes) uint32_t NumModels; // number of models to follow (4 bytes) tr_model Models[NumModels]; // model list (NumModels * 18 bytes) uint32_t NumStaticMeshes; // number of StaticMesh data records to follow (4 bytes) tr_staticmesh StaticMeshes[NumStaticMeshes]; // StaticMesh data (NumStaticMesh * 32 bytes) uint8_t SPR[3]; // S P R (0x53, 0x50, 0x52) uint32_t NumSpriteTextures; // number of sprite textures to follow (4 bytes) tr_sprite_texture SpriteTextures[NumSpriteTextures]; // sprite texture list (NumSpriteTextures * 16 bytes) uint32_t NumSpriteSequences; // number of sprite sequences records to follow (4 bytes) tr_sprite_sequence SpriteSequences[NumSpriteSequences]; // sprite sequence data (NumSpriteSequences * 8 bytes) uint32_t NumCameras; // number of camera data records to follow (4 bytes) tr_camera Cameras[NumCameras]; // camera data (NumCameras * 16 bytes) uint32_t NumFlybyCameras; // number of flyby camera data records to follow (4 bytes) tr4_flyby_camera FlybyCameras[NumFlybyCameras]; // flyby camera data (NumFlybyCameras * 40 bytes) uint32_t NumSoundSources; // number of sound source data records to follow (4 bytes) tr_sound_source SoundSources[NumSoundSources]; // sound source data (NumSoundSources * 16 bytes) uint32_t NumBoxes; // number of box data records to follow (4 bytes) tr2_box Boxes[NumBoxes]; // box data (NumBoxes * 8 bytes) uint32_t NumOverlaps; // number of overlap records to follow (4 bytes) uint16_t Overlaps[NumOverlaps]; // overlap data (NumOverlaps * 2 bytes) int16_t Zones[10*NumBoxes]; // zone data (NumBoxes * 20 bytes) uint32_t NumAnimatedTextures; // number of animated texture records to follow (4 bytes) uint16_t AnimatedTextures[NumAnimatedTextures]; // animated texture data (NumAnimatedTextures * 2 bytes) uint8_t AnimatedTexturesUVCount; uint8_t TEX[3]; // T E X (0x54, 0x45, 0x58) uint32_t NumObjectTextures; // number of object textures to follow (4 bytes) (after AnimatedTextures in TR3) tr4_object_texture ObjectTextures[NumObjectTextures]; // object texture list (NumObjectTextures * 38 bytes) uint32_t NumEntities; // number of entities to follow (4 bytes) tr4_entity Entities[NumEntities]; // entity list (NumEntities * 24 bytes) uint32_t NumAIObjects; // number of AI objects to follow (4 bytes) tr4_ai_object AIObjects[NumAIObjects]; // AI objects list (NumAIObjects * 24 bytes) uint16_t NumDemoData; // number of demo data records to follow (2 bytes) uint8_t DemoData[NumDemoData]; // demo data (NumDemoData bytes) int16_t SoundMap[370]; // sound map (740 bytes) uint32_t NumSoundDetails; // number of sound-detail records to follow (4 bytes) tr3_sound_details SoundDetails[NumSoundDetails]; // sound-detail list (NumSoundDetails * 8 bytes) uint32_t NumSampleIndices; // number of sample indices to follow (4 bytes) + uint32_t SampleIndices[NumSampleIndices]; // sample indices (NumSampleIndices * 4 bytes) uint8_t Separator[6]; // 6 0x00 bytes } uint32_t NumSamples; // number of sound samples (4 bytes) tr4_sample Samples[NumSamples]; // sound samples (this is the last part, so you can simply read until EOF)
What follows is the physical Tomb Raider V .TRC
file layout, byte for byte.
uint32_t Version; // version (4 bytes) uint16_t NumRoomImages; // number of non-bumpmapped room images (2 bytes) uint16_t NumObjImages; // number of object images (2 bytes) uint16_t NumBumpmaps; // number of bumpmaps (2 bytes) uint32_t Image32_UncompSize; // uncompressed size (in bytes) of the 32-bit textures chunk (4 bytes) uint32_t Image32_CompSize; // compressed size (in bytes) of the 32-bit textures chunk (4 bytes) uint8_t Image32_Compressed[Image32_CompSize]; // zlib-compressed 32-bit textures chunk (Image32_CompSize bytes) { tr4_image32 Image32[NumRoomImages + NumObjImages + NumBumpmaps]; } uint32_t Image16_UncompSize; // uncompressed size (in bytes) of the 16-bit textures chunk (4 bytes) uint32_t Image16_CompSize; // compressed size (in bytes) of the 16-bit textures chunk (4 bytes) uint8_t Image16_Compressed[Image32_CompSize]; // zlib-compressed 16-bit textures chunk (Image16_CompSize bytes) { tr_image16 Image16[NumRoomImages + NumObjImages + NumBumpmaps]; } uint32_t Image32Misc_UncompSize; // uncompressed size (in bytes) of the 32-bit misc textures chunk (4 bytes), should always be 786432 uint32_t Image32Misc_CompSize; // compressed size (in bytes) of the 32-bit misc textures chunk (4 bytes) uint8_t Image32Misc_Compressed[Image32Misc_CompSize]; // zlib-compressed 32-bit misc textures chunk (Image32Misc_CompSize bytes) { tr4_image32 Image32Misc[3]; } uint16_t LaraType; uint16_t WeatherType; uint8_t Padding[28]; uint32_t LevelData_UncompSize; // uncompressed size (in bytes) of the level data chunk (4 bytes) uint32_t LevelData_CompSize; // compressed size (in bytes) of the level data chunk, equal to LevelData_UncompSize (4 bytes) // NOT COMPRESSED uint32_t LevelNumber; // supposed to be the level number but TOM2PC didn't allow changing it, always 0 (4 bytes) uint16_t NumRooms; // number of rooms (2 bytes) tr5_room Rooms[NumRooms]; // room list (variable length) uint32_t NumFloorData; // number of floor data uint16_t's to follow (4 bytes) uint16_t FloorData[NumFloorData]; // floor data (NumFloorData * 2 bytes) uint32_t NumMeshData; // number of uint16_t's of mesh data to follow (=Meshes[]) (4 bytes) tr4_mesh Meshes[NumMeshPointers]; // note that NumMeshPointers comes AFTER Meshes[] uint32_t NumMeshPointers; // number of mesh pointers to follow (4 bytes) uint32_t MeshPointers[NumMeshPointers]; // mesh pointer list (NumMeshPointers * 4 bytes) uint32_t NumAnimations; // number of animations to follow (4 bytes) tr4_animation Animations[NumAnimations]; // animation list (NumAnimations * 40 bytes) uint32_t NumStateChanges; // number of state changes to follow (4 bytes) tr_state_change StateChanges[NumStateChanges]; // state-change list (NumStructures * 6 bytes) uint32_t NumAnimDispatches; // number of animation dispatches to follow (4 bytes) tr_anim_dispatch AnimDispatches[NumAnimDispatches]; // animation-dispatch list list (NumAnimDispatches * 8 bytes) uint32_t NumAnimCommands; // number of animation commands to follow (4 bytes) tr_anim_command AnimCommands[NumAnimCommands]; // animation-command list (NumAnimCommands * 2 bytes) uint32_t NumMeshTrees; // number of MeshTrees to follow (4 bytes) tr_meshtree_node MeshTrees[NumMeshTrees]; // MeshTree list (NumMeshTrees * 4 bytes) uint32_t NumFrames; // number of words of frame data to follow (4 bytes) uint16_t Frames[NumFrames]; // frame data (NumFrames * 2 bytes) uint32_t NumModels; // number of models to follow (4 bytes) tr_model Models[NumModels]; // model list (NumModels * 18 bytes) uint32_t NumStaticMeshes; // number of StaticMesh data records to follow (4 bytes) tr_staticmesh StaticMeshes[NumStaticMeshes]; // StaticMesh data (NumStaticMesh * 32 bytes) uint8_t SPR[4]; // S P R \0 (0x53, 0x50, 0x52, 0x00) uint32_t NumSpriteTextures; // number of sprite textures to follow (4 bytes) tr_sprite_texture SpriteTextures[NumSpriteTextures]; // sprite texture list (NumSpriteTextures * 16 bytes) uint32_t NumSpriteSequences; // number of sprite sequences records to follow (4 bytes) tr_sprite_sequence SpriteSequences[NumSpriteSequences]; // sprite sequence data (NumSpriteSequences * 8 bytes) uint32_t NumCameras; // number of camera data records to follow (4 bytes) tr_camera Cameras[NumCameras]; // camera data (NumCameras * 16 bytes) uint32_t NumFlybyCameras; // number of flyby camera data records to follow (4 bytes) tr4_flyby_camera FlybyCameras[NumFlybyCameras]; // flyby camera data (NumFlybyCameras * 40 bytes) uint32_t NumSoundSources; // number of sound source data records to follow (4 bytes) tr_sound_source SoundSources[NumSoundSources]; // sound source data (NumSoundSources * 16 bytes) uint32_t NumBoxes; // number of box data records to follow (4 bytes) tr2_box Boxes[NumBoxes]; // box data (NumBoxes * 8 bytes) uint32_t NumOverlaps; // number of overlap records to follow (4 bytes) uint16_t Overlaps[NumOverlaps]; // overlap data (NumOverlaps * 2 bytes) int16_t Zones[10*NumBoxes]; // zone data (NumBoxes * 20 bytes) uint32_t NumAnimatedTextures; // number of animated texture records to follow (4 bytes) uint16_t AnimatedTextures[NumAnimatedTextures]; // animated texture data (NumAnimatedTextures * 2 bytes) uint8_t AnimatedTexturesUVCount; uint8_t TEX[4]; // T E X \0 (0x54, 0x45, 0x58, 0x00) uint32_t NumObjectTextures; // number of object textures to follow (4 bytes) (after AnimatedTextures in TR3) tr4_object_texture ObjectTextures[NumObjectTextures]; // object texture list (NumObjectTextures * 38 bytes) uint32_t NumEntities; // number of entities to follow (4 bytes) tr4_entity Entities[NumEntities]; // entity list (NumEntities * 24 bytes) uint32_t NumAIObjects; // number of AI objects to follow (4 bytes) tr4_ai_object AIObjects[NumAIObjects]; // AI objects list (NumAIObjects * 24 bytes) uint16_t NumDemoData; // number of demo data records to follow (2 bytes) uint8_t DemoData[NumDemoData]; // demo data (NumDemoData bytes) int16_t SoundMap[450]; // sound map (740 bytes) uint32_t NumSoundDetails; // number of sound-detail records to follow (4 bytes) tr3_sound_details SoundDetails[NumSoundDetails]; // sound-detail list (NumSoundDetails * 8 bytes) uint32_t NumSampleIndices; // number of sample indices to follow (4 bytes) + uint32_t SampleIndices[NumSampleIndices]; // sample indices (NumSampleIndices * 4 bytes) uint8_t Separator[6]; // 6 0xCD bytes uint32_t NumSamples; // number of sound samples (4 bytes) tr4_sample Samples[NumSamples]; // sound samples (this is the last part, so you can simply read until EOF)
uint16_t
Diffuse1/2uint32_t
Unknown1/2uint8_t
s in TR2 and int32_t
's in TR1.int16_t
s in TR2, but 6 int16_t
s in TR1int16_t
s in TR2, but 256 int16_t
s in TR1.WaterScheme
, ReverbInfo
and null filler.uint32_t
s.struct tr4_sample { uint32_t UncompressedSize; uint32_t CompressedSize; char WaveFile[]; // Embedded sample in MS-ADPCM or PCM WAV format. }
LightType
, which specifies light mode or fog bulb mode.LightType
, there is a uint8_t Filler
value of 0xFF
.Intensity
is now uint8_t
.Fade
, there is a set of 4 float values: In
, Out
, Length
and CutOff
.Lighting
.NumSpriteTextures
field is preceeded by the 3 ASCII bytes SPR
.NumColoredRectangles
, ColoredRectangles[]
, NumColoredTriangles
, ColoredTriangles[]
no longer exist in the tr4_mesh structure.NumObjectTextures
field is now preceeded by 4 ASCII bytes \0TEX
uint16_t
values after last texture block specifying Lara type and weather type.uint32_t
values for Level data block are equal (reason below).uint16_t
value.int16_t
s in TR5, but 370 int16_t
s in TR4..TUB
) demos have their palettes moved to between the SpriteSequences and the Cameras.TOMBPC.DAT
, called DEMOPC.DAT
, which appears to have the exact same format as TOMBPC.DAT
.Version
field of TR4 levels is 0x00345254
(“TR4\0
”) for normal game and 0x63345254
(“TR4c
”) for demo.No rearrangements are known for the TR3 demos.
The internal gameflow, which levels come in what order, what item(s) Lara has at the beginning of each level, the filenames of the level and cut-scene files, all the visible text (e.g. “Save Game,” “Rusty Key,” etc.), and various other options are controlled by a script file called TOMBPC.DAT
/TOMBPSX.DAT
. The scripts were compiled using a utility known as GAMEFLOW.EXE
which was distributed by Eidos in the German release of Tomb Raider II Gold. Both TR2 and TR3 use these script files. From both games the format remained unchanged. TR1’s gameflow is hardcoded thus there is no external file controlling this resulting in loss of flexibility.
uint32_t Version; // The Script Version (Always 3 for TR2/3) uint8_t Description[256]; // Null-terminated string describing the script copyright info etc. Not encrypted. uint16_t GameflowSize; // Size in bytes of the game flow data, always 128 bytes int32_t FirstOption; // What to do when the game starts int32_t TitleReplace; // What to do when ExitToTitle is requested and "TitleDisabled" flag is set int32_t OnDeathDemoMode; // What to do when Lara dies during the demo mode int32_t OnDeathInGame; // What to do when Lara dies during the game uint32_t DemoTime; // Time in game ticks (1/30th of a second) to wait before starting a demo int32_t OnDemoInterrupt; // What to do when the demo mode is interrupted int32_t OnDemoEnd; // What to do when the demo mode ends uint8_t Unknown1[36]; // Filler uint16_t NumLevels; // Number of levels in the game, including the training level, not including the title level. uint16_t NumChapterScreens; // Chapter screens (Present in TR2, first used in TR3) uint16_t NumTitles; // Number of title elements (TITLE.TR2 level + the legal/title pictures in *.PCX format) uint16_t NumFMVs; // Number of FMV cutscenes PC - (*.RPL), PSX - (*.STR) uint16_t NumCutscenes; // Number of in-game (engine-rendered) cutscenes (CUT*.TR2) uint16_t NumDemoLevels; // Number of demo levels uint16_t TitleSoundID; // ID of title soundtrack (see below) uint16_t SingleLevel; // If doing only a single level, the level ID (starting at 1). -1 means disabled. uint8_t Unknown2[32]; // Filler uint16_t Flags; // Various flags, see below uint8_t Unknown3[6]; // Filler uint8_t XORKey; // Key used to encrypt/decrypt strings uint8_t LanguageID; // Script Language ID, see below uint16_t SecretSoundID; // ID of soundtrack to play when a secret is found (see below) uint8_t Unknown4[4]; // Filler // If Flags & UseXor true each character (except null-terminator) must be ^ XORKey to decrypt the string. TPCStringArray[NumLevels] LevelStrings; // level name strings TPCStringArray[NumChapterScreens] ChapterScreenStrings; // chapter screen strings TPCStringArray[NumTitles] TitleStrings; // title strings TPCStringArray[NumFMVs] FMVStrings; // FMV path strings TPCStringArray[NumLevels] LevelPathStrings; // level path strings TPCStringArray[NumCutscenes] CutscenePathStrings; // cutscene path strings uint16_t SequenceOffsets[NumLevels + 1]; // Relative offset to sequence info (the +1 is because the first one is the FrontEnd sequence, for when the game starts) uint16_t SequenceNumBytes; // Size of SequenceOffsets in bytes uint16_t[] Sequences[NumLevels + 1]; // Sequence info see explanation below (SIZE is dependant on first opcode) uint16_t DemoLevelIDs[NumDemoLevels]; #if PSX PSXFMVInfo[NumFMVs]; #endif uint16_t NumGameStrings; TPCStringArray[NumGameStrings] GameStrings; #if PSX TPCStringArray[size] PSXStrings; // size is 79 for the TR2 beta, 80 for all other versions #else TPCStringArray[41] PCStrings; #endif TPCStringArray[NumLevels] PuzzleStrings[4]; #if PSX && TR2_BETA TPCStringArray[NumLevels] SecretsStrings[4]; TPCStringArray[NumLevels] SpecialStrings[2]; #endif TPCStringArray[NumLevels] PickupStrings[2]; TPCStringArray[NumLevels] KeyStrings[4];
struct TPCStringArray // (variable length) { uint16_t Offsets[Count]; // List containing for each string an offset in the Data block (Count * 2 bytes) uint16_t TotalSize; // Total size, in bytes (2 bytes) uint8_t Data[TotalSize]; // Strings block, usually encrypted (XOR-ed with XORKey, see above) }
Accent | Code | becomes |
---|---|---|
Acute | )e | é |
Circumflex | (e | ê |
Grave | $e | è |
Diaeresis | ~e | ë |
The following non-ASCII characters are also replaced:
Character | becomes | |
---|---|---|
Eszett (German) | ß | = |
There are also some exceptions (strings that were badly encoded by Core):
String | becomes |
---|---|
Red)marrer un niveau (“e” missing after parenthesis, )e implied) | Redémarrer un niveau |
struct PSXFMVInfo // 8 bytes { uint32_t Start; // Start frame uint32_t End; // End frame };
This specific info is exclusive to TOMBPSX.DAT
.
Bit | Hex | Name | Description |
---|---|---|---|
0 | 0x01 | DemoVersion | Indicates that the game is a demo distribution. |
1 | 0x02 | TitleDisabled | Indicates that the game has no Title Screen. |
2 | 0x04 | CheatModeCheckDisabled | Indicates that the game does not look for the cheat sequence keystrokes and events. |
3 | 0x08 | NoInputTimeout | Indicates that the game waits forever if there is no input (won’t enter demo mode). |
4 | 0x10 | LoadSaveDisabled | Indicates that the game does not allow save games. |
5 | 0x20 | ScreenSizingDisabled | Indicates that the game does not allow screen resizing (with the function keys). |
6 | 0x40 | LockoutOptionRing | Indicates that the user has no access to the Option Ring while playing the game. |
7 | 0x80 | DozyCheatEnabled | Indicates that the game has the DOZY cheat enabled (flag only set in the final build of TR2 on PSX, but has no effect in-game). |
8 | 0x100 | UseXor | Indicates that a cypher byte was used to encrypt the strings in the script file, and is stored in the XorKey field. |
9 | 0x200 | GymEnabled | Is Gym available on title screen. |
10 | 0x400 | SelectAnyLevel | Enables level select when New Game is selected. |
11 | 0x800 | EnableCheatCode | It apparently has no effect on the PC game. |
TR3 only:
Each script has “sequence information”, Opcodes and Operands are all stored as uint16_t
. Sequences contain a set of commands to execute where an additional value (operand) is usually passed as a parameter to the function the command needs to call.
ID | Name | Description | Operand |
---|---|---|---|
0 | Picture | Unused. On PC, crashes if used and file missing. Otherwise, no action. Crashes under TR3 (PC). | Picture ID |
1 | ListStart | Define the start and the end of a list of commands which will be cut short if the user presses a key/button. | |
2 | ListEnd |
||
3 | FMV | Display Full Motion Video. | FMV ID |
4 | Level | Start a playable level. | Level ID |
5 | Cine | Display cut scene sequence. | Cutscene ID |
6 | Complete | Display level-completion statistics panel. | |
7 | Demo | Display demo sequence. | Demo level ID |
8 | JumpToSequence | Jump to another sequence. | Sequence ID |
9 | End | Close script sequence. | |
10 | Track | Play Soundtrack (it precedes opcodes of associated levels). | Track ID |
11 | Sunset | Gradually dim all lights in rooms with LightMode set to sunset (3). Used in Bartoli's Hideout. | |
12 | LoadPic | Show chapter screen on TR2 (PSX only ) and TR3. On TR2 PC, it's hardcoded to do nothing. | Picture ID |
13 | DeadlyWater | Not used anywhere in the game code. Used in Temple of Xian. Maybe not-implemented ancestor of TR3 Death_by_Drowning ? | |
14 | RemoveWeapons | Lara starts the level with no weapons. | |
15 | GameComplete | End of game, show the final statistics and start the credits sequence. | |
16 | CutAngle | Match the North-South orientation of the Room Editor and the North-South orientation of the 3D animated characters from a CAD application. | Horizontal rotation ($\text{angle in degrees} \cdot 65536 / 360$) |
17 | NoFloor | Lara dies when her feet reach the given depth. If falling, 4 to 5 extra blocks are added to Depth. | Depth ($\text{blocks} \times 1024$), relative to where Lara starts the level |
18 | StartInv / Bonus | Give item to lara at level-start (StartInv ) or at all-secrets-found (Bonus ). | Item ID |
19 | StartAnim | Lara starts the level with the given animation. | Animation ID |
20 | Secrets | If zero, the level does not account for secrets. Non-zero value means the level must be accounted for secrets. | |
21 | KillToComplete | Kill all enemies to finish the level. | |
22 | RemoveAmmo | Lara starts the level without ammunition or medi packs. |
The correct way to parse a sequence is to first read a uint16_t
opcode specifying what this command within the sequence does. In reference to the list above, certain commands MUST have an additional uint16_t
read from the sequence data directly after the opcode that’s the pairing operand to this opcode. Not all opcodes have an operand so this must be done correctly. The original games execute each sequence command 1 by 1 until it reaches End
(9), where it then runs the next sequence.
(repeat means give another)
By default, the item is given at level start (StartInv
). Adding 1000 to the item ID means it will be given when all secrets are found (Bonus
).
ID | Tomb Raider 2 | Tomb Raider 3 |
---|---|---|
0 | Pistols | |
1 | Shotgun | |
2 | Automatic pistols | Desert Eagle |
3 | Uzis | |
4 | Harpoon gun | |
5 | M-16 | MP5 |
6 | Grenade launcher | Rocket launcher |
7 | Pistol clip (no effect, infinite by default) | Grenade launcher |
8 | Shotgun-shell box (adds 2 shells) | Pistol clip (no effect, infinite by default) |
9 | Automatic-pistol clip (adds 2 shells) | Shotgun-shell box (adds 2 shells) |
10 | Uzi clip (adds 2 shells) | Desert eagle clip (adds 5 shells) |
11 | Harpoon bundle (adds 2 harpoons) | Uzi clip (adds 2 shells) |
12 | M-16 clip (add 2 shells) | Harpoon bundle (adds 2 harpoons) |
13 | Grenade pack (adds 1 grenade) | MP5 clip (add 2 shells) |
14 | Flare box (adds 1 flare) | Rocket pack (adds 1 rocket) |
15 | Small medipack (adds 1 pack) | Grenade pack (adds 1 grenade) |
16 | Big medipack (adds 1 pack) | Flare box (adds 1 flare) |
17 | Pickup 1 | Small medipack (adds 1 pack) |
18 | Pickup 2 | Big medipack (adds 1 pack) |
19 | Puzzle 1 | Pickup 1 |
20 | Puzzle 2 | Pickup 2 |
21 | Puzzle 3 | Puzzle 1 |
22 | Puzzle 4 | Puzzle 2 |
23 | Key 1 | Puzzle 3 |
24 | Key 2 | Puzzle 4 |
25 | Key 3 | Key 1 |
26 | Key 4 | Key 2 |
27 | / | Key 3 |
28 | / | Key 4 |
29 | / | Save crystal |
house.TR2
)jungle.TR2
)temple.TR2
)quadchas.TR2
)tonyboss.TR2
)shore.TR2
)crash.TR2
)rapids.TR2
)triboss.TR2
)roofs.TR2
)sewer.TR2
)tower.TR2
)office.TR2
)nevada.TR2
)compound.TR2
)area51.TR2
)antarc.TR2
)mines.TR2
)city.TR2
)chamber.TR2
)stpaul.TR2
)
FirstOption
, TitleReplace
, OnDeathDemoMode
, OnDeathInGame
, OnDemoInterrupt
and OnDemoEnd
can also be setup to perform specific actions. For example, OnDeathInGame
will be set to “0x500” which loads the title screen when Lara dies in-game.
Name | Code | Operand | Description |
---|---|---|---|
Level | 0x00000000 | Level | Load specified level (0 means Lara's Home (or do nothing, if in a demo), 1 means first level (Great Wall or Jungle), 2 means second level) |
SavedGame | 0x00000100 | Number between in [0, 15] | Load specified savegame file (load file savegame.N where N is the operand) |
Cutscene | 0x00000200 | Cutscene ID | Play specified cutscene |
FMV | 0x00000300 | FMV ID | Play specified FMV |
Demo | 0x00000400 | None | Load a random demo level |
ExitToTitle | 0x00000500 | Exit to Title Screen (or do action specified in TitleReplace if TitleDisabled flag is set) |
|
ExitGame | 0x00000700 | Exit the game | |
(anything else) | Exit the game |
Commands with operand are used like this: if you want to load the 2nd level, then you are going to do 0x000
(Level) + 2 which gives 0x003
.
0xFFFFFFFF
is often used by Core in fields to denotate that it should not happen. For example, in the standard (not demo)
games' scripts, the TitleReplace
field contains the value 0xFFFFFFFF
(which, as mentioned before, will crash-exit the game quietly) because this field should never be used, as the TitleDisabled
flag should never be set in standard games.
In this chapter we will describe full gameflow script specification for TR4/TR5 script file (usually called SCRIPT.DAT
) and language file, which contains all the strings used in game for specific language (e.g., ENGLISH.DAT
, FRENCH.DAT
, and so on).
The script is divided into several blocks (or headers), some of them are global (applicable to whole game instance), and some are per-level only.
This header contains general information not specific to particular level.
struct tr4_script_header // 9 bytes { uint8_t Options; uint8_t Filler[3]; // Unused uint32_t InputTimeout; uint8_t SecurityTag; }
Options
is a set of bit flags with several global game settings (name of the settings directly borrowed from original text scripts distributed with TRLE):
Hex | Bit | Description |
---|---|---|
0x0001 | 0 | CheatEnabled – Enables debug fly mode activated by typing DOZY ingame. |
0x0002 | 1 | LoadSaveEnabled – When this bit is not set, load and save features are disabled. This option was used for demo versions. |
0x0004 | 2 | TitleEnabled – Specifies if title screen should be displayed or not. If not set, game will start right away after user has launched an application. |
0x0008 | 3 | PlayAnyLevel – Gives an access to any level from the title screen. |
0x0070 | 4 | Language – Chooses which loading picture (US, GR, FR or UK) is showed |
5 | ||
6 | ||
0x0080 | 7 | DemoDisc – Unknown feature, probably related to game versions deployed on promotional CDs. |
InputTimeout
: in early TR4 demos (for example, version dated September 15, 1999) this parameter specified time interval, after which game will engage pre-recorded rolling demo, in case there was no user input. This feature became useless in final version.
SecurityTag
parameter meant to be a special “key” value used to encrypt script data. Encryption is done with simple XOR operation against the data. However, this value was never used, and instead, hardcoded one was specified. This matter will be discussed later.
This section defines platform-specific information, such as file extensions used in PC an PlayStation versions of the game. All the mentioned strings are null-terminated.
struct tr4_script_levelheader { uint8_t NumTotalLevels; uint8_t NumFilenames; uint8_t Filler; uint16_t LevelpathStringLen; uint16_t LevelBlockLen; uint8_t PSXLevelString [5]; // typically ".PSX" uint8_t PSXFMVString [5]; // typically ".FMV" uint8_t PSXCutString [5]; // typically ".CUT" uint8_t Filler [5]; // Possibly for some additional extension type? uint8_t PCLevelString [5]; // typically ".TR4" uint8_t PCFMVString [5]; // typically ".BIK" uint8_t PCCutString [5]; // typically ".TR4" uint8_t Filler [5]; }
NumTotalLevels
is an amount of levels included in script. Title flyby is also counted.
LevelpathStringLen
is a sum of lengths of all level path strings, including 0x00
s (empty ones).
LevelBlockLen
is a sum of lengths of each level script data length.
struct tr4_script_levellisting { uint16_t OffsetsToLevelpathString[NumTotalLevels]; uint8_t LevelpathStringBlock [LevelpathStringLen]; uint16_t OffsetsToLevelData [NumTotalLevels]; }
Note that the offsets in the offset table themselves are not relative to the file address 0
. The level-path offsets are relative to the first path string’s starting byte address (56 + NumTotalLevels * 2)
, while the level-data offsets are relative to the first level data’s starting byte address (56 +
NumTotalLevels * 2 + LevelpathStringLen + NumTotalLevels * 2)
.
It is also worth noting that the level-path strings in SCRIPT.DAT
are ordered the same way they were ordered in corresponding [Level]
blocks in uncompiled SCRIPT.TXT. For example, if the first [Level] in SCRIPT.TXT
defines Level=DATA\TEST1,101
and the second Level=DATA\TEST2,101
— then there will be 2 level-paths in SCRIPT.DAT
, in the order such as this: DATA\\TEST1.DATA\\TEST2;
where .
is the null-terminator (0x00
) byte.
To get to a certain level’s path within SCRIPT.DAT
knowing only its number, just look-up at OffsetsToLevelpathString[LevelNum]
and go to that offset (remember, it is not relative to file address 0
!).
Inside the level block, each level stores its own data describing certain parameters, such as level name, puzzle item names, load camera position, default background ambience soundtrack, and so on (the title level is no exception!).
While in SCRIPT.TXT
each parameter was given its own line and position within the file itself, in SCRIPT.DAT
this is not the case. Rather, bitfields are used for bool options (enabled/disabled; such as Lightning option) and the rest of the usually multi-byte data uses an opcode data structure.
That is, preceding a certain type of data you usually find a byte. That is the opcode byte — depending on its value, it can be determined what kind and how many arguments follow that need parsing. For example, chunk 0x81
indicates the level description opcode; with that info, the parser knows that 4 arguments follows: the string index, etc. This structure is somewhat akin to the AnimCommands structure of level files (see description above). The chunk order does matter; the original tomb4.exe
binary seems to crash if something is not ordered the way it should be.
The title screen is special in that it uses the 0x82
opcode the indicate the level-name and audio track information and it, naturally, lacks the string index integer as the title level has no name associated with it.
struct tr4_script_leveldata { uint8_t LevelData [LevelDataLen]; }
LevelData
is all of the level’s data continuously stored in memory. Number of level data sections is equal to overall amount of levels in game, and overall size of all level data sections comprise Level Block.
To get to a certain level’s data section, follow that particular level’s offset from inside the offset table you loaded (described above). The data sections themselves are ordered the very same way levels were ordered in SCRIPT.TXT
. For more info on the types of all available TR4 chunks and how to parse them, see the Script Opcodes section.
After the level block follows a simple array of ASCII strings which define all the language files the game can choose from. There are, however, no offset tables for this one, so one must simply read until a null-byte is reached, and then take that as the string and repeat onwards until EOF. Therefore, the last byte of SCRIPT.DAT
must always be the null-terminator (0x00
).
Here is a list of all available TR4 opcodes, their meaning and their corresponding arguments (order of arguments matters!):
It is important to note that the LoadCamera
opcode has been removed in TR5, which means that all the other opcodes (>= 0x92) are shifted by 1).
TR4 | TR5 | Opcode | Arguments | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0x80 | FMV | uint8_t: 4 least significant bits represent the FMV index; 4 most significant bits (y) represent the FMV trigger bitfield as in y=1↔bit 8 set |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x81 | Level | uint8_t stringIndex | uint16_t levelOptions | uint8_t pathIndex | uint8_t audio |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x82 | [Title] Level | uint8_t pathIndex | uint16_t titleOptions | uint8_t audio |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x83 | LEVEL_DATA_END | None – this opcode appears at the end of every level (incl. title) block | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x84 | Cut | uint8_t cutIndex |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x85 | ResidentCut1 | uint8_t cutIndex |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x86 | ResidentCut2 |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x87 | ResidentCut3 |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x88 | ResidentCut4 |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x89 | Layer1 | uint8_t red | uint8_t green | uint8_t blue | int8_t speed |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x8A | Layer2 |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x8B | UVrotate | int8_t speed |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x8C | Legend | uint8_t stringIndex |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x8D | LensFlare | uint16_t yClicks | bit16 zClicks | uint16_t xClicks | uint8_t red | uint8_t green | uint8_t blue |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x8E | Mirror | uint8_t room | int32_t xAxis |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x8F | Fog | uint8_t red | uint8_t green | uint8_t blue |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x90 | AnimatingMIP | uint8_t: 4 least significant bits represent animatingObjectIndex - 1; 4 most significant bits represent the distance |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x91 | XXX | LoadCamera | int32_t srcX | int32_t srcY | int32_t srcZ | int32_t targX | int32_t targY | int32_t targZ | uint8_t room |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x92 | 0x91 | ResetHUB | uint8_t levelIndex |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x93 | 0x92 | KEY_ITEM1 | uint16_t stringIndex | uint16_t height | uint16_t size | uint16_t yAngle | uint16_t zAngle | uint16_t xAngle | uint16_t flags |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x94 | 0x93 | KEY_ITEM2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x95 | 0x94 | KEY_ITEM3 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x96 | 0x95 | KEY_ITEM4 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x97 | 0x96 | KEY_ITEM5 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x98 | 0x97 | KEY_ITEM6 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x99 | 0x98 | KEY_ITEM7 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x9A | 0x99 | KEY_ITEM8 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x9B | 0x9A | KEY_ITEM9 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x9C | 0x9B | KEY_ITEM10 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x9D | 0x9C | KEY_ITEM11 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x9E | 0x9D | KEY_ITEM12 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0x9F | 0x9E | PUZZLE_ITEM1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xA0 | 0x9F | PUZZLE_ITEM2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xA1 | 0xA0 | PUZZLE_ITEM3 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xA2 | 0xA1 | PUZZLE_ITEM4 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xA3 | 0xA2 | PUZZLE_ITEM5 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xA4 | 0xA3 | PUZZLE_ITEM6 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xA5 | 0xA4 | PUZZLE_ITEM7 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xA6 | 0xA5 | PUZZLE_ITEM8 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xA7 | 0xA6 | PUZZLE_ITEM9 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xA8 | 0xA7 | PUZZLE_ITEM10 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xA9 | 0xA8 | PUZZLE_ITEM11 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xAA | 0xA9 | PUZZLE_ITEM12 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xAB | 0xAA | PICKUP_ITEM1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xAC | 0xAB | PICKUP_ITEM2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xAD | 0xAC | PICKUP_ITEM3 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xAE | 0xAD | PICKUP_ITEM4 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xAF | 0xAE | EXAMINE1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xB0 | 0xAF | EXAMINE2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xB1 | 0xB0 | EXAMINE3 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xB2 | 0xB1 | KEY_ITEM1_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xB3 | 0xB2 | KEY_ITEM1_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xB4 | 0xB3 | KEY_ITEM2_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xB5 | 0xB4 | KEY_ITEM2_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xB6 | 0xB5 | KEY_ITEM3_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xB7 | 0xB6 | KEY_ITEM3_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xB8 | 0xB7 | KEY_ITEM4_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xB9 | 0xB8 | KEY_ITEM4_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xBA | 0xB9 | KEY_ITEM5_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xBB | 0xBA | KEY_ITEM5_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xBC | 0xBB | KEY_ITEM6_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xBD | 0xBC | KEY_ITEM6_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xBE | 0xBD | KEY_ITEM7_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xBF | 0xBE | KEY_ITEM7_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xC0 | 0xBF | KEY_ITEM8_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xC1 | 0xC0 | KEY_ITEM8_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xC2 | 0xC1 | PUZZLE_ITEM1_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xC3 | 0xC2 | PUZZLE_ITEM1_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xC4 | 0xC3 | PUZZLE_ITEM2_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xC5 | 0xC4 | PUZZLE_ITEM2_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xC6 | 0xC5 | PUZZLE_ITEM3_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xC7 | 0xC6 | PUZZLE_ITEM3_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xC8 | 0xC7 | PUZZLE_ITEM4_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xC9 | 0xC8 | PUZZLE_ITEM4_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xCA | 0xC9 | PUZZLE_ITEM5_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xCB | 0xCA | PUZZLE_ITEM5_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xCC | 0xCB | PUZZLE_ITEM6_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xCD | 0xCC | PUZZLE_ITEM6_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xCE | 0xCD | PUZZLE_ITEM7_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xCF | 0xCE | PUZZLE_ITEM7_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xD0 | 0xCF | PUZZLE_ITEM8_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xD1 | 0xD0 | PUZZLE_ITEM8_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xD2 | 0xD1 | PICKUP_ITEM1_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xD3 | 0xD2 | PICKUP_ITEM1_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xD4 | 0xD3 | PICKUP_ITEM2_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xD5 | 0xD4 | PICKUP_ITEM2_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xD6 | 0xD5 | PICKUP_ITEM3_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xD7 | 0xD6 | PICKUP_ITEM3_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xD8 | 0xD7 | PICKUP_ITEM4_COMBO1 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xD9 | 0xD8 | PICKUP_ITEM4_COMBO2 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
XXX | 0xD9 | GiveItemAtStartup | uint16_t itemNumber |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
0xDA | LoseItemAtStartup | uint16_t itemNumber |
The uint16_t
values levelOptions
and titleOptions
are actually bit fields containing several boolean options, and are laid out as follows (per-bit description):
0x0001
) — YoungLara0x0002
) — Weather0x0004
) — Horizon0x0008
) — Layer1 used0x0010
) — Layer2 used0x0020
) — Starfield0x0040
) — Lightning0x0080
) — Train0x0100
) — Pulse0x0200
) — ColAddHorizon0x0400
) — ResetHUB used0x0800
) — LensFlare used0x1000
) — Timer0x2000
) — Mirror used0x4000
) — Remove Horus amulet from inventory0x8000
) — NoLevel
In contrary to TR2 and TR3, TR4 uses a more sophisticated language-handling scheme. Instead of storing the strings in SCRIPT.DAT
for every different language, TR4 splits the string definition ({LANGUAGE}.DAT
) and script definition (SCRIPT.DAT
) data into the two mentioned files. This allows for smaller files, finer grain of selectivity and easy localization.
This means that, within SCRIPT.DAT
, strings are always given as string indices, i.e. numbers that correspond to the array positions of the corresponding strings within {LANGUAGE}.DAT
, where {LANGUAGE}
can be any supported language filename.
From these files, the game will choose the first one that is available and use that as the string resource. See below for details on string selection.
The number of supported language files depends on what was defined in SCRIPT.TXT
, in the [Language] section. Also, the priority of loading is specified there (the first number before the comma). For example, if we have defined:
[Language] File= 0,ENGLISH.TXT File= 1,FRENCH.TXT File= 2,GERMAN.TXT File= 3,ITALIAN.TXT File= 4,SPANISH.TXT File= 5,US.TXT
…that would mean that the game will first look for ENGLISH.DAT
for loading. If that’s not present, it will look for FRENCH.DAT
. If not, it’ll look for GERMAN.DAT
, and so on. If none of the files are present, the game will crash. In SCRIPT.DAT
, these numbers reflect on the order of file name strings: in the above situation, the language file listing block at the end of SCRIPT.DAT
would look like this (highest→lowest priority):
ENGLISH.DAT FRENCH.DAT GERMAN.DAT ITALIAN DAT SPANISH.DAT US.DAT.
…where the splitting space between filenames specifies the null-terminator (0x00) byte.
The header of the language file follows this structure:
struct tr4_lang_header { uint16_t NumGenericStrings; uint16_t NumPSXStrings; uint16_t NumPCStrings; uint16_t GenericStringsLen; // including the null-terminator bytes uint16_t PSXStringsLen; // including the null-terminator bytes uint16_t PCStringsLen; // including the null-terminator bytes uint16_t StringOffsetTable[]; }
StringOffsetTable
is a table holding offsets which point to corresponding strings. Therefore, its size is NumGenericStrings + NumPSXStrings + NumPCStrings
.
In order to get an absolute offset of a string whose relative offset you retrieved from the offset table, do the following:
absoluteOffset = relativeOffset + sizeof(tr4_lang_header)
sizeof(tr4_lang_header)
depends, of course, on the number of strings in each group. Therefore, the header size is sizeof(uint16_t) * 6 + sizeof(OffsetTable)
.
In the usual TR4 situation, there are typically 359 strings (that is, usually NumTotalStrings = NumGenericStrings + NumPSXStrings + NumPCStrings = 359) defined. This, however, is not a limit nor rule of any kind.
All the strings defined within {LANGUAGE}.DAT
files are ASCII null-terminated strings. Every character (byte) contained in such a string is XOR-ed with byte 0xA5
(as mentioned above, it is done regardless of what byte was specified in SCRIPT.TXT
under the Security option).
After the above defined header section goes an array of strings, in a predefined order: Generic → PSX → PC.
The length of this array of total (NumTotalStrings) strings is therefore TotalStringsLen = GenericStringsLen + PSXStringsLen + PCStringsLen.
Hence the string array has the following format:
struct tr4_lang_stringdata { string_entry Strings[NumTotalStrings]; }
where string_entry
is simply a char
array, whose length depends on the corresponding string’s length. That can be calculated by subtracting the next string’s by the current string’s offset.
Original reverse-engineering work by sapper
The CUTSEQ.BIN
file is a file containing information about all the engine-rendered cutscenes (as opposite to FMVs, which are pre-rendered videos). In TR4, this file is compressed and contained in a PAK file, and the resulting file is called CUTSEQ.PAK
. In TR5, like many other things (e.g. level files), the file is not compressed, and is called CUTSEQ.BIN
.
struct Cutseq { uint8_t header[8]; // "(C) DEL!", for Derek Leigh-Gilchrist CutsceneHeader cutscenes[N]; // N = 30 for TR4, 44 for TR5, 4 for Times Demo uint8_t padding[]; // Empty space between header and data uint8_t cutsceneData[]; } struct CutsceneHeader // 8 bytes { uint32_t offset; // Offset relative to start of file uint32_t size; // Size in bytes } struct CutsceneData { uint16_t numActors; // Actor 1 is always Lara (slot ID 0) uint16_t numFrames; int32_t originX; // Origin coordinates are in TR world coordinates int32_t originY; // Negative Y is up int32_t originZ; int32_t audioTrackIndex; // -1 means no audio track uint32_t cameraDataOffset; ActorSlotHeader actors[numActors]; CameraData cameraData; uint8_t padding[]; uint8_t actorData[]; uint8_t padding[]; } struct ActorSlotHeader // 8 bytes { uint32_t dataOffset; uint16_t slotNumber; // TR model slot ID number uint16_t numNodes; // Same as number of meshes in model } struct CameraData { PositionHeader targetHeader; PositionHeader cameraHeader; PackedCoord targetPosition; PackedCoord cameraPosition; } struct ActorData { MeshHeader meshes[NumNodes]; MeshData meshData[NumNodes]; } struct MeshData { PackedCoord positionData; PackedCoord rotationData; } struct PositionHeader // 14 bytes { int16_t startX; int16_t startY; int16_t startZ; uint16_t axisBitsizes; // X = bits 14-11, Y = 9-6, Z = 4-1 uint16_t numValuesX; uint16_t numValuesY; uint16_t numValuesZ; } struct RotationHeader // 14 bytes { // 1024 = 360 degrees int16_t startX; int16_t startY; int16_t startZ; uint16_t axisBitsizes; // X = bits 14-11, Y = 9-6, Z = 4-1 uint16_t numValuesX; uint16_t numValuesY; uint16_t numValuesZ; } struct PackedCoord // (variable length) { dynamic xAxis; // todo: explain better dynamic yAxis; // core design, why did you make this dynamic zAxis; }
In TR4 CUTSEQ.PAK, DEL (Derek Leigh-Gilchrist) left a hidden message in the first Padding section:
Cutseq.JIZ , Compiled by Del using the one and only 'ASMLE.EXE' Ok, I`ve got about 1.5k of padding here, so enjoy my ramblings... Keep your greasy mits off my packed data... Greets to... Alex,Damon,Rich,Charlie,Jon,Dan,Dude,Martin,Jens,DaveS,DaveM,ZeoGrad and all the usual... Tombraider IV Delta-Packed Animation Data (C) 1999 Core Design. Sector padded for hotness... Format: dc.w num_actors dc.w num_frames dc.l orgx,orgy,orgz dc.l audio_track dc.l packed_camera_data_offset dc.l packed_actor_data_offset dc.w object_slot dc.w num_nodes nice eh? hack away my friends... NUDE CHEAT ALERT... NUDE CHEAT ALERT... maybe... EMAIL: del@nlights.demon.co.uk OR del@core-design.com Developer Credits: Coding: Del,Gibby,Chris,Rich & Tom Delta Compressor: MJ Animation: Jerr Art: Jibber,Pete,Phil,Andy,Rich,Jamie Sound: Pete FMV: Pete,Dave and some others... Thanks to... PsyQ,SCEE,MartinJ and the GNU people... Don`t forget, ** PC-Engine RULES ** BTW people, 30% of the entire game is MIPS. The rest is 'C' , but luckily GNU isn`t as dry as it used to be... Some decent(ish) links: http://www.nlights.demon.co.uk http://www.core-design.com http://www.hu6280.com http://www.geeknews.com http://www.hotmail.com http://www.hitbox.com http://www.tombraider.com http://www.ign64.com http://www.rareware.com http://www.eidos.com Special greet to my baby girl Abigail, and my Wife(?) Caroline... See you in TR5.... bwhahahah
TR5 CUTSEQ.BIN also contains a message:
'cutseq.asm' Compiled by Del - 18:08:53 Thursday 26th of October 2000
A limit of “None” means that the field is only limited by its size (8-bit, 16-bit, etc.)
Any other limit means that the engine purposely checks that the value is less or equal than a hard-coded value.
The limits are inclusive.
A greyed empty cell:
means that the field doesn't exist in that engine version.
These numbers are from the PC version of the games. The PSX versions may have different limits. TODO.
Field | TR1 | TR2 | TR3 | TR4 | TR5 |
---|---|---|---|---|---|
NumImages | None | ||||
NumRoomImages | None | ||||
NumObjImages | None | ||||
NumBumpmaps | None | ||||
NumRooms | 1024 | ||||
NumFloorData | None | ||||
NumMeshData | None | ||||
NumMeshPointers | None | ||||
NumAnimations | None | ||||
NumStateChanges | None | ||||
NumAnimDispatches | None | ||||
NumAnimCommands | None | ||||
NumMeshTrees | None | ||||
NumFrames | None | ||||
NumModels | None | ||||
NumStaticMeshes | None | ||||
NumObjectTextures | 2048 | 4096 | None | ||
NumSpriteTextures | None | ||||
NumSpriteSequences | None | ||||
NumCameras | None | ||||
NumFlybyCameras | None | ||||
NumSoundSources | None | ||||
NumBoxes | None | ||||
NumOverlaps | None | ||||
NumAnimatedTextures | None | ||||
AnimatedTexturesUVCount | None | ||||
NumEntities | 256 | None | |||
NumAIObjects | None | ||||
NumCinematicFrames | None | ||||
NumDemoData | None | ||||
NumSoundDetails | None | ||||
NumSamples | None | None | |||
NumSampleIndices | None |
The mobile (Android, iOS) version, made by Realtech VR, uses some structures. Those were reverse-engineered by Gh0stBlade.
struct AWBFile { uint32_t unknown0; uint32_t numFiles; uint32_t unknown1; uint32_t unknown2; AWBEntry entries[numFiles]; }
struct AWBEntry { uint32_t uncompressedSize; uint32_t compressedSize; uint32_t filePathLength; uint8_t filePath[filePathLength]; }
TR4 and TR5 use a file format with the extension PAK
for certain files (mostly pictures). This file format is actually a container for raw binary content, compressed using zlib.
struct pak_file { uint32_t UncompressedSize; // Uncompressed size of data (4 bytes) uint8_t CompressedData[]; // zlib-compressed data (read until EOF) }
UncompressedSize
field is used by the game to allocate the buffer for uncompressed data. Do not put a wrong value here, or it’ll result in unexpected and wrong behaviour.
uint8_t signature[8]; // "PROJFILE" uint32_t version; // Version of RoomEdit (49/0x31 for standard TRLE, 51/0x33 for leaked TR5 RoomEdit) uint32_t numRooms; PRJ_Room rooms[numRooms]; // variable size uint32_t numObjects; uint32_t maxObjects; uint32_t unusedObjects[2000]; uint32_t numLights; uint32_t unusedLights[768]; uint32_t numTriggers; uint32_t unusedTriggers[512]; uint8_t textureFile[]; // ASCII-encoded string, terminated by a space (0x20) <-- space NOT included in the string, just skip it and continue reading #if TextureFile != "NA" uint32_t numTextInfo; PRJ_TextInfo textInfo[numTextInfo]; #endif uint8_t objectFile[]; // ASCII string terminated by a non-included space, as above with textureFile uint32_t numObjectData; PRJ_ObjectData objectData[numObjectData]; uint32_t numAnimTextures; uint32_t unusedAnimTextures[40]; uint32_t animTextures[256]; PRJ_AnimText animRanges[40]; uint8_t terrain[256]; uint8_t bump[256];
Terrain
items can be one of the following values:
Bump
items can be one of the following values:
not in order, todo
struct PRJ_TextInfo // 8 bytes { uint8_t x; uint8_t y; uint16_t atlas; uint8_t flipX; uint8_t xSize; uint8_t flipY; uint8_t ySize; }
x
– The pixel offset of the left side of the texture within the TGA. This only permits the texture file to be 256 pixels wide at most. For full atlases, it must be a multiple of 64 (0, 64, 128, 192). For partial atlases, it may be any multiple of 16.y
– The pixel offset of the top of the texture within the TGA. The maximum height allowed for a TGA file is 4096 (for width of 256). Since 512 pixel wide TGA files are converted to 256, that makes 4096 the true maximum height. This offset can be at most 4032 for full atlases, or 4080 for partial atlases. For full atlases, it must be a multiple of 64. For partial atlases, it may be any multiple of 16.xSize
– This is the pixel offset of the rightmost pixel within the atlas. For full atlases, it must be 63. For partial atlases it must be 15, 31, 47, or 63. It is basically the pixel width of the texture minus 1. It cannot be set so that the texture would cross an atlas boundary.ySize
– This is the pixel offset of the bottom pixel within the atlas. For full atlases, it must be 63. For partial atlases it must be 15, 31, 47, or 63. It is basically the pixel height of the texture minus 1. It cannot be set so that the texture would cross an atlas boundary.struct PRJ_AnimText // 12 bytes { uint32_t defined; // 0 = animation range not used, 1 = being used uint32_t firstAtlas; // atlas number of the first atlas in the animation range uint32_t lastAtlas; // atlas number of the last atlas in the animation range }
struct PRJ_Color4 // 4 bytes { uint32_t red; uint32_t green; uint32_t blue; uint32_t alpha; }
struct PRJ_Camera // 40 bytes { uint16_t xPos; uint16_t zPos; int32_t unknown1; int32_t yPos; int8_t fov; int8_t camID; int32_t timer; int32_t worldZpos; int32_t worldYpos; int32_t worldXpos; int16_t xRot; int16_t yRot; int16_t zRot; // roll int16_t speed; // speed * 655 int16_t flags; // the buttons 0-15 in the camera settings }
struct PRJ_Sink { int16_t xPos; int16_t zPos; int16_t xSize; int16_t zSize; uint16_t yPos; uint16_t room; uint16_t slot; uint16_t timer; PRJObjectOrientation orientation; int32_t worldZpos; int32_t worldYpos; int32_t worldXpos; }
The PSX versions of the game use different formats than the PC ones. Although the level files and the script files tend to stay consistent between platforms (including Dreamcast and others), some files are only present on console.
This page describes the format used by classic TRs for the savegame.X files. Although many savegame editors have been released for all TR versions, almost no public documentation about the format itself is available.