"""File format classes for the game Lemmings (DOS, 1991).
Sources:
DAT compressor
http://www.camanis.net/lemmings/files/docs/lemmings_dat_file_format.txt
Level file format
http://www.camanis.net/lemmings/files/docs/lemmings_lvl_file_format.txt
Vgagr/Ground DAT file formats
http://www.camanis.net/lemmings/files/docs/lemmings_vgagrx_dat_groundxo_dat_file_format.txt
Main DAT file format
http://www.camanis.net/lemmings/files/docs/lemmings_main_dat_file_format.txt
Vgaspec compressor/DAT file format
http://www.camanis.net/lemmings/files/docs/lemmings_vgaspecx_dat_file_format.txt
Extra special thanks to ccexplore and Mindless
"""
import itertools
from enum import IntEnum
import logging
logger = logging.getLogger( __name__ )
from mrcrowbar import models as mrc
from mrcrowbar.lib.hardware import ibm_pc
from mrcrowbar.lib.images import base as img
from mrcrowbar import bits, utils
[docs]
class DATCompressor( mrc.Transform ):
@staticmethod
def _xor_checksum( data ):
lrc = 0
for b in data:
lrc ^= b
return lrc
[docs]
def import_data( self, buffer, parent=None ):
assert utils.is_bytes( buffer )
pointer = 0;
total_num_bytes = len( buffer )
bit_count = utils.from_uint8( buffer[pointer:pointer + 1] )
checksum = utils.from_uint8( buffer[pointer + 1:pointer + 2] )
decompressed_size = utils.from_uint32_be( buffer[pointer + 2:pointer + 6] )
compressed_size = utils.from_uint32_be( buffer[pointer + 6:pointer + 10] )
pointer += 10
total_num_bytes -= 10
compressed_size -= 10
compressed_data = bytearray( buffer[pointer:pointer + compressed_size] )
if checksum != self._xor_checksum( compressed_data ):
logger.warning( '{}: Checksum doesn\'t match header'.format( self ) )
pointer += compressed_size
total_num_bytes -= compressed_size
# first byte of compressed data is shifted wrongly, fix
compressed_data[-1] = (compressed_data[-1] << (8 - bit_count)) & 0xff
bs = bits.BitStream( compressed_data, start_offset=(compressed_size - 1, bit_count - 1), bytes_reverse=True, bit_endian='little', io_endian='big' )
def copy_prev_data( blocklen, offset_size, state ):
offset = bs.read( offset_size )
for i in range( blocklen ):
state['dptr'] -= 1
state['ddata'][state['dptr']] = state['ddata'][state['dptr'] + offset + 1]
return
def dump_data( num_bytes, state ):
for i in range( num_bytes ):
state['dptr'] -= 1
state['ddata'][state['dptr']] = bs.read( 8 )
return
state = {
'dptr': decompressed_size,
'ddata': bytearray( decompressed_size ),
}
while True:
if bs.read( 1 ) == 1:
test = bs.read( 2 )
if test==0:
copy_prev_data( 3, 9, state )
elif test==1:
copy_prev_data( 4, 10, state )
elif test==2:
copy_prev_data( bs.read( 8 ) + 1, 12, state )
elif test==3:
dump_data( bs.read( 8 )+9, state )
else:
test = bs.read( 1 )
if test==0:
dump_data( bs.read( 3 ) + 1, state )
elif test==1:
copy_prev_data( 2, 8, state )
if not (state['dptr'] > 0):
break
return mrc.TransformResult( payload=bytes( state['ddata'] ), end_offset=pointer )
[docs]
def export_data( self, buffer, parent=None ):
assert utils.is_bytes( buffer )
decompressed_size = len( buffer )
bs = bits.BitStream( bit_endian='big', io_endian='little' )
pointer = 0
def encode_raw_data( length, bs ):
assert length <= 255 + 9
if length > 8:
bs.write( length - 9, 8 )
bs.write( 0x7, 3 )
elif length > 0:
bs.write( length - 1, 3 )
bs.write( 0x0, 2 )
def find_reference():
# main form of compression is of the form:
# - while decompressing from end to start
# - look forward [up to max_offset] bytes in the decompressed data
# - copy [up to max_length] bytes to the current decompression position
# the largest offset supported by the file format is 4096, but this means
# every call to find_reference loops 4096 times.
# this takes foreeeever in Python!
# because the compression is worthless and time is money, max_offset has
# been slashed to 16 to speed up proceedings.
#max_offset = (1 << 12) + 1
max_offset = (1 << 4) + 1
# largest length supported by the file format is 256
max_length = (1 << 8) + 1
length = 4 # throw away short references
offset = 0
short_offset = [0, 0, 0]
for i in range( pointer+1, pointer+max_offset ):
temp_len = 0
while (temp_len < max_length) and (i + temp_len < decompressed_size):
# record short references
if (temp_len >= 2) and (temp_len <= 4):
if short_offset[temp_len - 2] == 0:
short_offset[temp_len - 2] = i - pointer
if buffer[pointer+temp_len] != buffer[i + temp_len]:
break
temp_len += 1
if temp_len == max_length:
temp_len -= 1
# largest reference so far? use it
if temp_len > length:
length = temp_len
offset = i - pointer
assert length < max_length
assert offset < max_offset
# no long references? try short
if (offset == 0):
for i in (2, 1, 0):
max_short_offset = (1 << (i + 8)) + 1
if (short_offset[i] > 0) and (short_offset[i] < max_short_offset):
length = i + 2
offset = short_offset[i]
break
return length, offset
raw = 0
while pointer < decompressed_size:
length, ref = find_reference()
if ref > 0:
if raw > 0:
encode_raw_data( raw, bs )
raw = 0
if length > 4:
bs.write( ref - 1, 12 )
bs.write( length - 1, 8 )
bs.write( 0x6, 3 )
elif length == 4:
bs.write( ref - 1, 10 )
bs.write( 0x5, 3 )
elif length == 3:
bs.write( ref - 1, 9 )
bs.write( 0x4, 3 )
elif length == 2:
bs.write( ref - 1, 8 )
bs.write( 0x1, 2 )
pointer += length
else:
bs.write( buffer[pointer], 8 )
raw += 1
if raw == 264:
encode_raw_data( raw, bs )
raw = 0
pointer += 1
encode_raw_data( raw, bs )
compressed_data = bytearray( bs.get_buffer() )
compressed_data[-1] = compressed_data[-1] >> (8 - bs.tell()[1])
compressed_size = len( compressed_data ) + 10
checksum = self._xor_checksum( compressed_data )
output = bytearray( 6 )
output[0:1] = utils.to_uint8( bs.tell()[1] )
output[1:2] = utils.to_uint8( checksum )
output[2:6] = utils.to_uint32_be( decompressed_size )
output[6:10] = utils.to_uint32_be( compressed_size )
output.extend( compressed_data )
return mrc.TransformResult( payload=bytes( output ) )
[docs]
class SpecialCompressor( mrc.Transform ):
DECOMPRESSED_SIZE = 14400
plan = img.Planarizer( bpp=3, width=960, height=40 )
[docs]
def import_data( self, buffer, parent=None ):
assert utils.is_bytes( buffer )
result = []
buf_out = []
i = 0
while i < len( buffer ):
# 0x00 <= n < 0x80: copy next n+1 bytes to output stream
if buffer[i] in range( 0x00, 0x80 ):
count = buffer[i]+1
buf_out.append( buffer[i+1:i+1+count] )
i += count+1
# n == 0x80: end of segment
elif buffer[i] == 0x80:
product = b''.join( buf_out )
if len( product ) != self.DECOMPRESSED_SIZE:
logger.warning( '{}: was expecting {} bytes of data, got {}'.format( self, self.DECOMPRESSED_SIZE, len( product ) ) )
result.append( product )
buf_out = []
i += 1
# 0x81 <= n < 0xff: repeat next byte (257-n) times
else:
count = 257-buffer[i]
buf_out.append( buffer[i+1:i+2]*count )
i += 2
if buf_out:
logger.warning( '{}: EOF reached before last RLE block closed'.format( self ) )
result.append( b''.join( buf_out ) )
# result is a 960x160 3bpp image, divided into 4x 40 scanline segments
unpack = (self.plan.import_data( x ).payload for x in result)
return mrc.TransformResult( payload=bytes( itertools.chain( *unpack ) ), end_offset=i )
[docs]
def export_data( self, buffer, parent=None ):
assert utils.is_bytes( buffer )
assert len( buffer ) == 960*160
segments = (buffer[960*40*i:960*40*(i+1)] for i in range(4))
segments = (self.plan.export_data( x ).payload for x in segments)
result = bytearray()
for segment in segments:
pointer = 0
while pointer < len( segment ):
start = pointer
end = pointer+1
if end >= len( segment ):
result.append( 0x00 )
result.append( segment[start] )
pointer += 1
elif segment[end] == segment[start]:
while ((end+1) < len( segment )) and (segment[end+1] == segment[end]) and (end-start < 127):
end += 1
result.append( 257-(end+1-start) )
result.append( segment[start] )
pointer = end+1
else:
while ((end+1) < len( segment )) and (segment[end+1] != segment[end]) and (end-1-start < 128):
end += 1
result.append( end-1-start )
result.extend( segment[start:end] )
pointer = end
result.append( 0x80 )
return mrc.TransformResult( payload=bytes( result ) )
# this palette is actually stored in the first half of each GroundDat palette block,
# but it's handy to have a static copy for e.g. checking out the MainAnims block
LEMMINGS_VGA_DEFAULT_PALETTE = (
ibm_pc.VGAColour( b'\x00\x00\x00' ),
ibm_pc.VGAColour( b'\x10\x10\x38' ),
ibm_pc.VGAColour( b'\x00\x2c\x00' ),
ibm_pc.VGAColour( b'\x3c\x34\x34' ),
ibm_pc.VGAColour( b'\x3c\x3c\x00' ),
ibm_pc.VGAColour( b'\x3c\x08\x08' ),
ibm_pc.VGAColour( b'\x20\x20\x20' ),
ibm_pc.VGAColour( b'\x38\x20\x08' ), # dirt colour
)
# the following palette is embedded in the Lemmings executable
LEMMINGS_VGA_MENU_PALETTE = (
ibm_pc.VGAColour( b'\x00\x00\x00' ),
ibm_pc.VGAColour( b'\x20\x10\x08' ),
ibm_pc.VGAColour( b'\x18\x0c\x08' ),
ibm_pc.VGAColour( b'\x0c\x00\x04' ),
ibm_pc.VGAColour( b'\x08\x02\x1f' ),
ibm_pc.VGAColour( b'\x10\x0b\x24' ),
ibm_pc.VGAColour( b'\x1a\x16\x29' ),
ibm_pc.VGAColour( b'\x26\x23\x2f' ),
ibm_pc.VGAColour( b'\x00\x14\x00' ),
ibm_pc.VGAColour( b'\x00\x18\x04' ),
ibm_pc.VGAColour( b'\x00\x1c\x08' ),
ibm_pc.VGAColour( b'\x00\x20\x10' ),
ibm_pc.VGAColour( b'\x34\x34\x34' ),
ibm_pc.VGAColour( b'\x2c\x2c\x00' ),
ibm_pc.VGAColour( b'\x10\x14\x2c' ),
ibm_pc.VGAColour( b'\x38\x20\x24' ),
)
##########
# levelXXX.dat parser
##########
[docs]
class Interactive( mrc.Block ):
"""Represents a single interactive piece placed in a level."""
#: Raw value for the x position of the left edge.
x_raw = mrc.Int16_BE( 0x00, range=range( -8, 1601 ) )
#: The y position of the top edge.
y = mrc.Int16_BE( 0x02, range=range( -41, 201 ) )
#: Index of the InteractiveInfo block in the accompanying GroundDAT.
obj_id = mrc.UInt16_BE( 0x04, range=range( 0, 16 ) )
#: If 1, blit image behind background.
draw_back = mrc.Bits( 0x06, 0b10000000 )
#: If 1, draw piece flipped vertically.
draw_masked = mrc.Bits( 0x06, 0b01000000 )
#: If 1, draw piece as a hole.
draw_upsidedown = mrc.Bits( 0x07, 0b10000000 )
#: Check to ensure the last chunk of the block is empty.
mod_check = mrc.Const( mrc.UInt16_BE( 0x06, bitmask=b'\x3f\x7f' ), 0x000f )
@property
def x( self ):
"""The x position of the left edge."""
return (self.x_raw-16) - ((self.x_raw-16) % 8)
@property
def repr( self ):
return "obj_id={}, x={}, y={}".format( self.obj_id, self.x, self.y )
[docs]
class Terrain( mrc.Block ):
"""Represents a single terrain piece placed in a level."""
#: Raw value for the x position of the left edge.
x_raw = mrc.UInt16_BE( 0x00, bitmask=b'\x0f\xff' )
#: If 1, blit image behind background.
draw_back = mrc.Bits( 0x00, 0b10000000 )
#: If 1, draw piece flipped vertically.
draw_upsidedown = mrc.Bits( 0x00, 0b01000000 )
#: If 1, draw piece as a hole.
draw_erase = mrc.Bits( 0x00, 0b00100000 )
#: Raw value (coarse component) for the y position of the top edge.
y_raw_coarse = mrc.Int8( 0x02 )
#: Raw value (fine component) for the y position of the top edge.
y_raw_fine = mrc.Bits( 0x03, 0b10000000 )
unknown_1 = mrc.Bits( 0x03, 0b01000000 )
#: Index of the TerrainInfo block in the accompanying GroundDAT.
obj_id = mrc.UInt8( 0x03, bitmask=b'\x3f', range=range( 0, 64 ) )
@property
def x( self ):
"""The x position of the left edge."""
return (self.x_raw-16)
@property
def y( self ):
"""The y position of the top edge."""
return (self.y_raw_coarse*2 + self.y_raw_fine)-4
@property
def repr( self ):
return "obj_id={}, x={}, y={}".format( self.obj_id, self.x, self.y )
[docs]
class SteelArea( mrc.Block ):
"""Represents an indestructable rectangular area in a level."""
#: Raw value (coarse component) for the x position of the left edge.
x_raw_coarse = mrc.UInt8( 0x00, range=range( 0, 200 ) )
#: Raw value (fine component) for the x position of the left edge.
x_raw_fine = mrc.Bits( 0x01, 0b10000000 )
#: Raw value for the y position of the area's top edge.
y_raw = mrc.UInt8( 0x01, bitmask=b'\x7f', range=range( 0, 128 ) )
#: Raw value for the width.
width_raw = mrc.Bits( 0x02, 0b11110000 )
#: Raw value for the height.
height_raw = mrc.Bits( 0x02, 0b00001111 )
#: Check to ensure the last byte of the block is empty.
mod_check = mrc.Const( mrc.UInt8( 0x03 ), 0x00 )
@property
def x( self ):
"""The x position of the left edge."""
return (self.x_raw_coarse*2 + self.x_raw_fine)*4-16
@property
def y( self ):
"""The y position of the top edge."""
return (self.y_raw*4)
@property
def width( self ):
"""Width of the steel area."""
return (self.width_raw+1)*4
@property
def height( self ):
"""Height of the steel area."""
return (self.height_raw+1)*4
@property
def repr( self ):
return "x={}, y={}, width={}, height={}".format( self.x, self.y, self.width, self.height )
[docs]
class Level( mrc.Block ):
"""Represents a single Lemmings level."""
#: Minimum Lemming release-rate.
release_rate = mrc.UInt16_BE( 0x0000, range=range( 0, 251 ) )
#: Number of Lemmings released.
num_released = mrc.UInt16_BE( 0x0002, range=range( 0, 115 ) )
#: Number of Lemmings required to be saved.
num_to_save = mrc.UInt16_BE( 0x0004, range=range( 0, 115 ) )
#: Time limit for the level (minutes).
time_limit_mins = mrc.UInt16_BE( 0x0006, range=range( 0, 256 ) )
#: Number of Climber skills.
num_climbers = mrc.UInt16_BE( 0x0008, range=range( 0, 251 ) )
#: Number of Floater skills.
num_floaters = mrc.UInt16_BE( 0x000a, range=range( 0, 251 ) )
#: Number of Bomber skills.
num_bombers = mrc.UInt16_BE( 0x000c, range=range( 0, 251 ) )
#: Number of Blocker skills.
num_blockers = mrc.UInt16_BE( 0x000e, range=range( 0, 251 ) )
#: Number of Builder skills.
num_builders = mrc.UInt16_BE( 0x0010, range=range( 0, 251 ) )
#: Number of Basher skills.
num_bashers = mrc.UInt16_BE( 0x0012, range=range( 0, 251 ) )
#: Number of Miner skills.
num_miners = mrc.UInt16_BE( 0x0014, range=range( 0, 251 ) )
#: Number of Digger skills.
num_diggers = mrc.UInt16_BE( 0x0016, range=range( 0, 251 ) )
#: Raw value for the start x position of the camera.
camera_x_raw = mrc.UInt16_BE( 0x0018, range=range( 0, 1265 ) )
#: Index denoting which graphical Style to use.
style_index = mrc.UInt16_BE( 0x001a )
#: Index denoting which Special graphic to use (optional).
custom_index = mrc.UInt16_BE( 0x001c )
#: List of Interactive object references (32 slots).
interactives = mrc.BlockField( Interactive, 0x0020, count=32, fill=b'\x00'*8 )
#: List of Terrain object references (400 slots).
terrains = mrc.BlockField( Terrain, 0x0120, count=400, fill=b'\xff'*4 )
#: List of SteelArea object references (32 slots).
steel_areas = mrc.BlockField( SteelArea, 0x0760, count=32, fill=b'\x00'*4 )
#: Name of the level (ASCII string).
name = mrc.Bytes( 0x07e0, length=32, default=b' ' )
@property
def camera_x( self ):
"""Start x position of the camera."""
return self.camera_x_raw - (self.camera_x_raw % 8)
@property
def repr( self ):
return self.name.strip().decode( 'utf8' )
[docs]
class LevelDAT( mrc.Block ):
levels = mrc.BlockField( Level, 0x0000, stream=True, transform=DATCompressor() )
##########
# oddtable.dat parser
##########
[docs]
class OddRecord( mrc.Block ):
"""Represents an alternative set of conditions for a level.
Used in Lemmings to repeat the same level with a different difficulty.
"""
#: Minimum Lemming release-rate.
release_rate = mrc.UInt16_BE( 0x0000, range=range( 0, 251 ) )
#: Number of Lemmings released.
num_released = mrc.UInt16_BE( 0x0002, range=range( 0, 115 ) )
#: Number of Lemmings required to be saved.
num_to_save = mrc.UInt16_BE( 0x0004, range=range( 0, 115 ) )
#: Time limit for the level (minutes).
time_limit_mins = mrc.UInt16_BE( 0x0006, range=range( 0, 256 ) )
#: Number of Climber skills.
num_climbers = mrc.UInt16_BE( 0x0008, range=range( 0, 251 ) )
#: Number of Floater skills.
num_floaters = mrc.UInt16_BE( 0x000a, range=range( 0, 251 ) )
#: Number of Bomber skills.
num_bombers = mrc.UInt16_BE( 0x000c, range=range( 0, 251 ) )
#: Number of Blocker skills.
num_blockers = mrc.UInt16_BE( 0x000e, range=range( 0, 251 ) )
#: Number of Builder skills.
num_builders = mrc.UInt16_BE( 0x0010, range=range( 0, 251 ) )
#: Number of Basher skills.
num_bashers = mrc.UInt16_BE( 0x0012, range=range( 0, 251 ) )
#: Number of Miner skills.
num_miners = mrc.UInt16_BE( 0x0014, range=range( 0, 251 ) )
#: Number of Digger skills.
num_diggers = mrc.UInt16_BE( 0x0016, range=range( 0, 251 ) )
#: Name of the level (ASCII string).
name = mrc.Bytes( 0x0018, length=32, default=b' ' )
@property
def repr( self ):
return self.name.strip().decode( 'utf8' )
[docs]
class OddtableDAT( mrc.Block ):
"""Represents a collection of OddRecord objects."""
#: List of OddRecord objects (80 slots).
records = mrc.BlockField( OddRecord, 0x0000, count=80 )
##########
# groundXo.dat and vgagrX.dat parser
##########
[docs]
class InteractiveImage( mrc.Block ):
"""Represents the sprite data for an interactive object."""
image_data = mrc.Bytes(
0x0000, transform=img.Planarizer(
bpp=4,
width=mrc.Ref( '_parent.width' ),
height=mrc.Ref( '_parent.height' ),
frame_count=mrc.Ref( '_parent.end_frame' ),
frame_stride=mrc.Ref( '_parent.frame_data_size' )
)
)
mask_data = mrc.Bytes(
mrc.Ref( '_parent.mask_rel_offset' ),
transform=img.Planarizer(
bpp=1,
width=mrc.Ref( '_parent.width' ),
height=mrc.Ref( '_parent.height' ),
frame_count=mrc.Ref( '_parent.end_frame' ),
frame_stride=mrc.Ref( '_parent.frame_data_size' )
)
)
def __init__( self, *args, **kwargs ):
super().__init__( *args, **kwargs )
self.image = img.IndexedImage(
self,
width=mrc.Ref( '_parent.width' ),
height=mrc.Ref( '_parent.height' ),
source=mrc.Ref( 'image_data' ),
frame_count=mrc.Ref( '_parent.end_frame' ),
palette=mrc.Ref( '_parent._parent.palette' ),
mask=mrc.Ref( 'mask_data' )
)
[docs]
class TriggerEffect( IntEnum ):
NONE = 0x00
EXIT_LEVEL = 0x01
TRAP = 0x04
DROWN = 0x05
DISINTEGRATE = 0x06
ONEWAY_LEFT = 0x07
ONEWAY_RIGHT = 0x08
STEEL = 0x09 # not used?
[docs]
class SoundEffect( IntEnum ):
NONE = 0x00
SKILL_SELECT = 0x01
HATCH_OPEN = 0x02
LETS_GO = 0x03
ASSIGN = 0x04
OH_NO = 0x05
ELECTRIC_TRAP = 0x06
CRUSH_TRAP = 0x07
SPLAT = 0x08
ROPE_TRAP = 0x09
HIT_STEEL = 0x0a
UNKNOWN_1 = 0x0b
BOMBER = 0x0c
FIRE_TRAP = 0x0d
HEAVY_TRAP = 0x0e
BEAR_TRAP = 0x0f
YIPPEE = 0x10
DROWN = 0x11
BUILDER = 0x12
[docs]
class InteractiveInfo( mrc.Block ):
"""Contains a Ground style definition for an interactive object."""
anim_flags = mrc.UInt16_LE( 0x0000 )
start_frame = mrc.UInt8( 0x0002 )
end_frame = mrc.UInt8( 0x0003 )
width = mrc.UInt8( 0x0004 )
height = mrc.UInt8( 0x0005 )
frame_data_size = mrc.UInt16_LE( 0x0006 )
mask_rel_offset = mrc.UInt16_LE( 0x0008 )
unknown_1 = mrc.UInt16_LE( 0x000a )
unknown_2 = mrc.UInt16_LE( 0x000c )
trigger_x_raw = mrc.UInt16_LE( 0x000e )
trigger_y_raw = mrc.UInt16_LE( 0x0010 )
trigger_width_raw = mrc.UInt8( 0x0012 )
trigger_height_raw = mrc.UInt8( 0x0013 )
trigger_effect = mrc.UInt8( 0x0014, enum=TriggerEffect )
base_offset = mrc.UInt16_LE( 0x0015 )
preview_frame = mrc.UInt16_LE( 0x0017 )
unknown_3 = mrc.UInt16_LE( 0x0019 )
#: Sound effect to play. Only used when trigger_effect is set to TRAP.
sound_effect = mrc.UInt8( 0x001b, enum=SoundEffect )
vgagr = mrc.StoreRef( InteractiveImage, mrc.Ref( '_parent._vgagr.interact_store.store' ), mrc.Ref( 'base_offset' ), mrc.Ref( 'size' ) )
@property
def size( self ):
return self.frame_data_size*self.end_frame
@property
def plane_padding( self ):
return self.width*self.height//8
@property
def trigger_x( self ):
return self.trigger_x_raw * 4
@property
def trigger_y( self ):
return self.trigger_y_raw * 4 - 4
@property
def trigger_width( self ):
return self.trigger_width_raw * 4
@property
def trigger_height( self ):
return self.trigger_height_raw * 4
[docs]
class TerrainImage( mrc.Block ):
image_data = mrc.Bytes(
0x0000, transform=img.Planarizer(
bpp=4,
width=mrc.Ref( '_parent.width' ),
height=mrc.Ref( '_parent.height' ),
)
)
mask_data = mrc.Bytes(
mrc.Ref( '_parent.mask_offset' ),
transform=img.Planarizer(
bpp=1,
width=mrc.Ref( '_parent.width' ),
height=mrc.Ref( '_parent.height' ),
)
)
def __init__( self, *args, **kwargs ):
super().__init__( *args, **kwargs )
self.image = img.IndexedImage(
self,
width=mrc.Ref( '_parent.width' ),
height=mrc.Ref( '_parent.height' ),
source=mrc.Ref( 'image_data' ),
palette=mrc.Ref( '_parent._parent.palette' ),
mask=mrc.Ref( 'mask_data' )
)
[docs]
class TerrainInfo( mrc.Block ):
width = mrc.UInt8( 0x0000 )
height = mrc.UInt8( 0x0001 )
base_offset = mrc.UInt16_LE( 0x0002 )
mask_rel_offset = mrc.UInt16_LE( 0x0004 )
unknown_1 = mrc.UInt16_LE( 0x0006 )
vgagr = mrc.StoreRef( TerrainImage, mrc.Ref( '_parent._vgagr.terrain_store.store' ), mrc.Ref( 'base_offset' ), mrc.Ref( 'size' ) )
@property
def size( self ):
return self.width*self.height*5//8
@property
def mask_offset( self ):
return self.mask_rel_offset-self.base_offset
#return self.width*self.height*4//8
@property
def mask_stride( self ):
return self.width*self.height*4//8
@property
def mask_size( self ):
return self.width*self.height//8
[docs]
class GroundDAT( mrc.Block ):
"""Represents a single graphical style."""
_vgagr = None # should be replaced by the correct VgagrDAT object
#: Information for every type of interactive piece.
interactive_info = mrc.BlockField( InteractiveInfo, 0x0000, count=16, fill=b'\x00'*28 )
#: Information for every type of terrain piece.
terrain_info = mrc.BlockField( TerrainInfo, 0x01c0, count=64, fill=b'\x00'*8 )
#: Extended EGA palette used for rendering the level preview.
palette_ega_preview = img.Palette( ibm_pc.EGAColour, 0x03c0, count=8 )
#: Copy of EGA palette used for rendering lemmings/action bar.
#: Colours 0-6 are not used by the game, instead there is a palette embedded
#: in the executable.
#: Colour 7 is used for drawing the minimap and dirt particles.
palette_ega_standard = img.Palette( ibm_pc.EGAColour, 0x03c8, count=8 )
#: EGA palette used for rendering interactive/terrain pieces.
palette_ega_custom = img.Palette( ibm_pc.EGAColour, 0x03d0, count=8 )
#: VGA palette used for rendering interactive/terrain pieces.
palette_vga_custom = img.Palette( ibm_pc.VGAColour, 0x03d8, count=8 )
#: Copy of VGA palette used for rendering lemmings/action bar.
#: Colours 0-6 are not used by the game, instead there is a palette embedded
#: in the executable.
#: Colour 7 is used for drawing the minimap and dirt particles.
palette_vga_standard = img.Palette( ibm_pc.VGAColour, 0x03f0, count=8 )
#: VGA palette used for rendering the level preview.
palette_vga_preview = img.Palette( ibm_pc.VGAColour, 0x0408, count=8 )
@property
def palette( self ):
if not hasattr( self, '_palette' ):
self._palette = [img.Transparent()] + self.palette_vga_standard[1:] + self.palette_vga_custom
return self._palette
[docs]
class VgagrStore( mrc.Block ):
"""Represents a blob store for style graphics."""
data = mrc.Bytes( 0x0000 )
def __init__( self, *args, **kwargs ):
super().__init__( *args, **kwargs )
self.store = mrc.Store( self, mrc.Ref( 'data' ) )
[docs]
class VgagrDAT( mrc.Block ):
stores = mrc.BlockField( VgagrStore, 0x0000, stream=True, transform=DATCompressor() )
@property
def terrain_store( self ):
return self.stores[0]
@property
def interact_store( self ):
return self.stores[1]
##########
# main.dat parser
##########
[docs]
class Anim( mrc.Block ):
image_data = mrc.Bytes( 0x0000, transform=img.Planarizer( bpp=mrc.Ref( 'bpp' ), width=mrc.Ref( 'width' ), height=mrc.Ref( 'height' ), frame_count=mrc.Ref( 'frame_count' ) ) )
def __init__( self, width, height, bpp, frame_count, *args, **kwargs ):
self.width = width
self.height = height
self.bpp = bpp
self.frame_count = frame_count
self.image = img.IndexedImage( self, width=width, height=height, source=mrc.Ref( 'image_data' ), frame_count=frame_count, palette=LEMMINGS_VGA_DEFAULT_PALETTE )
super().__init__( *args, **kwargs )
AnimField = lambda offset, width, height, bpp, frame_count: mrc.BlockField( Anim, offset, block_kwargs={ 'width': width, 'height': height, 'bpp': bpp, 'frame_count': frame_count } )
# the following animation/sprite lookup tables are embedded in the Lemmings executable
[docs]
class MainAnims( mrc.Block ):
anim_walker_r = AnimField( 0x0000, 16, 10, 2, 8 )
anim_bounder_r = AnimField( 0x0140, 16, 10, 2, 1 )
anim_walker_l = AnimField( 0x0168, 16, 10, 2, 8 )
anim_bounder_l = AnimField( 0x02a8, 16, 10, 2, 1 )
anim_digger = AnimField( 0x02d0, 16, 14, 3, 16 )
anim_climber_r = AnimField( 0x0810, 16, 12, 2, 8 )
anim_climber_l = AnimField( 0x0990, 16, 12, 2, 8 )
anim_drowner = AnimField( 0x0b10, 16, 10, 2, 16 )
anim_postclimber_r = AnimField( 0x0d90, 16, 12, 2, 8 )
anim_postclimber_l = AnimField( 0x0f10, 16, 12, 2, 8 )
anim_builder_r = AnimField( 0x1090, 16, 13, 3, 16 )
anim_builder_l = AnimField( 0x1570, 16, 13, 3, 16 )
anim_basher_r = AnimField( 0x1a50, 16, 10, 3, 32 )
anim_basher_l = AnimField( 0x21d0, 16, 10, 3, 32 )
anim_miner_r = AnimField( 0x2950, 16, 13, 3, 24 )
anim_miner_l = AnimField( 0x30a0, 16, 13, 3, 24 )
anim_faller_r = AnimField( 0x37f0, 16, 10, 2, 4 )
anim_faller_l = AnimField( 0x3890, 16, 10, 2, 4 )
anim_prefloater_r = AnimField( 0x3930, 16, 16, 3, 4 )
anim_floater_r = AnimField( 0x3ab0, 16, 16, 3, 4 )
anim_prefloater_l = AnimField( 0x3c30, 16, 16, 3, 4 )
anim_floater_l = AnimField( 0x3db0, 16, 16, 3, 4 )
anim_splatter = AnimField( 0x3f30, 16, 10, 2, 16 )
anim_leaver = AnimField( 0x41b0, 16, 13, 2, 8 )
anim_burner = AnimField( 0x4350, 16, 14, 4, 14 )
anim_blocker = AnimField( 0x4970, 16, 10, 2, 16 )
anim_shrugger_r = AnimField( 0x4bf0, 16, 10, 2, 8 )
anim_shrugger_l = AnimField( 0x4d30, 16, 10, 2, 8 )
anim_goner = AnimField( 0x4e70, 16, 10, 2, 16 )
anim_exploder = AnimField( 0x5070, 32, 32, 3, 1 )
[docs]
class MainMasks( mrc.Block ):
mask_basher_r = AnimField( 0x0000, 16, 10, 1, 4 )
mask_basher_l = AnimField( 0x0050, 16, 10, 1, 4 )
mask_miner_r = AnimField( 0x00a0, 16, 13, 1, 2 )
mask_miner_l = AnimField( 0x00d4, 16, 13, 1, 2 )
mask_exploder = AnimField( 0x0108, 16, 22, 1, 1 )
number_9 = AnimField( 0x0134, 8, 8, 1, 1 )
number_8 = AnimField( 0x013c, 8, 8, 1, 1 )
number_7 = AnimField( 0x0144, 8, 8, 1, 1 )
number_6 = AnimField( 0x014c, 8, 8, 1, 1 )
number_5 = AnimField( 0x0154, 8, 8, 1, 1 )
number_4 = AnimField( 0x015c, 8, 8, 1, 1 )
number_3 = AnimField( 0x0164, 8, 8, 1, 1 )
number_2 = AnimField( 0x016c, 8, 8, 1, 1 )
number_1 = AnimField( 0x0174, 8, 8, 1, 1 )
number_0 = AnimField( 0x017c, 8, 8, 1, 1 )
[docs]
class MainHUDGraphicsHP( mrc.Block ):
pass
[docs]
class MainMenuGraphics( mrc.Block ):
pass
[docs]
class MainMenuAnims( mrc.Block ):
pass
[docs]
class MainSection5( mrc.Block ):
pass
[docs]
class MainHUDGraphics( mrc.Block ):
pass
[docs]
class MainDAT( mrc.Block ):
pass
#main_anims = mrc.BlockField( MainAnims, 0x0000, stream=True, transform=DATCompressor() )
#main_masks = mrc.BlockField( MainMasks, mrc.EndOffset( 'main_anims' ), stream=True, transform=DATCompressor() )
#main_hud_graphics_hp = mrc.BlockField( MainHUDGraphicsHP, transform=DATCompressor() )
#main_menu_graphics = mrc.BlockField( MainMenuGraphics, transform=DATCompressor() )
#main_menu_anims = mrc.BlockField( MainMenuAnims, transform=DATCompressor() )
#main_section_5 = mrc.BlockField( MainSection5, transform=DATCompressor() )
#main_hud_graphics = mrc.BlockField( MainHUDGraphics, transform=DATCompressor() )
##########
# vgaspecX.dat parser
##########
[docs]
class Special( mrc.Block ):
palette_vga = img.Palette( ibm_pc.VGAColour, 0x0000, count=8 )
palette_ega = img.Palette( ibm_pc.EGAColour, 0x0018, count=8 )
palette_ega_preview = img.Palette( ibm_pc.EGAColour, 0x0020, count=8 )
image_data = mrc.Bytes( 0x0028, transform=SpecialCompressor() )
def __init__( self, *args, **kwargs ):
super().__init__( *args, **kwargs )
self.image = img.IndexedImage( self, width=960, height=160, source=mrc.Ref( 'image_data' ), palette=mrc.Ref( 'palette_vga' ) )
[docs]
class VgaspecDAT( mrc.Block ):
special = mrc.BlockField( Special, 0x0000, transform=DATCompressor() )
##########
# file loader
##########
[docs]
class Loader( mrc.Loader ):
_SEP = mrc.Loader._SEP
_LEMMINGS_FILE_CLASS_MAP = {
_SEP+'(ADLIB).DAT$': None,
_SEP+'(CGAGR)(\d).DAT$': None,
_SEP+'(CGAMAIN).DAT$': None,
_SEP+'(CGASPEC)(\d).DAT$': None,
_SEP+'(GROUND)(\d)O.DAT$': GroundDAT,
_SEP+'(LEVEL)00(\d).DAT$': LevelDAT,
_SEP+'(MAIN).DAT$': MainDAT,
_SEP+'(ODDTABLE).DAT$': OddtableDAT,
_SEP+'(RUSSELL).DAT$': None,
_SEP+'(TGAMAIN).DAT$': None,
_SEP+'(VGAGR)(\d).DAT$': VgagrDAT,
_SEP+'(VGASPEC)(\d).DAT$': VgaspecDAT,
}
_LEMMINGS_DEPS = [
(_SEP+'(GROUND)(\d)O.DAT$', _SEP+'(VGAGR)(\d).DAT$', ('VGAGR', '{1}'), '_vgagr')
]
def __init__( self ):
super().__init__( self._LEMMINGS_FILE_CLASS_MAP, dependency_list=self._LEMMINGS_DEPS )
[docs]
def post_load( self ):
# TODO: wire up inter-file class relations here
return