"""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
"""
from __future__ import annotations
import itertools
import logging
from enum import IntEnum
logger = logging.getLogger( __name__ )
from mrcrowbar import bits
from mrcrowbar import models as mrc
from mrcrowbar import utils
from mrcrowbar.lib.hardware import ibm_pc
from mrcrowbar.lib.images import base as img
[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( f"{self}: Checksum doesn't match header" )
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( f"{self}: EOF reached before last RLE block closed" )
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."""
_repr_values = ["obj_id", "x", "y"]
#: 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 ) -> int:
"""The x position of the left edge."""
return (self.x_raw - 16) - ((self.x_raw - 16) % 8)
[docs]class Terrain( mrc.Block ):
"""Represents a single terrain piece placed in a level."""
_repr_values = ["obj_id", "x", "y"]
#: 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
[docs]class SteelArea( mrc.Block ):
"""Represents an indestructable rectangular area in a level."""
_repr_values = ["x", "y", "width", "height"]
#: 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
[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 + r"(CGAGR)(\d).DAT$": None,
_SEP + "(CGAMAIN).DAT$": None,
_SEP + r"(CGASPEC)(\d).DAT$": None,
_SEP + r"(GROUND)(\d)O.DAT$": GroundDAT,
_SEP + r"(LEVEL)00(\d).DAT$": LevelDAT,
_SEP + "(MAIN).DAT$": MainDAT,
_SEP + "(ODDTABLE).DAT$": OddtableDAT,
_SEP + "(RUSSELL).DAT$": None,
_SEP + "(TGAMAIN).DAT$": None,
_SEP + r"(VGAGR)(\d).DAT$": VgagrDAT,
_SEP + r"(VGASPEC)(\d).DAT$": VgaspecDAT,
}
_LEMMINGS_DEPS = [
(
_SEP + r"(GROUND)(\d)O.DAT$",
_SEP + r"(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