EX files begin with a header with the following structure.
| Offset | Size (Bytes) | Description |
|---|---|---|
| 0x0 | 4 | Magic: ‘HEAD’ |
| 0x4 | 4 | 0xC (?) |
| 0x8 | 4 | Magic: ‘EXTF’ |
| 0xC | 4 | 0x1 (?) |
| 0x10 | 4 | Block count |
| 0x14 | 4 | Magic: ‘DATA’ |
| 0x18 | 4 | Compressed data size (bytes) |
| 0x1C | 4 | Uncompressed data size (bytes) |
| 0x20 | Varies | Encrypted & compressed data |
The compressed data is encrypted with a dictionary. It can be decrypted as follows,
uint8_t ex_decode_table[256];
void decode(uint8_t *compressed_data, size_t size)
{
// initialize table
for (int i = 0; i < 256; i++) {
int tmpfor = i;
int tmp = tmpfor;
tmp = (tmp & 0x55) + ((tmp >> 1) & 0x55);
tmp = (tmp & 0x33) + ((tmp >> 2) & 0x33);
tmp = (tmp & 0x0F) + ((tmp >> 4) & 0x0F);
if ((tmp & 0x01) == 0) {
tmpfor = ((tmpfor << (8 - tmp)) | (tmpfor >> tmp)) & 0xFF;
} else {
tmpfor = ((tmpfor >> (8 - tmp)) | (tmpfor << tmp)) & 0xFF;
}
ex_decode_table[i] = tmpfor;
}
// decode data
for (size_t i = 0; i < size; i++) {
compressed_data[i] = ex_decode_table[i];
}
}Once the data is decrypted, it can be uncompressed using zlib.
The uncompressed data is a list of blocks. Each block has a name, data type and associated data. The number of blocks in the list is given in the header described above.
| Size (Bytes) | Description |
|---|---|
| 4 | Data type |
| 4 | Block size |
| Varies | Name (ex-string) |
| Varies | Value (depends on data type) |
Strings in EX files are encoded as pascal strings. Sometimes (not always!) they are zero-padded so that the length is a multiple of 4. I will refer to such padded strings as ‘ex-strings’ throughout this document.
| Size (Bytes) | Description |
|---|---|
| 4 | Size |
| Varies | Data |
EX files can contain the following data types:
| Value | Name | Description |
|---|---|---|
| 1 | EX_INT | 32-bit integer |
| 2 | EX_FLOAT | 32-bit float |
| 3 | EX_STRING | Pascal string (32-bit size followed by string data) |
| 4 | EX_TABLE | Table |
| 5 | EX_LIST | List |
| 6 | EX_TREE | Tree |
A table is a list of fields, followed by the table data.
| Size (Bytes) | Description |
|---|---|
| 4 | Field count |
| Varies | Field descriptors |
| 4 | Column count |
| 4 | Row count |
| Varies | Rows |
Starting in Evenicle, the order of ‘Column count’ and ‘Row count’ is reversed.
| Size (Bytes) | Description | Notes |
|---|---|---|
| 4 | Data type | |
| Varies | Name (ex-string) | |
| 4 | Has value? | |
| 4 | Is index? | If 1, then this field should be indexed |
| Varies | Value (depends on data type) | Only present if ‘Has value?’ is 1 |
| 4 (0) | Subfield count | Only present if data type is EX_TABLE |
| Varies | Subfields | Only present if data type is EX_TABLE |
If a field’s data type is EX_TABLE, then ‘Subfield count’ and ‘Subfields’ are present. ‘Subfields’ is a nested array of field descriptors.
A row is a list of cells. The number of cells contained in each row is given by the ‘Column count’ of the table, and the number of rows in a table is given by the ‘Row count’. A cell is a simple type-value pair:
| Size (Bytes) | Description |
|---|---|
| 4 | Data type |
| Varies | Value (depends on data type) |
| Size (Bytes) | Description |
|---|---|
| 4 | Item count |
| Varies | List items |
| Size (Bytes) | Description |
|---|---|
| 4 | Data type |
| 4 | Data size |
| Varies | Value (depends on data type) |
Trees are encoded as a recursive node structure:
| Size (Bytes) | Description |
|---|---|
| Varies | Node name (ex-string) |
| 4 | Is leaf? |
| Varies | Node data |
If ‘Is leaf?’ is 1, then ‘Node data’ contains a leaf node descriptor. If ‘Is leaf?’ is 0, then ‘Node data’ contains an intermediate node descriptor.
| Size (Bytes) | Description |
|---|---|
| 4 | Data type |
| 4 | Data size |
| Varies | Leaf name (ex-string) |
| Varies | Value (depends on data type) |
| 4 | Reserved? (always 0) |
| Size (Bytes) | Description |
|---|---|
| 4 | Child count |
| Varies | Children |
Each entry in ‘Children’ is a full tree descriptor.
