changeset 49:cbe1cb50f2fd

Refactor ECLRunner/EnemyManager so that all VM stuff goes to ECLRunner
author Thibaut Girka <thib@sitedethib.com>
date Mon, 22 Aug 2011 19:23:00 +0200
parents 8353c33d53d4
children 811cefefb5c8
files eclviewer.py pytouhou/game/eclrunner.py pytouhou/game/enemymanager.py pytouhou/game/game.py
diffstat 4 files changed, 276 insertions(+), 198 deletions(-) [+]
line wrap: on
line diff
--- a/eclviewer.py
+++ b/eclviewer.py
@@ -18,6 +18,7 @@ from pytouhou.game.sprite import AnmWrap
 from pytouhou.game.background import Background
 from pytouhou.game.enemymanager import EnemyManager
 from pytouhou.opengl.texture import TextureManager
+from pytouhou.game.game import GameState
 
 import OpenGL
 OpenGL.FORWARD_COMPATIBLE_ONLY = True
@@ -61,7 +62,7 @@ def main(path, stage_num):
             pass
         else:
             anims.append(enemies2_anim)
-        enemy_manager = EnemyManager(stage, AnmWrapper(anims), ecl)
+        enemy_manager = EnemyManager(stage, AnmWrapper(anims), ecl, GameState([], stage_num, 0, 16))
         texture_manager.preload(anims)
 
         background_anim = Animations.read(BytesIO(archive.extract('stg%dbg.anm' % stage_num)))
--- a/pytouhou/game/eclrunner.py
+++ b/pytouhou/game/eclrunner.py
@@ -1,59 +1,128 @@
+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):
-    def __init__(self, ecl, sub, frame=0, instruction_pointer=0, implementation=None):
-        self.ecl = ecl
+    __metaclass__ = MetaRegistry
+    __slots__ = ('_ecl', '_enemy', '_game_state', 'variables', 'sub', 'frame',
+                 'instruction_pointer', '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.sub = sub
-        self.frame = frame
-        self.instruction_pointer = instruction_pointer
+        self.frame = 0
+        self.instruction_pointer = 0
 
         self.stack = []
 
-        self.implementation = {4: self.set_variable,
-                               5: self.set_variable,
-                               2: self.relative_jump,
-                               3: self.relative_jump_ex,
-                               20: self.add,
-                               21: self.substract,
-                               35: self.call,
-                               36: self.ret,
-                               109: self.memory_write}
-        if implementation:
-            self.implementation.update(implementation)
+
+    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
+        try:
+            while frame <= self.frame:
+                frame, instr_type, rank_mask, param_mask, args = self._ecl.subs[self.sub][self.instruction_pointer]
+
+                #TODO: skip bad ranks
+
+                if frame == self.frame:
+                    try:
+                        callback = self._handlers[instr_type]
+                    except KeyError:
+                        print('Warning: unhandled opcode %d!' % instr_type) #TODO
+                    else:
+                        callback(self, *args)
+                        frame, instr_type, rank_mask, param_mask, args = self._ecl.subs[self.sub][self.instruction_pointer]
+                if frame <= self.frame:
+                    self.instruction_pointer += 1
+        except IndexError:
+            return False
+
+        self.frame += 1
+        return True
 
 
-    def _get_value(self, value): #TODO: -10013 and beyond!
-        assert not -10025 <= value <= -10013
+    def _getval(self, value):
         if -10012 <= value <= -10001:
             return self.variables[int(-10001-value)]
+        elif -10025 <= value <= -10013:
+            raise NotImplementedError #TODO
         else:
             return value
 
 
-    def add(self, variable_id, a, b):
-        #TODO: proper variable handling
-        #TODO: int vs float thing
-        self.variables[-10001-variable_id] = self._get_value(a) + self._get_value(b)
+    def _setval(self, variable_id, value):
+        if -10012 <= value <= -10001:
+            self.variables[int(-10001-variable_id)] = value
+        elif -10025 <= value <= -10013:
+            raise NotImplementedError #TODO
+        else:
+            raise IndexError #TODO
+
+
+    @instruction(1)
+    def destroy(self, arg):
+        #TODO: arg?
+        self._enemy._removed = True
 
 
-    def substract(self, variable_id, a, b):
-        #TODO: proper variable handling
-        #TODO: int vs float thing
-        self.variables[-10001-variable_id] = self._get_value(a) - self._get_value(b)
-
+    @instruction(2)
+    def relative_jump(self, frame, instruction_pointer):
+        self.frame, self.instruction_pointer = frame, instruction_pointer
 
 
-    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(3)
+    def relative_jump_ex(self, frame, instruction_pointer, variable_id):
+        if self.variables[-10001-variable_id]:
+            self.variables[-10001-variable_id] -= 1
+            self.frame, self.instruction_pointer = frame, instruction_pointer
 
 
+    @instruction(4)
+    @instruction(5)
+    def set_variable(self, variable_id, value):
+        #TODO: -10013 and beyond!
+        self.variables[-10001-variable_id] = self._getval(value)
+
+
+    @instruction(35)
     def call(self, sub, param1, param2):
         self.stack.append((self.sub, self.frame, self.instruction_pointer,
                            self.variables))
@@ -65,43 +134,139 @@ class ECLRunner(object):
                           0,      0,  0,  0]
 
 
