Skip to content

RTL(X) & RTC(X) Format

Rise of the Triad (ROTT) uses two file extensions for level data, .rtl and .rtc. .rtc indicates that the file is for Comm-bat (multiplayer) play only and does not contain any enemies or exits. .rtl indicates the file can can be used for both Comm-bat and standard game levels. In Comm-bat, the enemies in RTL maps in standard play are not present during Comm-bat games and the exit and entrance arches behave like teleporters. Other than these differences, the two files are alike.

The RTL/RTC file format changed with the release of ROTT version 1.1. Since the shareware version of ROTT cannot use alternate levels, this should not be a problem for map designers. The new format is much more formal. If any changes are made in the format in the future, the first 8 bytes of the file will inform you if it is compatible with your editor/viewer.

The RTL/RTC file is broken into three sections: Version info, Header block, and Data block.

RTL/RTC Version Info

This 8 byte block of data indicates what type of file it is and which version of the RTL/RTC file format it is.

Offset Size Type Description
0 4 string Magic Number / Format Signature
4 4 u32le Version Number

Length: 8 bytes

Magic Number / Format Signature:

This is used to indicate what type of levels are contained within the file.

Version Description
RTL\0 Levels w/ Enemies
RTC\0 Levels w/o Enemies (For COMM-BAT)
RTR\0 Generated with RANDROTT
RXL\0 Levels w/ Enemies (Ludicrous Edition)
RXC\0 Levels w/o Enemies (For COMM-BAT) [Ludicrous Edition]

Version Number

This is the map format version, NOT the ROTT version.

Version Description
0x0101 Version 1.1
0x0200 Version 2.0 (Ludicrous Edition)

Extended Header

In the extended version of the format created for LE, there are two additional values as part of the header.

Offset Size Type Description
8 8 u64le Offset to Directory
16 8 u64le Entry count

Length: 32 bytes (including magic number/versioning)

Extended Lumps

Each lump is defined by the following format, starting at the offset for the directory.

Offset Size Type Description
0 16 string Lump name (null-terminated, padded)
16 8 u64le Absolute offset to lump data
24 8 u64le Size of lump data in bytes

Length: 32 bytes

Lump types

Name Description
MAPSET Map headers (offsets are still absolute from the entire file)
MAPINFO JSON metadata

Map Header

The map header block contains an array of 100 structures with the following format:

Offset Size Type Explanation
0 4 bool32 Used Flag
4 4 u32le CRC
8 4 u32le RLEW Tag
12 4 u32le Map Specials
16 4 u32le Wall plane offset
20 4 u32le Sprite plane offset
24 4 u32le Info plane offset
28 4 u32le Wall plane length
32 4 u32le Sprite plane length
36 4 u32le Info plane length
40 24 string Level name (22 byte limit)

Length: 64 bytes

Used Flag

This is non-zero if a map exists at this position.

CRC

This value is used to determine if all the players in a multiplayer game are using the same maps. You can use any method you like to calculate this value.

RLEW Tag

This is the run-length encoding tag used for compressing and decompressing the map data. The use of this will be described below (RLEW is an id-specific version of RLE based around 16-bit words).

Map Specials

This is used for flags that describe special conditions for the level. Currently only one flag is used.

Bit # Description
0 If set, all the pushwalls will be activated in Comm-bat mode. This is done in case there are player start locations within hidden areas and the player would be trapped until a pushwall was activated.

Offsets

The Wall, Sprite, and Info plane offsets are each absolute offsets of the data from the beginning of the file.

Lengths

The Wall, Sprite, and Info plane lengths are each lengths of the run-compressed data.

Level Name

This is a null-terminated string containing the name of the level. Although there is 24 bytes available, level names should be at most 22 bytes long.

RTL/RTC Data Block

When expanded, ROTT maps contain 3 planes of 128 by 128 word sized data. They are stored in the RTL/RTC files as 3 blocks of run-length encoded data. The procedure for decompressing them is as follows:

  1. Allocate 128 * 128 words of memory (32768 bytes).

  2. Read one word from compressed block.

  3. If word is equal to RLEWTag, then the next two words are a compressed run of data. The first word is the number of words to write. The second word is the value to write map. If word was not equal to RLEWTag, then simply write that word to the map.

  4. Go back to 2 until all data is written.

