changeset 97:ac2e5e1c2c3c

Refactor \o/
author Thibaut Girka <thib@sitedethib.com>
date Sun, 04 Sep 2011 23:50:00 +0200
parents 54929d495654
children d141c851c598
files eclviewer.py pytouhou/formats/pbg3.py pytouhou/formats/std.py pytouhou/game/bullet.py pytouhou/game/enemymanager.py pytouhou/game/game.py pytouhou/game/sprite.py pytouhou/opengl/texture.py pytouhou/resource/__init__.py pytouhou/resource/anmwrapper.py pytouhou/resource/loader.py pytouhou/utils/bitstream.py pytouhou/vm/eclrunner.py
diffstat 13 files changed, 311 insertions(+), 185 deletions(-) [+]
line wrap: on
line diff
--- a/eclviewer.py
+++ b/eclviewer.py
@@ -18,20 +18,14 @@ import os
 
 import struct
 from math import degrees, radians
-from io import BytesIO
 from itertools import chain
 
 import pygame
 
-from pytouhou.formats.pbg3 import PBG3
-from pytouhou.formats.std import Stage
-from pytouhou.formats.ecl import ECL
-from pytouhou.formats.anm0 import Animations
-from pytouhou.game.sprite import AnmWrapper
+from pytouhou.resource.loader import Loader
 from pytouhou.game.background import Background
-from pytouhou.game.enemymanager import EnemyManager
 from pytouhou.opengl.texture import TextureManager
-from pytouhou.game.game import GameState, Resources
+from pytouhou.game.game import Game
 from pytouhou.game.player import Player
 
 import OpenGL
@@ -60,56 +54,33 @@ def main(path, stage_num):
     glEnableClientState(GL_VERTEX_ARRAY)
     glEnableClientState(GL_TEXTURE_COORD_ARRAY)
 
-    texture_manager = TextureManager()
+    resource_loader = Loader()
+    texture_manager = TextureManager(resource_loader)
+    resource_loader.scan_archives(os.path.join(path, name)
+                                    for name in ('CM.DAT', 'ST.DAT'))
+    game = Game(resource_loader, [Player()], stage_num, 3, 16)
 
     # Load common data
-    with open(os.path.join(path, 'CM.DAT'), 'rb') as file:
-        archive = PBG3.read(file)
-        texture_manager.set_archive(archive)
-        etama_anm_wrappers = (AnmWrapper([Animations.read(BytesIO(archive.extract('etama3.anm')))]),
-                              AnmWrapper([Animations.read(BytesIO(archive.extract('etama4.anm')))]))
-        players_anm_wrappers = (AnmWrapper([Animations.read(BytesIO(archive.extract('player00.anm')))]),
-                                AnmWrapper([Animations.read(BytesIO(archive.extract('player01.anm')))]))
-        effects_anm_wrapper = AnmWrapper([Animations.read(BytesIO(archive.extract('eff00.anm')))])
-
-        for anm_wrapper in etama_anm_wrappers:
-            texture_manager.preload(anm_wrapper)
-
-        for anm_wrapper in players_anm_wrappers:
-            texture_manager.preload(anm_wrapper)
-
-        texture_manager.preload(effects_anm_wrapper)
-
-        resources = Resources(etama_anm_wrappers, players_anm_wrappers, effects_anm_wrapper)
-
-    game_state = GameState(resources, [Player()], stage_num, 3, 16)
+    etama_anm_wrappers = (resource_loader.get_anm_wrapper(('etama3.anm',)),
+                          resource_loader.get_anm_wrapper(('etama4.anm',)))
+    effects_anm_wrapper = resource_loader.get_anm_wrapper(('eff00.anm',))
 
     # Load stage data
-    with open(os.path.join(path, 'ST.DAT'), 'rb') as file:
-        archive = PBG3.read(file)
-        texture_manager.set_archive(archive)
+    stage = resource_loader.get_stage('stage%d.std' % stage_num)
+    enemies_anm_wrapper = resource_loader.get_anm_wrapper2(('stg%denm.anm' % stage_num,
+                                                            'stg%denm2.anm' % stage_num))
 
-        stage = Stage.read(BytesIO(archive.extract('stage%d.std' % stage_num)), stage_num)
+    background_anm_wrapper = resource_loader.get_anm_wrapper(('stg%dbg.anm' % stage_num,))
+    background = Background(stage, background_anm_wrapper)
 
