HNM (1)
HNM 1, developed and used by Cryo Interactive, is a second generation FMV format. It operates in fixed 320x200x256 resolution.
All numbers are little endian.
File Structure
File consist of series of chunks, where all single frame related chunks (e.g. it's video and audio portion) enclosed in a "superchunks".
Each file begins with a header chunk, carrying palette and superchunk offsets table:
word header_size - size of this header chunk
byte palette[] - palette block
byte padding[] - zero or more 0xFF bytes of padding
dword offsets[] - 32-bit offsets of each superchunk, to the end of header chunk.
All offsets are relative to the first superchunk.
Last table entry points to the end of file.
- It is not neccessary to use offsets table to (quickly) navigate through the file. All superchunks comes without gaps inbetween.
Header chunk followed by one or more frame superchunks. Superchunk format:
word superchunk_size - size of the whole superchunk byte chunk1[] ... - zero or more typed chunks byte chunkN[] byte videoframe[] - video chunk to the end of superchunk
As soon as videoframe chunk encountered and processed, decoder should continue to the next superchunk.
Typed chunk format:
word chunk_tag - TwoCC chunk id word chunk_size - size of the chunk byte data[chunk_size-4] - chunk data of (chunk_size-4) bytes total
Palette Block
Palette block contains one or more palette ranges to update.
byte start - index of the first entry to update byte count - number of entries to update (0 means 256) byte rgb[count*3] - RGB triplets of the new palette
Start and count both set to 0xFF indicates end of the palette block. Otherwise, change specified colors and process to the next range update block.
Typed chunks
pl Chunk
New palette block. See description above.
sd Chunk
Sound data. Contains complete VOC file splitted over several blocks.
pt Chunk
Game related, ignore.
kl Chunk
Collision map? Game related, ignore.
Video Chunk
Unlike typed chunks, video chunk has no standard chunk header, thus, it's the last chunk of superchunk. It has at least following 4-byte header in bits:
0..8 width - width of the frame
9 flag1 - supposedly, indicates video data presence
10 flag2 - supposedly, indicates that the frame is not overlayed
12..14 unused - you can use these (zero) bits to distinguish this chunk from typed ones
15 flag3 - rarely seen set on some frames, purpose not yet known
16..23 height - height of the frame
24..31 mode - frame rendering mode
- Each new frame may update only partial area of actual 320x200 viewport.
- Zero frame width/height indicates that video is not changed from the previous frame, thus, no any video data follows this header.
- Mode 0xFE renders whole unpacked frame over previous frame.
- Mode 0xFF overlays unpacked frame with previous frame, it's pixels with color index 0 indicates transparency.
Video Decompression
If video height is non-zero, frame chunks continues with actual compressed frame data. It may be either legacy LZ-compressed raster or RLE-encoded overlay frame.
First, calculate sum of the first 6 bytes of the frame data. Depending on the calculated value, choose apropriate method.
Video Block 171
A keyframe. Unpack whole block using following algorithm (same algorithm used to decode some other game's resources as well).
First, reinterpret 6-byte header (the one you've calculated checksum of before) as follows:
word unpacked_len - size of the unpacked data byte zero - should be zero word packed_len - size of the packed data, including this header byte salt - adjust checksum to 171 (0xAB)
Next, perform actual decompression:
Initialize bit-reader with 16-bits internal queue. When you need to take next bit from queue but it's already fully depleted, refill it with next 16-bits value from the stream. Straw queue bits from lowest to highest.
repeat
get next bit of the queue
if bit is non-zero
copy next byte of the input to the output
else
get next bit of the queue
if bit is non-zero
count = next 3 bits of the input
offset = next 13 bits of the input minus 8192
if count is zero
count = next 8 bits of the input
if count is zero
finish the unpacking
else
count = next bit of the queue * 2 + next bit of the queue
offset = next 8 bits of the input minus 256
count += 2
copy count bytes at (output + offset) to the output
Video Block 172
Looks like it's not used anywhere. TBD.
Video Block 173
This is an overlay block. Reinterpret block header as:
word frame_size - size of the final unpacked frame (in bytes)
word codebook_size - size of the unpacked frame codebook
byte flags - bit 2 : use zero x/y coordinates
bit 6 : use color base 128, 0 otherwise
bit 7 : compression method (long runs)
byte salt - fixes up header's checksum to be 173 (0xAD)
If flags bit 2 is zero, read extra block's x and y coordinates, 16 bits each. Otherwise, assume them both set to zero.
Following decompression routines use slightly modified bit-reader. Unlike 171 decompression, now it takes bits starting from the highest one. Other rules remains the same.
After processing the header, unpack LZ-compressed codebook as follows:
repeat while codebook_size of output not generated
tag = next byte of the input
if bit 7 of tag is set
if length_code not yet encountered or already depleted
length_code = next 8 byte of the input
temp = top nibble of the length_code
else
temp = low nibble of the length_code
assume length_code depleted
offset = 7 bits of tag merged with lowest bit of temp
count = top 3 bits of temp plus 2
copy count bytes at (output - offset - 1) to the output
else
if tag is non-zero
tag = tag + color base (see flags bit 6)
output tag
Immediately after codebook rle-compressed raster begins. Decode it as follows:
if using long runs method (see flags above)
repeat while frame_size of output not generated
while next bit is zero
ouput = next byte of codebook
pixel = next byte of codebook
if next bit is zero
if length_code not yet encountered or already depleted
length_code = next byte of the input
length = top nibble of the length_code
else
length = low nibble of the length_code
assume length_code depleted
if length is zero
length = next byte of the input plus 16
fill next length + 4 bytes of the output with pixel
else
if next bit is zero
fill next 2 bytes of the output with pixel
else
if next bit is zero
fill next 3 bytes of the output with pixel
else
fill next 4 bytes of the output with pixel
else (short runs, same as above, but reordered)
repeat while frame_size of output not generated
while next bit is zero
ouput = next byte of codebook
pixel = next byte of codebook
if next bit is zero
fill next 2 bytes of the output with pixel
else
if next bit is zero
fill next 3 bytes of the output with pixel
else
if next bit is zero
fill next 4 bytes of the output with pixel
else
if length_code not yet encountered or already depleted
length_code = next byte of the input
length = top nibble of the length_code
else
length = low nibble of the length_code
assume length_code depleted
if length is zero
length = next byte of the input plus 16
fill next length + 4 bytes of the output with pixel
Known issues
- Many movies from the game "Dune" do not have their own palettes and thereby reuse the current in-game palette.
- For some unknown reasons AABBBBB.HNM file from Dune decodes incorrectly.