Example in C
/*---------------------------------------------------------------------
   Function: RLEW_Expand

   Run-length encoded word decompression.
---------------------------------------------------------------------*/

void RLEW_Expand(u16le *source, u16le *dest, i32le length, u16le rlewtag) {
   u16le value;
   u16le count;
   u16le *end;

   end = dest + length;

   while (dest < end) {
      value = *source;
      source++;

      if (value != rlewtag) {
         //
         // uncompressed data
         //
         *dest = value;
         dest++;
         } else {
         //
         // compressed string
         //
         count = *source;
         source++;

         value = *source;
         source++;

         //
         // expand the data
         //
         while (count > 0) {
            *dest = value;
            dest++;
            count--;
         }
      }
   }
}
Example in Odin

From wodin.

@(require_results)
read_plane :: proc(tag: u32, data: []byte) -> (plane: Plane) {
   plane_data := make([]u16, len(data) / 2)
   for &value, i in plane_data {
      value = slice.to_type(data[i * 2:][:2], u16)
   }

   data_offset: uint = 0
   plane_offset: uint = 0

   for data_offset < len(plane_data) {
      keyword := plane_data[data_offset]
      if u32(keyword) == tag {
         length := plane_data[data_offset + 1]
         value := plane_data[data_offset + 2]

         for i in 0..<length {
            plane[plane_offset + uint(i)] = value
         }

         data_offset += 3
         plane_offset += uint(length)
      } else {
         plane[plane_offset] = plane_data[data_offset]
         data_offset += 1
         plane_offset += 1
      }
   }
   delete(plane_data)
   return
}

And some examples for loading the full ROTT map:

Example in C
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <io.h>

/*---------------------------------------------------------------------
   Map constants
---------------------------------------------------------------------*/

#define MAXLEVELNAMELENGTH           23
#define ALLOCATEDLEVELNAMELENGTH     24
#define NUMPLANES                    3
#define NUMHEADEROFFSETS             100
#define MAPWIDTH                     128
#define MAPHEIGHT                    128
#define MAP_SPECIAL_TOGGLE_PUSHWALLS 0x0001

#define WALL_PLANE    0
#define SPRITE_PLANE  1
#define INFO_PLANE    2

/*---------------------------------------------------------------------
   Type definitions
---------------------------------------------------------------------*/

RTLMAP :: struct {
   u32le                          used
   u32le                          CRC
   u32le                          RLEWtag
   u32le                          MapSpecials
   u32le                          planestart[NUMPLANES]
   u32le                          planelength[NUMPLANES]
   string /*(null-terminated)*/   Name[ALLOCATEDLEVELNAMELENGTH]
}


/*---------------------------------------------------------------------
   Global variables
---------------------------------------------------------------------*/

u16le *mapplanes[NUMPLANES];


/*---------------------------------------------------------------------
   Macros
---------------------------------------------------------------------*/

#define MAPSPOT(x, y, plane) \
   (mapplanes[plane][MAPWIDTH * (y) + (x)])

#define WALL_AT(x, y)   (MAPSPOT((x), (y), WALL_PLANE))
#define SPRITE_AT(x, y) (MAPSPOT((x), (y), SPRITE_PLANE))
#define INFO_AT(x, y)   (MAPSPOT((x), (y), INFO_PLANE))


/*---------------------------------------------------------------------
   Function: ReadROTTMap

   Read a map from a RTL/RTC file.
---------------------------------------------------------------------*/

void ReadROTTMap(string filename, int mapnum) {
   char            RTLSignature[4];
   unsigned long   RTLVersion;
   RTLMAP          RTLMap;
   int             filehandle;
   long            pos;
   long            compressed;
   long            expanded;
   int             plane;
   unsigned short *buffer;

   filehandle = open(filename, O_RDONLY | O_BINARY);

   //
   // Load RTL signature
   //
   read(filehandle, RTLSignature, sizeof(RTLSignature));

   //
   // Read the version number
   //
   read(filehandle, &RTLVersion, sizeof(RTLVersion));

   //
   // Load map header
   //
   lseek(filehandle, mapnum * sizeof(RTLMap), SEEK_CUR);
   read(filehandle, &RTLMap, sizeof(RTLMap));

   if (!RTLMap.used) {
      //
      // Exit on error
      //
      printf("ReadROTTMap: Tried to load a non existent map!");
      exit(1);
   }

   //
   // load the planes in
   //
   expanded = MAPWIDTH * MAPHEIGHT * 2;

  for(plane = 0; plane <= 2; plane++) {
      pos        = RTLMap.planestart[plane];
      compressed = RTLMap.planelength[plane];
      buffer     = malloc(compressed);

      lseek(filehandle, pos, SEEK_SET);
      read(filehandle, buffer, compressed);

      mapplanes[plane] = malloc(expanded);

      RLEW_Expand(buffer, mapplanes[plane], expanded >> 1, RTLMap.RLEWtag);

      free(buffer);
   }

   close(filehandle);
}

