File format description

On this page the docs I made describing the file format of TTF/Moktar's data files can be found.
If you're not the type of person who enjoys examining the guts of SQZ files, now is the time to run away in terror.
Still here? You can use the information below for your own evil masterplans as you see fit, don't forget to include some credit ;-).

Jump to:
Main
Decompression
Level files
Sample decompressor


Main

Titus the Fox / Moktar level file format by Jesses (mail at ttf dot mine dot nu)
Visit https://ttf.mine.nu for more TTF/Moktar stuff

Main file, v1.0.
Version history:
  1.0 - initial version

This set of files describes the file format of the Titus the Fox / Moktar data files.

Included files:
  main.txt        This file
  compress.txt    Describes how to decompress .SQZ
  format.txt      Format of uncompressed level files, and also .LVL files made with MTF
  SPREXP.SIZ      Sprite size table for Titus the Fox
  SPRITES.SIZ     Sprite size table for Moktar
  unpack.pl       Perl implementation of the decompression explained in compress.txt

SQZ files are compressed with either LZW or Huffman; the COMPRESS.TXT file explains how to uncompress them.

FORMAT.TXT deals with the uncompressed versions of LEVEL*.SQZ files (except LEVELA.SQZ). The 'x' in LEVELx.SQZ
signifies which level it contains, but these 'x' values are not equal to the actual level numbers. The order of
the 'x' values as the levels appear in the game is '0J123456789BCEFG' for Moktar, i.e. LEVEL7.SQZ contains
level 9. TTF lacks level F so it has only 15 levels (including the ending).

As for the remaining SQZ files that come with the game:
  FONTS.SQZ      - Contains bitmaps of the font used in the game.
  KEYB.SQZ       - This is unused in the full game; probably this holds the keyboard input sequence needed to
                   make the player move automatically in the non-playable demo.
  LEVELA.SQZ     - Holds no level, but the photo seen when completing the game. It is stored in 16-color planar
                   format, i.e. first a 320*200 monochrome image for plane 0, then three additional ones for
                   planes 1, 2, 3. Palette is simply grayscale.
  MENU.SQZ       - The menu. First 3*256 bytes are the palette, followed by a 256-color 320*200 image.
  MENUEGA.SQZ    - The menu, in 16-color planar format.
  SPREXP.SQZ     - This holds TTF's sprites used in-game. Each sprite is stored in 16-color planar format;
                   however, the sizes of each sprite vary and they are not stored in the file. You need the
                   sprite size table (which is stored inside the game's executable) to view sprites. The file
                   SPREXP.SIZ contains this table; it has two bytes per sprite, respectively the width and
                   height in pixels.
  SPRITES.SQZ    - Moktar version of SPREXP.SQZ. Sizes are in SPRITES.SIZ.
  TITRE(EGA).SQZ - Title screen, in 16 and 256 color versions.
  TITUS.SQZ      - Titus company logo, 256 colors.
Besides the above-mentioned 256-color bitmaps, the entire game is in 16 colors.

16-color bitmaps are stored in "planar" format. How does that look like? It's actually a one-bit-per-pixel
bitmap, repeated four times. So, there are four complete monochrome images, one after the other. For 16*16
bitmaps this means 16*16 bits (or 32 bytes) per monochrome image, and this four times.

To get a pixel's 4-bit color value, you have to combine the four individual bits from each monochrome image,
also called "bitplane". So, for a 16*16 4-bit planar image, pixel (0, 0) is composed of byte 0 bit 0,
byte 32 bit 0, byte 64 bit 0 and byte 96 bit 0.

Why this wacky format? It's how the PC's 16-color graphics modes organise memory internally, making it more
easy for the game to load its files.

All used 16-color palettes are again only stored inside the executable. For the level files, there is a basic
15-color palette (actually only 14 are used, due to two blacks) with a 16th color that changes per level.
Sprites just use the current level's palette, with the exception of color 0 being transparent.

Here is a table with the 16 RGB triples in a level palette, values ranging 0..63:
    0: 0, 0, 0
    1: 60, 60, 60
    2: 0 , 0 , 0
    3: 24, 8 , 0
    4: 28, 16, 0
    5: 40, 24, 16
    6: 48, 40, 24
    7: 60, 48, 32
    8: 16, 8 , 0
    9: 28, 20, 20
   10: 40, 32, 32
   11: 12, 12, 28
   12: 24, 24, 40
   13: 32, 32, 48
   14: varies between levels!
   15: 8 , 8 , 24
As colors 0 and 2 are the same, I made MTF change all colors 0 in the original LEVELx.SQZ files to 2 when
loading them, so color 0 is available for other funny tricks (none played yet). Meaning, if you end up making
some code that loads an original level and saves it somehow, do the 0->2 conversion as well ;-).