-        ecl = ECL.read(BytesIO(archive.extract('ecldata%d.ecl' % stage_num)))
-        enemies_anim = Animations.read(BytesIO(archive.extract('stg%denm.anm' % stage_num)))
-        anims = [enemies_anim]
-        try:
-            enemies2_anim = Animations.read(BytesIO(archive.extract('stg%denm2.anm' % stage_num)))
-        except KeyError:
-            pass
-        else:
-            anims.append(enemies2_anim)
-        enemy_manager = EnemyManager(stage, AnmWrapper(anims), ecl, game_state)
-        texture_manager.preload(anims)
+    # Preload textures
+    for anm_wrapper in chain(etama_anm_wrappers,
+                             (background_anm_wrapper, enemies_anm_wrapper,
+                              effects_anm_wrapper)):
+        texture_manager.preload(anm_wrapper)
 
-        background_anim = Animations.read(BytesIO(archive.extract('stg%dbg.anm' % stage_num)))
-        background = Background(stage, AnmWrapper((background_anim,)))
-        texture_manager.preload((background_anim,))
-
-    print(enemy_manager.stage.name)
-
-    frame = 0
+    # Let's go!
+    print(stage.name)
 
     # Main loop
     clock = pygame.time.Clock()
@@ -121,12 +92,11 @@ def main(path, stage_num):
             elif event.type == pygame.KEYDOWN:
                 if event.key == pygame.K_RETURN and event.mod & pygame.KMOD_ALT:
                     pygame.display.toggle_fullscreen()
+        keystate = 0 #TODO
 
         # Update game
-        enemy_manager.update(frame)
-        background.update(frame)
-
-        frame += 1
+        background.update(game.game_state.frame) #TODO
+        game.run_iter(keystate)
 
         # Draw everything
 #            glClearColor(0.0, 0.0, 1.0, 0)
@@ -176,7 +146,7 @@ def main(path, stage_num):
 
         glDisable(GL_FOG)
         objects_by_texture = {}
-        enemy_manager.get_objects_by_texture(objects_by_texture)
+        game.get_objects_by_texture(objects_by_texture)
         for (texture_key, blendfunc), (vertices, uvs, colors) in objects_by_texture.items():
             nb_vertices = len(vertices)
             glBlendFunc(GL_SRC_ALPHA, (GL_ONE_MINUS_SRC_ALPHA, GL_ONE)[blendfunc])
--- a/pytouhou/formats/pbg3.py
+++ b/pytouhou/formats/pbg3.py
@@ -43,6 +43,14 @@ class PBG3(object):
         self.bitstream = bitstream #TODO
 
 
+    def __enter__(self):
+        return self
+
+
+    def __exit__(self, type, value, traceback):
+        return self.bitstream.__exit__(type, value, traceback)
+
+
     @classmethod
     def read(cls, file):
         magic = file.read(4)
--- a/pytouhou/formats/std.py
+++ b/pytouhou/formats/std.py
@@ -29,8 +29,7 @@ class Model(object):
 
 
 class Stage(object):
-    def __init__(self, num):
-        self.num = num
+    def __init__(self):
         self.name = ''
         self.bgms = (('', ''), ('', ''), ('', ''))
         self.models = []
@@ -39,8 +38,8 @@ class Stage(object):
 
 
     @classmethod
-    def read(cls, file, num):
-        stage = Stage(num)
+    def read(cls, file):
+        stage = Stage()
 
         nb_models, nb_faces = unpack('<HH', file.read(4))
         object_instances_offset, script_offset = unpack('<II', file.read(8))
--- a/pytouhou/game/bullet.py
+++ b/pytouhou/game/bullet.py
@@ -108,8 +108,9 @@ class Bullet(object):
         if not self._sprite or self._sprite._removed:
             self._launched = True
             self._sprite = Sprite()
-            self._anmrunner = ANMRunner(self._game_state.resources.etama_anm_wrappers[0], #TODO
-                                        self.anim_idx, self._sprite, self.sprite_idx_offset)
+            anm_wrapper = self._game_state.resource_loader.get_anm_wrapper(('etama3.anm',)) #TODO
+            self._anmrunner = ANMRunner(anm_wrapper, self.anim_idx,
+                                        self._sprite, self.sprite_idx_offset)
 
         self._anmrunner.run_frame()
         self._sprite.update(angle_base=self.angle)
--- a/pytouhou/game/enemymanager.py
+++ b/pytouhou/game/enemymanager.py
@@ -262,90 +262,3 @@ class Enemy(object):
 
         self.frame += 1
 
