view pytouhou/formats/ecl.py @ 274:f037bca24f2d

Partially implement lasers. “Launch animations”/“energy circles” are missing, aswell as collision and grazing.
author Thibaut Girka <thib@sitedethib.com>
date Sun, 05 Feb 2012 23:41:55 +0100
parents 88361534c77e
children b6c068c8f7f2
line wrap: on
line source

# -*- encoding: utf-8 -*-
##
## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
## by the Free Software Foundation; version 3 only.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##

"""ECL files handling.

This module provides classes for handling the ECL file format.
The ECL format is a format used in Touhou 6: EoSD to script most of the gameplay
aspects of the game, such as enemy movements, attacks, and so on.
"""

import struct
from struct import pack, unpack, calcsize

from pytouhou.utils.helpers import get_logger

logger = get_logger(__name__)

class ECL(object):
    """Handle Touhou 6 ECL files.

    ECL files are binary script files used to describe the behavior of enemies.
    They are basically composed of a header and two additional sections.
    The first section is a list of subroutines, each composed of a list of timed
    instructions.
    The second section is a list of a different set of instructions describing
    enemy waves, triggering dialogs and level completion.

    Instance variables:
    main -- list of instructions describing waves and triggering dialogs
    subs -- list of subroutines
    """

    _instructions = {0: ('', 'noop?'),
                     1: ('I', 'delete?'),
                     2: ('Ii', 'relative_jump'),
                     3: ('Iii', 'relative_jump_ex'),
                     4: ('ii', 'set_int'),
                     5: ('if', 'set_float'),
                     6: ('ii', 'set_random_int'),
                     8: ('if', 'set_random_float'),
                     9: ('iff', 'set_random_float2'),
                     10: ('i', 'store_x'),
                     13: ('iii', 'add_int'),
                     14: ('iii', 'substract_int'),
                     15: ('iii', 'multiply_int'),
                     16: ('iii', 'divide_int'),
                     17: ('iii', 'modulo'),
                     18: ('i', 'increment'),
                     20: ('iff', 'add_float'),
                     21: ('iff', 'substract_float'),
                     23: ('iff', 'divide_float'),
                     25: ('iffff', 'get_direction'),
                     26: ('i', 'float_to_unit_circle'), #TODO: find a better name
                     27: ('ii', 'compare_ints'),
                     28: ('ff', 'compare_floats'),
                     29: ('ii', 'relative_jump_if_lower_than'),
                     30: ('ii', 'relative_jump_if_lower_or_equal'),
                     31: ('ii', 'relative_jump_if_equal'),
                     32: ('ii', 'relative_jump_if_greater_than'),
                     33: ('ii', 'relative_jump_if_greater_or_equal'),
                     34: ('ii', 'relative_jump_if_not_equal'),
                     35: ('iif', 'call'),
                     36: ('', 'return'),
                     39: ('iifii', 'call_if_equal'),
                     43: ('fff', 'set_position'),
                     45: ('ff', 'set_angle_and_speed'),
                     46: ('f', 'set_rotation_speed'),
                     47: ('f', 'set_speed'),
                     48: ('f', 'set_acceleration'),
                     49: ('ff', 'set_random_angle'),
                     50: ('ff', 'set_random_angle_ex'),
                     51: ('ff', 'set_speed_towards_player'),
                     52: ('iff', 'move_in_decel'),
                     56: ('ifff', 'move_to_linear'),
                     57: ('ifff', 'move_to_decel'),
                     59: ('iffi', 'move_to_accel'),
                     61: ('i', 'stop_in'),
                     63: ('i', 'stop_in_accel'),
                     65: ('ffff', 'set_screen_box'),
                     66: ('', 'clear_screen_box'),
                     67: ('hhiiffffi', 'set_bullet_attributes'),
                     68: ('hhiiffffi', 'set_bullet_attributes2'),
                     69: ('hhiiffffi', 'set_bullet_attributes3'),
                     70: ('hhiiffffi', 'set_bullet_attributes4'),
                     71: ('hhiiffffi', 'set_bullet_attributes5'),
                     74: ('hhiiffffi', 'set_bullet_attributes6'),
                     75: ('hhiiffffi', 'set_bullet_attributes7'),
                     76: ('i', 'set_bullet_interval'),
                     77: ('i', 'set_bullet_interval_ex'),
                     78: ('', 'delay_attack'),
                     79: ('', 'no_delay_attack'),
                     81: ('fff', 'set_bullet_launch_offset'),
                     82: ('iiiiffff', 'set_extended_bullet_attributes'),
                     83: ('', 'change_bullets_in_star_bonus'),
                     84: ('i', None),
                     85: ('hhffffffiiiiii', 'laser'),
                     86: ('hhffffffiiiiii', 'laser2'),
                     87: ('i', 'set_upcoming_id'),
                     88: ('if','alter_laser_angle'),
                     90: ('ifff', 'reposition_laser'),
                     92: ('i', 'cancel_laser'),
                     93: ('hhs', 'set_spellcard'),
                     94: ('', 'end_spellcard'),
                     95: ('ifffhhi', 'spawn_enemy'),
                     96: ('', 'kill_all_enemies'),
                     97: ('i', 'set_anim'),
                     98: ('hhhhhxx', 'set_multiple_anims'),
                     99: ('ii', None),
                     100: ('i', 'set_death_anim'),
                     101: ('i', 'set_boss_mode?'),
                     102: ('iffff', 'create_squares'),
                     103: ('fff', 'set_enemy_hitbox'),
                     104: ('i', 'set_collidable'),
                     105: ('i', 'set_damageable'),
                     106: ('i', 'play_sound'),
                     107: ('i', 'set_death_flags'),
                     108: ('i', 'set_death_callback?'),
                     109: ('ii', 'memory_write_int'),
                     111: ('i', 'set_life'),
                     112: ('i', 'set_ellapsed_time'),
                     113: ('i', 'set_low_life_trigger'),
                     114: ('i', 'set_low_life_callback'),
                     115: ('i', 'set_timeout'),
                     116: ('i', 'set_timeout_callback'),
                     117: ('i', 'set_touchable'),
                     118: ('iihh', None),
                     119: ('i', 'drop_bonus'),
                     120: ('i', 'set_automatic_orientation'),
                     121: ('ii', 'call_special_function'),
                     122: ('i', None),
                     123: ('i', 'skip_frames'),
                     124: ('i', 'drop_specific_bonus'),
                     125: ('', None),
                     126: ('i', 'set_remaining_lives'),
                     127: ('i', None),
                     128: ('i', 'set_smooth_disappear'),
                     129: ('ii', None),
                     130: ('i', None),
                     131: ('ffiiii', 'set_difficulty_coeffs'),
                     132: ('i', 'set_invisible'),
                     133: ('', None),
                     134: ('', None),
                     135: ('i', 'enable_spellcard_bonus')} #TODO

    _main_instructions = {0: ('fffhhI', 'spawn_enemy'),
                          2: ('fffhhI', 'spawn_enemy_mirrored'),
                          4: ('fffhhI', 'spawn_enemy_random'),
                          6: ('fffhhI', 'spawn_enemy_mirrored_random'),
                          8: ('', None),
                          9: ('', None),
                          10: ('II', None),
                          12: ('', None)}


    def __init__(self):
        self.main = []
        self.subs = [[]]


    @classmethod
    def read(cls, file):
        """Read an ECL file.

        Raise an exception if the file is invalid.
        Return a ECL instance otherwise.
        """

        sub_count, main_offset = unpack('<II', file.read(8))
        if file.read(8) != b'\x00\x00\x00\x00\x00\x00\x00\x00':
            raise Exception #TODO
        sub_offsets = unpack('<%dI' % sub_count, file.read(4 * sub_count))

        ecl = cls()
        ecl.subs = []
        ecl.main = []

        # Read subs
        for sub_offset in sub_offsets:
            file.seek(sub_offset)
            ecl.subs.append([])

            instruction_offsets = []

            while True:
                instruction_offsets.append(file.tell() - sub_offset)

                time, opcode = unpack('<IH', file.read(6))
                if time == 0xffffffff or opcode == 0xffff:
                    break

                size, rank_mask, param_mask = unpack('<HHH', file.read(6))
                data = file.read(size - 12)
                if opcode in cls._instructions:
                    fmt = '<%s' % cls._instructions[opcode][0]
                    if fmt.endswith('s'):
                        fmt = fmt[:-1]
                        fmt = '%s%ds' % (fmt, size - 12 - calcsize(fmt))
                    args = unpack(fmt, data)
                    if fmt.endswith('s'):
                        args = args[:-1] + (args[-1].decode('shift_jis'),)
                else:
                    args = (data, )
                    logger.warn('unknown opcode %d', opcode)

                ecl.subs[-1].append((time, opcode, rank_mask, param_mask, args))


            # Translate offsets to instruction pointers.
            # Indeed, jump instructions are relative and byte-based.
            # Since our representation doesn't conserve offsets, we have to
            # keep trace of where the jump is supposed to end up.
            for instr_offset, (i, instr) in zip(instruction_offsets, enumerate(ecl.subs[-1])):
                time, opcode, rank_mask, param_mask, args = instr
                if opcode in (2, 29, 30, 31, 32, 33, 34): # relative_jump
                    frame, relative_offset = args
                    args = frame, instruction_offsets.index(instr_offset + relative_offset)
                elif opcode == 3: # relative_jump_ex
                    frame, relative_offset, counter_id = args
                    args = frame, instruction_offsets.index(instr_offset + relative_offset), counter_id
                ecl.subs[-1][i] = time, opcode, rank_mask, param_mask, args


        # Read main
        file.seek(main_offset)
        while True:
            time, = unpack('<H', file.read(2))
            if time == 0xffff:
                break

            sub, opcode, size = unpack('<HHH', file.read(6))
            data = file.read(size - 8)

            if opcode in cls._main_instructions:
                args = unpack('<%s' % cls._main_instructions[opcode][0], data)
            else:
                args = (data,)
                logger.warn('unknown main opcode %d', opcode)

            ecl.main.append((time, sub, opcode, args))

        return ecl


    def write(self, file):
        """Write to an ECL file."""

        sub_count = len(self.subs)
        sub_offsets = []
        main_offset = 0

        # Skip header, it will be written later
        file.seek(8+8+4*sub_count)

        # Write subs
        for sub in self.subs:
            sub_offsets.append(file.tell())

            instruction_offsets = []
            instruction_datas = []
            for time, opcode, rank_mask, param_mask, args in sub:
                format = self._instructions[opcode][0]
                if format.endswith('s'):
                    args = list(args)
                    args[-1] = args[-1].encode('shift_jis')
                    format = '%s%ds' % (format[:-1], len(args[-1]))
                format = '<IHHHH%s' % format
                size = calcsize(format)
                instruction_offsets.append((instruction_offsets[-1] + len(instruction_datas[-1])) if instruction_offsets else 0)
                try:
                    instruction_datas.append(pack(format, time, opcode, size, rank_mask, param_mask, *args))
                except struct.error:
                    logger.error('Failed to assemble opcode %d' % opcode)
                    raise

            #TODO: clean up this mess
            for instruction, data, offset in zip(sub, instruction_datas, instruction_offsets):
                time, opcode, rank_mask, param_mask, args = instruction
                if opcode in (2, 29, 30, 31, 32, 33, 34): # relative_jump
                    frame, index = args
                    args = frame, instruction_offsets[index] - offset
                    format = '<IHHHH%s' % self._instructions[opcode][0]
                    size = calcsize(format)
                    data = pack(format, time, opcode, size, rank_mask, param_mask, *args)
                elif opcode == 3: # relative_jump_ex
                    frame, index, counter_id = args
                    args = frame, instruction_offsets[index] - offset, counter_id
                    format = '<IHHHH%s' % self._instructions[opcode][0]
                    size = calcsize(format)
                    data = pack(format, time, opcode, size, rank_mask, param_mask, *args)
                file.write(data)
            file.write(b'\xff' * 6 + b'\x0c\x00\x00\xff\xff\x00')

        # Write main
        main_offset = file.tell()
        for time, sub, opcode, args in self.main:
            format = '<HHHH%s' % self._main_instructions[opcode][0]
            size = calcsize(format)

            file.write(pack(format, time, sub, opcode, size, *args))
        file.write(b'\xff\xff\x04\x00')

        # Patch header
        file.seek(0)
        file.write(pack('<IIII%dI' % sub_count, sub_count, main_offset, 0, 0, *sub_offsets))