The varying color, #14, has the following palette in the first 14 levels, ordered as they appear in-game:
    1: 0,16,0
    2: 0,16,0
    3: 0,16,0
    4: 20,12,12
    5: 40,12,4
    6: 20,12,12
    7: 20,12,12
    8: 0,16,0
    9: 40,28,12
   10: 0,20,16
   11: 40,28,12
   12: 40,28,12
   13: 40,28,12
   14: 40,28,12
The remaining levels for Moktar:
   15: 40,28,12
   16: 48,8,0
And for TTF:
   15: 48,8,0

Decompression

Titus the Fox / Moktar SQZ file format by Jesses (mail at ttf dot mine dot nu)
Visit https://ttf.mine.nu for more TTF/Moktar stuff

SQZ decoding explanation, v1.0.
Version history:
  1.0 - initial version

This file describes how to decompress the SQZ files. One of two compression
algorithms is used: either LZW or Huffman+RLE. A byte at offset 1 determines
which one of them is actually in use.


File format:
Offset	Size	What
0	1	bits 0..3: high nibble of uncompressed size, bits 4..7 unused
1	1	0x10: LZW is used, something else (0): Huffman+RLE is used
		CDRUN.COM's unpacker considers values above 0x10 invalid
2	2	low word of uncompressed size

If LZW is used:
Offset	Size	What
4	rest	LZW bitstream

If Huffman is used:
Offset	Size	What
4	2	HTS: Huffman tree size in bytes
6	HTS	HT: Huffman tree as array of 16-bit words
6+HTS	rest	Huffman bitstream


A perl implementation is given in the file "unpack.pl".


How to decode LZW bitstream:

The bitstream consists of variable-length codewords; from here on, their
word size is named 'nbit'. Initially, nbit == 9.

LZW makes use of a dictionary; the codewords are used as indices to perform
lookups. This dictionary is not specified explicitly in the compressed stream
though, instead it is derived during decompression. Initially the dictionary
contains 258 entries. For entries 0..255, the value of each entry is simply
the ASCII binary form of its index: dict[i] = chr(i). The remaining two
entries, 0x100 and 0x101, have a special purpose and aren't used for lookups.
The dictionary has a maximum size of 0x1000 entries.

The bitstream codewords can be divided in two categories. The first category
is formed by the two special values mentioned above, CLEAR_CODE (0x100) and
END_CODE (0x101). Whenever CLEAR_CODE is encountered, the decoder should
reset its state to the initial condition, that is, set nbit to 9 and reset
the dictionary to its initial 258 entries. END_CODE, as expected, marks the
end of the stream.

The second category is formed by all other codeword values. For each
codeword, first a new entry is optionally added to the dictionary. No entry
is added immediately after a CLEAR_CODE, and neither when the dictionary is
full. Next, the codeword's value is used as an index in the dictionary: the
corresponding entry is output.

The value of the new dictionary entry is the output generated by the previous
codeword, plus one more byte. This byte is normally the first byte of the
current codeword's output. However, sometimes the current codeword points to
a not-yet-existing dictionary entry. In this case, the extra byte's value is
the first byte of the previous output. So, if the previous codeword generated
an output of 'ABC', and the current codeword points beyond the dictionary,
the new dictionary entry would be 'ABCA'.

For this to work correctly, the input stream must fulfill a few conditions,
which all apply only when the current codeword points to a not-yet-existing
entry. First, the previous codeword can't be a CLEAR_CODE, as there is no
previous output in that case. Secondly, the dictionary cannot be full, as
otherwise there is no room to add a new entry, which is needed to lookup the
output. Finally, the current codeword must point to the to-be-added entry,
for the same reason. It is the responsibility of the compressor to make sure
these conditions hold, and they do for all LZW-encoded SQZ files in both
Moktar and Titus the Fox.

If after adding an entry the number of elements in the dictionary is equal to
2^nbit, the value of nbit is incremented; unless nbit is equal to 12.

The number of unused input bits following END_CODE is at least one and at
most eight, so sometimes an entirely unused input byte is present at the end.

Regarding bit-order: consider the first three input bytes of a file, and
label the bits like this, with most-significant bits on the left in each byte:
	76543210 FEDCBA98 NMLKJIHG
then, the first two codewords (most-significant bit left) are:
	76543210F EDCBA98NM

Constants:
	CLEAR_CODE	= 0x100 (0x101 for CDRUN.COM)
	END_CODE	= 0x101 (0x100 for CDRUN.COM)
	MAX_TABLE	= 0x1000

Pseudocode:
	int nbit
	stringarray dict

	int prev := CLEAR_CODE
	while (prev != END_CODE) {
		if (prev == CLEAR_CODE) {
			nbit := 9
			dict := [chr(0)..chr(255), ?, ?] // 258 entries, ?=unused
		}
		int cw := next nbit-sized codeword
		if (cw != CLEAR_CODE && cw != END_CODE) {
			int newbyte
			if (cw < num_elem(dict)) {
				newbyte := first_byte(dict[cw])
			} else {
				// Assumption 1: prev != CLEAR_CODE
				// Assumption 2: num_elem(dict) < MAX_TABLE
				// Assumption 3: cw == num_elem(dict)
				newbyte := first_byte(dict[prev])
			}
			if ((prev != CLEAR_CODE) && (num_elem(dict) < MAX_TABLE)) {
				append dict[prev] ++ newbyte to dict
				if (num_elem(dict) == 2**nbit && nbit < 12) {
					nbit++
				}
			}
			output dict[cw]
		}
		prev := cw
	}

