# HG changeset patch # User Thibaut Girka # Date 1315173000 -7200 # Node ID ac2e5e1c2c3c3e2087a221c9eef80593841a44b1 # Parent 54929d495654a7c14938f1af0095817c4b66cf86 Refactor \o/ diff --git a/eclviewer.py b/eclviewer.py --- 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]) diff --git a/pytouhou/formats/pbg3.py b/pytouhou/formats/pbg3.py --- 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) diff --git a/pytouhou/formats/std.py b/pytouhou/formats/std.py --- 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(' 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 + diff --git a/pytouhou/game/sprite.py b/pytouhou/game/sprite.py --- 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 diff --git a/pytouhou/opengl/texture.py b/pytouhou/opengl/texture.py --- 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()): diff --git a/pytouhou/resource/__init__.py b/pytouhou/resource/__init__.py new file mode 100644 diff --git a/pytouhou/resource/anmwrapper.py b/pytouhou/resource/anmwrapper.py 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 diff --git a/pytouhou/resource/loader.py b/pytouhou/resource/loader.py 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) + diff --git a/pytouhou/utils/bitstream.py b/pytouhou/utils/bitstream.py --- 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 diff --git a/pytouhou/vm/eclrunner.py b/pytouhou/vm/eclrunner.py --- 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',