-
-
-class EnemyManager(object):
-    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.enemies = []
-        self.processes = []
-        self.bullets = []
-
-        # Populate main
-        for frame, sub, instr_type, args in ecl.main:
-            if not self.main or self.main[-1][0] < frame:
-                self.main.append((frame, [(sub, instr_type, args)]))
-            elif self.main[-1][0] == frame:
-                self.main[-1][1].append((sub, instr_type, args))
-
-
-    def get_objects_by_texture(self, objects_by_texture):
-        # Add enemies to vertices/uvs
-        for enemy in self.enemies:
-            enemy.get_objects_by_texture(objects_by_texture)
-
-        # Add bullets to vertices/uvs
-        for bullet in self.bullets:
-            bullet.get_objects_by_texture(objects_by_texture)
-
-
-    def update(self, frame):
-        if self.main and self.main[0][0] == frame:
-            for sub, instr_type, args in self.main.pop(0)[1]:
-                if instr_type in (0, 2, 4, 6) and not self._game_state.boss:
-                    x, y, z, life, unknown1, unknown2, unknown3 = args
-                    if instr_type & 4:
-                        if x < -990: #102h.exe@0x411820
-                            x = self._game_state.prng.rand_double() * 368
-                        if y < -990: #102h.exe@0x41184b
-                            y = self._game_state.prng.rand_double() * 416
-                        if z < -990: #102h.exe@0x411881
-                            y = self._game_state.prng.rand_double() * 800
-                    enemy = Enemy((x, y), life, instr_type, self.anm_wrapper, self._game_state)
-                    self.enemies.append(enemy)
-                    self.processes.append(ECLRunner(self.ecl, sub, enemy, self._game_state))
-
-
-        # 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
-        for enemy in self.enemies:
-            enemy.update()
-            for bullet in tuple(enemy.bullets):
-                if bullet._launched:
-                    enemy.bullets.remove(bullet)
-                self.bullets.append(bullet)
-
-        # Update bullets
-        for bullet in self.bullets:
-            bullet.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
-
-        # Filter out-of-screen enemies
-        for enemy in tuple(self.enemies):
-            if enemy._was_visible and not enemy in visible_enemies:
-                enemy._removed = True
-                self.enemies.remove(enemy)
-
-        # Filter out-of-scren bullets
-        for bullet in tuple(self.bullets):
-            if not bullet.is_visible(384, 448):
-                self.bullets.remove(bullet)
-
-
-        #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
-
--- a/pytouhou/game/game.py
+++ b/pytouhou/game/game.py
@@ -15,10 +15,16 @@
 
 from pytouhou.utils.random import Random
 
+from pytouhou.vm.eclrunner import ECLMainRunner
+
+from pytouhou.game.enemymanager import Enemy #TODO: enemymanager -> enemy
+
+
 class GameState(object):
-    __slots__ = ('resources', 'players', 'rank', 'difficulty', 'frame', 'stage', 'boss', 'prng')
-    def __init__(self, resources, players, stage, rank, difficulty):
-        self.resources = resources
+    __slots__ = ('resource_loader', 'players', 'rank', 'difficulty', 'frame',
+                 'stage', 'boss', 'prng')
+    def __init__(self, resource_loader, players, stage, rank, difficulty):
+        self.resource_loader = resource_loader
 
         self.stage = stage
         self.players = players
@@ -29,9 +35,78 @@ class GameState(object):
         self.frame = 0
 
 
-class Resources(object):
-    def __init__(self, etama_anm_wrappers, players_anm_wrappers, effects_anm_wrapper):
-        self.etama_anm_wrappers = etama_anm_wrappers
-        self.players_anm_wrappers = players_anm_wrappers
-        self.effects_anm_wrapper = effects_anm_wrapper
+
+class Game(object):
+    def __init__(self, resource_loader, players, stage, rank, difficulty):
+        self.game_state = GameState(resource_loader, players, stage, rank, difficulty)
+
+        self.enemies = []
+
+        self.bullets = []
+        self.bonuses = []
+
+        self.enm_anm_wrapper = resource_loader.get_anm_wrapper2(('stg%denm.anm' % stage,
+                                                                 'stg%denm2.anm' % stage))
+        ecl = resource_loader.get_ecl('ecldata%d.ecl' % stage)
+        self.ecl_runner = ECLMainRunner(ecl, self.new_enemy, self.game_state)
+
+
+    def get_objects_by_texture(self, objects_by_texture):
+        #TODO: move elsewhere
+        for enemy in self.enemies:
+            enemy.get_objects_by_texture(objects_by_texture)
+
+        for bullet in self.bullets:
+            bullet.get_objects_by_texture(objects_by_texture)
+
+
+    def new_enemy(self, pos, life, instr_type):
+        enemy = Enemy(pos, life, instr_type, self.enm_anm_wrapper, self.game_state)
+        self.enemies.append(enemy)
+        return enemy
+
+
+    def run_iter(self, keystate):
+        # 1. VMs.
+        self.ecl_runner.run_iter()
+
+        # 2. Filter out destroyed enemies
+        self.enemies[:] = (enemy for enemy in self.enemies if not enemy._removed)
 