Implementation note:
	The above pseudocode focuses on clarity, not efficiency. It is
	possible to apply some optimizations. A simple optimization is to not
	explicitly keep the first 258 entries of the dictionary in memory,
	since the first 256 are trivially computed and the remaining two are
	unused. This gains some memory bytes, at the cost of a few extra
	conditional statements and subtractions of 0x102.

	More interesting optimizations are possible. In particular, the use
	of string data types can be completely avoided. Any addition to the
	dictionary is formed by a prefix and a single byte, where the prefix
	already exists in the dictionary at index 'prev'. Instead of storing
	this prefix literally, it can also be stored as a pointer to the
	index where it occurs. This, together with the maximum dictionary
	size, allows for the array 'dict' to be of constant bytesize.

	The perl implementation in "unpack.pl" uses these two optimizations.

Example output:
	LEVEL1.SQZ is identical for both Moktar and Titus the Fox. The first
	twelve codewords, which will yield 38 decompressed bytes, are:
		01C 045 053 104 105 106 107 105 097 108 109 10B
	After processing these codewords, the dictionary contains literally:
		(1C 45, 45 53, 53[2x], 53[3x], 53[4x], 53[5x], 53[6x],
		 53[3x] 97, 97 53, 53[7x], 53[3x] 97 53)
	The same dictionary, shown in pointer+byte format as explained above:
		(01C+45, 045+53, 053+53, 104+53, 105+53, 106+53, 107+53,
		 105+97, 097+53, 108+53, 109+53)
	The first bytes of the decompressed output at offset 0x00000 are:
		1C 45 53[18x] 97 53[9x] 97 53[10x] 97 53[6x] 96 53[3x] 97
	At offset 0x00321 in the output, nbit changes its value for the first
	time. Surrounding output bytes, starting at offset 0x318, are:
		96 53[3x] 97 53 96 53[3x] 97 96 53[3x] 97 53 96 53[3x] 97
	At offset 0x012EC in the output, a CLEAR_CODE occurs in the input.
	The surrounding output bytes, starting at offset 0x012E0, are:
		81 BD B6[10x] EB EC B7 B6[9x] EB EC B7 B6[4x] EB 1C 45 53[4x]
	Finally, the last 20 bytes are:
		68 47 CA 47 FF 23 03 23 03 0B 00 08 CA 00[3x] B0 0D 40 02
	CRC-32 of the uncompressed LEVEL1.SQZ is 0x44800FE5.


How to decode Huffman+RLE bitstream:

In Huffman coding a fixed binary tree structure is used, with output
codewords stored at its leaves. The Huffman bitstream sequentially addresses
these leaf nodes. The tree is navigated node for node, starting at the root.
One bit is then read from the stream. This bit determines the next node to
visit: a bit value of 0 means visit the first (left) child, where a value of
1 means take the second (right) child. Whenever a leaf node is reached, its
value is output and the current node position is reset to the root.

In most Huffman implementations the tree is stored efficiently in what is
known as "canonical" form. In the SQZ files however, this is not the case and
the tree is stored in a very straightforward fashion, making it easy to use.
The binary Huffman tree HT is stored as array of 16-bit words, where each word
represents a node; each node is either an internal node or a leaf node. The
value stored in the array for a node has two parts: bit 15 is set if it is a
leaf node, bits 0..14 contain the node's value. For leaf nodes this value is
a codeword, for internal nodes it is 2*(index of first child), or put another
way: the byte offset in the array of the first child. The second child of an
internal node is stored immediately after its first child.

Bit order is such that for each byte, the most significant bit is processed
first.

Pseudocode for reading codewords (Huffman decoding):
	int node := 0
	while (bool b := next bit) {
		if (b) {
			node++
		}
		if (HT[node] < 0x8000) {
			// internal node
			node := HT[node] / 2
		} else {
			// leaf node
			int cw := HT[node] & 0x7FFF
			process codeword cw
			node := 0
		}
	}

This results in a sequence of RLE codewords. Each codeword 'cw' is processed
in the following way:

Let L == cw mod 256
Let H == cw div 256

Codeword	What to do
H==0		last := L; output 'last'
H!=0 && L==0	read next codeword; output cw times 'last'
H!=0 && L==1	read next codeword; count := L*256;
		read next codeword; count += L;
		output count times 'last'
H!=0 && L>=2	output L times 'last'

This is easily implemented using a state machine: the following pseudocode
can be directly inserted in the loop above.

The pseudocode uses three pre-existing variables:
	int state := 0
	byte last
	int count

