.SCN
From REWiki
This is the basic scene container file format. In some versions (european DW1 CD version known) .SCN files are named .GRA. It is also used for dialogue containers (*.TXT).
Contents |
basic container format
- all values are little-endian
- chunks:
u16 chunk_type u16 magic /* 34 33 LSB, 0x3334 */ u32 offset /* of next chunk (absolute) */ u8* data /* length is determined by offset of next chunk */
- Discworld Noir uses a compressed format
32 bit addresses
cross-file addressing is done in a special way:
- DW1:
- 9 bit file id (=> INDEX order,
addr & 0xFF800000 >> 23) - 23 bit offset (
addr & 0x007FFFFF)
- 9 bit file id (=> INDEX order,
- DW2:
- 7 bit file id (=> INDEX order,
addr & 0xFE000000 >> 25) - 25 bit offset (
addr & 0x01FFFFFF)
- 7 bit file id (=> INDEX order,
chunk types
| chunk ID | given title | DW1 | DW2 | DWN |
| 0x0001 | DIALOGUE | x | x | x |
| 0x0002 | SCNFILE | x | x | x |
| 0x0003 | BLOCK_LISTS | x | ||
| 0x0004 | BLOCKS | x | ||
| 0x0005 | PALETTES | x | x | |
| 0x0006 | GRAPHICS_INFO | x | x | x |
| 0x0007 | GRAPHICS_LIST | x | x | x |
| 0x0008 | SCENE SETUP/ANIMATIONS? | x | x | x |
| 0x0009 | UNKNOWN | x | ||
| 0x000A | SCRIPTS | x | x | x |
| 0x000B | SCRIPT LIST 1 | x | x | x |
| 0x000C | POLYGON DATA | x | x | |
| 0x000D | SCRIPT LIST 2 | x | x | |
| 0x000E | SCENE INFO | x | x | x |
| 0x000F | UNKNOWN | x | x | |
| 0x0012 | UNKNOWN | x | ||
| 0x0013 | UNKNOWN | x | ||
| 0x0018 | UNKNOWN | x | ||
| 0x0019 | GRAPHICS_DW2 | x | x | |
| 0x001B | UNKNOWN | x | x | |
| 0x001C | UNKNOWN | x | x | |
| 0x001D | UNKNOWN | x | x | |
| 0x001E | UNKNOWN | x | ||
| 0x0020 | UNKNOWN | x | ||
| 0x0031 | UNKNOWN | x |
0x0001
DIALOGUE
- multiple blocks (dialogue lines):
u8 length char* text
Each chunk contains 64 entries. Each chunk is padded with 0x00 bytes (when necessary) to a DWORD boundary.
0x0002
SCNFILE
- at beginning of every .scn file, no payload
- not present in .TXT files
0x0003
BLOCK_LISTS
- referenced by offset from section 0x0006
- list of block indexes in section 0x0004 (
u16 index), e.g. 320x200 image: (320 / 4) * (200 / 4) = 80 * 50 = 4000 blocks - some blocks have bit
0x8000set, from objects.scn?
0x0004
BLOCKS
- list of 4x4 pixel blocks (16 bytes per block)
- pixel value is palette index (see section 0x0005)
- referenced by index from section 0x0003
0x0005
PALETTES
- number of palettes determined by size of chunk: 1024 byte per palette:
typedef struct {
uint8 r, g, b, unused;
} color;
struct palette {
color[256];
};
0x0006
GRAPHICS_INFO
- number of graphics is section size divided by 16
- list of graphics:
uint16 width; /* lower 15 bit, highest bit unknown */ uint16 height; /* lower 15 bit, highest bit unknown */ uint32 unknown1; uint32 offset; uint32 offset_pal; /* lower 24 bit for DW2 */
0x0007
GRAPHICS_LIST
- list of offsets to section 0x0006:
uint32 offset; uint32 unknown1; /* always 0x00000000? */
0x0008
SCENE SETUP/ANIMATIONS?
- blocks of different length referenced from section 0x000A
- new block often starts with
0x0000000cor0x00000006 - examples:
...
00051dec: 0x00000006 0x00000002 0x09051bac 00051df8: 0x09051bc4 0x09051ccc 0x09051ce4 0x09050240 00051e08: 0x00000001 0x00000159 0x00000000 0x00000000 00051e18: 0x00000002 0x09050240 0x00000001 0xfffffffe
00052e2c: 0x0000000c 0x00000002 0x09052b9c 00052e38: 0x09052bb4 0x09052ce4 0x09052cfc 0x09050d40 00052e48: 0x00000001 0x0000015d 0x00000000 0x00000000 00052e58: 0x00000002 0x09050d40 0x00000001 0xfffffffe
...
00052e68: 0x0000000c 0x00000001 0x09052e44 0x09052e5c
legend:
offset in section 0x0007 |
offset in section 0x0008 (this one)
0x000A
SCRIPTS
- some information here is just guessed and may be wrong
- Tinsel uses a stack-based virtual machine to execute the scripts. Each opcode has no or one argument, which can be a byte, a uint16 or a uint32, depending on the opcode. Most opcodes have a variant for each argument type. The stack is made up of uint32 values.
- Note that jump addresses are absolute to the script start, NOT the start of the script chunk!
- Scripts always have no more than one RET opcode.
- TODO: Sort the opcodes and document more.
| opcode | name | argument(s) | description |
| 0x00 | ret | end of script, also used for padding | |
| 0x01
0x06 | push | uint32 | push argument onto the stack |
| 0x02 | push 0 | - | push 0 onto the stack |
| 0x03 | push 1 | - | push 1 onto the stack |
| 0x04 | push -1 | - | push -1 onto the stack |
| 0x0A | getglobal | uint32 | push global var with arg as index onto the stack |
| 0x4A | getglobal | byte | push global var with arg as index onto the stack |
| 0x8A | getglobal | uint16 | push global var with arg as index onto the stack |
| 0x0C | setglobal | uint32 | pop value from stack into global var with arg as index |
| 0x4C | setglobal | byte | pop value from stack into global var with arg as index |
| 0x8C | setglobal | uint16 | pop value from stack into global var with arg as index |
| 0x0E | call | uint32 | call internal function with arg as index |
| 0x4E | call | byte | call internal function with arg as index |
| 0x8E | call | uint16 | call internal function with arg as index |
| 0x10 | addsp | sint32 | add arg value to stack pointer |
| 0x50 | addsp | sint8 | add arg value to stack pointer |
| 0x90 | addsp | sint16 | add arg value to stack pointer |
| 0x91 | jump | uint16 | jump to absolute address given in arg |
| 0x92 | jump_false | uint16 | jump to absolute address given in arg if value popped from stack is 0 |
| 0x93 | jump_true | uint16 | jump to absolute address given in arg if value popped from stack is != 0 |
| 0x14 | eq | - | equal to |
| 0x15 | lt | - | lower than |
| 0x16 | le | - | lower than or equal |
| 0x17 | neq | - | not equal |
| 0x18 | gt | - | greater than |
| 0x19 | ge | - | greater than or equal |
| 0x1A | add | - | addition |
| 0x1B | sub | - | subtraction |
| 0x1C | bool_or | - | boolean or |
| 0x1D | mul | - | multiplication |
| 0x1E | div | - | division |
| 0x1F | mod | - | modulo |
| 0x21 | or | - | binary or |
| 0x22 | xor | - | binary xor |
| 0x23 | bool_and | - | boolean and |
| 0x24 | is_false | - | |
| 0x25 | not | - | boolean not |
| 0x26 | neg | - | negation |
| 0x27 | dup | - | duplicates value on top of the stack
|
0x000B
SCRIPT LIST 1
- list of:
uint32 unknown1; /* flags? */ uint32 offset; /* script start address? */
0x000C
POLYGON DATA
- list of polygons and links between them
0x000D
SCRIPT LIST 2
- list of:
uint32 unknown1; /* flags? */ uint32 unknown2; /* size? */ uint32 offset; /* script start address? */
0x000E
DIRECTORY
DW1:
- this section is present in all
.SCNfiles exceptdw.scnandobjects.scn - the
offset_*fields are the offset of the data part of the corresponding section, so it points to the bytes after the chunk type, magic and size - the
offset_*fields are sometimes0x00000000, if the section is not present
uint32 num_entries_0x000B; uint32 polygonCount; uint32 actorCount; uint32 unknown2; uint32 offset_0x000A; uint32 offset_0x000B; uint32 polygonResourceId; uint32 actorResourceId;
compression
The .SCN files in Discworld Noir are compressed with a LZSS-based algorithm.
The files are a headerless bitstream.
Single bits are read bytewise from MSB to LSB.
Integer numbers are read from the bitstream in MSB to LSB order.
During decompression, a 4096-byte big window is maintained, and an offset into the window. The offet is in the range 0 to 4095 (assuming the window offset is zero-based).
When a byte is written to the output, it's also written to the window to the current window offset, and the window offset is increased by 1. Make sure the offset never exceeds 4095.
To decompress the data, repeat the following steps until the LZ-offset is -1:
read a bit if bit is 0: read 'LZ-offset' = next 12 bits minus 1 if 'LZ-offset' = -1 finish decompression read 'LZ-len' = next 4 bits plus 2 copy 'LZ-len' bytes from the LZ window, starting at 'LZ-offset' (don't use memcpy etc. here since the bytes may overlap) if bit is 1: get 8 bits and write them to the output
An example implementation (Exe + C++ source) can be found at http://gamefileformats.the-underdogs.info/files/scnx-exe.zip and http://gamefileformats.the-underdogs.info/files/scnx-src.zip.
