changeset 69:a142e57218a0

Refactor. Move VMs to pytouhou.vm.
author Thibaut Girka <thib@sitedethib.com>
date Sat, 27 Aug 2011 10:58:54 +0200
parents a2459defd4b6
children 7c1f20407b3e
files pytouhou/formats/anm0.py pytouhou/game/background.py pytouhou/game/eclrunner.py pytouhou/game/enemymanager.py pytouhou/game/sprite.py pytouhou/vm/__init__.py pytouhou/vm/anmrunner.py pytouhou/vm/common.py pytouhou/vm/eclrunner.py
diffstat 8 files changed, 794 insertions(+), 684 deletions(-) [+]
line wrap: on
line diff
--- a/pytouhou/formats/anm0.py	Fri Aug 26 22:53:15 2011 +0200
+++ b/pytouhou/formats/anm0.py	Sat Aug 27 10:58:54 2011 +0200
@@ -13,12 +13,34 @@
 ##
 
 from struct import pack, unpack
-from pytouhou.utils.helpers import read_string
+from pytouhou.utils.helpers import read_string, get_logger
+
+
+logger = get_logger(__name__)
 
 #TODO: refactor/clean up
 
 
 class Animations(object):
+    _instructions = {0: ('', 'delete'),
+                     1: ('I', 'set_sprite'),
+                     2: ('ff', 'set_scale'),
+                     3: ('I', 'set_alpha'),
+                     4: ('BBBx', 'set_color'),
+                     5: ('I', 'jump'),
+                     7: ('', 'toggle_mirrored'),
+                     9: ('fff', 'set_3d_rotations'),
+                     10: ('fff', 'set_3d_rotations_speed'),
+                     11: ('ff', 'set_scale_speed'),
+                     12: ('ii', 'fade'),
+                     15: ('', 'keep_still'),
+                     16: ('i', 'set_random_sprite'),
+                     23: ('', 'set_corner_relative_placement'),
+                     27: ('f', 'shift_texture_x'),
+                     28: ('f', 'shift_texture_y'),
+                     30: ('ffi', 'scale_in')}
+
+
     def __init__(self):
         self.size = (0, 0)
         self.first_name = None
@@ -71,40 +93,24 @@
             while True:
                 #TODO
                 instruction_offsets.append(file.tell() - offset)
-                time, instr_type, length = unpack('<HBB', file.read(4))
+                time, opcode, length = unpack('<HBB', file.read(4))
                 data = file.read(length)
-                if instr_type == 1: # set_sprite
-                    args = unpack('<I', data)
-                elif instr_type == 2: # set_scale
-                    args = unpack('<ff', data)
-                elif instr_type == 3: # set_alpha
-                    args = unpack('<I', data)
-                elif instr_type == 4: # set_color
-                    args = unpack('<BBBx', data)
-                elif instr_type == 5: # jump
-                    # Translate offset to instruction index
-                    args = (instruction_offsets.index(unpack('<I', data)[0]),)
-                elif instr_type == 9: # set_3d_rotation
-                    args = unpack('<fff', data)
-                elif instr_type == 10: # set_3d_rotation_speed
-                    args = unpack('<fff', data)
-                elif instr_type == 11: # set_scale_speed
-                    args = unpack('<ff', data)
-                elif instr_type == 12: # fade
-                    args = unpack('<ii', data)
-                elif instr_type == 16: # set_random_sprite
-                    args = unpack('<ii', data)
-                elif instr_type == 27: # shift_texture_x
-                    args = unpack('<f', data)
-                elif instr_type == 28: # shift_texture_y
-                    args = unpack('<f', data)
-                elif instr_type == 30: # scale_in
-                    args = unpack('<ffi', data)
+                if opcode in cls._instructions:
+                    args = unpack('<%s' % cls._instructions[opcode][0], data)
                 else:
                     args = (data,)
-                anm.scripts[i].append((time, instr_type, args))
-                if instr_type == 0:
+                    logger.warn('unknown opcode %d', opcode)
+
+                anm.scripts[i].append((time, opcode, args))
+                if opcode == 0:
                     break
+
+            # Translate offsets to instruction pointers
+            for instr_offset, (j, instr) in zip(instruction_offsets, enumerate(anm.scripts[i])):
+                time, opcode, args = instr
+                if opcode == 5:
+                    args = (instruction_offsets.index(args[0]),)
+                anm.scripts[i][j] = time, opcode, args
         #TODO
 
         return anm
--- a/pytouhou/game/background.py	Fri Aug 26 22:53:15 2011 +0200
+++ b/pytouhou/game/background.py	Sat Aug 27 10:58:54 2011 +0200
@@ -19,6 +19,7 @@
 from itertools import chain
 
 from pytouhou.utils.interpolator import Interpolator
+from pytouhou.vm.anmrunner import ANMRunner
 from pytouhou.game.sprite import Sprite
 
 
@@ -72,8 +73,11 @@
             faces = []
             for script_index, ox, oy, oz, width_override, height_override in obj.quads:
                 #TODO: per-texture rendering
-                anm, sprite = self.anm_wrapper.get_sprite(script_index)
-                if sprite.update():
+                sprite = Sprite()
+                anm_runner = ANMRunner(self.anm_wrapper, script_index, sprite)
+                anm_runner.run_frame()
+                sprite.update()
+                if sprite._changed:
                     sprite.update_vertices_uvs_colors(width_override, height_override)
                 uvs, vertices = sprite._uvs, tuple((x + ox, y + oy, z + oz) for x, y, z in sprite._vertices)
                 colors = sprite._colors
