Version 1.1.1
by Molt
Last changed: 3. March 2015
.cif, .tab and .sal files have almost the same file format, for the only difference see the Entry
types below.
All file store text strings in an encrypted form.
.cif files represent .ini files and are the most common of the three.
.tab and .sal files store plain text data and are only used for languages.
C1_EncryptedFile
{
u4 magic; // 65601
array[2]
{
u4 numberOfEntries; // 2 times the exact same value
}
u4 constant; // 10
u4 indexLength;
encryptedIndexTable[indexLength]
{
u1 byte;
}
u2 one; // 1
u4 constant; // 10
u4 contentLength;
encryptedContentTable[contentLength]
{
u1 byte;
}
}
C2_EncryptedFile
{
u4 magic; // 1021
u4 zero; // 0
u4 one; // 1
array[3]
{
u4 numberOfEntries; // 3 times the exact same value
}
u4 contentLength;
u4 constant; // 1001
u4 zero; // 0
u4 indexLength;
encryptedIndexTable[indexLength]
{
u1 byte;
}
u1 byte; // 1
u4 constant; // 1001
u4 zero; // 0
u4 contentLength;
encryptedContentTable[contentLength]
{
u1 byte;
}
}
The first step to reading a .cif/.tab/.sal file after parsing the header is to decrypt encryptedIndexTable
and encryptedContentTable
. Treating both as encrypted byte arrays, they can both be converted to an unencrypted byte array of the same size.
c = 71;
d = 126;
for(i = 0; i < length; i++)
{
plainTable[i] = (encryptedTable[i] - 1) ^ c; // ^ = bitwise XOR
c += d;
d += 33;
}
Implemented in Java, this procedure could look like:
public static byte[] decryptTable(byte[] encryptedTable)
{
byte[] plainTable = new byte[encryptedTable.length];
int c = 71;
int d = 126;
for(int i = 0; i < encryptedTable.length; i++)
{
plainTable[i] = (byte)(((encryptedTable[i] & 0xFF) - 1) ^ c); // Just make sure you don't lose the highest
// bit when dealing with signed types.
c += d;
d += 33;
}
return plainTable;
}
Now let:
plainIndexTable = decryptTable(encryptedIndexTable);
plainContentTable = decryptTable(encryptedContentTable);
plainIndexTable
can now be used as an array of u4
(with length indexLength / 4
), of which every value is an offset in plainContentTable
.
plainIndexTable[indexLength / 4]
{
u4 index;
}
plainContentTable[indexLength / 4]
{
ContentEntry entry;
}
What ContentEntry
is depends on the file type.
For .cif files:
ContentEntry
{
u1 meta; // Determines the role of the following string.
// The only known values for this are:
// 1: Section name
// 2: Plain content
char[] string; // A C-Style string (with a terminating '\0' character)
// Note that you DO NOT KNOW its length in advance.
}
For .tab and .sal files:
ContentEntry
{
char[] string; // Same as above
}
At this point, .tab and .sal files are trivial to reconstruct: Each ContentEntry
equals one line of test.
Now for .cif files any entry with a meta
value of 2
can be appended as a new line to the file as well, but entries with a meta
value of 1
need to be enclosed by [ ]
to create a new ini section.
Hence, a plainIndexTable
containing the values 0, 5, 8, 11, 14, 19, 22, 25
and
// \x?? = character with hex value ??
plainContentTable = "\x01aaa\x00\x02a\x00\x02b\x00\x02c\x00\x01bbb\x00\x02d\x00\x02e\x00\x02f\x00"
should be converted to the following ini file:
[aaa]
a
b
c
[bbb]
d
e
f
There could be an empty newline before [bbb]
and I create my tools to procude one, but that's optional.
Without knowing the file suffix, your options to determine the file type are very limited, but they exist.
First you can take advantage of the fact that there are no .tab or .sal files in any other game that Cultures 1. If your file is a C2_EncryptedFile
, it's safe to assume it's a .cif file.
If your file is an C1_EncryptedFile
however, you can only look at ContentEntry.meta
.
\x01
or \x02
, it's most likely a .cif file.