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.