+    @instruction(36)
     def ret(self):
         self.sub, self.frame, self.instruction_pointer, self.variables = self.stack.pop()
 
 
-    def set_variable(self, variable_id, value):
-        #TODO: -10013 and beyond!
-        self.variables[-10001-variable_id] = self._get_value(value)
+    @instruction(20)
+    def add(self, variable_id, a, b):
+        #TODO: proper variable handling
+        #TODO: int vs float thing
+        self.variables[-10001-variable_id] = self._getval(a) + self._getval(b)
+
+
+    @instruction(21)
+    def substract(self, variable_id, a, b):
+        #TODO: proper variable handling
+        #TODO: int vs float thing
+        self.variables[-10001-variable_id] = self._getval(a) - self._getval(b)
+
+
+    @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
 
 
-    def relative_jump(self, frame, instruction_pointer):
-        self.frame, self.instruction_pointer = frame, instruction_pointer
+    @instruction(47)
+    def set_speed(self, speed):
+        self._enemy.speed = speed
+
+
+    @instruction(48)
+    def set_acceleration(self, acceleration):
+        self._enemy.acceleration = acceleration
+
+
+    @instruction(51)
+    def target_player(self, unknown, speed):
+        self._enemy.speed = speed #TODO: unknown
+        player_x, player_y = 192., 384.#TODO
+        self._enemy.angle = atan2(player_y - self._enemy.y, player_x - self._enemy.x) #TODO
 
 
-    def relative_jump_ex(self, frame, instruction_pointer, variable_id):
-        if self.variables[-10001-variable_id]:
-            self.variables[-10001-variable_id] -= 1
-            self.frame, self.instruction_pointer = frame, instruction_pointer
+    @instruction(57)
+    def move_to(self, duration, x, y, z):
+        self._enemy.move_to(duration, x, y, z)
+
+
+    @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
 
 
-    def update(self):
-        frame = self.frame
-        try:
-            while frame <= self.frame:
-                frame, instr_type, rank_mask, param_mask, args = self.ecl.subs[self.sub][self.instruction_pointer]
+    @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(103)
+    def set_hitbox(self, width, height, depth):
+        self._enemy.hitbox = (width, height)
+
+
+    @instruction(105)
+    def set_vulnerable(self, vulnerable):
+        self._enemy.vulnerable = bool(vulnerable & 1)
+
 
-                if frame == self.frame:
-                    try:
-                        callback = self.implementation[instr_type]
-                    except KeyError:
-                        print('Warning: unhandled opcode %d!' % instr_type) #TODO
-                    else:
-                        callback(*args)
-                        frame, instr_type, rank_mask, param_mask, args = self.ecl.subs[self.sub][self.instruction_pointer]
-                if frame <= self.frame:
-                    self.instruction_pointer += 1
-        except IndexError:
-            pass #TODO: script ended, destroy enemy
+    @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
+
 
-        self.frame += 1
+    @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(126)
+    def set_remaining_lives(self, lives):
+        self._enemy.remaining_lives = lives
+
--- a/pytouhou/game/enemymanager.py
+++ b/pytouhou/game/enemymanager.py
@@ -13,16 +13,18 @@ random = Random(0x39f4)
 
 
 class Enemy(object):
-    def __init__(self, pos, life, _type, ecl_runner, anm_wrapper):
-        self.anm_wrapper = anm_wrapper
-        self.anm = None
-        self.ecl_runner = ecl_runner
+    def __init__(self, pos, life, _type, anm_wrapper):
+        self._anm_wrapper = anm_wrapper
+        self._anm = None
+        self._sprite = None
+        self._removed = False
+        self._type = _type
+
+        self.frame = 0
+
         self.x, self.y = pos
         self.life = life
         self.max_life = life
-        self.type = _type
-        self.frame = 0
-        self.sprite = None
         self.pending_bullets = []
         self.bullet_attributes = None
         self.bullet_launch_offset = (0, 0)