--- a/pytouhou/game/eclrunner.py	Fri Aug 26 22:53:15 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,546 +0,0 @@
-# -*- 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.
-##
-
-
-from math import atan2, cos, sin
-
-from pytouhou.utils.helpers import get_logger
-
-logger = get_logger(__name__)
-
-
-
-class MetaRegistry(type):
-    def __new__(mcs, name, bases, classdict):
-        instruction_handlers = {}
-        for item in classdict.itervalues():
-            try:
-                instruction_ids = item._instruction_ids
-            except AttributeError:
-                pass
-            else:
-                for id_ in instruction_ids:
-                    instruction_handlers[id_] = item
-        classdict['_handlers'] = instruction_handlers
-        return type.__new__(mcs, name, bases, classdict)
-
-
-
-def instruction(instruction_id):
-    def _decorator(func):
-        if not hasattr(func, '_instruction_ids'):
-            func._instruction_ids = set()
-        func._instruction_ids.add(instruction_id)
-        return func
-    return _decorator
-
-
-
-class ECLRunner(object):
-    __metaclass__ = MetaRegistry
-    __slots__ = ('_ecl', '_enemy', '_game_state', 'variables', 'sub', 'frame',
-                 'instruction_pointer', 'comparison_reg', 'stack')
-
-    def __init__(self, ecl, sub, enemy, game_state):
-        # Things not supposed to change
-        self._ecl = ecl
-        self._enemy = enemy
-        self._game_state = game_state
-
-        # Things supposed to change (and be put in the stack)
-        self.variables = [0,  0,  0,  0,
-                          0., 0., 0., 0.,
-                          0,  0,  0,  0]
-        self.comparison_reg = 0
-        self.sub = sub
-        self.frame = 0
-        self.instruction_pointer = 0
-
-        self.stack = []
-
-
-    def run_iteration(self):
-        # First, if enemy is dead, return
-        if self._enemy._removed:
-            return False
-
-        # Then, check for callbacks
-        #TODO
-
-        # Now, process script
-        frame = self.frame
-        while True:
-            try:
-                frame, instr_type, rank_mask, param_mask, args = self._ecl.subs[self.sub][self.instruction_pointer]
-            except IndexError:
-                return False
-
-            if frame > self.frame:
-                break
-            else:
-                self.instruction_pointer += 1
-
-
-            #TODO: skip bad ranks
-            if not rank_mask & (0x100 << self._game_state.rank):
-                continue
-
-
-            if frame == self.frame:
-                try:
-                    callback = self._handlers[instr_type]
-                except KeyError:
-                    logger.warn('unhandled opcode %d (args: %r)', instr_type, args)
-                else:
-                    callback(self, *args)
-
-        self.frame += 1
-        return True
-
-
-    def _getval(self, value):
-        if -10012 <= value <= -10001:
-            return self.variables[int(-10001-value)]
-        elif -10025 <= value <= -10013:
-            if value == -10013:
-                return self._game_state.rank
-            elif value == -10014:
-                return self._game_state.difficulty
-            elif value == -10015:
-                return self._enemy.x
-            elif value == -10016:
-                return self._enemy.y
-            elif value == -10017:
-                return self._enemy.z
-            elif value == -10018:
-                player = self._enemy.select_player(self._game_state.players)
-                return player.x
-            elif value == -10019:
-                player = self._enemy.select_player(self._game_state.players)
-                return player.y
-            elif value == -10021:
-                player = self._enemy.select_player(self._game_state.players)
-                return self._enemy.get_player_angle(player)
-            elif value == -10022:
-                return self._enemy.frame
-            elif value == -10024:
-                return self._enemy.life
-            elif value == -10025:
-                return self._enemy.select_player(self._game_state.players).character
-            raise NotImplementedError(value) #TODO
-        else:
-            return value
-
-
-    def _setval(self, variable_id, value):
-        if -10012 <= variable_id <= -10001:
-            self.variables[int(-10001-variable_id)] = value
-        elif -10025 <= variable_id <= -10013:
-            if variable_id == -10015:
-                self._enemy.x = value
-            elif variable_id == -10016:
-                self._enemy.y = value
-            elif variable_id == -10017:
-                self._enemy.z = value
-            elif variable_id == -10022:
-                self._enemy.frame = value
-            elif variable_id == -10024:
-                self._enemy.life = value
-            else:
-                raise IndexError #TODO: proper exception
-        else:
-            raise IndexError #TODO: proper exception
-
-
-    @instruction(0)
-    def noop(self):
-        pass #TODO: Really?
-
-
-    @instruction(1)
-    def destroy(self, arg):
-        #TODO: arg?
-        self._enemy._removed = True
-
-
-    @instruction(2)
-    def relative_jump(self, frame, instruction_pointer):
-        """Jumps to a relative offset in the same subroutine.
-
-        Warning: the relative offset has been translated to an instruction pointer
-        by the ECL parsing code (see pytouhou.formats.ecl).
-        """
-        self.frame, self.instruction_pointer = frame, instruction_pointer
-
-
-    @instruction(3)
-    def relative_jump_ex(self, frame, instruction_pointer, variable_id):
-        """If the given variable is non-zero, decrease it by 1 and jump to a
-        relative offset in the same subroutine.
-
-        Warning: the relative offset has been translated to an instruction pointer
-        by the ECL parsing code (see pytouhou.formats.ecl).
-        """
-        counter_value = self._getval(variable_id)
-        if counter_value:
-            self._setval(variable_id, counter_value - 1)
-            self.frame, self.instruction_pointer = frame, instruction_pointer
-
-
-    @instruction(4)
-    @instruction(5)
-    def set_variable(self, variable_id, value):
-        self._setval(variable_id, self._getval(value))
-
-
-    @instruction(6)
-    def set_random_int(self, variable_id, maxval):
-        """Set the specified variable to a random int in the [0, maxval) range.
-        """
-        self._setval(variable_id, int(self._getval(maxval) * self._game_state.prng.rand_double()))
-
-
-    @instruction(8)
-    def set_random_float(self, variable_id, maxval):
-        """Set the specified variable to a random float in [0, maxval) range.
-        """
-        self._setval(variable_id, self._getval(maxval) * self._game_state.prng.rand_double())
-
-
-    @instruction(9)
-    def set_random_float2(self, variable_id, minval, amp):
-        self._setval(variable_id, self._getval(minval) + self._getval(amp) * self._game_state.prng.rand_double())
-
-
-    @instruction(13)
-    def set_random_int2(self, variable_id, minval, amp):
-        self._setval(variable_id, int(self._getval(minval)) + int(self._getval(amp)) * self._game_state.prng.rand_double())
-
-
-    @instruction(14)
-    @instruction(21)
-    def substract(self, variable_id, a, b):
-        #TODO: 14 takes only ints and 21 only floats.
-        # The original engine dereferences the variables in the type it waits for, so this isn't exactly the correct implementation, but the data don't contain such case.
-        self._setval(variable_id, self._getval(a) - self._getval(b))
-
-
-    @instruction(15)
-    def multiply_int(self, variable_id, a, b):
-        #TODO: takes only ints.
-        self._setval(variable_id, self._getval(a) * self._getval(b))
-
-
-    @instruction(16)
-    def divide_int(self, variable_id, a, b):
-        #TODO: takes only ints.
-        self._setval(variable_id, self._getval(a) // self._getval(b))
-
-
-    @instruction(17)
-    def modulo(self, variable_id, a, b):
-        self._setval(variable_id, self._getval(a) % self._getval(b))
-
-
-    @instruction(20)
-    def add_float(self, variable_id, a, b):
-        #TODO: takes only floats.
-        self._setval(variable_id, self._getval(a) + self._getval(b))
-
-
-    @instruction(23)
-    def divide_float(self, variable_id, a, b):
-        #TODO: takes only floats.
-        self._setval(variable_id, self._getval(a) / self._getval(b))
-
-
-    @instruction(27)
-    @instruction(28)
-    def compare(self, a, b):
-        #TODO: 27 takes only ints and 28 only floats.
-        a, b = self._getval(a), self._getval(b)
-        if a < b:
-            self.comparison_reg = -1
-        elif a == b:
-            self.comparison_reg = 0
-        else:
-            self.comparison_reg = 1
-
-
-    @instruction(29)
-    def relative_jump_if_lower_than(self, frame, instruction_pointer):
-        if self.comparison_reg == -1:
-            self.relative_jump(frame, instruction_pointer)
-
-
-    @instruction(30)
-    def relative_jump_if_lower_or_equal(self, frame, instruction_pointer):
-        if self.comparison_reg != 1:
-            self.relative_jump(frame, instruction_pointer)
-
-
-    @instruction(31)
-    def relative_jump_if_equal(self, frame, instruction_pointer):
-        if self.comparison_reg == 0:
-            self.relative_jump(frame, instruction_pointer)
-
-
-    @instruction(32)
-    def relative_jump_if_greater_than(self, frame, instruction_pointer):
-        if self.comparison_reg == 1:
-            self.relative_jump(frame, instruction_pointer)
-
-
-    @instruction(33)
-    def relative_jump_if_greater_or_equal(self, frame, instruction_pointer):
-        if self.comparison_reg != -1:
-            self.relative_jump(frame, instruction_pointer)
-
-
-    @instruction(34)
-    def relative_jump_if_not_equal(self, frame, instruction_pointer):
-        if self.comparison_reg != 0:
-            self.relative_jump(frame, instruction_pointer)
-
-
-    @instruction(35)
-    def call(self, sub, param1, param2):
-        self.stack.append((self.sub, self.frame, self.instruction_pointer,
-                           self.variables, self.comparison_reg))
-        self.sub = sub
-        self.frame = 0
-        self.instruction_pointer = 0
-        self.variables = [param1, 0,  0,  0,
-                          param2, 0., 0., 0.,
-                          0,      0,  0,  0]
-
-
-    @instruction(36)
-    def ret(self):
-        self.sub, self.frame, self.instruction_pointer, self.variables, self.comparison_reg = self.stack.pop()
-
-
-    @instruction(39)
-    def call_if_equal(self, sub, param1, param2, a, b):
-        if self._getval(a) == self._getval(b):
-            self.call(sub, param1, param2)
-
-
-    @instruction(43)
-    def set_pos(self, x, y, z):
-        self._enemy.set_pos(x, y, z)
-
-
-    @instruction(45)
-    def set_angle_speed(self, angle, speed):
-        self._enemy.angle, self._enemy.speed = angle, speed
-
-
-    @instruction(46)
-    def set_rotation_speed(self, speed):
-        self._enemy.rotation_speed = speed
-
-
-    @instruction(47)
-    def set_speed(self, speed):
-        self._enemy.speed = speed
-
-
-    @instruction(48)
-    def set_acceleration(self, acceleration):
-        self._enemy.acceleration = acceleration
-
-
-    @instruction(50)
-    def set_random_angle(self, min_angle, max_angle):
-        if self._enemy.screen_box:
-            minx, miny, maxx, maxy = self._enemy.screen_box
-        else:
-            minx, miny, maxx, maxy = (0., 0., 0., 0.)
-
-        angle = self._game_state.prng.rand_double() * (max_angle - min_angle) + min_angle
-        sa, ca = sin(angle), cos(angle)
-
-        distx = min(96.0, (maxx - minx) / 2.)
-        disty = min(96.0, (maxy - miny) / 2.)
-
-        if self._enemy.x > maxx - 96.0:
-            ca = -abs(ca)
-        elif self._enemy.x < minx + 96.0:
-            ca = abs(ca)
-
-        if self._enemy.y > maxy - 48.0:
-            sa = -abs(sa)
-        elif self._enemy.y < miny + 48.0:
-            sa = abs(sa)
-        self._enemy.angle = atan2(sa, ca)
-
-
-    @instruction(51)
-    def target_player(self, unknown, speed):
-        #TODO: unknown
-        self._enemy.speed = speed
-        self._enemy.angle = self._enemy.get_player_angle(self._enemy.select_player(self._game_state.players))
-
-
-    @instruction(57)
-    def move_to(self, duration, x, y, z):
-        self._enemy.move_to(duration, x, y, z, lambda x: 2. * x - x ** 2)
-
-
-    @instruction(59)
-    def move_to2(self, duration, x, y, z):
-        #TODO: not accurate
-        self._enemy.move_to(duration, x, y, z, lambda x: 1.0014 * x ** 2 - 0.0012 * x)
-
-
-    @instruction(61)
-    def stop_in(self, duration):
-        self._enemy.stop_in(duration)
-
-
-    @instruction(65)
-    def set_screen_box(self, xmin, ymin, xmax, ymax):
-        self._enemy.screen_box = xmin, ymin, xmax, ymax
-
-
-    @instruction(66)
-    def clear_screen_box(self):
-        self._enemy.screen_box = None
-
-
-    @instruction(67)
-    def set_bullet_attributes1(self, bullet_anim, launch_anim, bullets_per_shot,
-                              number_of_shots, speed, unknown, launch_angle,
-                              angle, flags):
-        self._enemy.set_bullet_attributes(1, bullet_anim, launch_anim,
-                                           bullets_per_shot, number_of_shots,
-                                           speed, unknown, launch_angle, angle,
-                                           flags)
-
-
-    @instruction(77)
-    def set_bullet_interval(self, value):
-        self._enemy.bullet_launch_interval = value
-
-
-    @instruction(78)
-    def set_delay_attack(self):
-        self._enemy.delay_attack = True
-
-
-    @instruction(79)
-    def set_no_delay_attack(self):
-        self._enemy.delay_attack = False
-
-
-    @instruction(81)
-    def set_bullet_launch_offset(self, x, y, z):
-        self._enemy.bullet_launch_offset = (x, y)
-
-
-    @instruction(97)
-    def set_anim(self, sprite_index):
-        self._enemy.set_anim(sprite_index)
-
-
-    @instruction(98)
-    def set_multiple_anims(self, default, end_left, end_right, left, right):
-        self._enemy.movement_dependant_sprites = end_left, end_right, left, right
-        self._enemy.set_anim(default)
-
-
-    @instruction(100)
-    def set_death_anim(self, sprite_index):
-        self._enemy.death_anim = sprite_index % 256 #TODO
-
-
-    @instruction(101)
-    def set_boss_mode(self, unknown):
-        #TODO: unknown
-        self._game_state.boss = self._enemy
-
-
-    @instruction(103)
-    def set_hitbox(self, width, height, depth):
-        self._enemy.hitbox = (width, height)
-
-
-    @instruction(105)
-    def set_damageable(self, vulnerable):
-        self._enemy.damageable = bool(vulnerable & 1)
-
-
-    @instruction(108)
-    def set_death_callback(self, sub):
-        self._enemy.death_callback = sub
-
-
-    @instruction(109)
-    def memory_write(self, value, index):
-        #TODO
-        #XXX: this is a hack to display bosses although we don't handle MSG :)
-        if index == 0:
-            self.sub = value
-            self.frame = 0
-            self.instruction_pointer = 0
-
-
-    @instruction(111)
-    def set_life(self, value):
-        self._enemy.life = value
-
-
-    @instruction(112)
-    def set_ellapsed_time(self, value):
-        """Sets the enemy's frame counter.
-        This is used for timeouts, where the framecounter is compared to the
-        timeout value (what's displayed is (enemy.timeout - enemy.frame) // 60).
-        """
-        self._enemy.frame = value
-
-
-    @instruction(113)
-    def set_low_life_trigger(self, value):
-        self._enemy.low_life_trigger = value
-
-
-    @instruction(114)
-    def set_low_life_callback(self, sub):
-        self._enemy.low_life_callback = sub
-
-
-    @instruction(115)
-    def set_timeout(self, timeout):
-        self._enemy.timeout = timeout
-
-
-    @instruction(116)
-    def set_timeout_callback(self, sub):
-        self._enemy.timeout_callback = sub
-
-
-    @instruction(117)
-    def set_touchable(self, value):
-        """Defines whether the enemy is “touchable”.
-        Bullets only collide with an enemy if it is “touchable”.
-        Likewise, ReimuA's homing attacks only target “touchable” enemies.
-        """
-        self._enemy.touchable = bool(value)
-
-
-    @instruction(126)
-    def set_remaining_lives(self, lives):
-        self._enemy.remaining_lives = lives
-
--- a/pytouhou/game/enemymanager.py	Fri Aug 26 22:53:15 2011 +0200
+++ b/pytouhou/game/enemymanager.py	Sat Aug 27 10:58:54 2011 +0200
@@ -18,7 +18,8 @@
 import os
 from struct import unpack, pack
 from pytouhou.utils.interpolator import Interpolator
-from pytouhou.game.eclrunner import ECLRunner
+from pytouhou.vm.eclrunner import ECLRunner
+from pytouhou.vm.anmrunner import ANMRunner
 from pytouhou.game.sprite import Sprite
 from math import cos, sin, atan2
 
@@ -26,8 +27,8 @@
 class Enemy(object):
     def __init__(self, pos, life, _type, anm_wrapper):
         self._anm_wrapper = anm_wrapper
-        self._anm = None
         self._sprite = None
+        self._anmrunner = None
         self._removed = False
         self._type = _type
         self._was_visible = False
@@ -91,7 +92,8 @@
 
 
     def set_anim(self, index):
-        self._anm, self._sprite = self._anm_wrapper.get_sprite(index)
+        self._sprite = Sprite()
+        self._anmrunner = ANMRunner(self._anm_wrapper, index, self._sprite)
 
 
     def set_pos(self, x, y, z):
@@ -139,7 +141,7 @@
 
     def get_objects_by_texture(self):
         objects_by_texture = {}
-        key = self._anm.first_name, self._anm.secondary_name
+        key = self._sprite.anm.first_name, self._sprite.anm.secondary_name
         if not key in objects_by_texture:
             objects_by_texture[key] = (0, [], [], [])
         vertices = tuple((x + self.x, y + self.y, z) for x, y, z in self._sprite._vertices)
@@ -150,7 +152,7 @@
         return objects_by_texture
 
 
-    def update(self, frame):
+    def update(self):
         x, y = self.x, self.y
         if self.interpolator:
             self.interpolator.update(self.frame)
@@ -195,20 +197,20 @@
 
 
         self.x, self.y = x, y
+
+        #TODO
+        if self._anmrunner and not self._anmrunner.run_frame():
+            self._anmrunner = None
+
         if self._sprite:
-            changed = self._sprite.update()
-            visible = self.is_visible(384, 448)
-            if changed and visible:
-                self._sprite.update_vertices_uvs_colors()
-            elif not self._sprite.playing:
-                visible = False
+            if self._sprite._removed:
                 self._sprite = None
-        else:
-            visible = False
-
+            else:
+                self._sprite.update()
+                if self._sprite._changed:
+                    self._sprite.update_vertices_uvs_colors()
 
         self.frame += 1
-        return visible
 
 
 
@@ -255,7 +257,11 @@
         self.enemies[:] = (enemy for enemy in self.enemies if not enemy._removed)
 
         # Update enemies
-        visible_enemies = [enemy for enemy in self.enemies if enemy.update(frame)]
+        for enemy in self.enemies:
+            enemy.update()
+
+        # Filter out non-visible enemies
+        visible_enemies = [enemy for enemy in self.enemies if enemy.is_visible(384, 448)] #TODO
         for enemy in visible_enemies:
             enemy._was_visible = True
 
@@ -265,12 +271,10 @@
                 enemy._removed = True
                 self.enemies.remove(enemy)
 
-
         #TODO: disable boss mode if it is dead/it has timeout
         if self._game_state.boss and self._game_state.boss._removed:
             self._game_state.boss = None
 
-
         # Add enemies to vertices/uvs
         self.objects_by_texture = {}
         for enemy in visible_enemies:
--- a/pytouhou/game/sprite.py	Fri Aug 26 22:53:15 2011 +0200
+++ b/pytouhou/game/sprite.py	Sat Aug 27 10:58:54 2011 +0200
@@ -13,30 +13,35 @@
 ##
 
 
-from random import randrange
-from struct import unpack
-
 from pytouhou.utils.matrix import Matrix
-from pytouhou.utils.helpers import get_logger
-
-logger = get_logger(__name__)
 
 
 class AnmWrapper(object):
     def __init__(self, anm_files):
         self.anm_files = list(anm_files)
 
-    def get_sprite(self, script_index):
+
+    def get_sprite(self, sprite_index):
+        for anm in self.anm_files:
+            if sprite_index in anm.sprites:
+                return anm, anm.sprites[sprite_index]
+        raise IndexError
+
+
+    def get_script(self, script_index):
         for anm in self.anm_files:
             if script_index in anm.scripts:
-                return anm, Sprite(anm, script_index)
+                return anm, anm.scripts[script_index]
+        raise IndexError
 
 
 
 class Sprite(object):
-    def __init__(self, anm, script_index):
-        self.anm = anm
-        self.script_index = script_index
+    def __init__(self):
+        self.anm = None
+        self._removed = False
+        self._changed = False
+
         self.texcoords = (0, 0, 0, 0) # x, y, width, height
         self.texoffsets = (0., 0.)
         self.mirrored = False
@@ -45,9 +50,6 @@
         self.rotations_3d = (0., 0., 0.)
         self.rotations_speed_3d = (0., 0., 0.)
         self.corner_relative_placement = False
-        self.instruction_pointer = 0
-        self.keep_still = False
-        self.playing = True
         self.frame = 0
         self.color = (255, 255, 255)
         self.alpha = 255
@@ -95,78 +97,11 @@
         self._uvs, self._vertices = uvs, zip(d[0], d[1], d[2])
 
 
-
     def update(self):
-        if not self.playing:
-            return False
-
-        changed = False
-        if not self.keep_still:
-            script = self.anm.scripts[self.script_index]
-            frame = self.frame
-            while True:
-                try:
-                    frame, instr_type, args = script[self.instruction_pointer]
-                except IndexError:
-                    self.playing = False
-                    return False
+        if self.rotations_speed_3d != (0., 0., 0.) or self.scale_speed != (0., 0.):
+            ax, ay, az = self.rotations_3d
+            sax, say, saz = self.rotations_speed_3d
+            self.rotations_3d = ax + sax, ay + say, az + saz
+            self.rescale = self.rescale[0] + self.scale_speed[0], self.rescale[1] + self.scale_speed[1]
+            self._changed = True
 
-                if frame > self.frame:
-                    break
-                else:
-                    self.instruction_pointer += 1
-                if frame == self.frame:
-                    changed = True
-                    if instr_type == 0:
-                        self.playing = False
-                        return False
-                    if instr_type == 1:
-                        #TODO: handle out-of-anm sprites
-                        self.texcoords = self.anm.sprites[args[0]]
-                    elif instr_type == 2:
-                        self.rescale = args
-                    elif instr_type == 3:
-                        self.alpha = args[0] % 256 #TODO
-                    elif instr_type == 4:
-                        b, g, r = args
-                        self.color = (r, g, b)
-                    elif instr_type == 5:
-                        self.instruction_pointer, = args
-                        self.frame = script[self.instruction_pointer][0]
-                    elif instr_type == 7:
-                        self.mirrored = not self.mirrored
-                    elif instr_type == 9:
-                        self.rotations_3d = args
-                    elif instr_type == 10:
-                        self.rotations_speed_3d = args
-                    elif instr_type == 11:
-                        self.scale_speed = args
-                    elif instr_type == 16:
-                        #TODO: handle out-of-anm sprites
-                        #TODO: use the game's PRNG?
-                        self.texcoords = self.anm.sprites[args[0] + randrange(args[1])]
-                    elif instr_type == 23:
-                        self.corner_relative_placement = True #TODO
-                    elif instr_type == 27:
-                        tox, toy = self.texoffsets
-                        self.texoffsets = tox + args[0], toy
-                    elif instr_type == 28:
-                        tox, toy = self.texoffsets
-                        self.texoffsets = tox, toy + args[0]
-                    elif instr_type in (15, 21):
-                        self.keep_still = True
-                        break
-                    else:
-                        logger.warn('unhandled instruction %d (args: %r)', instr_type, args)
-        self.frame += 1
-
-        ax, ay, az = self.rotations_3d
-        sax, say, saz = self.rotations_speed_3d
-        self.rotations_3d = ax + sax, ay + say, az + saz
-        self.rescale = self.rescale[0] + self.scale_speed[0], self.rescale[1] + self.scale_speed[1]
-
-        if self.rotations_speed_3d != (0., 0., 0.) or self.scale_speed != (0., 0.):
-            return True
-
-        return changed
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pytouhou/vm/anmrunner.py	Sat Aug 27 10:58:54 2011 +0200
@@ -0,0 +1,148 @@
+# -*- 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.
+##
+
+
+from random import randrange
+
+from pytouhou.utils.helpers import get_logger
+from pytouhou.vm.common import MetaRegistry, instruction
+
+logger = get_logger(__name__)
+
+
+class ANMRunner(object):
+    __metaclass__ = MetaRegistry
+    __slots__ = ('_anm_wrapper', '_sprite', '_running',
+                 'script', 'instruction_pointer', 'frame')
+
+
+    def __init__(self, anm_wrapper, script_id, sprite):
+        self._anm_wrapper = anm_wrapper
+        self._sprite = sprite
+        self._running = True
+
+        anm, self.script = anm_wrapper.get_script(script_id)
+        self.frame = 0
+        self.instruction_pointer = 0
+        pass
+
+
+    def run_frame(self):
+        if self._sprite._removed:
+            return False
+
+        while self._running:
+            try:
+                frame, instr_type, args = self.script[self.instruction_pointer]
+            except IndexError:
+                return False
+
+            if frame > self.frame:
+                break
+            else:
+                self.instruction_pointer += 1
+
+            if frame == self.frame:
+                try:
+                    callback = self._handlers[instr_type]
+                except KeyError:
+                    logger.warn('unhandled opcode %d (args: %r)', instr_type, args)
+                else:
+                    callback(self, *args)
+                    self._sprite._changed = True
+        self.frame += 1
+        return self._running
+
+
+    @instruction(0)
+    def remove(self):
+        self._sprite._removed = True
+        self._running = True
+
+
+    @instruction(1)
+    def load_sprite(self, sprite_index):
+        self._sprite.anm, self._sprite.texcoords = self._anm_wrapper.get_sprite(sprite_index)
+
+
+    @instruction(2)
+    def set_scale(self, sx, sy):
+        self._sprite.rescale = sx, sy
+
+
+    @instruction(3)
+    def set_alpha(self, alpha):
+        self._sprite.alpha = alpha % 256 #TODO
+
+
+    @instruction(4)
+    def set_color(self, b, g, r):
+        self._sprite.color = (r, g, b)
+
+
+    @instruction(5)
+    def jump(self, instruction_pointer):
+        #TODO: is that really how it works?
+        self.instruction_pointer = instruction_pointer
+        self.frame = self.script[self.instruction_pointer][0]
+
+
+    @instruction(7)
+    def toggle_mirrored(self):
+        self._sprite.mirrored = not self._sprite.mirrored
+
+
+    @instruction(9)
+    def set_rotations_3d(self, rx, ry, rz):
+        self._sprite.rotations_3d = rx, ry, rz
+
+
+    @instruction(10)
+    def set_rotations_speed_3d(self, srx, sry, srz):
+        self._sprite.rotations_speed_3d = srx, sry, srz
+
+
+    @instruction(11)
+    def set_scale_speed(self, ssx, ssy):
+        self._sprite.scale_speed = ssx, ssy
+
+
+    @instruction(16)
+    def load_random_sprite(self, min_idx, amp):
+        #TODO: use the game's PRNG?
+        self.load_sprite(min_idx + randrange(amp))
+
+
+    @instruction(23)
+    def set_corner_relative_placement(self):
+        self._sprite.corner_relative_placement = True #TODO
+
+
+    @instruction(27)
+    def shift_texture_x(self, dx):
+        tox, toy = self._sprite.texoffsets
+        self._sprite.texoffsets = tox + dx, toy
+
+
+    @instruction(28)
+    def shift_texture_y(self, dy):
+        tox, toy = self._sprite.texoffsets
+        self._sprite.texoffsets = tox, toy + dy
+
+
+    @instruction(15)
+    @instruction(21) #TODO
+    def keep_still(self):
+        self._running = False
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pytouhou/vm/common.py	Sat Aug 27 10:58:54 2011 +0200
@@ -0,0 +1,38 @@
+# -*- 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.
+##
+
+
+class MetaRegistry(type):
+    def __new__(mcs, name, bases, classdict):
+        instruction_handlers = {}
+        for item in classdict.itervalues():
+            try:
+                instruction_ids = item._instruction_ids
+            except AttributeError:
+                pass
+            else:
+                for id_ in instruction_ids:
+                    instruction_handlers[id_] = item
+        classdict['_handlers'] = instruction_handlers
+        return type.__new__(mcs, name, bases, classdict)
+
+
+
+def instruction(instruction_id):
+    def _decorator(func):
+        if not hasattr(func, '_instruction_ids'):
+            func._instruction_ids = set()
+        func._instruction_ids.add(instruction_id)
+        return func
+    return _decorator
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pytouhou/vm/eclrunner.py	Sat Aug 27 10:58:54 2011 +0200
@@ -0,0 +1,521 @@
+# -*- 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.
+##
+
+
+from math import atan2, cos, sin
+
+from pytouhou.utils.helpers import get_logger
+
+from pytouhou.vm.common import MetaRegistry, instruction
+
+logger = get_logger(__name__)
+
+
+
+class ECLRunner(object):
+    __metaclass__ = MetaRegistry
+    __slots__ = ('_ecl', '_enemy', '_game_state', 'variables', 'sub', 'frame',
+                 'instruction_pointer', 'comparison_reg', 'stack')
+
+    def __init__(self, ecl, sub, enemy, game_state):
+        # Things not supposed to change
+        self._ecl = ecl
+        self._enemy = enemy
+        self._game_state = game_state
+
+        # Things supposed to change (and be put in the stack)
+        self.variables = [0,  0,  0,  0,
+                          0., 0., 0., 0.,
+                          0,  0,  0,  0]
+        self.comparison_reg = 0
+        self.sub = sub
+        self.frame = 0
+        self.instruction_pointer = 0
+
+        self.stack = []
+
+
+    def run_iteration(self):
+        # First, if enemy is dead, return
+        if self._enemy._removed:
+            return False
+
+        # Then, check for callbacks
+        #TODO
+
+        # Now, process script
+        while True:
+            try:
+                frame, instr_type, rank_mask, param_mask, args = self._ecl.subs[self.sub][self.instruction_pointer]
+            except IndexError:
+                return False
+
+            if frame > self.frame:
+                break
+            else:
+                self.instruction_pointer += 1
+
+
+            #TODO: skip bad ranks
+            if not rank_mask & (0x100 << self._game_state.rank):
+                continue
+
+
+            if frame == self.frame:
+                try:
+                    callback = self._handlers[instr_type]
+                except KeyError:
+                    logger.warn('unhandled opcode %d (args: %r)', instr_type, args)
+                else:
+                    callback(self, *args)
+
+        self.frame += 1
+        return True
+
+
+    def _getval(self, value):
+        if -10012 <= value <= -10001:
+            return self.variables[int(-10001-value)]
+        elif -10025 <= value <= -10013:
+            if value == -10013:
+                return self._game_state.rank
+            elif value == -10014:
+                return self._game_state.difficulty
+            elif value == -10015:
+                return self._enemy.x
+            elif value == -10016:
+                return self._enemy.y
+            elif value == -10017:
+                return self._enemy.z
+            elif value == -10018:
+                player = self._enemy.select_player(self._game_state.players)
+                return player.x
+            elif value == -10019:
+                player = self._enemy.select_player(self._game_state.players)
+                return player.y
+            elif value == -10021:
+                player = self._enemy.select_player(self._game_state.players)
+                return self._enemy.get_player_angle(player)
+            elif value == -10022:
+                return self._enemy.frame
+            elif value == -10024:
+                return self._enemy.life
+            elif value == -10025:
+                return self._enemy.select_player(self._game_state.players).character
+            raise NotImplementedError(value) #TODO
+        else:
+            return value
+
+
+    def _setval(self, variable_id, value):
+        if -10012 <= variable_id <= -10001:
+            self.variables[int(-10001-variable_id)] = value
+        elif -10025 <= variable_id <= -10013:
+            if variable_id == -10015:
+                self._enemy.x = value
+            elif variable_id == -10016:
+                self._enemy.y = value
+            elif variable_id == -10017:
+                self._enemy.z = value
+            elif variable_id == -10022:
+                self._enemy.frame = value
+            elif variable_id == -10024:
+                self._enemy.life = value
+            else:
+                raise IndexError #TODO: proper exception
+        else:
+            raise IndexError #TODO: proper exception
+
+
+    @instruction(0)
+    def noop(self):
+        pass #TODO: Really?
+
+
+    @instruction(1)
+    def destroy(self, arg):
+        #TODO: arg?
+        self._enemy._removed = True
+
+
+    @instruction(2)
+    def relative_jump(self, frame, instruction_pointer):
+        """Jumps to a relative offset in the same subroutine.
+
+        Warning: the relative offset has been translated to an instruction pointer
+        by the ECL parsing code (see pytouhou.formats.ecl).
+        """
+        self.frame, self.instruction_pointer = frame, instruction_pointer
+
+
+    @instruction(3)
+    def relative_jump_ex(self, frame, instruction_pointer, variable_id):
+        """If the given variable is non-zero, decrease it by 1 and jump to a
+        relative offset in the same subroutine.
+
+        Warning: the relative offset has been translated to an instruction pointer
+        by the ECL parsing code (see pytouhou.formats.ecl).
+        """
+        counter_value = self._getval(variable_id)
+        if counter_value:
+            self._setval(variable_id, counter_value - 1)
+            self.frame, self.instruction_pointer = frame, instruction_pointer
+
+
+    @instruction(4)
+    @instruction(5)
+    def set_variable(self, variable_id, value):
+        self._setval(variable_id, self._getval(value))
+
+
+    @instruction(6)
+    def set_random_int(self, variable_id, maxval):
+        """Set the specified variable to a random int in the [0, maxval) range.
+        """
+        self._setval(variable_id, int(self._getval(maxval) * self._game_state.prng.rand_double()))
+
+
+    @instruction(8)
+    def set_random_float(self, variable_id, maxval):
+        """Set the specified variable to a random float in [0, maxval) range.
+        """
+        self._setval(variable_id, self._getval(maxval) * self._game_state.prng.rand_double())
+
+
+    @instruction(9)
+    def set_random_float2(self, variable_id, minval, amp):
+        self._setval(variable_id, self._getval(minval) + self._getval(amp) * self._game_state.prng.rand_double())
+
+
+    @instruction(13)
+    def set_random_int2(self, variable_id, minval, amp):
+        self._setval(variable_id, int(self._getval(minval)) + int(self._getval(amp)) * self._game_state.prng.rand_double())
+
+
+    @instruction(14)
+    @instruction(21)
+    def substract(self, variable_id, a, b):
+        #TODO: 14 takes only ints and 21 only floats.
+        # The original engine dereferences the variables in the type it waits for, so this isn't exactly the correct implementation, but the data don't contain such case.
+        self._setval(variable_id, self._getval(a) - self._getval(b))
+
+
+    @instruction(15)
+    def multiply_int(self, variable_id, a, b):
+        #TODO: takes only ints.
+        self._setval(variable_id, self._getval(a) * self._getval(b))
+
+
+    @instruction(16)
+    def divide_int(self, variable_id, a, b):
+        #TODO: takes only ints.
+        self._setval(variable_id, self._getval(a) // self._getval(b))
+
+
+    @instruction(17)
+    def modulo(self, variable_id, a, b):
+        self._setval(variable_id, self._getval(a) % self._getval(b))
+
+
+    @instruction(20)
+    def add_float(self, variable_id, a, b):
+        #TODO: takes only floats.
+        self._setval(variable_id, self._getval(a) + self._getval(b))
+
+
+    @instruction(23)
+    def divide_float(self, variable_id, a, b):
+        #TODO: takes only floats.
+        self._setval(variable_id, self._getval(a) / self._getval(b))
+
+
+    @instruction(27)
+    @instruction(28)
+    def compare(self, a, b):
+        #TODO: 27 takes only ints and 28 only floats.
+        a, b = self._getval(a), self._getval(b)
+        if a < b:
+            self.comparison_reg = -1
+        elif a == b:
+            self.comparison_reg = 0
+        else:
+            self.comparison_reg = 1
+
+
+    @instruction(29)
+    def relative_jump_if_lower_than(self, frame, instruction_pointer):
+        if self.comparison_reg == -1:
+            self.relative_jump(frame, instruction_pointer)
+
+
+    @instruction(30)
+    def relative_jump_if_lower_or_equal(self, frame, instruction_pointer):
+        if self.comparison_reg != 1:
+            self.relative_jump(frame, instruction_pointer)
+
+
+    @instruction(31)
+    def relative_jump_if_equal(self, frame, instruction_pointer):
+        if self.comparison_reg == 0:
+            self.relative_jump(frame, instruction_pointer)
+
+
+    @instruction(32)
+    def relative_jump_if_greater_than(self, frame, instruction_pointer):
+        if self.comparison_reg == 1:
+            self.relative_jump(frame, instruction_pointer)
+
+
+    @instruction(33)
+    def relative_jump_if_greater_or_equal(self, frame, instruction_pointer):
+        if self.comparison_reg != -1:
+            self.relative_jump(frame, instruction_pointer)
+
+
+    @instruction(34)
+    def relative_jump_if_not_equal(self, frame, instruction_pointer):
+        if self.comparison_reg != 0:
+            self.relative_jump(frame, instruction_pointer)
+
+
+    @instruction(35)
+    def call(self, sub, param1, param2):
+        self.stack.append((self.sub, self.frame, self.instruction_pointer,
+                           self.variables, self.comparison_reg))
+        self.sub = sub
+        self.frame = 0
+        self.instruction_pointer = 0
+        self.variables = [param1, 0,  0,  0,
+                          param2, 0., 0., 0.,
+                          0,      0,  0,  0]
+
+
+    @instruction(36)
+    def ret(self):
+        self.sub, self.frame, self.instruction_pointer, self.variables, self.comparison_reg = self.stack.pop()
+
+
+    @instruction(39)
+    def call_if_equal(self, sub, param1, param2, a, b):
+        if self._getval(a) == self._getval(b):
+            self.call(sub, param1, param2)
+
+
+    @instruction(43)
+    def set_pos(self, x, y, z):
+        self._enemy.set_pos(x, y, z)
+
+
+    @instruction(45)
+    def set_angle_speed(self, angle, speed):
+        self._enemy.angle, self._enemy.speed = angle, speed
+
+
+    @instruction(46)
+    def set_rotation_speed(self, speed):
+        self._enemy.rotation_speed = speed
+
+
+    @instruction(47)
+    def set_speed(self, speed):
+        self._enemy.speed = speed
+
+
+    @instruction(48)
+    def set_acceleration(self, acceleration):
+        self._enemy.acceleration = acceleration
+
+
+    @instruction(50)
+    def set_random_angle(self, min_angle, max_angle):
+        if self._enemy.screen_box:
+            minx, miny, maxx, maxy = self._enemy.screen_box
+        else:
+            minx, miny, maxx, maxy = (0., 0., 0., 0.)
+
+        angle = self._game_state.prng.rand_double() * (max_angle - min_angle) + min_angle
+        sa, ca = sin(angle), cos(angle)
+
+        distx = min(96.0, (maxx - minx) / 2.)
+        disty = min(96.0, (maxy - miny) / 2.)
+
+        if self._enemy.x > maxx - 96.0:
+            ca = -abs(ca)
+        elif self._enemy.x < minx + 96.0:
+            ca = abs(ca)
+
+        if self._enemy.y > maxy - 48.0:
+            sa = -abs(sa)
+        elif self._enemy.y < miny + 48.0:
+            sa = abs(sa)
+        self._enemy.angle = atan2(sa, ca)
+
+
+    @instruction(51)
+    def target_player(self, unknown, speed):
+        #TODO: unknown
+        self._enemy.speed = speed
+        self._enemy.angle = self._enemy.get_player_angle(self._enemy.select_player(self._game_state.players))
+
+
+    @instruction(57)
+    def move_to(self, duration, x, y, z):
+        self._enemy.move_to(duration, x, y, z, lambda x: 2. * x - x ** 2)
+
+
+    @instruction(59)
+    def move_to2(self, duration, x, y, z):
+        #TODO: not accurate
+        self._enemy.move_to(duration, x, y, z, lambda x: 1.0014 * x ** 2 - 0.0012 * x)
+
+
+    @instruction(61)
+    def stop_in(self, duration):
+        self._enemy.stop_in(duration)
+
+
+    @instruction(65)
+    def set_screen_box(self, xmin, ymin, xmax, ymax):
+        self._enemy.screen_box = xmin, ymin, xmax, ymax
+
+
+    @instruction(66)
+    def clear_screen_box(self):
+        self._enemy.screen_box = None
+
+
+    @instruction(67)
+    def set_bullet_attributes1(self, bullet_anim, launch_anim, bullets_per_shot,
+                              number_of_shots, speed, unknown, launch_angle,
+                              angle, flags):
+        self._enemy.set_bullet_attributes(1, bullet_anim, launch_anim,
+                                           bullets_per_shot, number_of_shots,
+                                           speed, unknown, launch_angle, angle,
+                                           flags)
+
+
+    @instruction(77)
+    def set_bullet_interval(self, value):
+        self._enemy.bullet_launch_interval = value
+
+
+    @instruction(78)
+    def set_delay_attack(self):
+        self._enemy.delay_attack = True
+
+
+    @instruction(79)
+    def set_no_delay_attack(self):
+        self._enemy.delay_attack = False
+
+
+    @instruction(81)
+    def set_bullet_launch_offset(self, x, y, z):
+        self._enemy.bullet_launch_offset = (x, y)
+
+
+    @instruction(97)
+    def set_anim(self, sprite_index):
+        self._enemy.set_anim(sprite_index)
+
+
+    @instruction(98)
+    def set_multiple_anims(self, default, end_left, end_right, left, right):
+        self._enemy.movement_dependant_sprites = end_left, end_right, left, right
+        self._enemy.set_anim(default)
+
+
+    @instruction(100)
+    def set_death_anim(self, sprite_index):
+        self._enemy.death_anim = sprite_index % 256 #TODO
+
+
+    @instruction(101)
+    def set_boss_mode(self, unknown):
+        #TODO: unknown
+        self._game_state.boss = self._enemy
+
+
+    @instruction(103)
+    def set_hitbox(self, width, height, depth):
+        self._enemy.hitbox = (width, height)
+
+
+    @instruction(105)
+    def set_damageable(self, vulnerable):
+        self._enemy.damageable = bool(vulnerable & 1)
+
+
+    @instruction(108)
+    def set_death_callback(self, sub):
+        self._enemy.death_callback = sub
+
+
+    @instruction(109)
+    def memory_write(self, value, index):
+        #TODO
+        #XXX: this is a hack to display bosses although we don't handle MSG :)
+        if index == 0:
+            self.sub = value
+            self.frame = 0
+            self.instruction_pointer = 0
+
+
+    @instruction(111)
+    def set_life(self, value):
+        self._enemy.life = value
+
+
+    @instruction(112)
+    def set_ellapsed_time(self, value):
+        """Sets the enemy's frame counter.
+        This is used for timeouts, where the framecounter is compared to the
+        timeout value (what's displayed is (enemy.timeout - enemy.frame) // 60).
+        """
+        self._enemy.frame = value
+
+
+    @instruction(113)
+    def set_low_life_trigger(self, value):
+        self._enemy.low_life_trigger = value
+
+
+    @instruction(114)
+    def set_low_life_callback(self, sub):
+        self._enemy.low_life_callback = sub
+
+
+    @instruction(115)
+    def set_timeout(self, timeout):
+        self._enemy.timeout = timeout
+
+
+    @instruction(116)
+    def set_timeout_callback(self, sub):
+        self._enemy.timeout_callback = sub
+
+
+    @instruction(117)
+    def set_touchable(self, value):
+        """Defines whether the enemy is “touchable”.
+        Bullets only collide with an enemy if it is “touchable”.
+        Likewise, ReimuA's homing attacks only target “touchable” enemies.
+        """
+        self._enemy.touchable = bool(value)
+
+
+    @instruction(126)
+    def set_remaining_lives(self, lives):
+        self._enemy.remaining_lives = lives
+