Skip to content

Format: Rct image

Robert Jordan edited this page May 17, 2021 · 10 revisions

Format: Rokucho T (Rct) image

File structure

Type Value Description
char[8] "六丁TC00" (\x98\x5a\x92\x9a TC00)
"六丁TS00" (\x98\x5a\x92\x9a TS00)
"六丁TC01" (\x98\x5a\x92\x9a TC01)
"六丁TS01" (\x98\x5a\x92\x9a TS01)
File Signature and Version
C = decrypted
S = encrypted
uint32 Width Image width in pixels
uint32 Height Image height in pixels
uint32 DataSize Size of image data buffer

Version V00

Type Value Description
byte[Size] DataBuffer Image data buffer

Version V01

Type Value Description
uint16 BaseNameLengh Size of Base name string + null terminator
char[Lengh] BaseName Filename of base layer this image is overlaid on top of
byte[Size] DataBuffer Image data buffer

BaseName overlay

When using RCT version V01, and the BaseNameLength field is non-zero, this image will be treated as a layer to place on top of the specified image file name.

Because RCT has no alpha channel, one color is reserved as the transparent color. In this case, #FF0000. Whenever pure Red is used, that pixel is replaced with the base image's pixel.

Base image layering can be used recursively, and supports using other base image types besides RCT. The only limitation is the base image cannot be an RC8 (or 8-bit indexed pixel format) image

Image data buffer

See here for information on image data encryption.

Encoding

Rct and Rc8 images are stored in a form of LZ77 coding. The image data buffer contains both raw data and length-position pair commands that determine how the next span of data is decoded.

TODO: Add note in Format: Rc8 image that palette is stored in RGB24 order (rather than BGR24).

Rc8/Rct differences

Constant Rc8 Rct Description
ByteDepth 1 3 Bytes per pixel
BaseTable Rc8[16] Rct[32] Shift table
CmdShiftStart 3 2 Bit offset of ShiftIndex
CmdShiftMask 0xf 0x1f Bitmask of ShiftIndex
CmdCountMask 0x7 0x3 Bitmask (max) of Count
CmdCountBase 3 1 Base number of pixels added to Count

Note that all values besides ByteDepth and CmdCountBase are derived from the length of BaseTable.

CmdCountBase is the minimum number of pixels copied, and is added to the Count value after unpacking it from the command and adding the optimal extra count.

Shift table

static sbyte[] Rc8ShiftTable = {
    -16, -32, -48, -64,
    49, 33, 17, 1, -15, -31, -47,
    34, 18, 2, -14, -30,
};

static sbyte[] RctShiftTable = {
    -16, -32, -48, -64, -80, -96,
    49, 33, 17, 1, -15, -31, -47,
    50, 34, 18, 2, -14, -30, -46,
    51, 35, 19, 3, -13, -29, -45,
    36, 20, 4, -12, -28
};

static int CalculateShift(int width, sbyte shift) {
    int shiftRow = (shift & 0xf);
    return (shift >> 4) - (shiftRow * width);
}

static int[] InitShiftTable(int width, sbyte[] baseTable) {
    return baseTable.Select(s => CalculateShift(width, s))
                    .ToArray();
}

Run-length command

Command (byte) Extra (uint16)
Read 0 nnnnnnn eeeeeeee eeeeeeee
Copy8 1 ssss nnn eeeeeeee eeeeeeee
CopyT 1 sssss nn eeeeeeee eeeeeeee
Flg Shift Num Extra num

Decoder

static void CopyOverlapped(byte[] data, int src, int dst, int count) {
    while(count > 0) {
        int preceding = Math.Min(dst - src, count);
        System.Buffer.BlockCopy(data, src, data, dst, preceding);
        dst += preceding;
        count -= preceding;
    }
}

// Unpack and return pixel array:
//   * 8-bit indexed | BGR24 pixel format
//   * No stride padding (4-byte alignment)
byte[] Unpack(BinaryReader reader) {
    int[] shiftTable = InitShiftTable(Width, BaseTable);
    byte[] pixels = new byte[Width * Height * ByteDepth];

    // Start by reading 1 pixel
    reader.Read(pixels, 0, ByteDepth);
    int pixelsLeft = pixels.Length - ByteDepth;  // Number of bytes left in pixels buffer
    int pos = ByteDepth;  // Byte position in pixels buffer

    while(pixelsLeft > 0) {
        byte cmd = reader.ReadByte();
        int count;
        if((cmd & 0x80) == 0) { // READ RUN MODE:
            count = cmd;
            if(count == 0x7f)  // Max value: Add extra count
                count += reader.ReadUInt16();
            count = (count + 1) * ByteDepth;  // Min: 1 pixel

            Debug.Assert(count <= pixelsLeft);  // Bounds check: end of pixels[]
            reader.Read(pixels, pos, count);
        }
        else {  // COPY RUN MODE:
            count = (cmd & CmdCountMask);
            if(count == CmdCountMask)  // Max value: Add extra count
                count += reader.ReadUInt16();
            //count = (count * ByteDepth) + 3;  // Min: 3 bytes
            count = (count + CmdCountBase) * ByteDepth;  // Min: 3 bytes

            // Shift determines where data is copied from,
            // relative to the current buffer position. (always negative)
            //  ( & 0x7f to mask shift index, regardless of format)
            int shiftIdx = (cmd & 0x7f) >> CmdShiftStart;
            int shift = shiftTable[shiftIdx] * ByteDepth;

            Debug.Assert(count <= pixelsLeft);  // Bounds check: end of pixels[]
            Debug.Assert(shift < 0 && pos+shift >= 0);  // Bounds check: initialized data,
                                                        //               start of pixels[]
            CopyOverlapped(pixels, pos + shift, pos, count);
        }
        pixelsLeft -= count;
        pos += count;
    }
    return pixels;
}

See also

External links

Clone this wiki locally