+        # 3. Let's play!
+        for enemy in self.enemies:
+            enemy.update()
+            for bullet in tuple(enemy.bullets):
+                if bullet._launched:
+                    enemy.bullets.remove(bullet)
+                self.bullets.append(bullet)
+        for bullet in self.bullets:
+            bullet.update()
+
+
+        # 4. Cleaning
+        self.cleanup()
+
+        self.game_state.frame += 1
+
+
+    def cleanup(self):
+        # Filter out non-visible enemies
+        for enemy in tuple(self.enemies):
+            if enemy.is_visible(384, 448): #TODO
+                enemy._was_visible = True
+            elif enemy._was_visible:
+                # Filter out-of-screen enemy
+                enemy._removed = True
+                self.enemies.remove(enemy)
+
+        # Filter out-of-scren bullets
+        # TODO: was_visible thing
+        for bullet in tuple(self.bullets):
+            if not bullet.is_visible(384, 448):
+                self.bullets.remove(bullet)
+
+        # 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
+
--- a/pytouhou/game/sprite.py
+++ b/pytouhou/game/sprite.py
@@ -18,26 +18,6 @@ from pytouhou.utils.matrix import Matrix
 from pytouhou.utils.interpolator import Interpolator
 
 
-class AnmWrapper(object):
-    def __init__(self, anm_files):
-        self.anm_files = list(anm_files)
-
-
-    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, anm.scripts[script_index]
-        raise IndexError
-
-
-
 class Sprite(object):
     def __init__(self):
         self.anm = None
--- a/pytouhou/opengl/texture.py
+++ b/pytouhou/opengl/texture.py
@@ -23,8 +23,8 @@ from OpenGL.GLU import *
 
 
 class TextureManager(object):
-    def __init__(self, archive=None):
-        self.archive = archive
+    def __init__(self, loader=None):
+        self.loader = loader
         self.textures = {}
 
     def __getitem__(self, key):
@@ -44,18 +44,14 @@ class TextureManager(object):
             texture = self[key]
 
 
-    def set_archive(self, archive):
-        self.archive = archive
-
-
     def load_texture(self, key):
         first_name, secondary_name = key
 
-        image_file = BytesIO(self.archive.extract(os.path.basename(first_name)))
+        image_file = self.loader.get_file(os.path.basename(first_name))
         textureSurface = pygame.image.load(image_file).convert_alpha()
 
         if secondary_name:
