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, 275 insertions(+), 165 deletions(-) [+]
line wrap: on
line diff
--- a/pytouhou/formats/anm0.py
+++ b/pytouhou/formats/anm0.py
@@ -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 @@ class Animations(object):
             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
+++ b/pytouhou/game/background.py
@@ -19,6 +19,7 @@ import struct
 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 @@ class Background(object):
             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/enemymanager.py
+++ b/pytouhou/game/enemymanager.py
@@ -18,7 +18,8 @@ from io import BytesIO
 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 @@ from math import cos, sin, atan2
 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 @@ class Enemy(object):
 
 
     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 @@ class Enemy(object):
 
     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 @@ class Enemy(object):
         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 @@ class Enemy(object):
 
 
         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 @@ class EnemyManager(object):
         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 @@ class EnemyManager(object):
                 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
+++ b/pytouhou/game/sprite.py
@@ -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 @@ class Sprite(object):
         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 @@ class Sprite(object):
         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
-
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/pytouhou/vm/anmrunner.py
@@ -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
+
new file mode 100644
--- /dev/null
+++ b/pytouhou/vm/common.py
@@ -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
rename from pytouhou/game/eclrunner.py
rename to pytouhou/vm/eclrunner.py
--- a/pytouhou/game/eclrunner.py
+++ b/pytouhou/vm/eclrunner.py
@@ -17,33 +17,9 @@ from math import atan2, cos, sin
 
 from pytouhou.utils.helpers import get_logger
 
-logger = get_logger(__name__)
-
-
+from pytouhou.vm.common import MetaRegistry, instruction
 
-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
+logger = get_logger(__name__)
 
 
 
@@ -79,7 +55,6 @@ class ECLRunner(object):
         #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]