Source code for mrcrowbar.lib.games.boppin


import array
import itertools
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.lib.compressors import lzss
from mrcrowbar import utils

# source: http://www.shikadi.net/moddingwiki/RES_Format_%28Boppin%29

BOPPIN_RES_FILENAMES = [
    'BOPPIN.CFG',
    'CANDY.IMG',
    'CANDY.TXB',
    'BOPPIN.SND',
    'BOPPIN.MUS',
    'SCORE.CFG',
    'WALLS.RES',
    'FLOORS.RES',
    'ELEVATOR.TIL',
    'SOURCE.TIL',
    'BOPPING.TIL',
    'BCKGND.TIL',
    'PRIZE.TIL',
    'MONSTER.TIL',
    'MISC.TIL',
    'CHAR.TIL',
    'UNKNOWN1',
    'UNKNOWN2',
    'UNKNOWN3',
    'UNKNOWN4',
    'BOPPIN1.LVL',
    'BOPPIN2.LVL',
    'BOPPIN3.LVL',
    'BOPPIN4.LVL',
    'ENDSCRN.BIN',
    'UNKNOWN5',
    'UNKNOWN6'
]

BOPPIN_MUS_FILENAMES = [
    'BOPBOP2.MID', 
    'MAGNETIC.MID', 
    'FRUSTRAT.MID', 
    'SAPPHIRE.MID', 
    'OPUS43.MID', 
    'FOUNDAT.MID', 
    'FELTIBUN.MID', 
    'STUMBUM.MID', 
    'STARBOP.MID', 
    'BUMBLE.MID', 
    'SKEPTICA.MID', 
    'STABHAPP.MID', 
    'STREETS.MID', 
    'DIXIE.MID', 
    'LOBERO.MID', 
    'APOGEE.MID'
]

BOPPIN_SND_FILENAMES = [
    'ADDCRED.VOC', 
    'ADDLIFE.VOC', 
    'BLKBNC1.VOC', 
    'BLKBNC2.VOC', 
    'BLKDRP.VOC', 
    'BLOCKPSH.VOC', 
    'BLOCKREZ.VOC', 
    'BODYTHUD.VOC', 
    'BOIKREZ1.VOC', 
    'BOIKREZ2.VOC', 
    'BOIKREZ3.VOC', 
    'BOIKSUI1.VOC', 
    'BOIKSUI2.VOC', 
    'YEETREZ1.VOC', 
    'YEETREZ3.VOC', 
    'YEETSUI1.VOC', 
    'YEETSU1A.VOC', 
    'YEETSUI2.VOC', 
    'BONUSTIM.VOC', 
    'BONUSTLY.VOC', 
    'BOPHIT.VOC', 
    'CRYING.VOC', 
    'DUCKING.VOC', 
    'ELEVATOR.VOC', 
    'EXPLOSN.VOC', 
    'FOOTTAP.VOC', 
    'JUMPJOY.VOC', 
    'LOGO.VOC', 
    'LOGO2.VOC', 
    'LOGO3.VOC', 
    'PING.VOC', 
    'PRIZDROP.VOC', 
    'PRIZE.VOC', 
    'PRIZELOS.VOC', 
    'PTRNFLSH.VOC', 
    'PTRNHIT.VOC', 
    'REFRACTR.VOC', 
    'WALKING.VOC', 
    'NOTEMAN.VOC', 
    'ECHOTINK.VOC', 
    'BEEFBRAS.VOC', 
    'CLEANBAS.VOC', 
    'DEEPBASS.VOC', 
    'FLICKBAS.VOC', 
    'JAHRMARK.VOC', 
    'LICKS.VOC', 
    'MARIMBA.VOC', 
    'NIGHTMAR.VOC', 
    'PIANO.VOC', 
    'SHAKER.VOC', 
    'SOFTTP12.VOC', 
    'STRINGS1.VOC', 
    'SYNBUZ.VOC', 
    'UNKNOWN.VOC', 
    'UNKNOWN2.VOC'
]


[docs] class BoppinCompressor( mrc.Transform ):
[docs] def import_data( self, buffer, parent=None ): if len( buffer ) == 0: return mrc.TransformResult() lc = lzss.LZSSCompressor() size_comp = utils.from_uint32_le( buffer[0:4] ) if size_comp != len( buffer ): logger.info( '{}: File not compressed'.format( self ) ) return mrc.TransformResult( payload=buffer, end_offset=len( buffer ) ) size_raw = utils.from_uint32_le( buffer[4:8] ) result = lc.import_data( buffer[8:] ) if len( result.payload ) != size_raw: logger.warning( '{}: Was expecting a decompressed size of {}, got {}!'.format( self, size_raw, len( result.payload ) ) ) return result
[docs] def export_data( self, buffer, parent=None ): # no header, no compression! return mrc.TransformResult( payload=buffer )
# levels can pick: # - 6 banks of 16x tiles # - 2 banks of 6x bopping blocks # - 2 banks of 2x source blocks # - 2 banks of 4x wall tiles # - 2 banks of 7x floor tiles # - 2 banks of 2x elevator tiles # # each bg bank has its own palette? might be common 256col palette for whole game # prize bank is common, has 128 items # each prize item has its own 16 colour palette # colours 24-27 and 28-30 are strobing palette
[docs] class Colour( img.Colour ): r_8 = mrc.UInt8( 0x00 ) unknown_1 = mrc.UInt8( 0x01 ) g_8 = mrc.UInt8( 0x02 ) unknown_2 = mrc.UInt8( 0x03 ) b_8 = mrc.UInt8( 0x04 ) unknown_3 = mrc.UInt8( 0x05 )
[docs] class FileLookup( mrc.Block ): offset = mrc.UInt32_LE( 0x00 ) size = mrc.UInt32_LE( 0x04 ) file = mrc.StoreRef( mrc.Unknown, store=mrc.Ref( '_parent.files_store' ), offset=mrc.Ref( 'offset' ), size=mrc.Ref( 'size' ), transform=BoppinCompressor() )
# stop looking for more lookup-table entries once we hit the first result that references the end of the file
[docs] def resource_stop_check( buffer, offset ): # we want to look at the previous entry to the current one, ignore if we're at the first entry if (offset < 8): return False # too near the end of the file? that's a paddling if (offset >= len( buffer )-8): return True block_offset = utils.from_uint32_le( buffer[offset-8:offset-4] ) block_size = utils.from_uint32_le( buffer[offset-4:offset] ) # ignore filler entries if (block_offset == 0xffffffff) and (block_size == 0): return False # check for references to the end of the file return block_offset+block_size >= len( buffer )-1
[docs] class Resource( mrc.Block ): files = mrc.BlockField( FileLookup, 0x00, stride=0x08, stream=True, stop_check=resource_stop_check, fill=b'\xff\xff\xff\xff\x00\x00\x00\x00' ) files_raw = mrc.Bytes( mrc.EndOffset( 'files' ) ) def __init__( self, *args, **kwargs ): self.files_store = mrc.Store( parent=self, source=mrc.Ref( 'files_raw' ), base_offset=mrc.EndOffset( 'files', neg=True ) ) super().__init__( *args, **kwargs )
[docs] class Loader( mrc.Loader ): _SEP = mrc.Loader._SEP _BOPPIN_FILE_CLASS_MAP = { _SEP+'(BOPPIN)(\d).(LVL)$': None, _SEP+'(BOPPIN).(RES)$': Resource, } def __init__( self ): super().__init__( self._BOPPIN_FILE_CLASS_MAP )