Map Weirdness

You can pretty much figure out most of the map data easily, but there are a few things in the map which are a little oddly set up. Here's a few helpful items.

The Upper Corner

The first row of a map contains vital information to setting up a map.

WALL Plane

(0, 0): Floor # (0xB4 through 0xC3, though we might cut some)

(1, 0): Ceiling # (0xC6 through 0xD5, or skies: 0xEA to 0xEE)

(2, 0): Brightness Level (0xD8 to 0xDF, from dark to light)

(3, 0): Rate at which light fades out at a distance (0xFC to 0x010B, fast to slow)

SPRITES Plane

(0, 0): Height of level (1-8 ranges from 0x5A to 0x61, 9-16 is from 0x01C2 to 0x01C9)

(1, 0): Height that sky is at relative to level (with same 1-16 arrangement) (not needed for level with a ceiling)

(2, 0): Icon for No Fog (0x68) or Fog (0x69)

Warning

The game will crash without one of these!

(3, 0): Light sourcing icon (0x8B: if present, enables dynamic lighting)

Items

These items can appear anywhere in the first eight tiles of a level.

SPRITES Plane

Lightning icon (0x0179)

Timer icon (0x79: third plane points 0xXXYY to X, Y location of timed thing--time in minutes/seconds there is MMSS in decimal digits, so 0130 is one minute thirty seconds--and to one side of that timed thing is the end time in the same format. This, for instance, would say when to shut the door that opened at the start time)

INFO Plane

Song number: 0xBAnn, where nn is song number. If not present, the game will choose song 0.

Warning

If greater than the number of level songs (18 in shareware), the game will crash.

Disks

Gravitational Anomaly Disks (GADs) are set up with a GAD icon in the second plane and a height in the third plane. The actual graphic has a disk in the top quarter, so to put one on the floor, you sort of have to put the object IN the floor, so the disk will be at the right height. Heights for objects start with 0xB0 and have that last byte as a tiles-off-the-floor nybble and sixteenths-of-a-tile fraction (4 pixels).

So 0xB000 is, for normal sprites, resting on the floor.

For disks, that would be a disk you could stand on to be one story (eight feet) in the air. The heights of disks usually go by sixes (that's the maximum they can be apart and you can still climb them like stairs) or fours (for a more gradual ascension). Here are three sets of height values. The values of 0xB0F1-$B0FE are into the floor, and $B0F6 is right about floor height.

By 6 By 4 By 2
B0F6 B0F6 B0F6
B0FC B0FA B0F8
B002 B0FE B0FA
B008 B002 B0FC
B00E B006 B0FE
B014 B00A B010
B01A B00E B012
B020 B012 B014
B026 B016 B016
B02C B01A B018
B032 B01E B01A
B038 B022 B01C
B03E B026 B01E
B044 B02A B020
B04A B02E B022
B050 B032 B024
B056 B036 B026
B05C B03A B028
B062 B03E B02A
B068 B042 B02C
B06E B046 B02E
B074 B04A B030
B07A B04E B032

If you need higher ones, calculate them yourself, guy.

Switches and Touchplates

Everything activated by a switch or touchplates points to the switch or touchplate that activates it, with the standard 0xXXYY format. This way tons of things can be activated by one switch. To make a door open with multiple switches/touchplates, make it a few tiles wide and have different parts of the door point to the different switches.

Locked Doors

Locked doors are normal doors with a key sprite icon placed on them.

Resources

Rise of the Triad Level Format - erysdren's WWW site