-            alpha_image_file = BytesIO(self.archive.extract(os.path.basename(secondary_name)))
+            alpha_image_file = self.loader.get_file(os.path.basename(secondary_name))
             alphaSurface = pygame.image.load(alpha_image_file)
             assert textureSurface.get_size() == alphaSurface.get_size()
             for x in range(alphaSurface.get_width()):
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/pytouhou/resource/anmwrapper.py
@@ -0,0 +1,17 @@
+class AnmWrapper(object):
+    def __init__(self, anm_files):
+        self.anm_files = list(anm_files)
+
+
+    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, anm.scripts[script_index]
+        raise IndexError
new file mode 100644
--- /dev/null
+++ b/pytouhou/resource/loader.py
@@ -0,0 +1,100 @@
+from io import BytesIO
+
+from pytouhou.formats.pbg3 import PBG3
+from pytouhou.formats.std import Stage
+from pytouhou.formats.ecl import ECL
+from pytouhou.formats.anm0 import Animations
+
+
+from pytouhou.resource.anmwrapper import AnmWrapper
+
+
+class ArchiveDescription(object):
+    _formats = {'PBG3': PBG3}
+
+    def __init__(self, path, format_class, file_list=None):
+        self.path = path
+        self.format_class = format_class
+        self.file_list = file_list or []
+
+
+    def open(self):
+        file = open(self.path, 'rb')
+        instance = self.format_class.read(file)
+        return instance
+
+
+    @classmethod
+    def get_from_path(cls, path):
+        with open(path, 'rb') as file:
+            magic = file.read(4)
+            file.seek(0)
+            format_class = cls._formats[magic]
+            instance = format_class.read(file)
+            file_list = instance.list_files()
+        return cls(path, format_class, file_list)
+
+
+
+class Loader(object):
+    def __init__(self):
+        self.known_files = {}
+        self.instanced_ecls = {}
+        self.instanced_anms = {}
+        self.instanced_stages = {}
+
+
+    def scan_archives(self, paths):
+        for path in paths:
+            archive_description = ArchiveDescription.get_from_path(path)
+            for name in archive_description.file_list:
+                self.known_files[name] = archive_description
+
+
+    def get_file_data(self, name):
+        with self.known_files[name].open() as archive:
+            content = archive.extract(name)
+        return content
+
+
+    def get_file(self, name):
+        with self.known_files[name].open() as archive:
+            content = archive.extract(name)
+        return BytesIO(content)
+
+
+    def get_anm(self, name):
+        if name not in self.instanced_anms:
+            file = self.get_file(name)
+            self.instanced_anms[name] = Animations.read(file) #TODO: modular
+        return self.instanced_anms[name]
+
+
+    def get_stage(self, name):
+        if name not in self.instanced_stages:
+            file = self.get_file(name)
+            self.instanced_stages[name] = Stage.read(file) #TODO: modular
+        return self.instanced_stages[name]
+
+
+    def get_ecl(self, name):
+        if name not in self.instanced_ecls:
+            file = self.get_file(name)
+            self.instanced_ecls[name] = ECL.read(file) #TODO: modular
+        return self.instanced_ecls[name]
+
+
+    def get_anm_wrapper(self, names):
+        return AnmWrapper(self.get_anm(name) for name in names)
+
+
+    def get_anm_wrapper2(self, names):
+        anims = []
+        try:
+            for name in names:
+                anims.append(self.get_anm(name))
+        except KeyError:
+            pass
+
+        return AnmWrapper(anims)
+
--- a/pytouhou/utils/bitstream.py
+++ b/pytouhou/utils/bitstream.py
@@ -19,6 +19,14 @@ class BitStream(object):
         self.byte = 0
 
 
+    def __enter__(self):
+        return self
+
+
+    def __exit__(self, type, value, traceback):
+        return self.io.__exit__(type, value, traceback)
+
+
     def seek(self, offset, whence=0):
         self.io.seek(offset, whence)
         self.byte = 0
--- a/pytouhou/vm/eclrunner.py
+++ b/pytouhou/vm/eclrunner.py
@@ -23,6 +23,65 @@ logger = get_logger(__name__)
 
 
 
+class ECLMainRunner(object):
+    __metaclass__ = MetaRegistry
+    __slots__ = ('_ecl', '_new_enemy_func', '_game_state', 'processes',
+                 'instruction_pointer')
+
+    def __init__(self, ecl, new_enemy_func, game_state):
+        self._ecl = ecl
+        self._new_enemy_func = new_enemy_func
+        self._game_state = game_state
+
+        self.processes = []
+
+        self.instruction_pointer = 0
+
+
+    def run_iter(self):
+        while True:
+            try:
+                frame, sub, instr_type, args = self._ecl.main[self.instruction_pointer]
+            except IndexError:
+                break
+
+            if frame > self._game_state.frame:
+                break
+            else:
+                self.instruction_pointer += 1
+
+            if frame == self._game_state.frame:
+                try:
+                    callback = self._handlers[instr_type]
+                except KeyError:
+                    logger.warn('unhandled opcode %d (args: %r)', instr_type, args)
+                else:
+                    callback(self, sub, instr_type, *args)
+
+        self.processes[:] = (process for process in self.processes
+                                                if process.run_iteration())
+
+
+    @instruction(0)
+    @instruction(2)
+    @instruction(4)
+    @instruction(6)
+    def pop_enemy(self, sub, instr_type, x, y, z, life, unknown1, unknown2, unknown3):
+        if self._game_state.boss:
+            return
+        if instr_type & 4:
+            if x < -990: #102h.exe@0x411820
+                x = self._game_state.prng.rand_double() * 368
+            if y < -990: #102h.exe@0x41184b
+                y = self._game_state.prng.rand_double() * 416
+            if z < -990: #102h.exe@0x411881
+                y = self._game_state.prng.rand_double() * 800
+        enemy = self._new_enemy_func((x, y), life, instr_type)
+        self.processes.append(ECLRunner(self._ecl, sub, enemy, self._game_state))
+
+
+
+
 class ECLRunner(object):
     __metaclass__ = MetaRegistry
     __slots__ = ('_ecl', '_enemy', '_game_state', 'variables', 'sub', 'frame',