# HG changeset patch # User Emmanuel Gil Peyrot # Date 1329059163 -3600 # Node ID 4838e9bab0f951e167199e340bfebfa06e4819ba # Parent 2100276c289dbefdb14183eaf6f4e96a69825dae Implement dialogs (MSG files). diff --git a/pytouhou/formats/ecl.py b/pytouhou/formats/ecl.py --- a/pytouhou/formats/ecl.py +++ b/pytouhou/formats/ecl.py @@ -157,9 +157,9 @@ class ECL(object): 2: ('fffhhI', 'spawn_enemy_mirrored'), 4: ('fffhhI', 'spawn_enemy_random'), 6: ('fffhhI', 'spawn_enemy_mirrored_random'), - 8: ('', None), - 9: ('', None), - 10: ('II', None), + 8: ('', 'call_msg'), + 9: ('', 'wait_msg'), + 10: ('II', 'resume_ecl'), 12: ('', 'stop_time')} diff --git a/pytouhou/formats/msg.py b/pytouhou/formats/msg.py --- a/pytouhou/formats/msg.py +++ b/pytouhou/formats/msg.py @@ -29,8 +29,8 @@ class MSG(object): 6: ('', 'add_enemy_sprite'), 7: ('I', 'change_music'), 8: ('hhs', 'display_character_line'), - 9: ('I', None), - 10: ('', None), + 9: ('I', 'show_scores'), + 10: ('', 'freeze'), 11: ('', 'next_level'), 12: ('', None), 13: ('I', None), @@ -38,7 +38,7 @@ class MSG(object): def __init__(self): - self.msgs = [[]] + self.msgs = {} @classmethod @@ -47,13 +47,13 @@ class MSG(object): entry_offsets = unpack('<%dI' % entry_count, file.read(4 * entry_count)) msg = cls() - msg.msgs = [] + msg.msgs = {} - for offset in entry_offsets: + for i, offset in enumerate(entry_offsets): if msg.msgs and offset == entry_offsets[0]: # In EoSD, Reimu’s scripts start at 0, and Marisa’s ones at 10. continue # If Reimu has less than 10 scripts, the remaining offsets are equal to her first. - msg.msgs.append([]) + msg.msgs[i] = [] file.seek(offset) while True: @@ -73,7 +73,7 @@ class MSG(object): args = (data, ) logger.warn('unknown msg opcode %d', opcode) - msg.msgs[-1].append((time, opcode, args)) + msg.msgs[i].append((time, opcode, args)) return msg diff --git a/pytouhou/game/face.py b/pytouhou/game/face.py new file mode 100644 --- /dev/null +++ b/pytouhou/game/face.py @@ -0,0 +1,47 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2012 Emmanuel Gil Peyrot +## +## 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 pytouhou.game.sprite import Sprite +from pytouhou.vm.anmrunner import ANMRunner + + +class Face(object): + __slots__ = ('_anm_wrapper', '_sprite', '_anmrunner', 'side', 'x', 'y') + + def __init__(self, anm_wrapper, effect, side): + self._anm_wrapper = anm_wrapper + self._sprite = Sprite() + self._anmrunner = ANMRunner(anm_wrapper, side * 2, self._sprite) + self.side = side + self.load(0) + self.animate(effect) + + #FIXME: the same as game.effect. + self.x = -32 + self.y = -16 + self._sprite.allow_dest_offset = True + + + def animate(self, effect): + self._anmrunner.interrupt(effect) + + + def load(self, index): + self._sprite.anm, self._sprite.texcoords = self._anm_wrapper.get_sprite(self.side * 8 + index) + self._anmrunner.run_frame() + + + def update(self): + self._anmrunner.run_frame() diff --git a/pytouhou/game/game.py b/pytouhou/game/game.py --- a/pytouhou/game/game.py +++ b/pytouhou/game/game.py @@ -17,6 +17,7 @@ from itertools import chain from pytouhou.utils.random import Random from pytouhou.vm.eclrunner import ECLMainRunner +from pytouhou.vm.msgrunner import MSGRunner from pytouhou.game.enemy import Enemy from pytouhou.game.item import Item @@ -55,6 +56,8 @@ class Game(object): self.difficulty_max = 20 if rank == 0 else 32 self.boss = None self.spellcard = None + self.msg_runner = None + self.msg_wait = False self.bonus_list = [0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 2] self.prng = prng or Random() @@ -73,6 +76,12 @@ class Game(object): self.deaths_count = self.prng.rand_uint16() % 3 self.next_bonus = self.prng.rand_uint16() % 8 + self.last_keystate = 0 + + + def msg_sprites(self): + return [] + def modify_difficulty(self, diff): self.difficulty_counter += diff @@ -138,6 +147,11 @@ class Game(object): return enemy + def new_msg(self, sub): + self.msg_runner = MSGRunner(self.msg, sub, self) + self.msg_runner.run_iteration() + + def run_iter(self, keystate): # 1. VMs. self.ecl_runner.run_iter() @@ -158,6 +172,9 @@ class Game(object): # Pri 6 is background self.update_effect() #TODO: Pri unknown + if self.msg_runner: + self.update_msg(keystate) # Pri ? + keystate &= ~3 # Remove the ability to attack (keystates 1 and 2). self.update_players(keystate) # Pri 7 self.update_enemies() # Pri 9 self.update_effects() # Pri 10 @@ -182,6 +199,15 @@ class Game(object): enemy.update() + def update_msg(self, keystate): + if keystate & 1 and not self.last_keystate & 1: + self.msg_runner.skip() + if keystate & 256 and self.msg_runner.allow_skip: + self.msg_runner.skip() + self.last_keystate = keystate + self.msg_runner.run_iteration() + + def update_players(self, keystate): for player in self.players: player.update(keystate) #TODO: differentiate keystates (multiplayer mode) @@ -295,6 +321,6 @@ class Game(object): self.items = items # Disable boss mode if it is dead/it has timeout - if self.boss and self.boss._removed: + if self.boss and self.boss._enemy._removed: self.boss = None diff --git a/pytouhou/games/eosd.py b/pytouhou/games/eosd.py --- a/pytouhou/games/eosd.py +++ b/pytouhou/games/eosd.py @@ -59,6 +59,21 @@ class EoSDGame(Game): ItemType(etama3, 5, 12), #1up ItemType(etama3, 6, 13)] #Star + player_face = player_states[0].character // 2 + enemy_face = [('face03a.anm', 'face03b.anm'), + ('face05a.anm',), + ('face06a.anm', 'face06b.anm'), + ('face08a.anm', 'face08b.anm'), + ('face09a.anm', 'face09b.anm'), + ('face09b.anm', 'face10a.anm', 'face10b.anm'), + ('face08a.anm', 'face12a.anm', 'face12b.anm', 'face12c.anm')] + self.msg = resource_loader.get_msg('msg%d.dat' % stage) + self.msg_anm_wrapper = resource_loader.get_anm_wrapper2(('face0%da.anm' % player_face, + 'face0%db.anm' % player_face, + 'face0%dc.anm' % player_face) + + enemy_face[stage - 1], + (0, 2, 4, 8, 10, 11, 12)) + characters = resource_loader.get_eosd_characters() players = [EoSDPlayer(state, self, resource_loader, characters[state.character]) for state in player_states] diff --git a/pytouhou/ui/gamerenderer.pyx b/pytouhou/ui/gamerenderer.pyx --- a/pytouhou/ui/gamerenderer.pyx +++ b/pytouhou/ui/gamerenderer.pyx @@ -85,6 +85,7 @@ cdef class GameRenderer(Renderer): self.render_elements(game.effects) self.render_elements(chain(game.players_bullets, game.players, + game.msg_sprites(), *(player.objects() for player in game.players))) self.render_elements(chain(game.bullets, game.lasers, game.cancelled_bullets, game.items)) #TODO: display item indicators diff --git a/pytouhou/vm/eclrunner.py b/pytouhou/vm/eclrunner.py --- a/pytouhou/vm/eclrunner.py +++ b/pytouhou/vm/eclrunner.py @@ -50,7 +50,8 @@ class ECLMainRunner(object): except IndexError: break - if frame > self.frame: + # The msg_wait instruction stops the reading of the ECL, not just the frame incrementation. + if frame > self.frame or self._game.msg_wait or self.time_stopped: break else: self.instruction_pointer += 1 @@ -66,7 +67,7 @@ class ECLMainRunner(object): self.processes[:] = (process for process in self.processes if process.run_iteration()) - if not self.time_stopped: + if not (self._game.msg_wait or self.time_stopped): self.frame += 1 @@ -94,6 +95,30 @@ class ECLMainRunner(object): self._pop_enemy(sub, instr_type, x, y, z, life, bonus_dropped, die_score) + @instruction(8) + def call_msg(self, sub, instr_type): + self._game.new_msg(sub) + + + @instruction(9) + def wait_msg(self, sub, instr_type): + self._game.msg_wait = True + + + @instruction(10) + def resume_ecl(self, sub, instr_type, unk1, unk2): + boss = self._game.boss + enemy = boss._enemy + self._game.msg_wait = False + if enemy.boss_callback: + boss.frame = 0 + boss.sub = enemy.boss_callback + boss.instruction_pointer = 0 + enemy.boss_callback = None + else: + raise Exception #TODO + + @instruction(12) def stop_time(self, sub, instr_type): self.time_stopped = True @@ -127,11 +152,6 @@ class ECLRunner(object): def handle_callbacks(self): #TODO: implement missing callbacks and clean up! enm = self._enemy - if enm.boss_callback is not None: #XXX: MSG's job! - self.frame = 0 - self.sub = enm.boss_callback - self.instruction_pointer = 0 - enm.boss_callback = None if enm.life <= 0 and enm.touchable: death_flags = enm.death_flags & 7 @@ -861,7 +881,7 @@ class ECLRunner(object): # but standard enemies are blocked only until any of them is killed. if value == 0: self._enemy.boss = True - self._game.boss = self._enemy + self._game.boss = self elif value == -1: self._enemy.boss = False self._game.boss = None @@ -924,6 +944,8 @@ class ECLRunner(object): @instruction(113) def set_low_life_trigger(self, value): + #XXX: this instruction takes 100 frames to fill the enemy's life bar + self.frame -= 100 self._enemy.low_life_trigger = value @@ -934,6 +956,7 @@ class ECLRunner(object): @instruction(115) def set_timeout(self, timeout): + self._enemy.frame = 0 self._enemy.timeout = timeout diff --git a/pytouhou/vm/msgrunner.py b/pytouhou/vm/msgrunner.py new file mode 100644 --- /dev/null +++ b/pytouhou/vm/msgrunner.py @@ -0,0 +1,138 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2012 Emmanuel Gil Peyrot +## +## 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 pytouhou.utils.helpers import get_logger + +from pytouhou.vm.common import MetaRegistry, instruction +from pytouhou.game.face import Face + +logger = get_logger(__name__) + + +class MSGRunner(object): + __metaclass__ = MetaRegistry + __slots__ = ('_msg', '_game', 'frame', 'sleep_time', 'allow_skip', + 'frozen', 'faces', 'ended', 'instruction_pointer') + + def __init__(self, msg, script, game): + self._msg = msg.msgs[script + 10 * (game.players[0].state.character // 2)] + self._game = game + self.frame = 0 + self.sleep_time = 0 + self.allow_skip = True + self.frozen = False + + self.faces = [None, None] + game.msg_sprites = self.objects + self.ended = False + + self.instruction_pointer = 0 + + + def objects(self): + return [face for face in self.faces if face] if not self.ended else [] + + + def run_iteration(self): + while True: + if self.ended: + return False + + try: + frame, instr_type, args = self._msg[self.instruction_pointer] + except IndexError: + self.end() + 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 msg opcode %d (args: %r)', instr_type, args) + else: + callback(self, *args) + + if not self.frozen: + if self.sleep_time > 0: + self.sleep_time -= 1 + else: + self.frame += 1 + + for face in self.faces: + if face: + face.update() + + return True + + + def skip(self): + self.sleep_time = 0 + + + def end(self): + self._game.msg_runner = None + self._game.msg_wait = False + self.ended = True + + + @instruction(0) + def unknown0(self): + if self.allow_skip: + raise Exception #TODO: seems to crash the game, but why? + self.end() + + + @instruction(1) + def enter(self, side, effect): + self.faces[side] = Face(self._game.msg_anm_wrapper, effect, side) + + + @instruction(2) + def change_face(self, side, index): + face = self.faces[side] + if face: + face.load(index) + + + @instruction(4) + def pause(self, duration): + self.sleep_time = duration + + + @instruction(5) + def animate(self, side, effect): + face = self.faces[side] + if face: + face.animate(effect) + + + @instruction(6) + def spawn_enemy_sprite(self): + self._game.msg_wait = False + + + @instruction(10) + def freeze(self): + self.frozen = True + + + @instruction(13) + def set_allow_skip(self, boolean): + self.allow_skip = bool(boolean)