@@ -36,7 +38,7 @@ class Enemy(object):
         self.bullet_launch_interval = 0
         self.delay_attack = False
 
-        self.death_sprite = None
+        self.death_anim = None
         self.movement_dependant_sprites = None
         self.direction = None
         self.interpolator = None #TODO
@@ -47,57 +49,6 @@ class Enemy(object):
 
         self.hitbox = (0, 0)
 
-        self.ecl_runner.implementation.update({67: self.set_bullet_attributes,
-                                               97: self.set_sprite,
-                                               98: self.set_multiple_sprites,
-                                               45: self.set_angle_speed,
-                                               43: self.set_pos,
-                                               46: self.set_rotation_speed,
-                                               47: self.set_speed,
-                                               48: self.set_acceleration,
-                                               51: self.target_player,
-                                               57: self.move_to,
-                                               77: self.set_bullet_interval,
-                                               78: self.set_delay_attack,
-                                               79: self.set_no_delay_attack,
-                                               81: self.set_bullet_launch_offset,
-                                               100: self.set_death_sprite,
-                                               103: self.set_hitbox,
-                                               105: self.set_vulnerable,
-                                               108: self.set_death_callback,
-                                               113: self.set_low_life_trigger,
-                                               114: self.set_low_life_callback,
-                                               115: self.set_timeout,
-                                               126: self.set_remaining_lives}) #TODO
-
-
-    def set_remaining_lives(self, lives):
-        self.remaining_lives = lives
-
-
-    def set_death_callback(self, sub):
-        self.death_callback = sub
-
-
-    def set_low_life_trigger(self, value):
-        self.low_life_trigger = value
-
-
-    def set_low_life_callback(self, sub):
-        self.low_life_callback = sub
-
-
-    def set_timeout(self, timeout):
-        self.timeout = timeout
-
-
-    def set_vulnerable(self, vulnerable):
-        self.vulnerable = bool(vulnerable & 1)
-
-
-    def set_bullet_launch_offset(self, x, y, z):
-        self.bullet_launch_offset = (x, y)
-
 
     def set_bullet_attributes(self, bullet_anim, launch_anim, bullets_per_shot,
                               number_of_shots, speed, unknown, launch_angle,
@@ -110,37 +61,8 @@ class Enemy(object):
             #TODO: actually fire
 
 
-    def set_bullet_interval(self, value):
-        self.bullet_launch_interval = value
-
-
-    def set_delay_attack(self):
-        self.delay_attack = True
-
-
-    def set_no_delay_attack(self):
-        self.delay_attack = False
-
-
-    def set_death_sprite(self, sprite_index):
-        self.death_sprite = sprite_index % 256 #TODO
-
-
-    def set_hitbox(self, width, height, depth):
-        self.hitbox = (width, height)
-
-
-    def set_sprite(self, sprite_index):
-        self.anm, self.sprite = self.anm_wrapper.get_sprite(sprite_index)
-
-
-    def set_multiple_sprites(self, default, end_left, end_right, left, right):
-        self.movement_dependant_sprites = end_left, end_right, left, right
-        self.anm, self.sprite = self.anm_wrapper.get_sprite(default)
-
-
-    def set_angle_speed(self, angle, speed):
-        self.angle, self.speed = angle, speed
+    def set_anim(self, index):
+        self._anm, self._sprite = self._anm_wrapper.get_sprite(index)
 
 
     def set_pos(self, x, y, z):
@@ -149,34 +71,16 @@ class Enemy(object):
         self.interpolator.set_interpolation_start(self.frame, (x, y))
 
 
-    def set_rotation_speed(self, speed):
-        self.rotation_speed = speed
-
-
-    def set_speed(self, speed):
-        self.speed = speed
-
-
-    def set_acceleration(self, acceleration):
-        self.acceleration = acceleration
-
-
-    def target_player(self, unknown, speed):
-        self.speed = speed #TODO: unknown
-        player_x, player_y = 192., 384.#TODO
-        self.angle = atan2(player_y - self.y, player_x - self.x)
-
-
     def move_to(self, duration, x, y, z):
         self.interpolator.set_interpolation_end(self.frame + duration, (x, y))
 
 
     def is_visible(self, screen_width, screen_height):
-        if not self.sprite:
+        if not self._sprite:
             return False
 
-        tx, ty, tw, th = self.sprite.texcoords
-        if self.sprite.corner_relative_placement:
+        tx, ty, tw, th = self._sprite.texcoords
+        if self._sprite.corner_relative_placement:
             raise Exception #TODO
         else:
             max_x = tw / 2.
@@ -194,20 +98,18 @@ class Enemy(object):
 
     def get_objects_by_texture(self):
         objects_by_texture = {}
-        key = self.anm.first_name, self.anm.secondary_name
+        key = self._anm.first_name, self._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)
+        vertices = tuple((x + self.x, y + self.y, z) for x, y, z in self._sprite._vertices)
         objects_by_texture[key][1].extend(vertices)