Pseudocode for processing a codeword cw (RLE decoding):
	byte L := cw & 255
	byte H := cw >> 8
	if (state == 0) {
		if (H == 0) {
			last := L
			output 'last'
		} else if (L == 0) {
			state := 1
		} else if (L == 1) {
			state := 2
		} else {
			output L times 'last'
		}
	} else if (state == 1) { // cw == repeat count
		output cw times 'last'
		state := 0
	} else if (state == 2) { // L == high byte of 'count'
		count := L*256
		state := 3
	} else if (state == 3) { // L == low byte of 'count'
		count += L
		output count times 'last'
		state := 0
	}

Example output:
	The games only have a few Huffman-encoded files. Moktar's only file
	is SPRITES.SQZ, Titus the Fox has SPREXP.SQZ. In SPRITES.SQZ the
	first 9 input bit groups corresponding to codewords are:
		11 1011 10100 0000101110 11 10010 0001110001 10011 10100
	The first 12 intermediate codewords, after Huffman but before RLE:
		0000 0100 0003 007A 0000 0001 0098 0080 0003 0065 00C0 0000
	The first 16 bytes of decompressed output for SPRITES.SQZ are:
		00 00 00 00 7A 00 01 98 80 03 65 C0 00 98 40 02
	Now for SPREXP.SQZ, the first 12 input bit groups:
		11 11 011010 11 11 0100101 001100000 11 11 0100011 011101 11
	The first 12 intermediate codewords:
		0000 0000 0008 0000 0000 0030 0028 0000 0000 0018 0040 0000
	And the first 16 output bytes:
		00 00 08 00 00 30 28 00 00 18 40 00 00 3C 08 00
	In both files, there is only one codeword where H!=0 && L>=1. The
	surrounding codewords, for SPRITES.SQZ starting at output offset
	0x2B131 and for SPREXP.SQZ at 0x2AE29, are:
		0000 0100 0009 000C 0000 0101 0001 0008 0020 0000 0100 0008
	The output generated by these codewords is:
		00 00[9x] 0C 00 00[256x] 00[8x] 20 00 00[8x]
	Finally, the last 16 bytes of both files are:
		00 00 0C 60 0E E0 07 C0 07 80 01 00 00 00 00 00
	CRC-32 of the uncompressed SPRITES.SQZ is 0xAA81B13A; for SPREXP.SQZ
	it is 0xEB0338C9.

Level files

Titus the Fox / Moktar level file format by Jesses (mail at ttf dot mine dot nu)
Visit https://ttf.mine.nu for more TTF/Moktar stuff

Level file format, v1.0.
Version history:
  1.0 - initial version

This describes the format of the LEVEL?.SQZ files, after uncompressing them.

LEVELA.SQZ is not a level file but a picture. The format of the other files is as follows:

(all numbers are decimal, unless prefixed with 0x)

Offset	Size in bytes	What it is
--------------------------------------------------------------
0	256*LevelSize	Level tile map
x-32768	256*128		Tile bitmaps for each of the 256 tiles
x	256		Horizontal flags for each tile
x+256	256		Floor flags for each tile
x+512	256		Ceiling flags for each tile
x+768	40*6		Objects
x+1008	2		Vertical scrolling limit - 12
x+1010	2		X-coordinate of entrance
x+1012	2		Y-coordinate of entrance
x+1014	50*26		Enemies
x+2314	100*4		Bonuses
x+2714	2		Horizontal scrolling limit - 20
x+2716	20*7		Gates
x+2856	10*20		Elevators
x+3056	2		X-coordinate of exit
x+3058	2		Y-coordinate of exit

where	LevelSize = (filesize - 35828) / 256
and	x = 256*LevelSize + 32768


Sprite number fields mentioned below contain several values:
Bit	What it is
------------------
0..12	Sprite number
13	Supporting
14	Flash (internal use only)
15	If set, sprite is horizontally flipped

Sprites have their origin at the center pixel of the bottom line; to convert level-file coords to regular (0, 0) origin do:
(SpriteX, SpriteY) = (FileX - SpriteWidth/2, FileY - SpriteHeight)
This does not apply to Elevator sprites however, which have regular (0, 0) origin already.

A detailed description of each item follows:

Level tile map
--------------
Each byte specifies which tile should be at that position. All levels are 256 tiles wide, the height (LevelSize) varies.

Tile bitmaps for each of the 256 tiles
--------------------------------------
Each bitmap is 16 colors, 16*16 pixels and stored in planar format.

