changeset 286:4838e9bab0f9

Implement dialogs (MSG files).
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Sun, 12 Feb 2012 16:06:03 +0100
parents 2100276c289d
children 981d1893d564
files pytouhou/formats/ecl.py pytouhou/formats/msg.py pytouhou/game/face.py pytouhou/game/game.py pytouhou/games/eosd.py pytouhou/ui/gamerenderer.pyx pytouhou/vm/eclrunner.py pytouhou/vm/msgrunner.py
diffstat 8 files changed, 269 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- 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')}
 
 
--- 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
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/face.py
@@ -0,0 +1,47 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2012 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+##
+## 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()
--- 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
 
--- 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]
 
--- 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
--- 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
 
 
new file mode 100644
--- /dev/null
+++ b/pytouhou/vm/msgrunner.py
@@ -0,0 +1,138 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2012 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+##
+## 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)