-        objects_by_texture[key][2].extend(self.sprite._uvs)
-        objects_by_texture[key][3].extend(self.sprite._colors)
+        objects_by_texture[key][2].extend(self._sprite._uvs)
+        objects_by_texture[key][3].extend(self._sprite._colors)
         #TODO: effects/bullet launch
         return objects_by_texture
 
 
     def update(self, frame):
-        self.ecl_runner.update()
-
         x, y = self.x, self.y
         if self.interpolator:
             self.interpolator.update(self.frame)
@@ -217,7 +119,7 @@ class Enemy(object):
         self.angle += self.rotation_speed #TODO: units? Execution order?
 
         dx, dy = cos(self.angle) * self.speed, sin(self.angle) * self.speed
-        if self.type & 2:
+        if self._type & 2:
             x -= dx
         else:
             x += dx
@@ -226,24 +128,24 @@ class Enemy(object):
         if self.movement_dependant_sprites:
             #TODO: is that really how it works?
             if x < self.x:
-                self.anm, self.sprite = self.anm_wrapper.get_sprite(self.movement_dependant_sprites[2])
+                self.set_anim(self.movement_dependant_sprites[2])
                 self.direction = -1
             elif x > self.x:
-                self.anm, self.sprite = self.anm_wrapper.get_sprite(self.movement_dependant_sprites[3])
+                self.set_anim(self.movement_dependant_sprites[3])
                 self.direction = +1
             elif self.direction is not None:
-                self.anm, self.sprite = self.anm_wrapper.get_sprite(self.movement_dependant_sprites[{-1: 0, +1:1}[self.direction]])
+                self.set_anim(self.movement_dependant_sprites[{-1: 0, +1:1}[self.direction]])
                 self.direction = None
 
         self.x, self.y = x, y
-        if self.sprite:
-            changed = self.sprite.update()
+        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:
+                self._sprite.update_vertices_uvs_colors()
+            elif not self._sprite.playing:
                 visible = False
-                self.sprite = None
+                self._sprite = None
         else:
             visible = False
 
@@ -254,13 +156,15 @@ class Enemy(object):
 
 
 class EnemyManager(object):
-    def __init__(self, stage, anm_wrapper, ecl):
+    def __init__(self, stage, anm_wrapper, ecl, game_state):
+        self._game_state = game_state
         self.stage = stage
         self.anm_wrapper = anm_wrapper
         self.main = []
         self.ecl = ecl
         self.objects_by_texture = {}
         self.enemies = []
+        self.processes = []
 
         # Populate main
         for frame, sub, instr_type, args in ecl.main:
@@ -270,12 +174,6 @@ class EnemyManager(object):
                 self.main[-1][1].append((sub, instr_type, args))
 
 
-    def make_enemy_deleter(self, enemy):
-        def _enemy_deleter(unknown): #TODO: unknown
-            self.enemies.remove(enemy)
-        return _enemy_deleter
-
-
     def update(self, frame):
         if self.main and self.main[0][0] == frame:
             for sub, instr_type, args in self.main.pop(0)[1]:
@@ -288,11 +186,16 @@ class EnemyManager(object):
                             y = random.rand_double() * 416 #102h.exe@0x41184b
                         if z < -990:
                             y = random.rand_double() * 800 #102h.exe@0x411881
-                    ecl_runner = ECLRunner(self.ecl, sub)
-                    enemy = Enemy((x, y), life, instr_type, ecl_runner, self.anm_wrapper)
-                    ecl_runner.implementation[1] = self.make_enemy_deleter(enemy)
+                    enemy = Enemy((x, y), life, instr_type, self.anm_wrapper)
+                    self.enemies.append(enemy)
+                    self.processes.append(ECLRunner(self.ecl, sub, enemy, self._game_state))
+
 
-                    self.enemies.append(enemy)
+        # Run processes
+        self.processes[:] = (process for process in self.processes if process.run_iteration())
+
+        # Filter of destroyed enemies
+        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)]
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/game.py
@@ -0,0 +1,9 @@
+class GameState(object):
+    __slots__ = ('players', 'rank', 'difficulty', 'frame', 'stage', 'boss')
+    def __init__(self, players, stage, rank, difficulty):
+        self.stage = stage
+        self.players = players
+        self.rank = rank
+        self.difficulty = difficulty
+        self.boss = None
+        self.frame = 0