Horizontal/floor/ceiling flags for each tile
--------------------------------------------
These specify the behaviour of a tile, i.e. if you can stand on it, if it's a wall, if it kills you or even if it's a bonus.

	Horizontal
	----------
	Valid values are:

	0: no wall
	1: wall
	2: health bonus
	4: level code
	5: padlock bonus
	6: level 14 code

	Floor
	-----
	Valid values are:

	0: no floor
	1: floor
	2: slightly slippery floor
	3: slippery floor
	4: very slippery floor
	5: drop-through (duck and you'll fall through)
	6: ladder
	7: health bonus
	8: deadly (eats most objects; allows objects to bounce, but only the ball remains in rest; i.e. water)
	9: deadly (eats all objects; i.e. fire)
	10: deadly (supports all objects; i.e. spikes)
	11: level code
	12: padlock bonus
	13: level 14 code

	Ceiling
	-------
	If bit 7 is set, this tile and the two following it are cycled in-game.
	Valid values for bit 0..6 are:

	0: no ceiling
	1: ceiling
	2: ladder
	3: padlock bonus
	4: deadly

Objects
-------
There are 40 object records, each of which has this format:

	Offset	Size	What it is
	-----------------------------
	0	2	Sprite number
	2	2	X coordinate
	4	2	Y coordinate

An object record is skipped if Sprite number = 0xFFFF.

Vertical scrolling limit
------------------------
The game will not scroll below this line unless the player actually is below it.
As soon as the player is above the line again, the game scrolls up.

Coordinates of entrance
-----------------------
Here the fun starts.

Enemies
-------
There are 50 enemy records. Some fields have to be set to a particular value upon save; this is indicated by '=='.
Each record has this format:

	Offset	Size	What it is
	-------------------------------------
	0	2	NMI_X: X-coordinate
	2	2	NMI_Y: Y-coordinate
	4	2	Sprite number (add 101 to bits 0..12 to get the actual sprite)
	6	2	Bit 0..12: enemy type (bits 13..15 used internally, should be 0 in the file)
	8	2	NMI_Speed
	10	1	Health: the enemy's health, defaults to 1 for non-boss enemies. Only valid
			if inside an MFL file; else non-boss enemies always have health	1 and boss
			enemy health depends on level number. Default boss health: 2 for level 1,
			4 for level 5 and 10 for the rest. See MFLFORM.TXT.
	12	2	NMI_Power: Enemies' power (determines speed at which you fly away after a hit)
			If this is 0, the enemy doesn't do any harm.
	14	1	== 0

An enemy record is skipped if Sprite number = 0xFFFF.

Enemy type determines the meaning of the remaining bytes:
	Type    Meaning
	---------------
	0,1:	Noclip walk	;NPPower, NPSpeed, NPRange
		Walk from NMI_Center - NMI_Dist to NMI_Center + NMI_Dist
		If NMI_X lies outside this range, first walk towards it
		Extra fields:
		Offset	Size	What it is
		--------------------------
		15	2	NMI_Center: midpoint of the walking range
		17	2	NMI_Dist: range of the walker
	2:	Shoot	;NPPower, NPZone, NPFreq
		no moving, hit/shoot with NMI_Freq if player in NMI_Zone
		Extra fields:
		Offset	Size	What it is
		--------------------------
		15	1	== NMI_Freq
		16	1	NMI_Freq: delay between shots (10..255)
		17	2	NMI_Zone: bit 0..13 = range, bit 14..15 = direction (0 = both, 1 = left, 2 = right)
	3,4:	Noclip walk, jump to player	;NPPower, NPSpeed, NPRange, NPVZone
		like 0, but jump up to player if it is max NMI_VZone pixels above
		Extra fields:
		Offset	Size	What it is
		--------------------------
		15	2	NMI_Center: midpoint of the walking range
		17	2	NMI_Dist: range of the walker
		19	1	NMI_VZone: jump height
		20	2	== 0
	5,6:	Noclip walk, move to player	;NPPower, NPSpeed, NPRange, NPVZone, NPVZoneDown
		like 0, but fly up/down to player if it is max NMI_VZone pixels above/below
		Pause up/down moving when not on-screen!
		Extra fields:
		Offset	Size	What it is
		--------------------------
		15	2	NMI_Center: midpoint of the walking range
		17	2	NMI_Dist: range of the walker
		19	1	NMI_VZone: vertical range
		20	2	== 0
	7:	Gravity walk, hit when near	;NPPower, NPSpeed2, NPZone2
		awake when player comes nearer than NMI_Zone2 horizontal, 200 vertical and is not above enemy
		walk, bouncing off walls and falling in holes
		when near player, move to hit
		reset when player is more than 2 screens horizontal or vertical away
		Extra fields:
		Offset	Size	What it is
		--------------------------
		15	2	== NMI_X
		17	2	== NMI_Y
		19	1	NMI_Speed (overrides word at offset 8, which should equal this)
		20	1	== 0
		21	2	== 0
		23	2	NMI_Zone2: range
	8:	Gravity walk when off-screen	;NPPower, NPSpeed2
		awake when player is more than 1 screen away horizontal or vertical
		walk, bouncing off walls and falling in holes
		reset when player is more than 2 screens horizontal away
		Extra fields:
		Offset	Size	What it is
		--------------------------
		15	2	== NMI_X
		17	2	== NMI_Y
		19	1	NMI_Speed (overrides word at offset 8, which should equal this)
		20	1	== 0
		21	2	== 0
		23	2	== 0
	9:	Walk and periodically pop-up	;NPPower, NPSpeed2, NPZone2
		awake when player comes nearer than NMI_Zone2 horizontal, 60 vertical
		walk, bouncing off walls
		periodically, pop up and change direction towards player.
		reset when player is more than 4 screens horizontal away
		Extra fields:
		Offset	Size	What it is
		--------------------------
		15	2	== NMI_X
		17	2	== NMI_Y
		19	1	NMI_Speed (overrides word at offset 8, which should equal this)
		20	1	== 0
		21	2	== 0
		23	2	NMI_Zone2: range
	10:	Alert when near, walk when nearer	;NPPower, NPSpeed2, NPZone2, NPZone2Min50
		awake when player comes nearer than NMI_Zone2 horizontal, 26 vertical; don't move yet
		start walking when player comes nearer than NMI_Zone2 - 50 horizontal, 60 vertical
		walk, bouncing off walls
		reset when player is more than 2 screens horizontal away
		Extra fields:
		Offset	Size	What it is
		--------------------------
		15	2	== NMI_X
		17	2	== NMI_Y
		19	1	NMI_Speed (overrides word at offset 8, which should equal this)
		20	1	== 0
		21	2	== 0
		23	2	NMI_Zone2: range
	11:	Walk and shoot	;NPPower, NPSpeed2, NPZone2
		awake when player comes nearer than NMI_Zone2 horizontal, 26 vertical
		walk, bouncing off walls
		periodically, check if player is nearer than 64 hor and 20 ver; if so, change direction towards player and shoot
		reset when player is more than 2 screens horizontal away
		Extra fields:
		Offset	Size	What it is
		--------------------------
		15	2	== NMI_X
		17	2	== NMI_Y
		19	1	NMI_Speed (overrides word at offset 8, which should equal this)
		20	1	== 0
		21	2	== 0
		23	2	NMI_Zone2: range
	12:	Jump (immortal)	;NPPower, NPPowerS, NPFreq2
		jump up to .5*NMI_PowerS^2 + .5*NMI_PowerS
		wait NMI_Freq2 between jumps
		Extra fields:
		Offset	Size	What it is
		--------------------------
		15	2	NMI_PowerS: jump power
		19	1	NMI_Freq2: time between jumps
	13:	Bounce	;NPPower, NPSpeed, NPZone2, NPCount2
		awake when player is nearer than NMI_Zone2 horizontal, 40 vertical
		jump towards player, with hor-speed NMI_Speed and ver-speed 10
		wait NMI_Count2
		sleep (don't reset position)
		Extra fields:
		Offset	Size	What it is
		--------------------------
		20	1	NMI_Count2
		23	2	NMI_Zone2
	14:	Gravity walk when off-screen (immortal)
		like 8 but immortal
	15:	Nothing (immortal)
		do nothing, immortal
	16:	Nothing
		like 15 but not immortal
	17:	Drop (immortal)	;NPZone3, NPFreq3
		awake periodically (period NMI_Freq3)
		fire if player is within NMI_Zone3 horizontal and NMI_ZoneY3 vertical
		Extra fields:
		Offset	Size	What it is
		--------------------------
		8	2	== 0 (NMI_SPEED) := -1
		15	2	NMI_Zone3: horizontal range
		17	2	NMI_Freq3: time between drops
		21	2	NMI_ZoneY3: vertical range
		19	2	== 0
	18:	Guard	;NPPower, NPSpeed, NPZone4, NPYSpeed4
		If player is within NMI_XZone4 horizontal and NMI_YZone4 vertical from (NMI_X,NMI_Y), move to player
		Else, move back to (NMI_X,NMI_Y)
		moving with speed (NMI_Speed, NMI_YSpeed4)
		Extra fields:
		Offset	Size	What it is
		--------------------------
		15	2	NMI_XZone4
		17	2	NMI_YZone4
		19	1	NMI_YSpeed4
		20	2	== NMI_X
		22	2	== NMI_Y

Most enemy types have a list of eNemy Parameter names after a semicolon in the table above.
These names define parameter groups used for that type; the following list shows
for each parameter group its values, their valid range and a sensible default value.

Unless otherwise indicated, words are signed and bytes unsigned

NPCount2:	NMI_Count2 [B20], default 50

NPPower:	NMI_Power [W12], default 50

NPPowerS:	NMI_PowerS [W15] >= 1, default 15

NPRange:	NMI_Center [W15], default NMI_X - 8
		NMI_Dist [W17] >= 0, default 16

NPSpeed:	NMI_Speed [W8], default 1

NPSpeed2:	NMI_Speed [W8] < 256, default 1

NPYSpeed4:	NMI_YSpeed4 [B19] >= 0 (signed), default 1

NPVZone:	NMI_VZone [B19], default 100
		NPVZoneDown indicates the zone is also below, besides above

NPFreq:		NMI_Freq [B16] >= 10, default 50

NPFreq2:	NMI_Freq2 [B19], default 50

NPFreq3:	NMI_Freq3 [W17] >= 1, default 50

NPZone:		NMI_Zone [W17] bit 14..15 != 3, default 0
		NMI_Zone [W17] bit 0..13 >= 0, default 100

NPZone2:	NMI_Zone2 [W23] >= 0, default 100
		NPZone2Min50 indicates the activate zone lies 50 pixels within the alert zone

NPZone3:	NMI_ZoneY3 [W21] >= 0, default 100
		NMI_Zone3 [W15] >= 0, default 100

NPZone4:	NMI_XZone4 [W15] >= 0, default 100
		NMI_YZone4 [W17] >= 0, default 100


Bonuses
-------
There are 100 bonus records, each of which has this format:

	Offset	Size	What it is
	-----------------------------
	0	1	Bonus tile
	1	1	Tile that replaces the bonus after it's picked up
	2	2	X/Y coordinates

A bonus record is skipped if X = 0xFF and Y = 0xFF.

Only tiles 253-255 can be health bonuses, the other ones' type depends on the tile flags.

Horizontal scrolling limit
--------------------------
The game will not scroll to the right of this line unless the player actually is to the right of it.
When the player is on the left again, the game does not scroll back immediately.

Gates
-----
There are 20 gate records, each of which has this format:

	Offset	Size	What it is
	-----------------------------
	0	2	X/Y coordinates of entrance
	2	2	X/Y coordinates of screen position after passing through a gate
	4	2	X/Y coordinates of exit
	6	1	Scrolling; if this is non-zero, scrolling is disabled after passing through

A gate record is skipped if Y-coordinate of entrance >= LevelSize

Elevators
---------
There are 10 elevator records, each of which has this format:

	Offset	Size	What it is
	-----------------------------
	4	2	Sprite number - 30. Bit 13 (supporting) should be set (before doing -30).
	7	1	Speed (0..7); number of pixels moved per frame
	10	2	Range; every n (>0) frames, reverse the direction
	12	2	X-coordinate
	14	2	Y-coordinate
	16	1	Direction: 0=up, 1=right, 2=down, 3=left

An elevator record is skipped if either Sprite number = 0xFFFF, Speed >= 8, X < -16 or Y < 0.

Coordinates of exit
-------------------
Here the fun ends.

Sample decompressor

#!/usr/bin/perl -w
use strict;

# Titus the Fox / Moktar level file format by Jesses (mail at ttf dot mine dot nu)
# Visit https://ttf.mine.nu for more TTF/Moktar stuff

# Sample SQZ decoder in perl, v1.2.
# Version history:
#   1.2 - add '-altlzw' option
#   1.1 - ask for input/output filenames if STDIN is a terminal
#   1.0 - initial version

# This program reads SQZ-encoded data from STDIN, and writes the result to STDOUT.
# Usage: ./unpack.pl [-altlzw] < INPUT.SQZ > OUTPUT
# The optional '-altlzw' parameter enables alternate LZW decompression, as used by CDRUN.COM on the '10 Great Games' CDROM by 'Telstar Fun and Games'.
# If STDIN is detected to be a terminal, it will instead first prompt for filenames.

# If "< INPUT.SQZ" was not used to redirect input, this prompts for filenames and redirects STDIN/STDOUT
if (-t STDIN) {
	print STDERR "Enter filename of INPUT file: ";
	my $inputFile = <STDIN>;
	chomp($inputFile);
	die "Could not read file '$inputFile'!\n" unless (-r $inputFile);
	print STDERR "Enter filename of OUTPUT file: ";
	my $outputFile = <STDIN>;
	chomp($outputFile);
	if (-e $outputFile) {
		print STDERR "Warning: file '$outputFile' already exists! Press ENTER to overwrite it or Ctrl-C to abort...\n";
		<STDIN>;
	}
	print STDERR "Unpacking '$inputFile' to '$outputFile'\n";
	open(STDIN, "<", $inputFile) or die $!;
	open(STDOUT, ">", $outputFile) or die $!;
}

binmode(STDIN);
binmode(STDOUT);

# Enable altlzw mode if parameter was given
my $altlzw = (@ARGV >= 1) && ($ARGV[0] eq '-altlzw');

# Read first 4 bytes
my $buf;
read(STDIN, $buf, 4);

# Get uncompressed size from bytes 0, 2, 3
my $unpsize = (unpack("C", $buf) << 16) + unpack("x2v", $buf);

my $size; # Number of bytes written to STDOUT, for size-check later on

# Check byte 1 for format and call the appropriate sub
if (unpack("xC", $buf) == 0x10) {
	# LZW
	print STDERR "Unpacking LZW...\n";
	$size = decodeLZW($altlzw);
} else {
	# Huffman
	print STDERR "Unpacking Huffman...\n";
	$size = decodeHuffman();
}

# Emit a warning if the unpacked size differs from the one specified in the stream
if ($size != $unpsize) {
	warn "Warning: Unpacked file should be ".$unpsize." bytes but is ".$size." bytes!\n".
	     "The '-altlzw' option may help, especially if this is an SQZ executable from the '10 Great Games' CDROM by 'Telstar Fun and Games'.\n"
}

# Done.


sub nextbyte { # return the next byte from STDIN, or zero if there are no more bytes
	my $c = getc;
	return (defined $c ? ord($c) : 0);
}

sub decodeLZW {
	my ($altlzw) = @_;

	# constants; first two are swapped if altlzw is enabled
	my $CLEAR_CODE	= 0x100;
	my $END_CODE	= 0x101;
	my $FIRST	= 0x102;
	my $MAX_TABLE	= 0x1000;
	if ($altlzw) {
		print STDERR "altlzw mode enabled.\n";
		$CLEAR_CODE	= 0x101;
		$END_CODE	= 0x100;
	}

	# variables
	my $nbit;	# Current word size
	my @dict;	# Dictionary: each entry holds [prefix_pointer, postfix, first_byte(prefix)]
	my $dictsize = $FIRST;

	my $size = 0;	# Returned to caller, number of bytes written to STDOUT

	# variables for extracting the next codeword
	my $buf24 = (ord(getc) << 16) + (ord(getc) << 8) + ord(getc);
	my $bitpos = 0;

	my $prev = $CLEAR_CODE; # Previous codeword
	while ($prev != $END_CODE) {
		if ($prev == $CLEAR_CODE) {
			$nbit = 9;
			$dictsize = $FIRST;
		}
		# Get next codeword
		$bitpos += $nbit;
		my $cw = ($buf24 >> (24-$bitpos)) & (2**$nbit - 1);
		$buf24 = ($buf24 << 8) + nextbyte();
		if ($bitpos >= 16) {
			$buf24 = ($buf24 << 8) + nextbyte();
		}
		$buf24 &= 0xFFFFFF;
		$bitpos &= 7;
		# Process the codeword $cw
		if (($cw != $CLEAR_CODE) && ($cw != $END_CODE)) {
			my $newbyte;
			if ($cw < $dictsize) {
				$newbyte = $cw < $FIRST ? $cw : ${$dict[$cw - $FIRST]}[2];
			} else {
				die "prev == CLEAR_CODE!" unless ($prev != $CLEAR_CODE);
				die "num_elem(dict) >= MAX_TABLE!" unless ($dictsize < $MAX_TABLE);
				die "cw != num_elem(dict)!" unless ($cw == $dictsize);
				$newbyte = $prev < $FIRST ? $prev : ${$dict[$prev - $FIRST]}[2];
			}
			if (($prev != $CLEAR_CODE) && ($dictsize < $MAX_TABLE)) {
				$dict[$dictsize - $FIRST] = [$prev, $newbyte, $prev < $FIRST ? $prev : ${$dict[$prev - $FIRST]}[2]];
				$dictsize++;
				if (($dictsize == 2**$nbit) && ($nbit < 12)) {
					$nbit++;
				}
			}
			my $output = '';
			my $outcw = $cw;
			while ($outcw >= $FIRST) {
				my ($prefix, $byte) = @{$dict[$outcw - $FIRST]};
				$outcw = $prefix;
				$output .= chr($byte);
			}
			$output .= chr($outcw);
			$output = reverse($output);
			print $output;
			$size += length($output);
		}
		$prev = $cw;
	}
	return $size;
}

sub decodeHuffman {
	my $HTS = ord(getc) + (ord(getc) << 8);
	my $buf;
	read(STDIN, $buf, $HTS);
	my @HT = unpack("v".($HTS/2), $buf);

	my $size = 0;	# Returned to caller, number of bytes written to STDOUT

	# Huffman decoding variables
	my $node = 0;

	# RLE decoding variables
	my $state = 0;
	my $last;
	my $count;

	while (defined(my $word = getc)) {
		$word = ord($word);
		for (my $bit=128; $bit>=1; $bit >>= 1) {
			if ($word & $bit) {
				$node++;
			}
			unless ($HT[$node] & 0x8000) {
				$node = $HT[$node] / 2;
			} else {
				my $cw = $HT[$node] & 0x7FFF;
				my $L = $cw & 255;
				my $H = $cw >> 8;
				if ($state == 0) {
					if ($H == 0) {
						$last = chr($L);
						print $last;
						$size++;
					} elsif ($L == 0) {
						$state = 1;
					} elsif ($L == 1) {
						$state = 2;
					} else {
						print $last x $L;
						$size += $L;
					}
				} elsif ($state == 1) {
					print $last x $cw;
					$size += $cw;
					$state = 0;
				} elsif ($state == 2) {
					$count = $L*256;
					$state = 3;
				} elsif ($state == 3) {
					$count += $L;
					print $last x $count;
					$size += $count;
					$state = 0;
				}
				$node = 0;
			}
		}
	}
	return $size;
}