changeset 486:2f53be1b2f60

Merge netplay branch.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Fri, 27 Sep 2013 19:01:47 +0200
parents ca22df9e70bc (diff) c099802e2435 (current diff)
children 711c75115675
files eosd pytouhou/game/enemy.pyx pytouhou/game/game.pxd pytouhou/game/game.pyx pytouhou/ui/gamerunner.pyx
diffstat 96 files changed, 7707 insertions(+), 1847 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore
+++ b/.hgignore
@@ -1,1 +1,8 @@
-.pyc
+\.pyc$
+\.pyxbldc$
+\.c$
+\.o$
+\.so$
+\.pyd$
+^build$
+^scripts$
--- a/README
+++ b/README
@@ -14,7 +14,10 @@ Dependencies:
 Running:
     * Python2 (>= 2.6)
     * Cython
-    * Pyglet
+    * A working OpenGL driver
+    * SDL2
+    * SDL2_image, SDL2_mixer, SDL2_ttf
+    * A TTF font file, placed as “font.ttf” in the game directory.
 
 
 Building sample data:
@@ -27,7 +30,7 @@ Documentation:
 
 The code should be sufficiently documented for anyone interested to learn how the EoSD engine work,
 but additional documentation is available at:
-http://linkmauve.fr/doc/touhou/
+http://pytouhou.linkmauve.fr/
 
 
 
new file mode 100644
--- /dev/null
+++ b/TODO
@@ -0,0 +1,56 @@
+Engine
+- homing bullets
+- MSG texts
+- boss OSD
+- bombs
+- vm END
+- score display
+- stage change in story mode
+- update score.dat and disallow the launch of locked stages
+
+- Rumia’s Moonlight Ray isn’t directed towards the player
+- Cirno’s last spell, killed before even starting it
+- Meiling’s rainbow in normal, doesn’t look like it should
+- Meiling’s blue/red thing, should be directed toward the player?
+- slowdowns with the three maid fairies just before Patchouli
+- Patchouli’s spellcard background
+- Patchouli’s [MarisaB] yellow bullets change direction too abruptly
+- Patchouli’s red bullets don’t “explode” in four
+- Patchouli’s yellow big bullets too quick (?) and star bonus outside of the screen
+- Sakuya’s knife manipulation during time freeze
+- look at the the last spellcard of Remi
+- huge slowdowns with the spamming fairies of extra
+- Patchouli’s books are too agressive
+- Flandre’s And Then Will There Be None? is done two times
+- Flandre’s QED is impossible
+- Patchouli replaces Flandre in the last MSG
+
+ECL
+- 118 and 102
+- 121
+ * 12
+ * 14
+- 122
+ * 2, for Meiling, meaning and implementation
+ * 5, implementation
+ * 6, implementation
+ * 10, meaning and implementation
+ * 15, implementation
+- 125
+- 127
+- 130
+- 133
+- 134
+- 135
+
+ANM
+- 31
+
+MSG
+- 0
+- 12
+- 14
+- fix the end
+
+Ideas
+- make a cache for the labels, to speed up the spellcard bonus
new file mode 100755
--- /dev/null
+++ b/anmviewer
@@ -0,0 +1,50 @@
+#!/usr/bin/env python2
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2011 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.
+##
+
+import argparse
+import os
+
+from pytouhou.ui.window import Window
+from pytouhou.resource.loader import Loader
+from pytouhou.ui.anmrenderer import ANMRenderer
+
+
+def main(path, data, name, script, entry, sprites, fixed_pipeline):
+    resource_loader = Loader()
+    resource_loader.scan_archives(os.path.join(path, name) for name in data)
+
+    window = Window((384, 448), fixed_pipeline=fixed_pipeline, sound=False)
+
+    # Get out animation
+    anm = resource_loader.get_anm(name)
+    renderer = ANMRenderer(window, resource_loader, anm[entry], script, sprites)
+    window.set_runner(renderer)
+    window.run()
+
+
+parser = argparse.ArgumentParser(description='Viewer of ANM files, archives containing animations used in Touhou games.')
+
+parser.add_argument('data', metavar='DAT', default=('CM.DAT', 'ST.DAT'), nargs='*', help='Game’s .DAT data files')
+parser.add_argument('-p', '--path', metavar='DIRECTORY', default='.', help='Game directory path.')
+parser.add_argument('--anm', metavar='ANM', required=True, help='Select an ANM')
+parser.add_argument('--script', metavar='SCRIPT', type=int, default=0, help='First script to play')
+parser.add_argument('--entry', metavar='ENTRY', type=int, default=0, help='Entry to display, in multi-entries ANMs.')
+parser.add_argument('--sprites', action='store_true', default=False, help='Display sprites instead of scripts.')
+parser.add_argument('--fixed-pipeline', action='store_true', help='Use the fixed pipeline instead of the new programmable one.')
+
+args = parser.parse_args()
+
+main(args.path, tuple(args.data), args.anm, args.script, args.entry, args.sprites,
+     args.fixed_pipeline)
--- a/data/ST/Makefile
+++ b/data/ST/Makefile
@@ -57,11 +57,11 @@ stg1enm2.anm: stg1enm2.script
 
 
 ecldata1.ecl: make_ecl.py
-	PYTHONPATH=../../ python make_ecl.py
+	PYTHONPATH=../../ python2 make_ecl.py
 
 
 stage1.std: make_stage.py
-	PYTHONPATH=../../ python make_stage.py
+	PYTHONPATH=../../ python2 make_stage.py
 
 
 msg1.dat: msg1.script
--- a/eosd
+++ b/eosd
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python2
 # -*- encoding: utf-8 -*-
 ##
 ## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
@@ -16,36 +16,130 @@
 import argparse
 import os
 
-import pyximport
-pyximport.install()
+
+pathsep = os.path.pathsep
+default_data = (pathsep.join(('CM.DAT', 'th06*_CM.DAT', '*CM.DAT', '*cm.dat')),
+                pathsep.join(('ST.DAT', 'th6*ST.DAT', '*ST.DAT', '*st.dat')),
+                pathsep.join(('IN.DAT', 'th6*IN.DAT', '*IN.DAT', '*in.dat')),
+                pathsep.join(('MD.DAT', 'th6*MD.DAT', '*MD.DAT', '*md.dat')),
+                pathsep.join(('102h.exe', '102*.exe', '東方紅魔郷.exe', '*.exe')))
+
+
+parser = argparse.ArgumentParser(description='Libre reimplementation of the Touhou 6 engine.')
 
+parser.add_argument('data', metavar='DAT', default=default_data, nargs='*', help='Game’s data files')
+parser.add_argument('-p', '--path', metavar='DIRECTORY', default='.', help='Game directory path.')
+parser.add_argument('-s', '--stage', metavar='STAGE', type=int, default=None, help='Stage, 1 to 7 (Extra).')
+parser.add_argument('-r', '--rank', metavar='RANK', type=int, default=0, help='Rank, from 0 (Easy, default) to 3 (Lunatic).')
+parser.add_argument('-c', '--character', metavar='CHARACTER', type=int, default=0, help='Select the character to use, from 0 (ReimuA, default) to 3 (MarisaB).')
+parser.add_argument('--replay', metavar='REPLAY', help='Select a replay')
+parser.add_argument('--save-replay', metavar='REPLAY', help='Save the upcoming game into a replay file')
+parser.add_argument('--skip-replay', action='store_true', help='Skip the replay and start to play when it’s finished')
+parser.add_argument('-b', '--boss-rush', action='store_true', help='Fight only bosses')
+parser.add_argument('--single-buffer', action='store_true', help='Disable double buffering')
+parser.add_argument('--fps-limit', metavar='FPS', default=-1, type=int, help='Set fps limit. A value of 0 disables fps limiting, while a negative value limits to 60 fps if and only if vsync doesn’t work.')
+parser.add_argument('--debug', action='store_true', help='Set unlimited continues, and perhaps other debug features.')
+parser.add_argument('--fixed-pipeline', action='store_true', help='Use the fixed pipeline instead of the new programmable one.')
+parser.add_argument('--no-background', action='store_false', help='Disable background display (huge performance boost on slow systems).')
+parser.add_argument('--no-particles', action='store_false', help='Disable particles handling (huge performance boost on slow systems).')
+parser.add_argument('--no-music', action='store_false', help='Disable background music.')
+parser.add_argument('--hints', metavar='HINTS', default=None, help='Hints file, to display text while playing.')
+parser.add_argument('--verbosity', metavar='VERBOSITY', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], help='Select the wanted logging level.')
+parser.add_argument('--game', metavar='GAME', choices=['EoSD'], default='EoSD', help='Select the game engine to use.')
+parser.add_argument('--port', metavar='PORT', type=int, default=0, help='Port to use for netplay')
+parser.add_argument('--remote', metavar='REMOTE', default=None, help='Remote address')
+
+args = parser.parse_args()
+
+
+import sys
+import logging
+
+from pytouhou.lib.sdl import SDL
+from pytouhou.ui.window import Window
 from pytouhou.resource.loader import Loader
-from pytouhou.game.background import Background
 from pytouhou.ui.gamerunner import GameRunner
-from pytouhou.games.eosd import EoSDGame
-from pytouhou.game.player import PlayerState
-from pytouhou.formats.t6rp import T6RP
+from pytouhou.game.player import PlayerState, GameOver
+from pytouhou.formats.t6rp import T6RP, Level
+from pytouhou.utils.random import Random
+from pytouhou.vm.msgrunner import NextStage
+from pytouhou.formats.hint import Hint
+from pytouhou.network import Network
+
+
+if args.game == 'EoSD':
+    from pytouhou.games.eosd import EoSDCommon as Common, EoSDGame as Game
 
 
-def main(path, stage_num, rank, character, replay, data, port, remote):
-    players = [PlayerState(character=character)]
+class GameBossRush(Game):
+    def run_iter(self, keystate):
+        for i in range(20):
+            skip = not (self.enemies or self.items or self.lasers
+                        or self.bullets or self.cancelled_bullets)
+            if skip:
+                keystate &= ~1
+            Game.run_iter(self, keystate | 256 if i == 0 else 0)
+            if not self.enemies and self.frame % 90 == 0:
+                for player in self.players:
+                    if player.state.power < 128:
+                        player.state.power += 1
+            if not skip:
+                break
+
+    def cleanup(self):
+        boss_wait = any(ecl_runner.boss_wait for ecl_runner in self.ecl_runners)
+        if not (self.boss or self.msg_wait or boss_wait):
+            self.enemies = [enemy for enemy in self.enemies
+                            if enemy.boss_callback != -1 or enemy.frame > 1]
+            self.lasers = [laser for laser in self.lasers if laser.frame > 1]
+            self.effects = [effect for effect in self.effects
+                            if not hasattr(effect, '_laser')
+                            or effect._laser in self.lasers]
+            self.bullets = [bullet for bullet in self.bullets if bullet.frame > 1]
+        Game.cleanup(self)
+
+
+def main(window, path, data, stage_num, rank, character, replay, save_filename,
+         skip_replay, boss_rush, debug, enable_background, enable_particles,
+         enable_music, hints, verbosity, port, remote):
+
+    resource_loader = Loader(path)
+
+    try:
+        resource_loader.scan_archives(data)
+    except IOError:
+        sys.stderr.write('Some data files were not found, did you forget the -p option?\n')
+        exit(1)
+
+    if stage_num is None:
+        story = True
+        stage_num = 1
+        continues = 3
+    else:
+        story = False
+        continues = 0
+
+    if debug:
+        if not verbosity:
+            verbosity = 'DEBUG'
+        continues = -1  # Infinite lives
+
+    if verbosity:
+        logging.basicConfig(level=logging.__getattribute__(verbosity))
 
     if replay:
         with open(replay, 'rb') as file:
             replay = T6RP.read(file)
         rank = replay.rank
         character = replay.character
-        if not replay.levels[stage_num-1]:
-            raise Exception
-        from pytouhou.utils.random import Random
-        prng = Random(replay.levels[stage_num-1].random_seed)
-    else:
-        prng = None
+
+    save_keystates = None
+    if save_filename:
+        save_replay = T6RP()
+        save_replay.rank = rank
+        save_replay.character = character
 
     if port != 0:
-        from pytouhou.network import Network
-        from pytouhou.utils.random import Random
-
         players = [PlayerState(character=0), PlayerState(character=2)]
 
         if remote:
@@ -61,34 +155,102 @@ def main(path, stage_num, rank, characte
     else:
         con = None
 
-    resource_loader = Loader()
-    resource_loader.scan_archives(os.path.join(path, name)
-                                    for name in data)
-    game = EoSDGame(resource_loader, players, stage_num, rank, 16,
-                    prng=prng)
+    if hints:
+        with open(hints, 'rb') as file:
+            hints = Hint.read(file)
+
+    difficulty = 16
+    default_power = [0, 64, 128, 128, 128, 128, 0][stage_num - 1]
+    states = [PlayerState(character=character, power=default_power, continues=continues)]
+
+    game_class = GameBossRush if boss_rush else Game
+
+    common = Common(resource_loader)
+    runner = GameRunner(window, resource_loader, skip=skip_replay, con=con)
+    while True:
+        if replay:
+            level = replay.levels[stage_num - 1]
+            if not level:
+                raise Exception
+
+            prng = Random(level.random_seed)
+
+            #TODO: apply the replay to the other players.
+            #TODO: see if the stored score is used or if it’s the one from the previous stage.
+            if stage_num != 1 and stage_num - 2 in replay.levels:
+                previous_level = replay.levels[stage_num - 1]
+                states[0].score = previous_level.score
+                states[0].effective_score = previous_level.score
+            states[0].points = level.point_items
+            states[0].power = level.power
+            states[0].lives = level.lives
+            states[0].bombs = level.bombs
+            difficulty = level.difficulty
+        elif port == 0:
+            prng = Random()
 
-    # Load stage data
-    stage = resource_loader.get_stage('stage%d.std' % stage_num)
+        if save_filename:
+            if not replay:
+                save_replay.levels[stage_num - 1] = level = Level()
+                level.score = states[0].score
+                level.random_seed = prng.seed
+                level.point_items = states[0].points
+                level.power = states[0].power
+                level.lives = states[0].lives
+                level.bombs = states[0].bombs
+                level.difficulty = difficulty
+            save_keystates = []
+
+        hints_stage = hints.stages[stage_num - 1] if hints else None
+
+        game = game_class(resource_loader, states, stage_num, rank, difficulty, common, prng=prng, hints=hints_stage)
+
+        if not enable_particles:
+            def new_particle(pos, anim, amp, number=1, reverse=False, duration=24):
+                pass
+            game.new_particle = new_particle
+
+        background = game.background if enable_background else None
+        bgms = game.std.bgms if enable_music else None
 
-    background_anm_wrapper = resource_loader.get_anm_wrapper(('stg%dbg.anm' % stage_num,))
-    background = Background(stage, background_anm_wrapper)
+        # Main loop
+        runner.load_game(game, background, bgms, replay, save_keystates)
+        window.set_runner(runner)
+        try:
+            window.run()
+            break
+        except NextStage:
+            if not story or stage_num == (7 if boss_rush else 6 if rank > 0 else 5):
+                break
+            stage_num += 1
+            states = [player.state for player in game.players]
+        except GameOver:
+            print('Game over')
+            break
+        finally:
+            if save_filename:
+                last_key = -1
+                for time, key in enumerate(save_keystates):
+                    if key != last_key:
+                        level.keys.append((time, key, 0))
+                    last_key = key
 
-    # Main loop
-    runner = GameRunner(resource_loader, game, background, replay=replay, con=con)
-    runner.start()
+    window.set_runner(None)
+
+    if save_filename:
+        with open(save_filename, 'wb+') as file:
+            save_replay.write(file)
 
 
-parser = argparse.ArgumentParser(description='Libre reimplementation of the Touhou 6 engine.')
+with SDL():
+    window = Window(double_buffer=(not args.single_buffer),
+                    fps_limit=args.fps_limit,
+                    fixed_pipeline=args.fixed_pipeline)
 
-parser.add_argument('data', metavar='DAT', default=('CM.DAT', 'ST.DAT'), nargs='*', help='Game’s .DAT data files')
-parser.add_argument('-p', '--path', metavar='DIRECTORY', default='.', help='Game directory path.')
-parser.add_argument('-s', '--stage', metavar='STAGE', type=int, required=True, help='Stage, 1 to 7 (Extra).')
-parser.add_argument('-r', '--rank', metavar='RANK', type=int, default=0, help='Rank, from 0 (Easy, default) to 3 (Lunatic).')
-parser.add_argument('-c', '--character', metavar='CHARACTER', type=int, default=0, help='Select the character to use, from 0 (ReimuA, default) to 3 (MarisaB).')
-parser.add_argument('--replay', metavar='REPLAY', help='Select a replay')
-parser.add_argument('--port', metavar='PORT', type=int, default=0, help='Port to use for netplay')
-parser.add_argument('--remote', metavar='REMOTE', default=None, help='Remote address')
+    main(window, args.path, tuple(args.data), args.stage, args.rank,
+         args.character, args.replay, args.save_replay, args.skip_replay,
+         args.boss_rush, args.debug, args.no_background, args.no_particles,
+         args.no_music, args.hints, args.verbosity, args.port, args.remote)
 
-args = parser.parse_args()
-
-main(args.path, args.stage, args.rank, args.character, args.replay, tuple(args.data), args.port, args.remote)
+    import gc
+    gc.collect()
new file mode 100755
--- /dev/null
+++ b/music.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python2
+# -*- 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.
+##
+
+import argparse
+import os
+
+import pyximport
+pyximport.install()
+
+from pytouhou.resource.loader import Loader
+from pytouhou.ui.music import InfiniteWaveSource, ZwavSource
+from pyglet.app import run
+
+
+def get_wav_source(bgm, resource_loader):
+    posname = bgm.replace('bgm/', '').replace('.mid', '.pos')
+    track = resource_loader.get_track(posname)
+    wavname = os.path.join(resource_loader.game_dir, bgm.replace('.mid', '.wav'))
+    try:
+        source = InfiniteWaveSource(wavname, track.start, track.end)
+    except IOError:
+        source = None
+    return source
+
+
+def get_zwav_source(track, resource_loader):
+    fmt = resource_loader.get_fmt('thbgm.fmt')
+    try:
+        source = ZwavSource('thbgm.dat', fmt[track])
+    except IOError:
+        source = None
+    return source
+
+
+def main(path, track, zwav, data):
+    resource_loader = Loader(path)
+    resource_loader.scan_archives(data)
+
+    if not zwav:
+        source = get_wav_source('bgm/th06_%02d.mid' % track, resource_loader)
+    else:
+        source = get_zwav_source(track, resource_loader)
+
+    source.play()
+
+    run()
+
+
+pathsep = os.path.pathsep
+default_data = (pathsep.join(('MD.DAT', 'th6*MD.DAT', '*MD.DAT', '*md.dat')),)
+
+
+parser = argparse.ArgumentParser(description='Player for Touhou 6 music.')
+
+parser.add_argument('data', metavar='DAT', default=default_data, nargs='*', help='Game’s data files')
+parser.add_argument('-p', '--path', metavar='DIRECTORY', default='.', help='Game directory path.')
+parser.add_argument('-t', '--track', metavar='TRACK', type=int, required=True, help='The track to play, in game order.')
+parser.add_argument('-z', '--zwav', action='store_true', default=False, help='Must be set when playing from PCB or newer.')
+
+args = parser.parse_args()
+
+main(args.path, args.track, args.zwav, tuple(args.data))
--- a/pcb
+++ b/pcb
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python2
 # -*- encoding: utf-8 -*-
 ##
 ## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
@@ -27,9 +27,8 @@ from pytouhou.game.player import PlayerS
 
 
 def main(path, stage_num, rank, character, data):
-    resource_loader = Loader()
-    resource_loader.scan_archives(os.path.join(path, name)
-                                    for name in data)
+    resource_loader = Loader(path)
+    resource_loader.scan_archives(data)
     game = PCBGame(resource_loader, [PlayerState(character=character)], stage_num, rank, 16)
 
     # Load stage data
--- a/pytouhou/formats/__init__.py
+++ b/pytouhou/formats/__init__.py
@@ -3,3 +3,9 @@
 This package provides modules to handle the various proprietary file formats
 used by Touhou games.
 """
+
+class WrongFormatError(Exception):
+    pass
+
+class ChecksumError(Exception):
+    pass
--- a/pytouhou/formats/anm0.py
+++ b/pytouhou/formats/anm0.py
@@ -23,45 +23,105 @@ Almost everything rendered in the game i
 from struct import pack, unpack
 from pytouhou.utils.helpers import read_string, get_logger
 
+from pytouhou.formats import WrongFormatError
+from pytouhou.formats.thtx import Texture
+
 
 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'),
-                     13: ('', 'set_blendmode_add'),
-                     14: ('', 'set_blendmode_alphablend'),
-                     15: ('', 'keep_still'),
-                     16: ('ii', 'set_random_sprite'),
-                     17: ('fff', 'set_3d_translation'),
-                     18: ('fffi', 'move_to_linear'),
-                     19: ('fffi', 'move_to_decel'),
-                     20: ('fffi', 'move_to_accel'),
-                     21: ('', None),
-                     22: ('i', None),
-                     23: ('', 'set_corner_relative_placement'),
-                     24: ('', None),
-                     25: ('i', 'set_allow_offset'), #TODO: better name
-                     26: ('i', 'set_automatic_orientation'),
-                     27: ('f', 'shift_texture_x'),
-                     28: ('f', 'shift_texture_y'),
-                     30: ('ffi', 'scale_in'),
-                     31: ('i', None)}
+class Script(list):
+    def __init__(self):
+        list.__init__(self)
+        self.interrupts = {}
+
+
+
+class ANM0(object):
+    _instructions = {0: {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'),
+                         13: ('', 'set_blendmode_add'),
+                         14: ('', 'set_blendmode_alphablend'),
+                         15: ('', 'keep_still'),
+                         16: ('ii', 'set_random_sprite'),
+                         17: ('fff', 'set_3d_translation'),
+                         18: ('fffi', 'move_to_linear'),
+                         19: ('fffi', 'move_to_decel'),
+                         20: ('fffi', 'move_to_accel'),
+                         21: ('', 'wait'),
+                         22: ('i', 'interrupt_label'),
+                         23: ('', 'set_corner_relative_placement'),
+                         24: ('', 'wait_ex'),
+                         25: ('i', 'set_allow_offset'), #TODO: better name
+                         26: ('i', 'set_automatic_orientation'),
+                         27: ('f', 'shift_texture_x'),
+                         28: ('f', 'shift_texture_y'),
+                         29: ('i', 'set_visible'),
+                         30: ('ffi', 'scale_in'),
+                         31: ('i', None)},
+
+                     2: {0: ('', 'noop'),
+                         1: ('', 'delete'),
+                         2: ('', 'keep_still'),
+                         3: ('I', 'set_sprite'),
+                         4: ('II', 'jump_bis'),
+                         5: ('III', 'jump_ex'),
+                         6: ('fff', 'set_3d_translation'),
+                         7: ('ff', 'set_scale'),
+                         8: ('I', 'set_alpha'),
+                         9: ('BBBx', 'set_color'),
+                         10: ('', 'toggle_mirrored'),
+                         12: ('fff', 'set_3d_rotations'),
+                         13: ('fff', 'set_3d_rotations_speed'),
+                         14: ('ff', 'set_scale_speed'),
+                         15: ('ii', 'fade'),
+                         16: ('I', 'set_blendmode'),
+                         17: ('fffi', 'move_to_linear'),
+                         18: ('fffi', 'move_to_decel'),
+                         19: ('fffi', 'move_to_accel'),
+                         20: ('', 'wait'),
+                         21: ('i', 'interrupt_label'),
+                         22: ('', 'set_corner_relative_placement'),
+                         23: ('', 'wait_ex'),
+                         24: ('i', 'set_allow_offset'), #TODO: better name
+                         25: ('i', 'set_automatic_orientation'),
+                         26: ('f', 'shift_texture_x'),
+                         27: ('f', 'shift_texture_y'),
+                         28: ('i', 'set_visible'),
+                         29: ('ffi', 'scale_in'),
+                         30: ('i', None),
+                         31: ('I', None),
+                         32: ('IIfff', 'move_in_linear_bis'),
+                         33: ('IIBBBx', 'change_color_in'),
+                         34: ('III', 'fade_bis'),
+                         35: ('IIfff', 'rotate_in_bis'),
+                         36: ('IIff', 'scale_in_bis'),
+                         37: ('II', 'set_int'),
+                         38: ('ff', 'set_float'),
+                         42: ('ff', 'decrement_float'),
+                         50: ('fff', 'add_float'),
+                         52: ('fff', 'substract_float'),
+                         55: ('III', 'divide_int'),
+                         59: ('II', 'set_random_int'),
+                         60: ('ff', 'set_random_float'),
+                         69: ('IIII', 'branch_if_not_equal'),
+                         79: ('I', 'wait_duration'),
+                         80: ('I', None)}}
 
 
     def __init__(self):
+        self.version = 0
         self.size = (0, 0)
         self.first_name = None
         self.secondary_name = None
@@ -71,67 +131,113 @@ class Animations(object):
 
     @classmethod
     def read(cls, file):
-        nb_sprites, nb_scripts, zero1 = unpack('<III', file.read(12))
-        width, height, format, zero2 = unpack('<IIII', file.read(16))
-        first_name_offset, unused, secondary_name_offset = unpack('<III', file.read(12))
-        version, unknown1, thtxoffset, hasdata, nextoffset = unpack('<IIIII', file.read(20))
-        if version != 0:
-            raise Exception #TODO
-        file.read(4) #TODO
+        anm_list = []
+        start_offset = 0
+        while True:
+            file.seek(start_offset)
+            nb_sprites, nb_scripts, zero1 = unpack('<III', file.read(12))
+            width, height, fmt, unknown1 = unpack('<IIII', file.read(16))
+            first_name_offset, unused, secondary_name_offset = unpack('<III', file.read(12))
+            version, unknown2, texture_offset, has_data, next_offset, unknown3 = unpack('<IIIIII', file.read(24))
 
-        sprite_offsets = [unpack('<I', file.read(4))[0] for i in range(nb_sprites)]
-        script_offsets = [unpack('<II', file.read(8)) for i in range(nb_scripts)]
+            if version == 0:
+                assert zero1 == 0
+                assert unknown3 == 0
+                assert has_data == 0
+            elif version == 2:
+                assert zero1 == 0
+                assert secondary_name_offset == 0
+                assert has_data == 1 # Can be false but we don’t support that yet.
+            else:
+                raise WrongFormatError(version)
+
+            instructions = cls._instructions[version]
 
-        anm = Animations()
+            sprite_offsets = [unpack('<I', file.read(4))[0] for i in range(nb_sprites)]
+            script_offsets = [unpack('<II', file.read(8)) for i in range(nb_scripts)]
 
-        anm.size = (width, height)
+            self = cls()
+
+            self.size = (width, height)
+            self.version = version
 
-        # Names
-        if first_name_offset:
-            file.seek(first_name_offset)
-            anm.first_name = read_string(file, 32, 'ascii') #TODO: 32, really?
-        if secondary_name_offset:
-            file.seek(secondary_name_offset)
-            anm.secondary_name = read_string(file, 32, 'ascii') #TODO: 32, really?
+            # Names
+            if first_name_offset:
+                file.seek(start_offset + first_name_offset)
+                self.first_name = read_string(file, 32, 'ascii') #TODO: 32, really?
+            if secondary_name_offset:
+                file.seek(start_offset + secondary_name_offset)
+                self.secondary_name = read_string(file, 32, 'ascii') #TODO: 32, really?
+
+
+            # Sprites
+            for offset in sprite_offsets:
+                file.seek(start_offset + offset)
+                idx, x, y, width, height = unpack('<Iffff', file.read(20))
+                self.sprites[idx] = x, y, width, height
 
 
-        # Sprites
-        file.seek(64)
-        anm.sprites = {}
-        for offset in sprite_offsets:
-            file.seek(offset)
-            idx, x, y, width, height = unpack('<Iffff', file.read(20))
-            anm.sprites[idx] = x, y, width, height
+            # Scripts
+            for i, offset in script_offsets:
+                self.scripts[i] = Script()
+                instruction_offsets = []
+                file.seek(start_offset + offset)
+                while True:
+                    instruction_offsets.append(file.tell() - (start_offset + offset))
+                    if version == 0:
+                        time, opcode, size = unpack('<HBB', file.read(4))
+                    elif version == 2:
+                        opcode, size, time, mask = unpack('<HHHH', file.read(8))
+                        if opcode == 0xffff:
+                            break
+                        size -= 8
+                    data = file.read(size)
+                    if opcode in instructions:
+                        args = unpack('<%s' % instructions[opcode][0], data)
+                    else:
+                        args = (data,)
+                        logger.warn('unknown opcode %d', opcode)
 
+                    self.scripts[i].append((time, opcode, args))
+                    if version == 0 and opcode == 0:
+                        break
 
-        # Scripts
-        anm.scripts = {}
-        for i, offset in script_offsets:
-            anm.scripts[i] = []
-            instruction_offsets = []
-            file.seek(offset)
-            while True:
-                #TODO
-                instruction_offsets.append(file.tell() - offset)
-                time, opcode, size = unpack('<HBB', file.read(4))
+                # Translate offsets to instruction pointers and register interrupts
+                for instr_offset, (j, instr) in zip(instruction_offsets, enumerate(self.scripts[i])):
+                    time, opcode, args = instr
+                    if version == 0:
+                        if opcode == 5:
+                            args = (instruction_offsets.index(args[0]),)
+                        elif opcode == 22:
+                            interrupt = args[0]
+                            self.scripts[i].interrupts[interrupt] = j + 1
+                    elif version == 2:
+                        if opcode == 4:
+                            args = (instruction_offsets.index(args[0]), args[1])
+                        elif opcode == 5:
+                            args = (args[0], instruction_offsets.index(args[1]), args[2])
+                        elif opcode == 21:
+                            interrupt = args[0]
+                            self.scripts[i].interrupts[interrupt] = j + 1
+                        elif opcode == 69:
+                            args = (args[0], args[1], instruction_offsets.index(args[2]), args[3])
+                    self.scripts[i][j] = time, opcode, args
+
+            # Texture
+            if has_data:
+                file.seek(start_offset + texture_offset)
+                magic = file.read(4)
+                assert magic == b'THTX'
+                zero, fmt, width, height, size = unpack('<HHHHI', file.read(12))
+                assert zero == 0
                 data = file.read(size)
-                if opcode in cls._instructions:
-                    args = unpack('<%s' % cls._instructions[opcode][0], data)
-                else:
-                    args = (data,)
-                    logger.warn('unknown opcode %d', opcode)
+                self.texture = Texture(width, height, fmt, data)
 
-                anm.scripts[i].append((time, opcode, args))
-                if opcode == 0:
-                    break
+            anm_list.append(self)
 
-            # 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
+            if next_offset:
+                start_offset += next_offset
+            else:
+                break
 
-        return anm
-
+        return anm_list
--- a/pytouhou/formats/ecl.py
+++ b/pytouhou/formats/ecl.py
@@ -37,7 +37,7 @@ class ECL(object):
     enemy waves, triggering dialogs and level completion.
 
     Instance variables:
-    main -- list of instructions describing waves and triggering dialogs
+    mains -- list of lists of instructions describing waves and triggering dialogs
     subs -- list of subroutines
     """
 
@@ -108,7 +108,7 @@ class ECL(object):
                      86: ('hhffffffiiiiii', 'laser2'),
                      87: ('i', 'set_upcoming_id'),
                      88: ('if','alter_laser_angle'),
-                     90: ('iiii', 'translate_laser'),
+                     90: ('ifff', 'reposition_laser'),
                      92: ('i', 'cancel_laser'),
                      93: ('hhs', 'set_spellcard'),
                      94: ('', 'end_spellcard'),
@@ -134,7 +134,7 @@ class ECL(object):
                      115: ('i', 'set_timeout'),
                      116: ('i', 'set_timeout_callback'),
                      117: ('i', 'set_touchable'),
-                     118: ('iihh', None),
+                     118: ('iIbbbb', 'drop_particles'),
                      119: ('i', 'drop_bonus'),
                      120: ('i', 'set_automatic_orientation'),
                      121: ('ii', 'call_special_function'),
@@ -149,7 +149,7 @@ class ECL(object):
                      130: ('i', None),
                      131: ('ffiiii', 'set_difficulty_coeffs'),
                      132: ('i', 'set_invisible'),
-                     133: ('', None),
+                     133: ('', 'copy_callbacks?'),
                      134: ('', None),
                      135: ('i', 'enable_spellcard_bonus')} #TODO
 
@@ -157,33 +157,37 @@ 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),
-                          12: ('', None)}
+                          8: ('', 'call_msg'),
+                          9: ('', 'wait_msg'),
+                          10: ('II', 'resume_ecl'),
+                          12: ('', 'stop_time')}
+
+    _parameters = {6: {'main_count': 1,
+                       'nb_main_offsets': 3,
+                       'jumps_list': {2: 1, 3: 1, 29: 1, 30: 1, 31: 1, 32: 1, 33: 1, 34: 1}}}
 
 
     def __init__(self):
-        self.main = []
-        self.subs = [[]]
+        self.mains = []
+        self.subs = []
 
 
     @classmethod
-    def read(cls, file):
+    def read(cls, file, version=6):
         """Read an ECL file.
 
         Raise an exception if the file is invalid.
         Return a ECL instance otherwise.
         """
 
-        sub_count, main_offset = unpack('<II', file.read(8))
-        if file.read(8) != b'\x00\x00\x00\x00\x00\x00\x00\x00':
-            raise Exception #TODO
+        parameters = cls._parameters[version]
+
+        sub_count, main_count = unpack('<HH', file.read(4))
+
+        main_offsets = unpack('<%dI' % parameters['nb_main_offsets'], file.read(4 * parameters['nb_main_offsets']))
         sub_offsets = unpack('<%dI' % sub_count, file.read(4 * sub_count))
 
         ecl = cls()
-        ecl.subs = []
-        ecl.main = []
 
         # Read subs
         for sub_offset in sub_offsets:
@@ -222,39 +226,44 @@ class ECL(object):
             # keep trace of where the jump is supposed to end up.
             for instr_offset, (i, instr) in zip(instruction_offsets, enumerate(ecl.subs[-1])):
                 time, opcode, rank_mask, param_mask, args = instr
-                if opcode in (2, 29, 30, 31, 32, 33, 34): # relative_jump
-                    frame, relative_offset = args
-                    args = frame, instruction_offsets.index(instr_offset + relative_offset)
-                elif opcode == 3: # relative_jump_ex
-                    frame, relative_offset, counter_id = args
-                    args = frame, instruction_offsets.index(instr_offset + relative_offset), counter_id
-                ecl.subs[-1][i] = time, opcode, rank_mask, param_mask, args
+                if opcode in parameters['jumps_list']:
+                    num = parameters['jumps_list'][opcode]
+                    args = list(args)
+                    args[num] = instruction_offsets.index(instr_offset + args[num])
+                    ecl.subs[-1][i] = time, opcode, rank_mask, param_mask, tuple(args)
 
 
         # Read main
-        file.seek(main_offset)
-        while True:
-            time, = unpack('<H', file.read(2))
-            if time == 0xffff:
+        for main_offset in main_offsets:
+            if main_offset == 0:
                 break
 
-            sub, opcode, size = unpack('<HHH', file.read(6))
-            data = file.read(size - 8)
+            file.seek(main_offset)
+            ecl.mains.append([])
+            while True:
+                time, sub = unpack('<HH', file.read(4))
+                if time == 0xffff and sub == 4:
+                    break
 
-            if opcode in cls._main_instructions:
-                args = unpack('<%s' % cls._main_instructions[opcode][0], data)
-            else:
-                args = (data,)
-                logger.warn('unknown main opcode %d', opcode)
+                opcode, size = unpack('<HH', file.read(4))
+                data = file.read(size - 8)
 
-            ecl.main.append((time, sub, opcode, args))
+                if opcode in cls._main_instructions:
+                    args = unpack('<%s' % cls._main_instructions[opcode][0], data)
+                else:
+                    args = (data,)
+                    logger.warn('unknown main opcode %d', opcode)
+
+                ecl.mains[-1].append((time, sub, opcode, args))
 
         return ecl
 
 
-    def write(self, file):
+    def write(self, file, version=6):
         """Write to an ECL file."""
 
+        parameters = self._parameters[version]
+
         sub_count = len(self.subs)
         sub_offsets = []
         main_offset = 0
@@ -286,15 +295,10 @@ class ECL(object):
             #TODO: clean up this mess
             for instruction, data, offset in zip(sub, instruction_datas, instruction_offsets):
                 time, opcode, rank_mask, param_mask, args = instruction
-                if opcode in (2, 29, 30, 31, 32, 33, 34): # relative_jump
-                    frame, index = args
-                    args = frame, instruction_offsets[index] - offset
-                    format = '<IHHHH%s' % self._instructions[opcode][0]
-                    size = calcsize(format)
-                    data = pack(format, time, opcode, size, rank_mask, param_mask, *args)
-                elif opcode == 3: # relative_jump_ex
-                    frame, index, counter_id = args
-                    args = frame, instruction_offsets[index] - offset, counter_id
+                if opcode in parameters['jumps_list']:
+                    num = parameters['jumps_list'][opcode]
+                    args = list(args)
+                    args[num] = instruction_offsets[args[num]] - offset
                     format = '<IHHHH%s' % self._instructions[opcode][0]
                     size = calcsize(format)
                     data = pack(format, time, opcode, size, rank_mask, param_mask, *args)
@@ -302,15 +306,17 @@ class ECL(object):
             file.write(b'\xff' * 6 + b'\x0c\x00\x00\xff\xff\x00')
 
         # Write main
-        main_offset = file.tell()
-        for time, sub, opcode, args in self.main:
-            format = '<HHHH%s' % self._main_instructions[opcode][0]
-            size = calcsize(format)
+        main_offsets = [0] * parameters['nb_main_offsets']
+        for i, main in enumerate(self.mains):
+            main_offsets[i] = file.tell()
+            for time, sub, opcode, args in main:
+                format = '<HHHH%s' % self._main_instructions[opcode][0]
+                size = calcsize(format)
 
-            file.write(pack(format, time, sub, opcode, size, *args))
-        file.write(b'\xff\xff\x04\x00')
+                file.write(pack(format, time, sub, opcode, size, *args))
+            file.write(b'\xff\xff\x04\x00')
 
         # Patch header
         file.seek(0)
-        file.write(pack('<IIII%dI' % sub_count, sub_count, main_offset, 0, 0, *sub_offsets))
+        file.write(pack('<I%dI%dI' % (parameters['nb_main_offsets'], sub_count), sub_count, *(main_offsets + sub_offsets)))
 
new file mode 100644
--- /dev/null
+++ b/pytouhou/formats/exe.py
@@ -0,0 +1,220 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
+## Copyright (C) 2011 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 copy import copy
+from struct import Struct, unpack
+
+from pytouhou.utils.pe import PEFile
+
+from pytouhou.utils.helpers import get_logger
+
+logger = get_logger(__name__)
+
+
+SQ2 = 2. ** 0.5 / 2.
+
+
+class InvalidExeException(Exception):
+    pass
+
+
+class Shot(object):
+    def __init__(self):
+        self.interval = 0
+        self.delay = 0
+        self.pos = (0., 0.)
+        self.hitbox = (0., 0.)
+        self.angle = 0.
+        self.speed = 0.
+        self.damage = 0
+        self.orb = 0
+        self.type = 0
+        self.sprite = 0
+        self.unknown1 = None
+
+
+class SHT(object):
+    def __init__(self):
+        #self.unknown1 = None
+        #self.bombs = 0.
+        #self.unknown2 = None
+        self.hitbox = 2.
+        self.graze_hitbox = 21.
+        self.autocollection_speed = 8.
+        self.item_hitbox = 19.
+        # No percentage_of_cherry_loss_on_die
+        self.point_of_collection = 128 #TODO: find the real default.
+        self.horizontal_vertical_speed = 0.
+        self.horizontal_vertical_focused_speed = 0.
+        self.diagonal_speed = 0.
+        self.diagonal_focused_speed = 0.
+        self.shots = {}
+
+
+    @classmethod
+    def find_character_defs(cls, pe_file):
+        """Generator returning the possible VA of character definition blocks.
+
+        Based on knowledge of the structure, it tries to find valid definition blocks
+        without embedding any copyrighted material or hard-coded offsets that would
+        only be useful for a specific build of the game.
+        """
+
+        format = Struct('<4f2I')
+        data_section = [section for section in pe_file.sections
+                            if section.Name.startswith('.data')][0]
+        text_section = [section for section in pe_file.sections
+                            if section.Name.startswith('.text')][0]
+        data_va = pe_file.image_base + data_section.VirtualAddress
+        data_size = data_section.SizeOfRawData
+        text_va = pe_file.image_base + text_section.VirtualAddress
+        text_size = text_section.SizeOfRawData
+
+        # Search the whole data segment for 4 successive character definitions
+        for addr in xrange(data_va, data_va + data_size, 4):
+            for character_id in xrange(4):
+                pe_file.seek_to_va(addr + character_id * 24)
+                (speed1, speed2, speed3, speed4,
+                 ptr1, ptr2) = format.unpack(pe_file.file.read(format.size))
+
+                # Check whether the character's speed make sense,
+                # and whether the function pointers point to valid addresses
+                if not (all(0. < x < 10. for x in (speed1, speed2, speed3, speed4))
+                        and speed2 <= speed1
+                        and 0 <= ptr1 - text_va < text_size - 8
+                        and 0 <= ptr2 - text_va < text_size - 8):
+                    break
+
+                # So far, this character definition seems to be valid.
+                # Now, make sure the shoot function wrappers pass valid addresses
+
+                # Search for the “push” instruction
+                for i in xrange(20):
+                    # Find the “push” instruction
+                    pe_file.seek_to_va(ptr1 + i)
+                    instr1, shtptr1 = unpack('<BI', pe_file.file.read(5))
+                    pe_file.seek_to_va(ptr2 + i)
+                    instr2, shtptr2 = unpack('<BI', pe_file.file.read(5))
+                    if instr1 == 0x68 and instr2 == 0x68 and (0 <= shtptr1 - data_va < data_size - 12
+                                                              and 0 <= shtptr2 - data_va < data_size - 12):
+                        # It is unlikely this character record is *not* valid, but
+                        # just to be sure, let's check the first SHT definition.
+                        pe_file.seek_to_va(shtptr1)
+                        nb_shots, power, shotsptr = unpack('<III', pe_file.file.read(12))
+                        if (0 < nb_shots <= 1000
+                            and 0 <= power < 1000
+                            and 0 <= shotsptr - data_va < data_size - 36*nb_shots):
+                            break
+                # Check if everything is fine...
+                if not (0 <= shtptr1 - data_va < data_size - 12
+                        and 0 <= shtptr2 - data_va < data_size - 12
+                        and 0 < nb_shots <= 1000
+                        and 0 <= power < 1000
+                        and 0 <= shotsptr - data_va < data_size - 36*nb_shots):
+                    break
+
+            else:
+                # XXX: Obscure python feature! This only gets executed if the
+                # XXX: loop ended without a break statement.
+                # In our case, it's only executed if all the 4 character
+                # definitions are considered valid.
+                yield addr
+
+
+    @classmethod
+    def read(cls, file):
+        pe_file = PEFile(file)
+        data_section = [section for section in pe_file.sections
+                            if section.Name.startswith('.data')][0]
+        data_va = pe_file.image_base + data_section.VirtualAddress
+        data_size = data_section.SizeOfRawData
+
+        try:
+            character_records_va = next(cls.find_character_defs(pe_file))
+        except StopIteration:
+            raise InvalidExeException
+
+        characters = []
+        shots_offsets = {}
+        for character in xrange(4):
+            sht = cls()
+
+            pe_file.seek_to_va(character_records_va + 6*4*character)
+
+            data = unpack('<4f2I', file.read(6*4))
+            (speed, speed_focused, speed_unknown1, speed_unknown2,
+             shots_func_offset, shots_func_offset_focused) = data
+
+            sht.horizontal_vertical_speed = speed
+            sht.horizontal_vertical_focused_speed = speed_focused
+            sht.diagonal_speed = speed * SQ2
+            sht.diagonal_focused_speed = speed_focused * SQ2
+
+            # Characters might have different shot types whether they are
+            # focused or not, but properties read earlier apply to both modes.
+            focused_sht = copy(sht)
+            characters.append((sht, focused_sht))
+
+            for sht, func_offset in ((sht, shots_func_offset), (focused_sht, shots_func_offset_focused)):
+                # Search for the “push” instruction
+                for i in xrange(20):
+                    # Find the “push” instruction
+                    pe_file.seek_to_va(func_offset + i)
+                    instr, offset = unpack('<BI', file.read(5))
+                    if instr == 0x68 and 0 <= offset - data_va < data_size - 12:
+                        pe_file.seek_to_va(offset)
+                        nb_shots, power, shotsptr = unpack('<III', pe_file.file.read(12))
+                        if (0 < nb_shots <= 1000
+                            and 0 <= power < 1000
+                            and 0 <= shotsptr - data_va < data_size - 36*nb_shots):
+                            break
+                if offset not in shots_offsets:
+                    shots_offsets[offset] = []
+                shots_offsets[offset].append(sht)
+
+        for shots_offset, shts in shots_offsets.iteritems():
+            pe_file.seek_to_va(shots_offset)
+
+            level_count = 9
+            levels = []
+            for i in xrange(level_count):
+                shots_count, power, offset = unpack('<III', file.read(3*4))
+                levels.append((shots_count, power, offset))
+
+            shots = {}
+
+            for shots_count, power, offset in levels:
+                shots[power] = []
+                pe_file.seek_to_va(offset)
+
+                for i in xrange(shots_count):
+                    shot = Shot()
+
+                    data = unpack('<HH6fHBBhh', file.read(36))
+                    (shot.interval, shot.delay, x, y, hitbox_x, hitbox_y,
+                     shot.angle, shot.speed, shot.damage, shot.orb, shot.type,
+                     shot.sprite, shot.unknown1) = data
+
+                    shot.pos = (x, y)
+                    shot.hitbox = (hitbox_x, hitbox_y)
+
+                    shots[power].append(shot)
+
+            for sht in shts:
+                sht.shots = shots
+
+
+        return characters
+
new file mode 100644
--- /dev/null
+++ b/pytouhou/formats/fmt.py
@@ -0,0 +1,70 @@
+# -*- 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 struct import unpack
+
+
+class Track(object):
+    def __init__(self):
+        self.name = ''
+
+        # loop info
+        self.intro = 0
+        #self.unknown
+        self.start = 0
+        self.duration = 0
+
+        # WAVE header
+        self.wFormatTag = 1
+        self.wChannels = 2
+        self.dwSamplesPerSec = 44100
+        self.dwAvgBytesPerSec = 176400
+        self.wBlockAlign = 4
+        self.wBitsPerSample = 16
+
+
+class FMT(list):
+    @classmethod
+    def read(cls, file):
+        self = cls()
+
+        file.seek(0)
+        while True:
+            track = Track()
+            track.name = unpack('<16s', file.read(16))[0]
+            if not ord(track.name[0]):
+                break
+
+            # loop info
+            track.intro, unknown, track.start, track.duration = unpack('<IIII', file.read(16))
+
+            # WAVE header
+            (track.wFormatTag,
+             track.wChannels,
+             track.dwSamplesPerSec,
+             track.dwAvgBytesPerSec,
+             track.wBlockAlign,
+             track.wBitsPerSample) = unpack('<HHLLHH', file.read(16))
+
+            assert track.wFormatTag == 1 # We don’t support non-PCM formats
+            assert track.dwAvgBytesPerSec == track.dwSamplesPerSec * track.wBlockAlign
+            assert track.wBlockAlign == track.wChannels * track.wBitsPerSample // 8
+            zero = file.read(4)
+            assert b'\00\00\00\00' == zero
+
+            self.append(track)
+
+        return self
+
new file mode 100644
--- /dev/null
+++ b/pytouhou/formats/hint.py
@@ -0,0 +1,135 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2013 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 collections import OrderedDict
+
+
+def _read_n_int(value):
+    values = value.split(', ')
+    return tuple(int(value) for value in values)
+
+
+class Stage(list):
+    def __init__(self, number):
+        list.__init__(self)
+        self.number = number
+
+
+class Hint(object):
+    _fields = {'Stage': int,
+               'Tips': None,
+               'Remain': int,
+               'Text': lambda v: v[1:-1],
+               'Pos': _read_n_int,
+               'Count': int,
+               'Base': str,
+               'Align': str,
+               'Time': int,
+               'Alpha': int,
+               'Color': _read_n_int,
+               'Scale': float,
+               'End': None,
+               'StageEnd': None}
+
+
+    def __init__(self):
+        self.version = 0.0
+        self.stages = []
+
+
+    @classmethod
+    def read(cls, file):
+        tokens = []
+
+        for line in file:
+            line = line.strip()
+
+            if not line:
+                continue
+            if line[0] == '#':
+                continue
+            if line == 'Version = 0.0':
+                continue
+
+            field, _, value = line.partition(':')
+            field = field.rstrip()
+            value = value.lstrip()
+            parser = cls._fields[field]
+
+            if parser:
+                tokens.append((field, parser(value)))
+            else:
+                tokens.append((field, None))
+
+        stage_mode = False
+        tip_mode = False
+        stage = None
+        tip = None
+        hints = cls()
+        stages = hints.stages
+
+        for token in tokens:
+            key = token[0]
+            value = token[1]
+
+            if stage_mode:
+                if key != 'StageEnd':
+                    if tip_mode:
+                        if key != 'End':
+                            tip[key] = value
+                        else:
+                            assert tip_mode == True
+                            stage.append(tip)
+                            tip_mode = False
+                    elif key == 'Tips':
+                        assert tip_mode == False
+                        tip = OrderedDict()
+                        tip_mode = True
+                else:
+                    assert stage_mode == True
+                    stages.append(stage)
+                    stage_mode = False
+            elif key == 'Stage':
+                assert stage_mode == False
+                stage = Stage(value)
+                stage_mode = True
+
+        return hints
+
+
+    def write(self, file):
+        file.write('# Hints file generated with PyTouhou\n\n\n')
+
+        file.write('Version = {}\n\n'.format(self.version))
+
+        for stage in self.stages:
+            file.write('# ================================== \n')
+            file.write('Stage : {}\n\n'.format(stage.number))
+
+            for tip in stage:
+                file.write('Tips\n')
+
+                for key, value in tip.items():
+                    if key == 'Text':
+                        value = '"{}"'.format(value)
+                    elif key == 'Pos':
+                        key = 'Pos\t'
+                    if isinstance(value, tuple):
+                        value = str(value)[1:-1]
+                    file.write('\t{}\t: {}\n'.format(key, value))
+
+                file.write('End\n\n')
+
+            file.write('StageEnd\n')
--- a/pytouhou/formats/msg.py
+++ b/pytouhou/formats/msg.py
@@ -13,7 +13,6 @@
 ##
 
 from struct import pack, unpack, calcsize
-from pytouhou.utils.helpers import read_string
 
 from pytouhou.utils.helpers import get_logger
 
@@ -29,8 +28,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 +37,7 @@ class MSG(object):
 
 
     def __init__(self):
-        self.msgs = [[]]
+        self.msgs = {}
 
 
     @classmethod
@@ -47,13 +46,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 +72,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/formats/music.py
@@ -0,0 +1,29 @@
+# -*- 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 struct import unpack
+
+
+class Track(object):
+    def __init__(self):
+        self.start = 0
+        self.end = 0
+
+
+    @classmethod
+    def read(cls, file):
+        self = cls()
+        self.start, self.end = unpack('<II', file.read(8))
+        return self
--- a/pytouhou/formats/pbg3.py
+++ b/pytouhou/formats/pbg3.py
@@ -28,6 +28,8 @@ import pytouhou.utils.lzss as lzss
 
 from pytouhou.utils.helpers import get_logger
 
+from pytouhou.formats import WrongFormatError
+
 logger = get_logger(__name__)
 
 
@@ -79,7 +81,7 @@ class PBG3(object):
     """
 
     def __init__(self, entries=None, bitstream=None):
-        self.entries = entries or []
+        self.entries = entries or {}
         self.bitstream = bitstream #TODO
 
 
@@ -101,7 +103,7 @@ class PBG3(object):
 
         magic = file.read(4)
         if magic != b'PBG3':
-            raise Exception #TODO
+            raise WrongFormatError(magic)
 
         bitstream = PBG3BitStream(file)
         entries = {}
new file mode 100644
--- /dev/null
+++ b/pytouhou/formats/score.py
@@ -0,0 +1,152 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2012 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 struct import pack, unpack, Struct
+from collections import namedtuple
+from io import BytesIO
+
+from pytouhou.formats import ChecksumError
+
+
+class TH6Score(object):
+    entry_types = {
+        b'TH6K': (Struct('<I'),
+                  namedtuple('TH6K', ('unknown',))),
+        b'HSCR': (Struct('<IIBBB8sx'),
+                  namedtuple('HSCR', ('unknown', 'score', 'character',
+                                      'rank', 'stage', 'name'))),
+        b'PSCR': (Struct('<IIBBBx'),
+                  namedtuple('PSCR', ('unknown', 'score', 'character',
+                                      'rank', 'stage'))),
+        b'CLRD': (Struct('<I5B5BBx'),
+                  namedtuple('CLRD', ('unknown',
+                                      'easy', 'normal', 'hard', 'lunatic',
+                                      'extra',
+                                      'easy_continue', 'normal_continue',
+                                      'hard_continue', 'lunatic_continue',
+                                      'extra_continue',
+                                      'character'))),
+        b'CATK': (Struct('<I I HH I 34s H HH'),
+                  namedtuple('CATK', ('unknown', 'unknown2', 'num',
+                                      'unknown3', 'padding',
+                                      'name', 'padding2',
+                                      'seen',
+                                      'defeated'))),
+    }
+
+    def __init__(self):
+        self.key1 = 0
+        self.key2 = 0
+        self.unknown1 = 0
+        self.unknown2 = 16
+        self.unknown3 = 0
+        self.unknown4 = 0
+        self.entries = []
+
+
+    @classmethod
+    def read(cls, file, decrypt=True, verify=True):
+        self = cls()
+
+        # Decrypt data
+        if decrypt:
+            decrypted_file = BytesIO()
+            decrypted_file.write(file.read(1))
+            key = 0
+            for c in file.read():
+                encrypted = ord(c)
+                key = ((key << 3) & 0xFF) | ((key >> 5) & 7)
+                clear = encrypted ^ key
+                key += clear
+                decrypted_file.write(chr(clear))
+            file = decrypted_file
+
+        # Read first-part header
+        file.seek(0)
+        self.unknown1, self.key1, checksum = unpack('<BBH', file.read(4))
+
+        # Verify checksum
+        if verify:
+            #TODO: is there more to it?
+            real_sum = sum(ord(c) for c in file.read()) & 0xFFFF
+            if checksum != real_sum:
+                raise ChecksumError(checksum, real_sum)
+            file.seek(4)
+
+        # Read second-part header
+        data = unpack('<HBBIII', file.read(16))
+        self.unknown2, self.key2, self.unknown3, offset, self.unknown4, size = data
+
+        #TODO: verify size
+
+        # Read tags
+        file.seek(offset)
+        while True:
+            tag = file.read(4)
+            if not tag:
+                break
+            size, size2 = unpack('<HH', file.read(4))
+            assert size == size2
+            assert size >= 8
+            data = file.read(size-8)
+            data = cls.entry_types[tag][0].unpack(data)
+            data = cls.entry_types[tag][1](*data)
+            self.entries.append((tag, data))
+
+        return self
+
+
+    def write(self, file, encrypt=True):
+        if encrypt:
+            clearfile = BytesIO()
+        else:
+            clearfile = file
+
+        # Write data
+        clearfile.seek(20)
+        for entry in self.entries:
+            #TODO
+            tag, data = entry
+            format = TH6Score.entry_types[tag][0]
+            clearfile.write(tag)
+            clearfile.write(pack('<H', format.size + 8) * 2)
+            clearfile.write(format.pack(*data))
+
+        # Patch header
+        size = clearfile.tell()
+        clearfile.seek(0)
+        clearfile.write(pack('<BBHHBBIII',
+                             self.unknown1, self.key1, 0, self.unknown2,
+                             self.key2, self.unknown3, 20, self.unknown4,
+                             size))
+
+        # Patch checksum
+        clearfile.seek(4)
+        checksum = sum(ord(c) for c in clearfile.read()) & 0xFFFF
+        clearfile.seek(2)
+        clearfile.write(pack('<H', checksum))
+
+        # Encrypt
+        if encrypt:
+            clearfile.seek(0)
+            file.write(clearfile.read(1))
+            key = 0
+            for c in clearfile.read():
+                clear = ord(c)
+                key = ((key << 3) & 0xFF) | ((key >> 5) & 7)
+                encrypted = clear ^ key
+                key += clear
+                file.write(chr(encrypted))
+
--- a/pytouhou/formats/sht.py
+++ b/pytouhou/formats/sht.py
@@ -22,20 +22,20 @@ logger = get_logger(__name__)
 class Shot(object):
     def __init__(self):
         self.interval = 0
-        self.unknown1 = None
+        self.delay = 0
         self.pos = (0., 0.)
         self.hitbox = (0., 0.)
         self.angle = 0.
         self.speed = 0.
         self.damage = 0
         self.orb = 0
+        self.type = 0
+        self.sprite = 0
+        self.unknown1 = None
         self.unknown2 = None
-        self.sprite = 0
         self.unknown3 = None
         self.unknown4 = None
-        self.homing = False
         self.unknown5 = None
-        self.unknown6 = None
 
 
 class SHT(object):
@@ -79,20 +79,20 @@ class SHT(object):
             file.seek(offset)
 
             while True:
-                interval, unknown1 = unpack('<HH', file.read(4))
-                if interval == 0xffff and unknown1 == 0xffff:
+                interval, delay = unpack('<HH', file.read(4))
+                if interval == 0xffff and delay == 0xffff:
                     break
 
                 shot = Shot()
 
                 shot.interval = interval
-                shot.unknown1 = unknown1
+                shot.delay = delay
 
                 data = unpack('<6fHBBhh4I', file.read(48))
                 (x, y, hitbox_x, hitbox_y, shot.angle, shot.speed,
-                 shot.damage, shot.orb, shot.unknown2, shot.sprite,
-                 shot.unknown3, shot.unknown4, shot.homing, shot.unknown5,
-                 shot.unknown6) = data
+                 shot.damage, shot.orb, shot.shot_type, shot.sprite,
+                 shot.unknown1, shot.unknown2, shot.unknown3, shot.unknown4,
+                 shot.unknown5) = data
 
                 shot.pos = (x, y)
                 shot.hitbox = (hitbox_x, hitbox_y)
--- a/pytouhou/formats/std.py
+++ b/pytouhou/formats/std.py
@@ -81,9 +81,8 @@ class Stage(object):
         stage = Stage()
 
         nb_models, nb_faces = unpack('<HH', file.read(4))
-        object_instances_offset, script_offset = unpack('<II', file.read(8))
-        if file.read(4) != b'\x00\x00\x00\x00':
-            raise Exception #TODO
+        object_instances_offset, script_offset, zero = unpack('<III', file.read(12))
+        assert zero == 0
 
         stage.name = read_string(file, 128, 'shift_jis')
 
@@ -97,7 +96,8 @@ class Stage(object):
         bgm_c_path = read_string(file, 128, 'ascii')
         bgm_d_path = read_string(file, 128, 'ascii')
 
-        stage.bgms = [(bgm_a, bgm_a_path), (bgm_b, bgm_b_path), (bgm_c, bgm_c_path), (bgm_d, bgm_d_path)] #TODO: handle ' '
+        stage.bgms = [None if bgm[0] == u' ' else bgm
+            for bgm in ((bgm_a, bgm_a_path), (bgm_b, bgm_b_path), (bgm_c, bgm_c_path), (bgm_d, bgm_d_path))]
 
         # Read model definitions
         offsets = unpack('<%s' % ('I' * nb_models), file.read(4 * nb_models))
@@ -115,8 +115,7 @@ class Stage(object):
                 unknown, size = unpack('<HH', file.read(4))
                 if unknown == 0xffff:
                     break
-                if size != 0x1c:
-                    raise Exception #TODO
+                assert size == 0x1c
                 script_index, x, y, z, width, height = unpack('<Hxxfffff', file.read(24))
                 model.quads.append((script_index, x, y, z, width, height))
             stage.models.append(model)
@@ -128,19 +127,17 @@ class Stage(object):
             obj_id, unknown, x, y, z = unpack('<HHfff', file.read(16))
             if (obj_id, unknown) == (0xffff, 0xffff):
                 break
-            if unknown != 256:
-                raise Exception #TODO
+            assert unknown == 256 #TODO: really?
             stage.object_instances.append((obj_id, x, y, z))
 
 
-        # Read other funny things (script)
+        # Read the script
         file.seek(script_offset)
         while True:
             frame, opcode, size = unpack('<IHH', file.read(8))
             if (frame, opcode, size) == (0xffffffff, 0xffff, 0xffff):
                 break
-            if size != 0x0c:
-                raise Exception #TODO
+            assert size == 0x0c
             data = file.read(size)
             if opcode in cls._instructions:
                 args = unpack('<%s' % cls._instructions[opcode][0], data)
--- a/pytouhou/formats/t6rp.py
+++ b/pytouhou/formats/t6rp.py
@@ -20,13 +20,12 @@ a game of EoSD. Since the EoSD engine is
 replay file is sufficient to unfold a full game.
 """
 
-from struct import unpack
+from struct import unpack, pack
 from io import BytesIO
+from time import strftime
 
-from pytouhou.utils.random import Random
-from pytouhou.utils.helpers import read_string
-
-from pytouhou.utils.helpers import get_logger
+from pytouhou.utils.helpers import read_string, get_logger
+from pytouhou.formats import ChecksumError
 
 logger = get_logger(__name__)
 
@@ -35,20 +34,43 @@ class Level(object):
     def __init__(self):
         self.score = 0
         self.random_seed = 0
+        self.point_items = 0
 
         self.power = 0
         self.lives = 2
         self.bombs = 3
         self.difficulty = 16
+        self.unknown = 0
         self.keys = []
 
 
+    def iter_keystates(self):
+        counter = 0
+        previous = 0
+        for frame, keystate, unknown in self.keys:
+            while frame >= counter:
+                yield previous
+                counter += 1
+            previous = keystate
+
+
+
 class T6RP(object):
     def __init__(self):
         self.version = 0x102
         self.character = 0
         self.rank = 0
+        self.unknown1 = 0
+        self.unknown2 = 0
         self.key = 0
+        self.unknown3 = 0
+        self.date = strftime('%d/%m/%y')
+        self.name = 'PyTouhou'
+        self.unknown4 = 0
+        self.score = 0
+        self.unknown5 = 0
+        self.slowdown = 0.
+        self.unknown6 = 0
         self.levels = [None] * 7
 
 
@@ -64,8 +86,8 @@ class T6RP(object):
         verify -- whether or not to verify the file's checksum (default True)
         """
 
-        if file.read(4) != b'T6RP':
-            raise Exception
+        magic = file.read(4)
+        assert magic == b'T6RP'
 
         replay = cls()
 
@@ -85,10 +107,11 @@ class T6RP(object):
         if verify:
             data = file.read()
             file.seek(15)
-            if checksum != (sum(ord(c) for c in data) + 0x3f000318 + replay.key) & 0xffffffff:
-                raise Exception #TODO
+            real_sum = (sum(ord(c) for c in data) + 0x3f000318 + replay.key) & 0xffffffff
+            if checksum != real_sum:
+                raise ChecksumError(checksum, real_sum)
 
-        replay.unknown3 = unpack('<B', file.read(1))
+        replay.unknown3, = unpack('<B', file.read(1))
         replay.date = file.read(9) #read_string(file, 9, 'ascii')
         replay.name = file.read(9) #read_string(file, 9, 'ascii').rstrip()
         replay.unknown4, replay.score, replay.unknown5, replay.slowdown, replay.unknown6 = unpack('<HIIfI', file.read(18))
@@ -103,8 +126,8 @@ class T6RP(object):
             replay.levels[i] = level
 
             file.seek(offset)
-            (level.score, level.random_seed, level.unknown1, level.power,
-             level.lives, level.bombs, level.difficulty, level.unknown2) = unpack('<IHHBbbBI', file.read(16))
+            (level.score, level.random_seed, level.point_items, level.power,
+             level.lives, level.bombs, level.difficulty, level.unknown) = unpack('<IHHBbbBI', file.read(16))
 
             while True:
                 time, keys, unknown = unpack('<IHH', file.read(8))
@@ -115,3 +138,64 @@ class T6RP(object):
                 level.keys.append((time, keys, unknown))
 
         return replay
+
+
+    def write(self, file, encrypt=True):
+        if encrypt:
+            encrypted_file = file
+            file = BytesIO()
+
+        file.write(b'T6RP')
+        file.write(pack('<HBB', self.version, self.character, self.rank))
+
+        checksum_offset = file.tell()
+        file.seek(4, 1) # For checksum
+        file.write(pack('<BBB', self.unknown1, self.unknown2, self.key))
+
+        file.write(pack('<B', self.unknown3))
+
+        #TODO: find a more elegant method.
+        n = 9 - len(self.date)
+        file.write(self.date)
+        file.write('\0' * n)
+        n = 9 - len(self.name)
+        file.write(self.name)
+        file.write('\0' * n)
+
+        file.write(pack('<HIIfI', self.unknown4, self.score, self.unknown5, self.slowdown, self.unknown6))
+
+        stages_offsets_offset = file.tell()
+        file.seek(7*4, 1) # Skip the stages offsets.
+
+        stages_offsets = []
+        for level in self.levels:
+            if not level:
+                stages_offsets.append(0)
+                continue
+
+            stages_offsets.append(file.tell())
+            file.write(pack('<IHHBbbBI', level.score, level.random_seed,
+                            level.point_items, level.power, level.lives,
+                            level.bombs, level.difficulty, level.unknown))
+
+            for time, keys, unknown in level.keys:
+                file.write(pack('<IHH', time, keys, unknown))
+
+            file.write(pack('<IHH', 9999999, 0, 0))
+
+        file.seek(stages_offsets_offset)
+        file.write(pack('<7I', *stages_offsets))
+
+        # Write checksum
+        file.seek(15)
+        data = file.read()
+        checksum = (sum(ord(c) for c in data) + 0x3f000318 + self.key) & 0xffffffff
+        file.seek(checksum_offset)
+        file.write(pack('<I', checksum))
+
+        # Encrypt
+        if encrypt:
+            file.seek(0)
+            encrypted_file.write(file.read(15))
+            encrypted_file.write(b''.join(chr((ord(c) + self.key + 7*i) & 0xff) for i, c in enumerate(file.read())))
+
new file mode 100644
--- /dev/null
+++ b/pytouhou/formats/thtx.py
@@ -0,0 +1,21 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2013 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.
+##
+
+
+class Texture(object):
+    def __init__(self, width, height, fmt, data):
+        self.width = width
+        self.height = height
+        self.fmt = fmt
+        self.data = data
--- a/pytouhou/game/background.py
+++ b/pytouhou/game/background.py
@@ -19,9 +19,10 @@ from pytouhou.game.sprite import Sprite
 
 
 class Background(object):
-    def __init__(self, stage, anm_wrapper):
+    def __init__(self, stage, anm):
         self.stage = stage
-        self.anm_wrapper = anm_wrapper
+        self.anm = anm
+        self.last_frame = -1
 
         self.models = []
         self.object_instances = []
@@ -52,8 +53,7 @@ class Background(object):
             quads = []
             for script_index, ox, oy, oz, width_override, height_override in obj.quads:
                 sprite = Sprite(width_override, height_override)
-                anm_runner = ANMRunner(self.anm_wrapper, script_index, sprite)
-                anm_runner.run_frame()
+                anm_runner = ANMRunner(self.anm, script_index, sprite)
                 quads.append((ox, oy, oz, width_override, height_override, sprite))
                 self.anm_runners.append(anm_runner)
             self.models.append(quads)
@@ -61,7 +61,7 @@ class Background(object):
 
     def update(self, frame):
         for frame_num, message_type, args in self.stage.script:
-            if frame_num == frame:
+            if self.last_frame < frame_num <= frame:
                 if message_type == 0:
                     self.position_interpolator.set_interpolation_start(frame_num, args)
                 elif message_type == 1:
@@ -78,11 +78,14 @@ class Background(object):
                 self.position_interpolator.set_interpolation_end(frame_num, args)
                 break
 
-        for anm_runner in tuple(self.anm_runners):
-            if not anm_runner.run_frame():
-                self.anm_runners.remove(anm_runner)
+        for i in range(frame - self.last_frame):
+            for anm_runner in tuple(self.anm_runners):
+                if not anm_runner.run_frame():
+                    self.anm_runners.remove(anm_runner)
 
         self.position2_interpolator.update(frame)
         self.fog_interpolator.update(frame)
         self.position_interpolator.update(frame)
 
+        self.last_frame = frame
+
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/bullet.pxd
@@ -0,0 +1,29 @@
+from pytouhou.game.element cimport Element
+from pytouhou.game.game cimport Game
+from pytouhou.game.bullettype cimport BulletType
+from pytouhou.utils.interpolator cimport Interpolator
+
+
+cdef enum State:
+    LAUNCHING, LAUNCHED, CANCELLED
+
+
+cdef class Bullet(Element):
+    cdef public State state
+    cdef public unsigned long flags, frame, sprite_idx_offset, damage
+    cdef public double dx, dy, angle, speed
+    cdef public bint player_bullet, was_visible, grazed
+    cdef public Element target
+    cdef public BulletType _bullet_type
+    cdef public list attributes
+
+    cdef double hitbox[2]
+    cdef Interpolator speed_interpolator
+    cdef Game _game
+
+    cdef bint is_visible(self, unsigned int screen_width, unsigned int screen_height) except? False
+    cpdef set_anim(self, sprite_idx_offset=*)
+    cdef void launch(self) except *
+    cdef void collide(self) except *
+    cdef void cancel(self) except *
+    cdef void update(self) except *
rename from pytouhou/game/bullet.py
rename to pytouhou/game/bullet.pyx
--- a/pytouhou/game/bullet.py
+++ b/pytouhou/game/bullet.pyx
@@ -12,45 +12,44 @@
 ## GNU General Public License for more details.
 ##
 
-from math import cos, sin, atan2, pi
+from libc.math cimport cos, sin, atan2, M_PI as pi
 
-from pytouhou.utils.interpolator import Interpolator
 from pytouhou.vm.anmrunner import ANMRunner
-from pytouhou.game.sprite import Sprite
+from pytouhou.game.sprite cimport Sprite
 
 
-class Bullet(object):
-    def __init__(self, pos, bullet_type, sprite_idx_offset,
-                       angle, speed, attributes, flags, player, game,
-                       player_bullet=False, damage=0, hitbox=None):
+cdef class Bullet(Element):
+    def __init__(self, pos, BulletType bullet_type, unsigned long sprite_idx_offset,
+                       double angle, double speed, attributes, unsigned long flags, target, Game game,
+                       bint player_bullet=False, unsigned long damage=0, tuple hitbox=None):
+        cdef double launch_mult
+
+        Element.__init__(self, pos)
+
         self._game = game
-        self._sprite = None
-        self._anmrunner = None
-        self._removed = False
-        self._launched = False
         self._bullet_type = bullet_type
+        self.state = LAUNCHING
+        self.was_visible = True
 
-        if hitbox:
-            self.hitbox_half_size = (hitbox[0] / 2., hitbox[1] / 2.)
+        if hitbox is not None:
+            self.hitbox[:] = [hitbox[0], hitbox[1]]
         else:
-            self.hitbox_half_size = (bullet_type.hitbox_size / 2., bullet_type.hitbox_size / 2.)
+            self.hitbox[:] = [bullet_type.hitbox_size, bullet_type.hitbox_size]
 
         self.speed_interpolator = None
         self.frame = 0
         self.grazed = False
 
-        self.player = player
+        self.target = target
 
         self.sprite_idx_offset = sprite_idx_offset
 
         self.flags = flags
         self.attributes = list(attributes)
 
-        self.x, self.y = pos
         self.angle = angle
         self.speed = speed
-        dx, dy = cos(angle) * speed, sin(angle) * speed
-        self.delta = dx, dy
+        self.dx, self.dy = cos(angle) * speed, sin(angle) * speed
 
         self.player_bullet = player_bullet
         self.damage = damage
@@ -66,27 +65,28 @@ class Bullet(object):
             else:
                 index = bullet_type.launch_anim8_index
                 launch_mult = bullet_type.launch_anim_penalties[2]
-            self.launch_delta = dx * launch_mult, dy * launch_mult
-            self._sprite = Sprite()
-            self._anmrunner = ANMRunner(bullet_type.anm_wrapper,
-                                        index, self._sprite,
+            self.dx, self.dy = self.dx * launch_mult, self.dy * launch_mult
+            self.sprite = Sprite()
+            self.anmrunner = ANMRunner(bullet_type.anm,
+                                        index, self.sprite,
                                         bullet_type.launch_anim_offsets[sprite_idx_offset])
-            self._anmrunner.run_frame()
         else:
             self.launch()
 
         if self.player_bullet:
-            self._sprite.angle = angle - pi
+            self.sprite.angle = angle - pi
         else:
-            self._sprite.angle = angle
+            self.sprite.angle = angle
 
 
-    def is_visible(self, screen_width, screen_height):
-        tx, ty, tw, th = self._sprite.texcoords
+    cdef bint is_visible(self, unsigned int screen_width, unsigned int screen_height):
+        cdef double tw, th
+
+        tw, th = self.sprite.texcoords[2:]
         x, y = self.x, self.y
 
-        max_x = tw / 2.
-        max_y = th / 2.
+        max_x = tw / 2
+        max_y = th / 2
 
         if (max_x < x - screen_width
             or max_x < -x
@@ -96,136 +96,121 @@ class Bullet(object):
         return True
 
 
-    def set_anim(self, sprite_idx_offset=None):
+    cpdef set_anim(self, sprite_idx_offset=None):
         if sprite_idx_offset is not None:
             self.sprite_idx_offset = sprite_idx_offset
 
         bt = self._bullet_type
-        self._sprite = Sprite()
+        self.sprite = Sprite()
         if self.player_bullet:
-            self._sprite.angle = self.angle - pi
+            self.sprite.angle = self.angle - pi
         else:
-            self._sprite.angle = self.angle
-        self._anmrunner = ANMRunner(bt.anm_wrapper, bt.anim_index,
-                                    self._sprite, self.sprite_idx_offset)
-        self._anmrunner.run_frame()
+            self.sprite.angle = self.angle
+        self.anmrunner = ANMRunner(bt.anm, bt.anim_index,
+                                   self.sprite, self.sprite_idx_offset)
 
 
-    def launch(self):
-        self._launched = True
-        self.update = self.update_full
+    cdef void launch(self):
+        self.state = LAUNCHED
+        self.frame = 0
         self.set_anim()
+        self.dx, self.dy = cos(self.angle) * self.speed, sin(self.angle) * self.speed
+
         if self.flags & 1:
             self.speed_interpolator = Interpolator((self.speed + 5.,), 0,
                                                    (self.speed,), 16)
 
 
-    def collide(self):
+    cdef void collide(self):
         self.cancel()
+        self._game.new_particle((self.x, self.y), 10, 256) #TODO: find the real size.
 
 
-    def cancel(self):
+    cdef void cancel(self):
         # Cancel animation
         bt = self._bullet_type
-        self._sprite = Sprite()
+        self.sprite = Sprite()
         if self.player_bullet:
-            self._sprite.angle = self.angle - pi
+            self.sprite.angle = self.angle - pi
         else:
-            self._sprite.angle = self.angle
-        self._anmrunner = ANMRunner(bt.anm_wrapper, bt.cancel_anim_index,
-                                    self._sprite, bt.launch_anim_offsets[self.sprite_idx_offset])
-        self._anmrunner.run_frame()
-        self.delta = self.delta[0] / 2., self.delta[1] / 2.
-
-        # Change update method
-        self.update = self.update_cancel
+            self.sprite.angle = self.angle
+        self.anmrunner = ANMRunner(bt.anm, bt.cancel_anim_index,
+                                   self.sprite, bt.launch_anim_offsets[self.sprite_idx_offset])
+        self.dx, self.dy = self.dx / 2., self.dy / 2.
 
-        # Do not use this one for collisions anymore
-        if self.player_bullet:
-            self._game.players_bullets.remove(self)
-        else:
-            self._game.bullets.remove(self)
-        self._game.cancelled_bullets.append(self)
-
-
-    def update(self):
-        dx, dy = self.launch_delta
-        self.x += dx
-        self.y += dy
-
-        if not self._anmrunner.run_frame():
-            self.launch()
+        self.state = CANCELLED
 
 
-    def update_cancel(self):
-        dx, dy = self.delta
-        self.x += dx
-        self.y += dy
-
-        if not self._anmrunner.run_frame():
-            self._removed = True
-
+    cdef void update(self):
+        cdef int frame, count, game_width, game_height
+        cdef double length, angle, speed, acceleration, angular_speed
 
-    def update_simple(self):
-        dx, dy = self.delta
-        self.x += dx
-        self.y += dy
-
-
-    def update_full(self):
-        sprite = self._sprite
+        if self.anmrunner is not None and not self.anmrunner.run_frame():
+            if self.state == LAUNCHING:
+                #TODO: check if it doesn't skip a frame
+                self.launch()
+            elif self.state == CANCELLED:
+                self.removed = True
+            else:
+                self.anmrunner = None
 
-        if self._anmrunner is not None and not self._anmrunner.run_frame():
-            self._anmrunner = None
-
-        #TODO: flags
-        x, y = self.x, self.y
-        dx, dy = self.delta
-
-        if self.flags & 16:
+        if self.state == LAUNCHING:
+            pass
+        elif self.state == CANCELLED:
+            pass
+        elif self.flags & 1:
+            # Initial speed burst
+            #TODO: use frame instead of interpolator?
+            if not self.speed_interpolator:
+                self.flags &= ~1
+        elif self.flags & 16:
+            # Each frame, add a vector to the speed vector
             length, angle = self.attributes[4:6]
             angle = self.angle if angle < -900.0 else angle #TODO: is that right?
-            dx, dy = dx + cos(angle) * length, dy + sin(angle) * length
-            self.angle = sprite.angle = atan2(dy, dx)
-            if sprite.automatic_orientation:
-                sprite._changed = True
-            self.delta = dx, dy
+            self.dx += cos(angle) * length
+            self.dy += sin(angle) * length
+            self.speed = (self.dx ** 2 + self.dy ** 2) ** 0.5
+            self.angle = self.sprite.angle = atan2(self.dy, self.dx)
+            if self.sprite.automatic_orientation:
+                self.sprite.changed = True
             if self.frame == self.attributes[0]: #TODO: include last frame, or not?
-                self.flags ^= 16
+                self.flags &= ~16
         elif self.flags & 32:
+            # Each frame, accelerate and rotate
             #TODO: check
             acceleration, angular_speed = self.attributes[4:6]
             self.speed += acceleration
             self.angle += angular_speed
-            dx, dy = cos(self.angle) * self.speed, sin(self.angle) * self.speed
-            self.delta = dx, dy
-            sprite.angle = self.angle
-            if sprite.automatic_orientation:
-                sprite._changed = True
+            self.dx = cos(self.angle) * self.speed
+            self.dy = sin(self.angle) * self.speed
+            self.sprite.angle = self.angle
+            if self.sprite.automatic_orientation:
+                self.sprite.changed = True
             if self.frame == self.attributes[0]:
-                self.flags ^= 32
+                self.flags &= ~32
         elif self.flags & 448:
             #TODO: check
             frame, count = self.attributes[0:2]
             angle, speed = self.attributes[4:6]
             if self.frame % frame == 0:
-                count = count - 1
+                count -= 1
 
                 if self.frame != 0:
-                    self.speed = speed
+                    self.speed = self.speed if speed < -900 else speed
 
                     if self.flags & 64:
                         self.angle += angle
                     elif self.flags & 128:
-                        self.angle = atan2(self.player.y - y, self.player.x - x) + angle
+                        self.angle = atan2(self.target.y - self.y,
+                                           self.target.x - self.x) + angle
                     elif self.flags & 256:
                         self.angle = angle
 
-                    dx, dy = cos(self.angle) * speed, sin(self.angle) * speed
-                    self.delta = dx, dy
-                    sprite.angle = self.angle
-                    if sprite.automatic_orientation:
-                        sprite._changed = True
+                    self.dx = cos(self.angle) * self.speed
+                    self.dy = sin(self.angle) * self.speed
+                    self.sprite.angle = self.angle
+                    if self.sprite.automatic_orientation:
+                        self.sprite.changed = True
 
                 if count >= 0:
                     self.speed_interpolator = Interpolator((self.speed,), self.frame,
@@ -234,17 +219,40 @@ class Bullet(object):
                     self.flags &= ~448
 
                 self.attributes[1] = count
-        #TODO: other flags
-        elif not self.speed_interpolator and self._anmrunner is None:
-            self.update = self.update_simple
+
+        # Common updates
 
         if self.speed_interpolator:
             self.speed_interpolator.update(self.frame)
-            self.speed, = self.speed_interpolator.values
-            dx, dy = cos(self.angle) * self.speed, sin(self.angle) * self.speed
-            self.delta = dx, dy
+            speed, = self.speed_interpolator.values
+            self.dx = cos(self.angle) * speed
+            self.dy = sin(self.angle) * speed
 
-        self.x, self.y = x + dx, y + dy
+        self.x += self.dx
+        self.y += self.dy
 
         self.frame += 1
 
+        game_width, game_height = self._game.width, self._game.height
+
+        # Filter out-of-screen bullets and handle special flags
+        if self.flags & 448:
+            self.was_visible = False
+        elif self.is_visible(game_width, game_height):
+            self.was_visible = True
+        elif self.was_visible:
+            self.removed = True
+            if self.flags & (1024 | 2048) and self.attributes[0] > 0:
+                # Bounce!
+                if self.x < 0 or self.x > game_width:
+                    self.angle = pi - self.angle
+                    self.removed = False
+                if self.y < 0 or ((self.flags & 1024) and self.y > game_height):
+                    self.angle = -self.angle
+                    self.removed = False
+                self.sprite.angle = self.angle
+                if self.sprite.automatic_orientation:
+                    self.sprite.changed = True
+                self.dx = cos(self.angle) * self.speed
+                self.dy = sin(self.angle) * self.speed
+                self.attributes[0] -= 1
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/bullettype.pxd
@@ -0,0 +1,7 @@
+cdef class BulletType:
+    cdef public long type_id
+    cdef long anim_index, hitbox_size, cancel_anim_index
+    cdef long launch_anim2_index, launch_anim4_index, launch_anim8_index
+    cdef tuple launch_anim_offsets
+    cdef float launch_anim_penalties[3]
+    cdef object anm
--- a/pytouhou/game/bullettype.py
+++ b/pytouhou/game/bullettype.py
@@ -1,16 +1,19 @@
 class BulletType(object):
-    def __init__(self, anm_wrapper, anim_index, cancel_anim_index,
+    def __init__(self, anm, anim_index, cancel_anim_index,
                  launch_anim2_index, launch_anim4_index, launch_anim8_index,
                  hitbox_size,
                  launch_anim_penalties=(0.5, 0.4, 1./3.),
-                 launch_anim_offsets=(0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 0)):
-        self.anm_wrapper = anm_wrapper
+                 launch_anim_offsets=(0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 0),
+                 type_id=0):
+        self.type_id = type_id
+        self.anm = anm
         self.anim_index = anim_index
         self.cancel_anim_index = cancel_anim_index
         self.launch_anim2_index = launch_anim2_index
         self.launch_anim4_index = launch_anim4_index
         self.launch_anim8_index = launch_anim8_index
         self.hitbox_size = hitbox_size
-        self.launch_anim_penalties = launch_anim_penalties
+        assert 3 == len(launch_anim_penalties)
+        for i in xrange(3):
+            self.launch_anim_penalties[i] = launch_anim_penalties[i]
         self.launch_anim_offsets = launch_anim_offsets
-
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/effect.pxd
@@ -0,0 +1,12 @@
+from pytouhou.game.element cimport Element
+from pytouhou.utils.interpolator cimport Interpolator
+
+cdef class Effect(Element):
+    cpdef update(self)
+
+
+cdef class Particle(Effect):
+    cdef long frame, duration
+    cdef Interpolator pos_interpolator
+
+    cpdef update(self)
rename from pytouhou/game/effect.py
rename to pytouhou/game/effect.pyx
--- a/pytouhou/game/effect.py
+++ b/pytouhou/game/effect.pyx
@@ -12,82 +12,53 @@
 ## GNU General Public License for more details.
 ##
 
-
-from pytouhou.game.sprite import Sprite
+from pytouhou.game.sprite cimport Sprite
 from pytouhou.vm.anmrunner import ANMRunner
-from pytouhou.utils.interpolator import Interpolator
-from math import pi
+
+from pytouhou.game.game cimport Game
+
+
+cdef class Effect(Element):
+    def __init__(self, pos, index, anm):
+        Element.__init__(self, pos)
+        self.sprite = Sprite()
+        self.anmrunner = ANMRunner(anm, index, self.sprite)
+
+
+    cpdef update(self):
+        if self.anmrunner is not None and not self.anmrunner.run_frame():
+            self.anmrunner = None
+
+        if self.sprite is not None:
+            if self.sprite.removed:
+                self.sprite = None
+                self.removed = True
 
 
 
-class Effect(object):
-    def __init__(self, pos, index, anm_wrapper):
-        self._sprite = Sprite()
-        self._anmrunner = ANMRunner(anm_wrapper, index, self._sprite)
-        self._anmrunner.run_frame()
-        self._removed = False
+cdef class Particle(Effect):
+    def __init__(self, pos, index, anm, long amp, Game game, bint reverse=False, long duration=24):
+        Effect.__init__(self, pos, index, anm)
 
-        self.x, self.y = pos
+        self.frame = 0
+        self.duration = duration
 
+        random_pos = (self.x + amp * <double>game.prng.rand_double() - amp / 2,
+                      self.y + amp * <double>game.prng.rand_double() - amp / 2)
 
-    def update(self):
-        if self._anmrunner and not self._anmrunner.run_frame():
-            self._anmrunner = None
-
-        if self._sprite:
-            if self._sprite._removed:
-                self._sprite = None
+        if not reverse:
+            self.pos_interpolator = Interpolator((self.x, self.y), 0,
+                                                 random_pos, duration, formula=(lambda x: 2. * x - x ** 2))
+        else:
+            self.pos_interpolator = Interpolator(random_pos, 0,
+                                                 (self.x, self.y), duration, formula=(lambda x: 2. * x - x ** 2))
+            self.x, self.y = random_pos
 
 
-class Particle(object):
-    def __init__(self, start_pos, index, anm_wrapper, size, amp, game):
-        self._sprite = Sprite()
-        self._sprite.anm, self._sprite.texcoords = anm_wrapper.get_sprite(index)
-        self._game = game
-        self._removed = False
-
-        self.x, self.y = start_pos
-        self.frame = 0
-        self._sprite.alpha = 128
-        self._sprite.blendfunc = 1
-        self._sprite.rescale = (size, size)
-
-        self.pos_interpolator = None
-        self.scale_interpolator = None
-        self.rotations_interpolator = None
-
-        self.amp = amp
-
-
-    def set_end_pos(self, amp):
-        end_pos = (self.x + amp * self._game.prng.rand_double() - amp/2,
-                   self.y + amp * self._game.prng.rand_double() - amp/2)
+    cpdef update(self):
+        Effect.update(self)
 
-        self.pos_interpolator = Interpolator((self.x, self.y), 0,
-                                             end_pos, 24, formula=(lambda x: 2. * x - x ** 2))
-        self.scale_interpolator = Interpolator(self._sprite.rescale, 0,
-                                               (0., 0.), 24)
-        self.rotations_interpolator = Interpolator(self._sprite.rotations_3d, 0,
-                                                   (0., 0., 2*pi), 24)
-
-
-    def update(self):
-        if self.frame == 0:
-            self.set_end_pos(self.amp)
-
-        if self.pos_interpolator:
-            self.pos_interpolator.update(self.frame)
-            self.x, self.y = self.pos_interpolator.values
-
-            self.scale_interpolator.update(self.frame)
-            self._sprite.rescale = self.scale_interpolator.values
-
-            self.rotations_interpolator.update(self.frame)
-            self._sprite.rotations_3d = self.rotations_interpolator.values
-
-            self._sprite._changed = True
-
-        if self.frame == 24:
-            self._removed = True
+        self.pos_interpolator.update(self.frame)
+        self.x, self.y = self.pos_interpolator.values
 
         self.frame += 1
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/element.pxd
@@ -0,0 +1,8 @@
+from pytouhou.game.sprite cimport Sprite
+
+cdef class Element:
+    cdef public double x, y
+    cdef public bint removed
+    cdef public Sprite sprite
+    cdef public list objects
+    cdef public object anmrunner
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/element.py
@@ -0,0 +1,23 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2013 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.
+##
+
+class Element(object):
+    def __init__(self, pos=None):
+        self.sprite = None
+        self.anmrunner = None
+        self.removed = False
+        self.objects = [self]
+
+        if pos is not None:
+            self.x, self.y = pos
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/enemy.pxd
@@ -0,0 +1,43 @@
+from pytouhou.game.element cimport Element
+from pytouhou.game.game cimport Game
+from pytouhou.game.player cimport Player
+from pytouhou.utils.interpolator cimport Interpolator
+
+cdef class Enemy(Element):
+    cdef public double z, angle, speed, rotation_speed, acceleration
+    cdef public long _type, bonus_dropped, die_score, frame, life, death_flags, current_laser_id, death_callback, boss_callback, low_life_callback, low_life_trigger, timeout, timeout_callback, remaining_lives, bullet_launch_interval, bullet_launch_timer, death_anim, direction, update_mode
+    cdef public bint visible, was_visible, touchable, collidable, damageable, boss, automatic_orientation, delay_attack
+    cdef public tuple difficulty_coeffs, extended_bullet_attributes, bullet_attributes, bullet_launch_offset, movement_dependant_sprites, screen_box
+    cdef public dict laser_by_id
+    cdef public list aux_anm
+    cdef public Interpolator interpolator, speed_interpolator
+    cdef public object _anms, process
+
+    cdef Game _game
+    cdef double[2] hitbox_half_size
+
+    cpdef play_sound(self, index)
+    cpdef set_hitbox(self, double width, double height)
+    cpdef set_bullet_attributes(self, type_, anim, sprite_idx_offset,
+                                bullets_per_shot, number_of_shots, speed, speed2,
+                                launch_angle, angle, flags)
+    cpdef set_bullet_launch_interval(self, long value, unsigned long start=*)
+    cpdef fire(self, offset=*, bullet_attributes=*, launch_pos=*)
+    cpdef new_laser(self, variant, laser_type, sprite_idx_offset, angle, speed,
+                    start_offset, end_offset, max_length, width,
+                    start_duration, duration, end_duration,
+                    grazing_delay, grazing_extra_duration, unknown,
+                    offset=*)
+    cpdef Player select_player(self, list players=*)
+    cpdef double get_player_angle(self, tuple pos=*, Player player=*) except 42
+    cpdef set_anim(self, index)
+    cdef void die_anim(self) except *
+    cdef void drop_particles(self, long number, long color) except *
+    cpdef set_aux_anm(self, long number, long index)
+    cpdef set_pos(self, x, y, z)
+    cpdef move_to(self, duration, x, y, z, formula)
+    cpdef stop_in(self, duration, formula)
+    cdef bint is_visible(self, long screen_width, long screen_height) except? False
+    cdef void check_collisions(self) except *
+    cdef void handle_callbacks(self) except *
+    cdef void update(self) except *
rename from pytouhou/game/enemy.py
rename to pytouhou/game/enemy.pyx
--- a/pytouhou/game/enemy.py
+++ b/pytouhou/game/enemy.pyx
@@ -12,32 +12,33 @@
 ## GNU General Public License for more details.
 ##
 
+from libc.math cimport cos, sin, atan2, M_PI as pi
 
-from pytouhou.utils.interpolator import Interpolator
 from pytouhou.vm.anmrunner import ANMRunner
 from pytouhou.game.sprite import Sprite
-from pytouhou.game.bullet import Bullet
-from math import cos, sin, atan2, pi
+from pytouhou.game.bullet cimport Bullet, LAUNCHED
+from pytouhou.game.laser cimport Laser, PlayerLaser
+from pytouhou.game.effect cimport Effect
 
 
-class Enemy(object):
-    def __init__(self, pos, life, _type, bonus_dropped, die_score, anm_wrapper, game):
+cdef class Enemy(Element):
+    def __init__(self, pos, long life, long _type, long bonus_dropped, long die_score, anms, Game game):
+        Element.__init__(self)
+
         self._game = game
-        self._anm_wrapper = anm_wrapper
-        self._sprite = None
-        self._anmrunner = None
-        self._removed = False
-        self._visible = True
+        self._anms = anms
         self._type = _type
-        self._bonus_dropped = bonus_dropped
-        self._die_score = die_score #TODO: use it
-        self._was_visible = False
+
+        self.process = None
+        self.visible = True
+        self.was_visible = False
+        self.bonus_dropped = bonus_dropped
+        self.die_score = die_score
 
         self.frame = 0
 
-        self.x, self.y = pos
+        self.x, self.y, self.z = pos
         self.life = 1 if life < 0 else life
-        self.max_life = life
         self.touchable = True
         self.collidable = True
         self.damageable = True
@@ -45,15 +46,17 @@ class Enemy(object):
         self.boss = False
         self.difficulty_coeffs = (-.5, .5, 0, 0, 0, 0)
         self.extended_bullet_attributes = (0, 0, 0, 0, 0., 0., 0., 0.)
+        self.current_laser_id = 0
+        self.laser_by_id = {}
         self.bullet_attributes = None
         self.bullet_launch_offset = (0, 0)
-        self.death_callback = None
-        self.boss_callback = None
-        self.low_life_callback = None
-        self.low_life_trigger = None
-        self.timeout = None
-        self.timeout_callback = None
-        self.remaining_lives = -1
+        self.death_callback = -1
+        self.boss_callback = -1
+        self.low_life_callback = -1
+        self.low_life_trigger = -1
+        self.timeout = -1
+        self.timeout_callback = -1
+        self.remaining_lives = 0
 
         self.automatic_orientation = False
 
@@ -63,22 +66,53 @@ class Enemy(object):
 
         self.death_anim = 0
         self.movement_dependant_sprites = None
-        self.direction = None
+        self.direction = 0
         self.interpolator = None #TODO
         self.speed_interpolator = None
+        self.update_mode = 0
         self.angle = 0.
         self.speed = 0.
         self.rotation_speed = 0.
         self.acceleration = 0.
 
-        self.hitbox = (0, 0)
-        self.hitbox_half_size = (0, 0)
+        self.hitbox_half_size[:] = [0, 0]
         self.screen_box = None
 
+        self.aux_anm = 8 * [None]
 
-    def set_bullet_attributes(self, type_, anim, sprite_idx_offset,
-                              bullets_per_shot, number_of_shots, speed, speed2,
-                              launch_angle, angle, flags):
+
+    property objects:
+        def __get__(self):
+            return [self] + [anm for anm in self.aux_anm if anm is not None]
+
+
+    cpdef play_sound(self, index):
+        name = {
+            5: 'power0',
+            6: 'power1',
+            7: 'tan00',
+            8: 'tan01',
+            9: 'tan02',
+            14: 'cat00',
+            16: 'lazer00',
+            17: 'lazer01',
+            18: 'enep01',
+            22: 'tan00', #XXX
+            24: 'tan02', #XXX
+            25: 'kira00',
+            26: 'kira01',
+            27: 'kira02'
+        }[index]
+        self._game.sfx_player.play('%s.wav' % name)
+
+
+    cpdef set_hitbox(self, double width, double height):
+        self.hitbox_half_size[:] = [width / 2, height / 2]
+
+
+    cpdef set_bullet_attributes(self, type_, anim, sprite_idx_offset,
+                                bullets_per_shot, number_of_shots, speed, speed2,
+                                launch_angle, angle, flags):
 
         # Apply difficulty-specific modifiers
         speed_a, speed_b, nb_a, nb_b, shots_a, shots_b = self.difficulty_coeffs
@@ -96,24 +130,27 @@ class Enemy(object):
             self.fire()
 
 
-    def set_bullet_launch_interval(self, value, start=0.):
+    cpdef set_bullet_launch_interval(self, long value, unsigned long start=0):
         # Apply difficulty-specific modifiers:
-        value *= 1. - .4 * (self._game.difficulty - 16.) / 32.
+        #TODO: check every value possible! Look around 102h.exe@0x408720
+        value -= value * (<long>self._game.difficulty - 16) // 80
 
-        self.bullet_launch_interval = int(value)
-        self.bullet_launch_timer = int(value * start)
+        self.bullet_launch_interval = value
+        self.bullet_launch_timer = start % value if value > 0 else 0
 
 
-    def fire(self, offset=None, bullet_attributes=None, launch_pos=None):
+    cpdef fire(self, offset=None, bullet_attributes=None, launch_pos=None):
         (type_, type_idx, sprite_idx_offset, bullets_per_shot, number_of_shots,
          speed, speed2, launch_angle, angle, flags) = bullet_attributes or self.bullet_attributes
 
         bullet_type = self._game.bullet_types[type_idx]
 
-        if not launch_pos:
+        if launch_pos is None:
             ox, oy = offset or self.bullet_launch_offset
             launch_pos = self.x + ox, self.y + oy
 
+        if speed < 0.3 and speed != 0.0:
+            speed = 0.3
         if speed2 < 0.3:
             speed2 = 0.3
 
@@ -122,10 +159,8 @@ class Enemy(object):
         player = self.select_player()
 
         if type_ in (67, 69, 71):
-            launch_angle += self.get_player_angle(player, launch_pos)
-        if type_ in (69, 70, 71, 74):
-            angle = 2. * pi / bullets_per_shot
-        if type_ == 71:
+            launch_angle += self.get_player_angle(launch_pos, player)
+        if type_ == 71 and bullets_per_shot % 2 or type_ in (69, 70) and not bullets_per_shot % 2:
             launch_angle += pi / bullets_per_shot
         if type_ != 75:
             launch_angle -= angle * (bullets_per_shot - 1) / 2.
@@ -133,10 +168,12 @@ class Enemy(object):
         bullets = self._game.bullets
         nb_bullets_max = self._game.nb_bullets_max
 
-        for shot_nb in range(number_of_shots):
+        for shot_nb in xrange(number_of_shots):
             shot_speed = speed if shot_nb == 0 else speed + (speed2 - speed) * float(shot_nb) / float(number_of_shots)
             bullet_angle = launch_angle
-            for bullet_nb in range(bullets_per_shot):
+            if type_ in (69, 70, 71, 74):
+                launch_angle += angle
+            for bullet_nb in xrange(bullets_per_shot):
                 if nb_bullets_max is not None and len(bullets) == nb_bullets_max:
                     break
 
@@ -148,77 +185,110 @@ class Enemy(object):
                                       bullet_angle, shot_speed,
                                       self.extended_bullet_attributes,
                                       flags, player, self._game))
-                bullet_angle += angle
+
+                if type_ in (69, 70, 71, 74):
+                    bullet_angle += 2. * pi / bullets_per_shot
+                else:
+                    bullet_angle += angle
 
 
-    def select_player(self, players=None):
-        return min(players or self._game.players, key=lambda p: ((p.x - self.x) ** 2 + (p.y - self.y) ** 2, p.state.character)) #TODO
-
-
-    def get_player_angle(self, player=None, pos=None):
-        player = player or self.select_player()
-        x, y = pos or (self.x, self.y)
-        return atan2(player.y - y, player.x - x)
+    cpdef new_laser(self, variant, laser_type, sprite_idx_offset, angle, speed,
+                    start_offset, end_offset, max_length, width,
+                    start_duration, duration, end_duration,
+                    grazing_delay, grazing_extra_duration, unknown,
+                    offset=None):
+        ox, oy = offset or self.bullet_launch_offset
+        launch_pos = self.x + ox, self.y + oy
+        if variant == 86:
+            angle += self.get_player_angle(launch_pos)
+        laser = Laser(launch_pos, self._game.laser_types[laser_type],
+                      sprite_idx_offset, angle, speed,
+                      start_offset, end_offset, max_length, width,
+                      start_duration, duration, end_duration, grazing_delay,
+                      grazing_extra_duration, self._game)
+        self._game.lasers.append(laser)
+        self.laser_by_id[self.current_laser_id] = laser
 
 
-    def set_anim(self, index):
-        self._sprite = Sprite()
-        self._anmrunner = ANMRunner(self._anm_wrapper, index, self._sprite)
-        self._anmrunner.run_frame()
+    cpdef Player select_player(self, list players=None):
+        if players is None:
+            players = self._game.players
+        return min(players, key=self.select_player_key)
+
+
+    cpdef double get_player_angle(self, tuple pos=None, Player player=None):
+        cdef double x, y
+        if player is None:
+            player = self.select_player()
+        x, y = pos or (self.x, self.y)
+        return atan2(player.state.y - y, player.state.x - x)
 
 
-    def die_anim(self):
-        self._game.new_death((self.x, self.y), self.death_anim)
+    cpdef set_anim(self, index):
+        entry = 0 if index in self._anms[0].scripts else 1
+        self.sprite = Sprite()
+        self.anmrunner = ANMRunner(self._anms[entry], index, self.sprite)
 
 
-    def drop_particles(self, number, color):
-        #TODO: white particles are only used in stage 3 to 6,
-        # in other stages they are blue.
+    cdef void die_anim(self):
+        anim = {0: 3, 1: 4, 2: 5}[self.death_anim % 256] # The TB is wanted, if index isn’t in these values the original game crashs.
+        self._game.new_effect((self.x, self.y), anim)
+        self._game.sfx_player.play('enep00.wav')
+
+
+    cdef void drop_particles(self, long number, long color):
         if color == 0:
             if self._game.stage in [1, 2, 7]:
                 color = 3
-        for i in range(number):
-            self._game.new_particle((self.x, self.y), color, 4., 256) #TODO: find the real size.
-
-
-    def set_pos(self, x, y, z):
-        self.x, self.y = x, y
-        self.interpolator = Interpolator((x, y))
-        self.interpolator.set_interpolation_start(self._game.frame, (x, y))
+        color += 9
+        for i in xrange(number):
+            self._game.new_particle((self.x, self.y), color, 256) #TODO: find the real size.
 
 
-    def move_to(self, duration, x, y, z, formula):
-        if not self.interpolator:
-            frame = self._game.frame
-            self.interpolator = Interpolator((self.x, self.y), formula)
-            self.interpolator.set_interpolation_start(frame, (self.x, self.y))
-            self.interpolator.set_interpolation_end(frame + duration - 1, (x, y))
+    cpdef set_aux_anm(self, long number, long index):
+        entry = 0 if index in self._anms[0].scripts else 1
+        self.aux_anm[number] = Effect((self.x, self.y), index, self._anms[entry])
+
 
-            self.speed = 0.
-            self.angle = atan2(y - self.y, x - self.x)
+    cpdef set_pos(self, x, y, z):
+        self.x, self.y = x, y
+        self.update_mode = 1
+        self.interpolator = Interpolator((x, y), self._game.frame)
 
 
-    def stop_in(self, duration, formula):
-        if not self.speed_interpolator:
-            frame = self._game.frame
-            self.speed_interpolator = Interpolator((self.speed,), formula)
-            self.speed_interpolator.set_interpolation_start(frame, (self.speed,))
-            self.speed_interpolator.set_interpolation_end(frame + duration - 1, (0.,))
+    cpdef move_to(self, duration, x, y, z, formula):
+        frame = self._game.frame
+        self.speed_interpolator = None
+        self.update_mode = 1
+        self.interpolator = Interpolator((self.x, self.y), frame,
+                                         (x, y), frame + duration - 1,
+                                         formula)
 
-            self.speed = 0.
+        self.angle = atan2(y - self.y, x - self.x)
 
 
-    def is_visible(self, screen_width, screen_height):
-        if self._sprite:
-            tx, ty, tw, th = self._sprite.texcoords
-            if self._sprite.corner_relative_placement:
+    cpdef stop_in(self, duration, formula):
+        frame = self._game.frame
+        self.interpolator = None
+        self.update_mode = 1
+        self.speed_interpolator = Interpolator((self.speed,), frame,
+                                               (0.,), frame + duration - 1,
+                                               formula)
+
+
+    cdef bint is_visible(self, long screen_width, long screen_height):
+        cdef double tw, th
+
+        if self.sprite is not None:
+            if self.sprite.corner_relative_placement:
                 raise Exception #TODO
+            _, _, tw, th = self.sprite.texcoords
         else:
-            tx, ty, tw, th = 0., 0., 0., 0.
+            tw, th = 0, 0
 
         x, y = self.x, self.y
-        max_x = tw / 2.
-        max_y = th / 2.
+        max_x = tw / 2
+        max_y = th / 2
 
         if (max_x < x - screen_width
             or max_x < -x
@@ -228,10 +298,17 @@ class Enemy(object):
         return True
 
 
-    def check_collisions(self):
+    cdef void check_collisions(self):
+        cdef Bullet bullet
+        cdef Player player
+        cdef PlayerLaser laser
+        cdef long damages
+        cdef double half_size[2], phalf_size
+
         # Check for collisions
         ex, ey = self.x, self.y
-        ehalf_size_x, ehalf_size_y = self.hitbox_half_size
+        ehalf_size_x = self.hitbox_half_size[0]
+        ehalf_size_y = self.hitbox_half_size[1]
         ex1, ex2 = ex - ehalf_size_x, ex + ehalf_size_x
         ey1, ey2 = ey - ehalf_size_y, ey + ehalf_size_y
 
@@ -239,7 +316,10 @@ class Enemy(object):
 
         # Check for enemy-bullet collisions
         for bullet in self._game.players_bullets:
-            half_size = bullet.hitbox_half_size
+            if bullet.state != LAUNCHED:
+                continue
+            half_size[0] = bullet.hitbox[0]
+            half_size[1] = bullet.hitbox[1]
             bx, by = bullet.x, bullet.y
             bx1, bx2 = bx - half_size[0], bx + half_size[0]
             by1, by2 = by - half_size[1], by + half_size[1]
@@ -248,15 +328,31 @@ class Enemy(object):
                     or by2 < ey1 or by1 > ey2):
                 bullet.collide()
                 damages += bullet.damage
-                self.drop_particles(1, 1)
+                self._game.sfx_player.play('damage00.wav')
+
+        # Check for enemy-laser collisions
+        for laser in self._game.players_lasers:
+            if not laser:
+                continue
+
+            half_size[0] = laser.hitbox[0]
+            half_size[1] = laser.hitbox[1]
+            lx, ly = laser.x, laser.y * 2.
+            lx1, lx2 = lx - half_size[0], lx + half_size[0]
+
+            if not (lx2 < ex1 or lx1 > ex2
+                    or ly < ey1):
+                damages += laser.damage
+                self._game.sfx_player.play('damage00.wav')
+                self.drop_particles(1, 1) #TODO: don’t call each frame.
 
         # Check for enemy-player collisions
         ex1, ex2 = ex - ehalf_size_x * 2. / 3., ex + ehalf_size_x * 2. / 3.
         ey1, ey2 = ey - ehalf_size_y * 2. / 3., ey + ehalf_size_y * 2. / 3.
         if self.collidable:
             for player in self._game.players:
-                px, py = player.x, player.y
-                phalf_size = player.hitbox_half_size
+                px, py = player.state.x, player.state.y
+                phalf_size = player.sht.hitbox
                 px1, px2 = px - phalf_size, px + phalf_size
                 py1, py2 = py - phalf_size, py + phalf_size
 
@@ -268,43 +364,129 @@ class Enemy(object):
 
         # Adjust damages
         damages = min(70, damages)
-        score = (damages // 5) * 10 #TODO: give to which player?
+        score = (damages // 5) * 10
+        self._game.players[0].state.score += score #TODO: better distribution amongst the players.
 
-        if self._game.spellcard:
-            #TODO: there is a division by 3, somewhere... where is it?
-            if damages <= 7:
-                damages = 1 if damages else 0
-            else:
-                damages //= 7
+        if self.damageable:
+            if self._game.spellcard is not None:
+                #TODO: there is a division by 3, somewhere... where is it?
+                if damages <= 7:
+                    damages = 1 if damages else 0
+                else:
+                    damages //= 7
 
-        # Apply damages
-        if self.damageable:
+            # Apply damages
             self.life -= damages
 
 
-    def update(self):
-        x, y = self.x, self.y
-        if self.interpolator:
-            self.interpolator.update(self._game.frame)
-            x, y = self.interpolator.values
+    cdef void handle_callbacks(self):
+        #TODO: implement missing callbacks and clean up!
+        if self.life <= 0 and self.touchable:
+            self.timeout = -1 #TODO: not really true, the timeout is frozen
+            self.timeout_callback = -1
+            death_flags = self.death_flags & 7
+
+            self.die_anim()
+
+            #TODO: verify if the score is added with all the different flags.
+            self._game.players[0].state.score += self.die_score #TODO: better distribution amongst the players.
+
+            #TODO: verify if that should really be there.
+            if self.boss:
+                self._game.change_bullets_into_bonus()
+
+            if death_flags < 4:
+                if self.bonus_dropped > -1:
+                    self.drop_particles(7, 0)
+                    self._game.drop_bonus(self.x, self.y, self.bonus_dropped)
+                elif self.bonus_dropped == -1:
+                    if self._game.deaths_count % 3 == 0:
+                        self.drop_particles(10, 0)
+                        self._game.drop_bonus(self.x, self.y, self._game.bonus_list[self._game.next_bonus])
+                        self._game.next_bonus = (self._game.next_bonus + 1) % 32
+                    else:
+                        self.drop_particles(4, 0)
+                    self._game.deaths_count += 1
+                else:
+                    self.drop_particles(4, 0)
+
+                if death_flags == 0:
+                    self.removed = True
+                    return
 
-        self.speed += self.acceleration #TODO: units? Execution order?
-        self.angle += self.rotation_speed #TODO: units? Execution order?
+                if death_flags == 1:
+                    if self.boss:
+                        self.boss = False #TODO: really?
+                        self._game.boss = None
+                    self.touchable = False
+                elif death_flags == 2:
+                    pass # Just that?
+                elif death_flags == 3:
+                    if self.boss:
+                        self.boss = False #TODO: really?
+                        self._game.boss = None
+                    self.damageable = False
+                    self.life = 1
+                    self.death_flags = 0
+
+            if death_flags != 0 and self.death_callback > -1:
+                self.process.switch_to_sub(self.death_callback)
+                self.death_callback = -1
+        elif self.life <= self.low_life_trigger and self.low_life_callback > -1:
+            self.process.switch_to_sub(self.low_life_callback)
+            self.low_life_callback = -1
+            self.low_life_trigger = -1
+            self.timeout_callback = -1
+        elif self.timeout != -1 and self.frame == self.timeout:
+            self.frame = 0
+            self.timeout = -1
+            self._game.kill_enemies()
+            self._game.cancel_bullets()
+
+            if self.low_life_trigger > 0:
+                self.life = self.low_life_trigger
+                self.low_life_trigger = -1
+
+            if self.timeout_callback > -1:
+                self.process.switch_to_sub(self.timeout_callback)
+                self.timeout_callback = -1
+            #TODO: this is only done under certain (unknown) conditions!
+            # but it shouldn't hurt anyway, as the only option left is to crash!
+            elif self.death_callback > -1:
+                self.life = 0 #TODO: do this next frame? Bypass self.touchable?
+            else:
+                raise Exception('What the hell, man!')
 
 
-        if self.speed_interpolator:
-            self.speed_interpolator.update(self._game.frame)
-            self.speed, = self.speed_interpolator.values
+    cdef void update(self):
+        cdef double x, y, speed
+
+        if self.process is not None:
+            self.process.run_iteration()
+
+        x, y = self.x, self.y
 
+        if self.update_mode == 1:
+            speed = 0.
+            if self.interpolator:
+                self.interpolator.update(self._game.frame)
+                x, y = self.interpolator.values
+            if self.speed_interpolator:
+                self.speed_interpolator.update(self._game.frame)
+                speed, = self.speed_interpolator.values
+        else:
+            speed = self.speed
+            self.speed += self.acceleration
+            self.angle += self.rotation_speed
 
-        dx, dy = cos(self.angle) * self.speed, sin(self.angle) * self.speed
+        dx, dy = cos(self.angle) * speed, sin(self.angle) * speed
         if self._type & 2:
             x -= dx
         else:
             x += dx
         y += dy
 
-        if self.movement_dependant_sprites:
+        if self.movement_dependant_sprites is not None:
             #TODO: is that really how it works? Almost.
             # Sprite determination is done only once per changement, and is
             # superseeded by ins_97.
@@ -315,12 +497,12 @@ class Enemy(object):
             elif x > self.x and self.direction != +1:
                 self.set_anim(right)
                 self.direction = +1
-            elif x == self.x and self.direction is not None:
+            elif x == self.x and self.direction != 0:
                 self.set_anim({-1: end_left, +1: end_right}[self.direction])
-                self.direction = None
+                self.direction = 0
 
 
-        if self.screen_box:
+        if self.screen_box is not None:
             xmin, ymin, xmax, ymax = self.screen_box
             x = max(xmin, min(x, xmax))
             y = max(ymin, min(y, ymax))
@@ -329,15 +511,15 @@ class Enemy(object):
         self.x, self.y = x, y
 
         #TODO
-        if self._anmrunner and not self._anmrunner.run_frame():
-            self._anmrunner = None
+        if self.anmrunner is not None and not self.anmrunner.run_frame():
+            self.anmrunner = None
 
-        if self._sprite and self._visible:
-            if self._sprite._removed:
-                self._sprite = None
+        if self.sprite is not None and self.visible:
+            if self.sprite.removed:
+                self.sprite = None
             else:
-                self._sprite.update_orientation(self.angle,
-                                                self.automatic_orientation)
+                self.sprite.update_orientation(self.angle,
+                                               self.automatic_orientation)
 
 
         if self.bullet_launch_interval != 0:
@@ -349,5 +531,15 @@ class Enemy(object):
         if self.touchable:
             self.check_collisions()
 
+        for anm in self.aux_anm:
+            if anm is not None:
+                anm.x, anm.y = self.x, self.y
+                anm.update()
+
+        self.handle_callbacks()
+
         self.frame += 1
 
+
+    def select_player_key(self, p):
+        return ((p.x - self.x) ** 2 + (p.y - self.y) ** 2, p.state.character)
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.element import Element
+from pytouhou.game.sprite import Sprite
+from pytouhou.vm.anmrunner import ANMRunner
+
+
+class Face(Element):
+    __slots__ = ('_anms', 'side')
+
+    def __init__(self, anms, effect, side):
+        Element.__init__(self, (-32, -16))
+
+        self._anms = anms
+        self.sprite = Sprite()
+        self.anmrunner = ANMRunner(self._anms[0][0][0], side * 2, self.sprite)
+        self.side = side
+        self.load(0)
+        self.animate(effect)
+
+        #FIXME: the same as game.effect.
+        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._anms[self.side][index]
+
+
+    def update(self):
+        self.anmrunner.run_frame()
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/game.pxd
@@ -0,0 +1,46 @@
+from pytouhou.game.effect cimport Effect
+from pytouhou.game.player cimport Player
+from pytouhou.game.text cimport Text, NativeText
+
+cdef class Game:
+    cdef public long width, height, nb_bullets_max, stage, rank, difficulty, difficulty_counter, difficulty_min, difficulty_max, frame, last_keystate
+    cdef public list bullet_types, laser_types, item_types, players, enemies, effects, bullets, lasers, cancelled_bullets, players_bullets, players_lasers, items, labels, faces, texts, hints, bonus_list
+    cdef public object interface, boss, msg_runner, prng, sfx_player
+    cdef public double continues
+    cdef public Effect spellcard_effect
+    cdef public tuple spellcard
+    cdef public bint time_stop, msg_wait
+    cdef public unsigned short deaths_count, next_bonus
+
+    cdef list msg_sprites(self)
+    cdef list lasers_sprites(self)
+    cdef void modify_difficulty(self, long diff) except *
+    cpdef enable_spellcard_effect(self)
+    cpdef disable_spellcard_effect(self)
+    cdef void set_player_bomb(self) except *
+    cdef void unset_player_bomb(self) except *
+    cpdef drop_bonus(self, double x, double y, long _type, end_pos=*)
+    cdef void autocollect(self, Player player) except *
+    cdef void cancel_bullets(self) except *
+    cdef void cancel_player_lasers(self) except *
+    cpdef change_bullets_into_star_items(self)
+    cpdef change_bullets_into_bonus(self)
+    cpdef kill_enemies(self)
+    cpdef new_effect(self, pos, long anim, anm=*, long number=*)
+    cpdef new_particle(self, pos, long anim, long amp, long number=*, bint reverse=*, long duration=*)
+    cpdef new_enemy(self, pos, life, instr_type, bonus_dropped, die_score)
+    cpdef new_msg(self, sub)
+    cdef Text new_label(self, tuple pos, bytes text)
+    cpdef NativeText new_native_text(self, tuple pos, unicode text, align=*)
+    cpdef Text new_hint(self, hint)
+    cpdef new_face(self, side, effect)
+    cpdef run_iter(self, list keystates)
+    cdef void update_background(self) except *
+    cdef void update_enemies(self) except *
+    cdef void update_msg(self, long keystate) except *
+    cdef void update_players(self, list keystates) except *
+    cdef void update_effects(self) except *
+    cdef void update_hints(self) except *
+    cdef void update_faces(self) except *
+    cdef void update_bullets(self) except *
+    cpdef cleanup(self)
rename from pytouhou/game/game.py
rename to pytouhou/game/game.pyx
--- a/pytouhou/game/game.py
+++ b/pytouhou/game/game.pyx
@@ -12,37 +12,42 @@
 ## GNU General Public License for more details.
 ##
 
-
-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
-from pytouhou.game.effect import Effect
-from pytouhou.game.effect import Particle
+from pytouhou.game.element cimport Element
+from pytouhou.game.bullet cimport Bullet, LAUNCHED, CANCELLED
+from pytouhou.game.enemy cimport Enemy
+from pytouhou.game.item cimport Item
+from pytouhou.game.effect cimport Particle
+from pytouhou.game.laser cimport Laser, PlayerLaser
+from pytouhou.game.face import Face
 
 
-
-class Game(object):
-    def __init__(self, resource_loader, players, stage, rank, difficulty,
-                 bullet_types, item_types,
-                 nb_bullets_max=None, width=384, height=448, prng=None):
-        self.resource_loader = resource_loader
-
+cdef class Game:
+    def __init__(self, players, long stage, long rank, long difficulty, bullet_types,
+                 laser_types, item_types, long nb_bullets_max=0, long width=384,
+                 long height=448, prng=None, interface=None, hints=None):
         self.width, self.height = width, height
 
         self.nb_bullets_max = nb_bullets_max
         self.bullet_types = bullet_types
+        self.laser_types = laser_types
         self.item_types = item_types
 
         self.players = players
         self.enemies = []
         self.effects = []
         self.bullets = []
+        self.lasers = []
         self.cancelled_bullets = []
         self.players_bullets = []
+        self.players_lasers = [None, None]
         self.items = []
+        self.labels = []
+        self.faces = [None, None]
+        self.texts = [None, None, None, None, None, None]
+        self.interface = interface
+        self.hints = hints
 
         self.stage = stage
         self.rank = rank
@@ -52,26 +57,33 @@ class Game(object):
         self.difficulty_max = 20 if rank == 0 else 32
         self.boss = None
         self.spellcard = None
+        self.time_stop = False
+        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()
+        self.prng = prng
         self.frame = 0
+        self.sfx_player = None
 
-        self.enm_anm_wrapper = resource_loader.get_anm_wrapper2(('stg%denm.anm' % stage,
-                                                                 'stg%denm2.anm' % stage))
-        self.etama4 = resource_loader.get_anm_wrapper(('etama4.anm',))
-        ecl = resource_loader.get_ecl('ecldata%d.ecl' % stage)
-        self.ecl_runner = ECLMainRunner(ecl, self)
-
-        self.effect_anm_wrapper = resource_loader.get_anm_wrapper(('eff0%d.anm' % stage,))
-        self.effect = None
+        self.spellcard_effect = None
 
         # See 102h.exe@0x413220 if you think you're brave enough.
         self.deaths_count = self.prng.rand_uint16() % 3
         self.next_bonus = self.prng.rand_uint16() % 8
 
+        self.last_keystate = 0
 
-    def modify_difficulty(self, diff):
+
+    cdef list msg_sprites(self):
+        return [face for face in self.faces if face is not None] if self.msg_runner is not None and not self.msg_runner.ended else []
+
+
+    cdef list lasers_sprites(self):
+        return [laser for laser in self.players_lasers if laser is not None]
+
+
+    cdef void modify_difficulty(self, long diff):
         self.difficulty_counter += diff
         while self.difficulty_counter < 0:
             self.difficulty -= 1
@@ -85,203 +97,462 @@ class Game(object):
             self.difficulty = self.difficulty_max
 
 
-    def enable_effect(self):
-        self.effect = Effect((-32., -16.), 0, self.effect_anm_wrapper) #TODO: find why this offset is necessary.
-        self.effect._sprite.allow_dest_offset = True #TODO: should be the role of anm’s 25th instruction. Investigate!
+    cpdef enable_spellcard_effect(self):
+        pos = (-32, -16)
+        self.spellcard_effect = Effect(pos, 0,
+                                       self.spellcard_effect_anm)
+        self.spellcard_effect.sprite.allow_dest_offset = True
+
+        face = Effect(pos, 3, self.msg_anm[0][0][0])
+        face.sprite.allow_dest_offset = True
+        face.sprite.anm, face.sprite.texcoords = self.msg_anm[1][self.spellcard[2]]
+        self.effects.append(face)
+
+        self.texts[5] = self.new_native_text((384-24, 24), self.spellcard[1], align='right')
 
 
-    def disable_effect(self):
-        self.effect = None
+    cpdef disable_spellcard_effect(self):
+        self.spellcard_effect = None
+        self.texts[5] = None
 
 
-    def drop_bonus(self, x, y, _type, end_pos=None):
+    cdef void set_player_bomb(self):
+        face = Effect((-32, -16), 1, self.msg_anm[0][0][0])
+        face.sprite.allow_dest_offset = True
+        self.effects.append(face)
+        self.texts[4] = self.new_native_text((24, 24), u'Player Spellcard')
+
+
+    cdef void unset_player_bomb(self):
+        self.texts[4] = None
+
+
+    cpdef drop_bonus(self, double x, double y, long _type, end_pos=None):
         if _type > 6:
             return
+        if len(self.items) >= self.nb_bullets_max:
+            return #TODO: check
         item_type = self.item_types[_type]
-        item = Item((x, y), _type, item_type, self, end_pos=end_pos)
-        self.items.append(item)
+        self.items.append(Item((x, y), _type, item_type, self, end_pos=end_pos))
+
+
+    cdef void autocollect(self, Player player):
+        cdef Item item
+
+        for item in self.items:
+            item.autocollect(player)
 
 
-    def autocollect(self, player):
-        for item in self.items:
-            if not item.player:
-                item.autocollect(player)
+    cdef void cancel_bullets(self):
+        cdef Bullet bullet
+        cdef Laser laser
+
+        for bullet in self.bullets:
+            bullet.cancel()
+        for laser in self.lasers:
+            laser.cancel()
+
+    cdef void cancel_player_lasers(self):
+        cdef PlayerLaser laser
+        for laser in self.players_lasers:
+            if laser is not None:
+                laser.cancel()
 
 
-    def change_bullets_into_star_items(self):
-        player = min(self.players, key=lambda x: (x.state.score, x.state.character)) #TODO
+    cpdef change_bullets_into_star_items(self):
+        cdef Player player
+        cdef Bullet bullet
+        cdef Laser laser
+
+        player = min(self.players, key=select_player_key)
         item_type = self.item_types[6]
-        self.items.extend(Item((bullet.x, bullet.y), 6, item_type, self, player=player) for bullet in self.bullets)
+        self.items.extend([Item((bullet.x, bullet.y), 6, item_type, self, player=player)
+                             for bullet in self.bullets])
+        for laser in self.lasers:
+            self.items.extend([Item(pos, 6, item_type, self, player=player)
+                                 for pos in laser.get_bullets_pos()])
+            laser.cancel()
         self.bullets = []
 
 
-    def new_death(self, pos, index):
-        anim = {0: 3, 1: 4, 2: 5}[index % 256] # The TB is wanted, if index isn’t in these values the original game crashs.
-        self.effects.append(Effect(pos, anim, self.etama4))
+    cpdef change_bullets_into_bonus(self):
+        cdef Player player
+        cdef Bullet bullet
+
+        player = self.players[0] #TODO
+        score = 0
+        bonus = 2000
+        for bullet in self.bullets:
+            self.new_label((bullet.x, bullet.y), str(bonus))
+            score += bonus
+            bonus += 10
+        self.bullets = []
+        player.state.score += score
+        #TODO: display the final bonus score.
 
 
-    def new_particle(self, pos, color, size, amp):
-        self.effects.append(Particle(pos, 7 + 4 * color + self.prng.rand_uint16() % 4, self.etama4, size, amp, self))
+    cpdef kill_enemies(self):
+        cdef Enemy enemy
+
+        for enemy in self.enemies:
+            if enemy.boss:
+                pass # Bosses are immune to 96
+            elif enemy.touchable:
+                enemy.life = 0
+            elif enemy.death_callback > 0:
+                #TODO: check
+                enemy.process.switch_to_sub(enemy.death_callback)
+                enemy.death_callback = -1
 
 
-    def new_enemy(self, pos, life, instr_type, bonus_dropped, die_score):
-        enemy = Enemy(pos, life, instr_type, bonus_dropped, die_score, self.enm_anm_wrapper, self)
+    cpdef new_effect(self, pos, long anim, anm=None, long number=1):
+        number = min(number, self.nb_bullets_max - len(self.effects))
+        for i in xrange(number):
+            self.effects.append(Effect(pos, anim, anm or self.etama[1]))
+
+
+    cpdef new_particle(self, pos, long anim, long amp, long number=1, bint reverse=False, long duration=24):
+        number = min(number, self.nb_bullets_max - len(self.effects))
+        for i in xrange(number):
+            self.effects.append(Particle(pos, anim, self.etama[1], amp, self, reverse=reverse, duration=duration))
+
+
+    cpdef new_enemy(self, pos, life, instr_type, bonus_dropped, die_score):
+        enemy = Enemy(pos, life, instr_type, bonus_dropped, die_score, self.enm_anm, self)
         self.enemies.append(enemy)
         return enemy
 
 
-    def run_iter(self, keystates):
+    cpdef new_msg(self, sub):
+        self.msg_runner = MSGRunner(self.msg, sub, self)
+        self.msg_runner.run_iteration()
+
+
+    cdef Text new_label(self, tuple pos, bytes text):
+        label = Text(pos, self.interface.ascii_anm, text=text, xspacing=8, shift=48)
+        label.set_timeout(60, effect='move')
+        self.labels.append(label)
+        return label
+
+
+    cpdef NativeText new_native_text(self, tuple pos, unicode text, align='left'):
+        label = NativeText(pos, text, shadow=True, align=align)
+        return label
+
+
+    cpdef Text new_hint(self, hint):
+        pos = hint['Pos']
+        #TODO: Scale
+
+        pos = pos[0] + 192, pos[1]
+        label = Text(pos, self.interface.ascii_anm, text=hint['Text'], align=hint['Align'])
+        label.set_timeout(hint['Time'])
+        label.set_alpha(hint['Alpha'])
+        label.set_color(None, hint['Color']) #XXX
+        self.labels.append(label)
+        return label
+
+
+    cpdef new_face(self, side, effect):
+        face = Face(self.msg_anm, effect, side)
+        self.faces[side] = face
+        return face
+
+
+    cpdef run_iter(self, list keystates):
+        cdef Laser laser
+
         # 1. VMs.
-        self.ecl_runner.run_iter()
+        for runner in self.ecl_runners:
+            runner.run_iter()
+
+        # 2. Modify difficulty
         if self.frame % (32*60) == (32*60): #TODO: check if that is really that frame.
             self.modify_difficulty(+100)
 
-        # 2. Filter out destroyed enemies
-        self.enemies = [enemy for enemy in self.enemies if not enemy._removed]
-        self.effects = [enemy for enemy in self.effects if not enemy._removed]
-        self.bullets = [bullet for bullet in self.bullets if not bullet._removed]
-        self.cancelled_bullets = [bullet for bullet in self.cancelled_bullets if not bullet._removed]
-        self.items = [item for item in self.items if not item._removed]
+        # 3. Filter out destroyed enemies
+        self.enemies = filter_removed(self.enemies)
+        self.effects = filter_removed(self.effects)
+        self.bullets = filter_removed(self.bullets)
+        self.cancelled_bullets = filter_removed(self.cancelled_bullets)
+        self.items = filter_removed(self.items)
 
-
-        # 3. Let's play!
+        # 4. Let's play!
         # In the original game, updates are done in prioritized functions called "chains"
         # We have to mimic this functionnality to be replay-compatible with the official game.
 
         # Pri 6 is background
-        self.update_effect() #TODO: Pri unknown
+        self.update_background() #TODO: Pri unknown
+        if self.msg_runner is not None:
+            self.update_msg(keystates[0]) # Pri ?
+            for i in xrange(len(keystates)):
+                keystates[i] &= ~3 # Remove the ability to attack (keystates 1 and 2).
         self.update_players(keystates) # Pri 7
         self.update_enemies() # Pri 9
         self.update_effects() # Pri 10
         self.update_bullets() # Pri 11
-        # Pri 12 is HUD
+        for laser in self.lasers: #TODO: what priority is it?
+            laser.update()
+        self.interface.update() # Pri 12
+        if self.hints:
+            self.update_hints() # Not from this game, so unknown.
+        for label in self.labels: #TODO: what priority is it?
+            label.update()
+        for text in self.texts: #TODO: what priority is it?
+            if text is not None:
+                text.update()
+        for text in self.native_texts: #TODO: what priority is it?
+            if text is not None:
+                text.update()
+        self.update_faces() # Pri XXX
 
-        # 4. Cleaning
+        # 5. Clean up
         self.cleanup()
 
         self.frame += 1
 
 
-    def update_effect(self):
-        if self.effect is not None:
-            self.effect.update()
+    cdef void update_background(self):
+        if self.time_stop:
+            return
+        if self.spellcard_effect is not None:
+            self.spellcard_effect.update()
+        #TODO: update the actual background here?
 
 
-    def update_enemies(self):
+    cdef void update_enemies(self):
+        cdef Enemy enemy
+
         for enemy in self.enemies:
             enemy.update()
 
 
-    def update_players(self, keystates):
-        for player, keystate in zip(self.players, keystates):
-            player.update(keystate) #TODO: differentiate keystates (multiplayer mode)
-            if player.state.x < 8.:
-                player.state.x = 8.
-            if player.state.x > self.width - 8:
-                player.state.x = self.width - 8
-            if player.state.y < 16.:
-                player.state.y = 16.
-            if player.state.y > self.height - 16:
-                player.state.y = self.height -16
+    cdef void update_msg(self, long keystate):
+        cdef long k
+
+        for k in (1, 256):
+            if keystate & k and not self.last_keystate & k:
+                self.msg_runner.skip()
+                break
+        self.msg_runner.skipping = bool(keystate & 256)
+        self.last_keystate = keystate
+        self.msg_runner.run_iteration()
+
+
+    cdef void update_players(self, list keystates):
+        cdef Bullet bullet
+        cdef Player player
+        cdef long keystate
+
+        if self.time_stop:
+            return
 
         for bullet in self.players_bullets:
             bullet.update()
 
+        for player, keystate in zip(self.players, keystates):
+            player.update(keystate) #TODO: differentiate keystates (multiplayer mode)
 
-    def update_effects(self):
+            #XXX: Why 78910? Is it really the right value?
+            player.state.effective_score = min(player.state.effective_score + 78910,
+                                               player.state.score)
+            #TODO: give extra lives to the player
+
+
+    cdef void update_effects(self):
+        cdef Element effect
+
         for effect in self.effects:
             effect.update()
 
 
-    def update_bullets(self):
+    cdef void update_hints(self):
+        for hint in self.hints:
+            if hint['Count'] == self.frame and hint['Base'] == 'start':
+                self.new_hint(hint)
+
+
+    cdef void update_faces(self):
+        for face in self.faces:
+            if face:
+                face.update()
+
+
+    cdef void update_bullets(self):
+        cdef Player player
+        cdef Bullet bullet
+        cdef Item item
+        cdef PlayerLaser player_laser
+        cdef Laser laser
+        cdef double player_pos[2]
+
+        if self.time_stop:
+            return
+
         for bullet in self.cancelled_bullets:
             bullet.update()
 
         for bullet in self.bullets:
             bullet.update()
 
+        for player_laser in self.players_lasers:
+            if player_laser is not None:
+                player_laser.update()
+
         for item in self.items:
             item.update()
 
         for player in self.players:
-            if not player.state.touchable:
+            player_state = player.state
+
+            if not player_state.touchable:
                 continue
 
-            px, py = player.x, player.y
-            phalf_size = player.hitbox_half_size
+            px, py = player_state.x, player_state.y
+            player_pos[:] = [px, py]
+            phalf_size = <double>player.sht.hitbox
             px1, px2 = px - phalf_size, px + phalf_size
             py1, py2 = py - phalf_size, py + phalf_size
 
-            ghalf_size = player.graze_hitbox_half_size
+            ghalf_size = <double>player.sht.graze_hitbox
             gx1, gx2 = px - ghalf_size, px + ghalf_size
             gy1, gy2 = py - ghalf_size, py + ghalf_size
 
+            for laser in self.lasers:
+                if laser.check_collision(player_pos):
+                    if player_state.invulnerable_time == 0:
+                        player.collide()
+                elif laser.check_grazing(player_pos):
+                    player_state.graze += 1 #TODO
+                    player_state.score += 500 #TODO
+                    player.play_sound('graze')
+                    self.modify_difficulty(+6) #TODO
+                    self.new_particle((px, py), 9, 192) #TODO
+
             for bullet in self.bullets:
-                half_size = bullet.hitbox_half_size
+                if bullet.state != LAUNCHED:
+                    continue
+
+                bhalf_width = bullet.hitbox[0]
+                bhalf_height = bullet.hitbox[1]
                 bx, by = bullet.x, bullet.y
-                bx1, bx2 = bx - half_size[0], bx + half_size[0]
-                by1, by2 = by - half_size[1], by + half_size[1]
+                bx1, bx2 = bx - bhalf_width, bx + bhalf_width
+                by1, by2 = by - bhalf_height, by + bhalf_height
 
                 if not (bx2 < px1 or bx1 > px2
                         or by2 < py1 or by1 > py2):
                     bullet.collide()
-                    if player.state.invulnerable_time == 0:
+                    if player_state.invulnerable_time == 0:
                         player.collide()
 
                 elif not bullet.grazed and not (bx2 < gx1 or bx1 > gx2
                         or by2 < gy1 or by1 > gy2):
                     bullet.grazed = True
-                    player.state.graze += 1
-                    player.state.score += 500 # found experimentally
+                    player_state.graze += 1
+                    player_state.score += 500 # found experimentally
+                    player.play_sound('graze')
                     self.modify_difficulty(+6)
-                    self.new_particle((px, py), 0, .8, 192) #TODO: find the real size and range.
+                    self.new_particle((px, py), 9, 192) #TODO: find the real size and range.
                     #TODO: display a static particle during one frame at
                     # 12 pixels of the player, in the axis of the “collision”.
 
             #TODO: is it the right place?
-            if py < 128 and player.state.power >= 128: #TODO: check py.
+            if py < 128 and player_state.power >= 128: #TODO: check py.
                 self.autocollect(player)
 
+            ihalf_size = <double>player.sht.item_hitbox
             for item in self.items:
-                half_size = item.hitbox_half_size
                 bx, by = item.x, item.y
-                bx1, bx2 = bx - half_size, bx + half_size
-                by1, by2 = by - half_size, by + half_size
+                bx1, bx2 = bx - ihalf_size, bx + ihalf_size
+                by1, by2 = by - ihalf_size, by + ihalf_size
 
                 if not (bx2 < px1 or bx1 > px2
                         or by2 < py1 or by1 > py2):
                     item.on_collect(player)
 
 
-    def cleanup(self):
+    cpdef cleanup(self):
+        cdef Enemy enemy
+        cdef Bullet bullet
+        cdef Item item
+        cdef PlayerLaser laser
+        cdef long i
+
         # Filter out non-visible enemies
-        for enemy in tuple(self.enemies):
+        for enemy in self.enemies:
             if enemy.is_visible(self.width, self.height):
-                enemy._was_visible = True
-            elif enemy._was_visible:
+                enemy.was_visible = True
+            elif enemy.was_visible:
                 # Filter out-of-screen enemy
-                enemy._removed = True
-                self.enemies.remove(enemy)
+                enemy.removed = True
+
+        self.enemies = filter_removed(self.enemies)
 
         # Filter out-of-scren bullets
-        # TODO: was_visible thing
-        self.bullets = [bullet for bullet in self.bullets
-                            if bullet.is_visible(self.width, self.height)]
-        self.cancelled_bullets = [bullet for bullet in self.cancelled_bullets
-                            if bullet.is_visible(self.width, self.height)]
-        self.players_bullets = [bullet for bullet in self.players_bullets
-                            if bullet.is_visible(self.width, self.height)]
+        cancelled_bullets = []
+        bullets = []
+        players_bullets = []
+
+        for bullet in self.cancelled_bullets:
+            if bullet.state == CANCELLED and not bullet.removed:
+                cancelled_bullets.append(bullet)
+
+        for bullet in self.bullets:
+            if not bullet.removed:
+                if bullet.state == CANCELLED:
+                    cancelled_bullets.append(bullet)
+                else:
+                    bullets.append(bullet)
+
+        for bullet in self.players_bullets:
+            if not bullet.removed:
+                if bullet.state == CANCELLED:
+                    cancelled_bullets.append(bullet)
+                else:
+                    players_bullets.append(bullet)
+
+        self.cancelled_bullets = cancelled_bullets
+        self.bullets = bullets
+        self.players_bullets = players_bullets
+
+        # Filter “timed-out” lasers
+        for i, laser in enumerate(self.players_lasers):
+            if laser is not None and laser.removed:
+                self.players_lasers[i] = None
+
+        self.lasers = filter_removed(self.lasers)
 
         # Filter out-of-scren items
         items = []
         for item in self.items:
-            if item.y < 448:
+            if item.y < self.height:
                 items.append(item)
             else:
                 self.modify_difficulty(-3)
         self.items = items
 
+        self.effects = filter_removed(self.effects)
+        self.labels = filter_removed(self.labels)
+        #self.native_texts = filter_removed(self.native_texts)
+
+        for i, text in enumerate(self.texts):
+            if text is not None and text.removed:
+                self.texts[i] = None
+
         # 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
 
+
+cdef list filter_removed(list elements):
+    cdef Element element
+
+    filtered = []
+    for element in elements:
+        if not element.removed:
+            filtered.append(element)
+    return filtered
+
+
+def select_player_key(player):
+    return (player.state.score, player.state.character)
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/item.pxd
@@ -0,0 +1,27 @@
+from pytouhou.game.element cimport Element
+from pytouhou.game.game cimport Game
+from pytouhou.game.player cimport Player
+from pytouhou.game.itemtype cimport ItemType
+from pytouhou.utils.interpolator cimport Interpolator
+
+
+cdef class Indicator(Element):
+    cdef Item _item
+
+    cdef void update(self) nogil
+
+
+cdef class Item(Element):
+    cdef public ItemType _item_type
+
+    cdef unsigned long frame
+    cdef long _type
+    cdef double angle, speed
+    cdef Game _game
+    cdef Player player
+    cdef Indicator indicator
+    cdef Interpolator speed_interpolator, pos_interpolator
+
+    cdef void autocollect(self, Player player) except *
+    cdef void on_collect(self, Player player) except *
+    cpdef update(self)
rename from pytouhou/game/item.py
rename to pytouhou/game/item.pyx
--- a/pytouhou/game/item.py
+++ b/pytouhou/game/item.pyx
@@ -12,32 +12,44 @@
 ## GNU General Public License for more details.
 ##
 
+from libc.math cimport cos, sin, atan2, M_PI as pi
 
-from math import cos, sin, atan2, pi
+
+cdef class Indicator(Element):
+    def __init__(self, Item item):
+        Element.__init__(self)
 
-from pytouhou.utils.interpolator import Interpolator
+        self._item = item
+        self.sprite = item._item_type.indicator_sprite.copy()
+
+        self.x = self._item.x
+        self.y = self.sprite.texcoords[2] / 2.
 
 
-class Item(object):
-    def __init__(self, start_pos, _type, item_type, game, angle=pi/2, player=None, end_pos=None):
+    cdef void update(self) nogil:
+        #TODO: alpha
+        self.x = self._item.x
+
+
+
+cdef class Item(Element):
+    def __init__(self, start_pos, long _type, ItemType item_type, Game game, double angle=pi/2, Player player=None, end_pos=None):
+        Element.__init__(self, start_pos)
+
         self._game = game
-        self._sprite = item_type.sprite
-        self._removed = False
         self._type = _type
         self._item_type = item_type
-
-        self.hitbox_half_size = item_type.hitbox_size / 2.
+        self.sprite = item_type.sprite
 
         self.frame = 0
-        self.x, self.y = start_pos
         self.angle = angle
+        self.indicator = None
 
-        if player:
+        if player is not None:
             self.autocollect(player)
         else:
             self.player = None
 
-        if not player:
             #TODO: find the formulae in the binary.
             self.speed_interpolator = None
             if end_pos:
@@ -47,17 +59,31 @@ class Item(object):
                 self.speed_interpolator = Interpolator((-2.,), 0,
                                                        (0.,), 60)
 
-        self._sprite.angle = angle
+        self.sprite.angle = angle
+
+
+    property objects:
+        def __get__(self):
+            if self.indicator is not None:
+                return [self.indicator]
+            return [self]
 
 
-    def autocollect(self, player):
-        self.player = player
-        self.speed = player.sht.autocollection_speed if hasattr(player, 'sht') else 8.
+    cdef void autocollect(self, Player player):
+        if self.player is None:
+            self.player = player
+            self.speed = player.sht.autocollection_speed
 
 
-    def on_collect(self, player):
+    cdef void on_collect(self, Player player):
+        cdef long level, poc
+
         player_state = player.state
         old_power = player_state.power
+        score = 0
+        label = None
+        color = 'white'
+        player.play_sound('item00')
 
         if self._type == 0 or self._type == 2: # power or big power
             if old_power < 128:
@@ -66,6 +92,12 @@ class Item(object):
                 player_state.power += (1 if self._type == 0 else 8)
                 if player_state.power > 128:
                     player_state.power = 128
+                for level in (8, 16, 32, 48, 64, 96):
+                    if old_power < level and player_state.power >= level:
+                        label = self._game.new_label((self.x, self.y), b':') # Actually a “PowerUp” character.
+                        color = 'blue'
+                        label.set_color(color)
+                        labeled = True
             else:
                 bonus = player_state.power_bonus + (1 if self._type == 0 else 8)
                 if bonus > 30:
@@ -78,20 +110,20 @@ class Item(object):
                     score = (bonus - 17) * 1000
                 elif bonus == 30:
                     score = 51200
+                    color = 'yellow'
                 player_state.power_bonus = bonus
-            player_state.score += score
             self._game.modify_difficulty(+1)
 
         elif self._type == 1: # point
             player_state.points += 1
-            poc = player.sht.point_of_collection if hasattr(player, 'sht') else 128 #TODO: find the exact poc in EoSD.
+            poc = player.sht.point_of_collection
             if player_state.y < poc:
                 score = 100000
                 self._game.modify_difficulty(+30)
+                color = 'yellow'
             else:
-                score = 0 #TODO: find the formula.
+                #score =  #TODO: find the formula.
                 self._game.modify_difficulty(+3)
-            player_state.score += score
 
         elif self._type == 3: # bomb
             if player_state.bombs < 8:
@@ -99,31 +131,42 @@ class Item(object):
             self._game.modify_difficulty(+5)
 
         elif self._type == 4: # full power
-            player_state.score += 1000
+            score = 1000
             player_state.power = 128
 
         elif self._type == 5: # 1up
             if player_state.lives < 8:
                 player_state.lives += 1
             self._game.modify_difficulty(+200)
+            player.play_sound('extend')
 
         elif self._type == 6: # star
-            player_state.score += 500
+            score = 500
 
-        if old_power < 128 and player_state.power >= 128:
+        if old_power < 128 and player_state.power == 128:
             #TODO: display “full power”.
             self._game.change_bullets_into_star_items()
 
-        self._removed = True
+        if score > 0:
+            player_state.score += score
+            if label is None:
+                label = self._game.new_label((self.x, self.y), str(score))
+                if color != 'white':
+                    label.set_color(color)
+
+        self.removed = True
 
 
-    def update(self):
+    cpdef update(self):
+        cdef bint offscreen
+
         if self.frame == 60:
             self.speed_interpolator = Interpolator((0.,), 60,
                                                    (3.,), 180)
 
         if self.player is not None:
-            self.angle = atan2(self.player.y - self.y, self.player.x - self.x)
+            player_state = self.player.state
+            self.angle = atan2(player_state.y - self.y, player_state.x - self.x)
             self.x += cos(self.angle) * self.speed
             self.y += sin(self.angle) * self.speed
         elif self.speed_interpolator is None:
@@ -136,5 +179,12 @@ class Item(object):
             self.x += dx
             self.y += dy
 
+        offscreen = self.y < -(<double>self.sprite.texcoords[2] / 2.)
+        if offscreen:
+            if self.indicator is None:
+                self.indicator = Indicator(self)
+            self.indicator.update()
+        else:
+            self.indicator = None
+
         self.frame += 1
-
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/itemtype.pxd
@@ -0,0 +1,5 @@
+from pytouhou.game.sprite cimport Sprite
+
+cdef class ItemType:
+    cdef Sprite sprite, indicator_sprite
+    cdef object anm
--- a/pytouhou/game/itemtype.py
+++ b/pytouhou/game/itemtype.py
@@ -1,10 +1,11 @@
 from pytouhou.game.sprite import Sprite
 
 class ItemType(object):
-    def __init__(self, anm_wrapper, sprite_index, indicator_sprite_index, hitbox_size=38.):
-        self.anm_wrapper = anm_wrapper
+    def __init__(self, anm, sprite_index, indicator_sprite_index):
+        self.anm = anm
         self.sprite = Sprite()
-        self.sprite.anm, self.sprite.texcoords = anm_wrapper.get_sprite(sprite_index)
+        self.sprite.anm = anm
+        self.sprite.texcoords = anm.sprites[sprite_index]
         self.indicator_sprite = Sprite()
-        self.indicator_sprite.anm, self.indicator_sprite.texcoords = anm_wrapper.get_sprite(indicator_sprite_index)
-        self.hitbox_size = hitbox_size
+        self.indicator_sprite.anm = anm
+        self.indicator_sprite.texcoords = anm.sprites[indicator_sprite_index]
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/laser.pxd
@@ -0,0 +1,45 @@
+from pytouhou.game.element cimport Element
+from pytouhou.game.sprite cimport Sprite
+from pytouhou.game.game cimport Game
+from pytouhou.game.lasertype cimport LaserType
+
+cdef enum State:
+    STARTING, STARTED, STOPPING
+
+
+cdef class LaserLaunchAnim(Element):
+    cdef Laser _laser
+
+    cpdef update(self)
+
+
+cdef class Laser(Element):
+    cdef public unsigned long frame
+    cdef public double angle
+
+    cdef unsigned long start_duration, duration, stop_duration, grazing_delay,
+    cdef unsigned long grazing_extra_duration, sprite_idx_offset
+    cdef double base_pos[2], speed, start_offset, end_offset, max_length, width
+    cdef State state
+    cdef Game _game
+    cdef LaserType _laser_type
+
+    cdef void set_anim(self, long sprite_idx_offset=*) except *
+    cpdef set_base_pos(self, double x, double y)
+    cdef bint _check_collision(self, double point[2], double border_size)
+    cdef bint check_collision(self, double point[2])
+    cdef bint check_grazing(self, double point[2])
+    #def get_bullets_pos(self)
+    cpdef cancel(self)
+    cpdef update(self)
+
+
+cdef class PlayerLaser(Element):
+    cdef double hitbox[2], angle, offset
+    cdef unsigned long frame, duration, sprite_idx_offset, damage
+    cdef Element origin
+    cdef LaserType _laser_type
+
+    cdef void set_anim(self, long sprite_idx_offset=*) except *
+    cdef void cancel(self) except *
+    cdef void update(self) except *
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/laser.pyx
@@ -0,0 +1,253 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2012 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 libc.math cimport cos, sin, M_PI as pi
+
+from pytouhou.vm.anmrunner import ANMRunner
+
+
+cdef class LaserLaunchAnim(Element):
+    def __init__(self, Laser laser, anm, unsigned long index):
+        Element.__init__(self, (0, 0))
+
+        self._laser = laser
+        self.sprite = Sprite()
+        self.sprite.anm = anm
+        self.sprite.texcoords = anm.sprites[index]
+        self.sprite.blendfunc = 1
+
+
+    cpdef update(self):
+        laser = self._laser
+        length = <double>min(laser.end_offset - laser.start_offset, laser.max_length)
+        offset = laser.end_offset - length
+        dx, dy = cos(laser.angle), sin(laser.angle)
+
+        self.x = laser.base_pos[0] + offset * dx
+        self.y = laser.base_pos[1] + offset * dy
+
+        scale = laser.width / 10. - (offset - laser.start_offset) #TODO: check
+        self.sprite.rescale = (scale, scale)
+        self.sprite.changed = True
+
+        if laser.removed or scale <= 0.:
+            self.removed = True
+
+
+
+cdef class Laser(Element):
+    def __init__(self, tuple base_pos, LaserType laser_type,
+                 unsigned long sprite_idx_offset, double angle, double speed,
+                 double start_offset, double end_offset, double max_length,
+                 double width, unsigned long start_duration,
+                 unsigned long duration, unsigned long stop_duration,
+                 unsigned long grazing_delay,
+                 unsigned long grazing_extra_duration, Game game):
+        Element.__init__(self, (0, 0))
+
+        self._game = game
+        launch_anim = LaserLaunchAnim(self, laser_type.anm,
+                                      laser_type.launch_anim_offsets[sprite_idx_offset]
+                                      + laser_type.launch_sprite_idx)
+        self._game.effects.append(launch_anim)
+        self._laser_type = laser_type
+        self.state = STARTING
+
+        #TODO: hitbox
+
+        self.frame = 0
+        self.start_duration = start_duration
+        self.duration = duration
+        self.stop_duration = stop_duration
+        self.grazing_delay = grazing_delay
+        self.grazing_extra_duration = grazing_extra_duration
+
+        self.sprite_idx_offset = sprite_idx_offset
+        self.set_base_pos(base_pos[0], base_pos[1])
+        self.angle = angle
+        self.speed = speed
+        self.start_offset = start_offset
+        self.end_offset = end_offset
+        self.max_length = max_length
+        self.width = width
+
+        self.set_anim()
+
+
+    cdef void set_anim(self, long sprite_idx_offset=-1):
+        if sprite_idx_offset >= 0:
+            self.sprite_idx_offset = sprite_idx_offset
+
+        lt = self._laser_type
+        self.sprite = Sprite()
+        self.sprite.angle = self.angle
+        self.anmrunner = ANMRunner(lt.anm, lt.anim_index,
+                                   self.sprite, self.sprite_idx_offset)
+
+
+    cpdef set_base_pos(self, double x, double y):
+        self.base_pos[:] = [x, y]
+
+
+    cdef bint _check_collision(self, double point[2], double border_size):
+        cdef double c1[2], c2[2], c3[2]
+
+        x, y = point[0] - self.base_pos[0], point[1] - self.base_pos[1]
+        dx, dy = cos(self.angle), sin(self.angle)
+        dx2, dy2 = -dy, dx
+
+        length = <double>min(self.end_offset - self.start_offset, self.max_length)
+        offset = self.end_offset - length - border_size / 2.
+        end_offset = self.end_offset + border_size / 2.
+        half_width = self.width / 4. + border_size / 2.
+
+        c1[:] = [dx * offset - dx2 * half_width, dy * offset - dy2 * half_width]
+        c2[:] = [dx * offset + dx2 * half_width, dy * offset + dy2 * half_width]
+        c3[:] = [dx * end_offset + dx2 * half_width, dy * end_offset + dy2 * half_width]
+        vx, vy = x - c2[0], y - c2[1]
+        v1x, v1y = c1[0] - c2[0], c1[1] - c2[1]
+        v2x, v2y = c3[0] - c2[0], c3[1] - c2[1]
+
+        return (0 <= vx * v1x + vy * v1y <= v1x * v1x + v1y * v1y
+                and 0 <= vx * v2x + vy * v2y <= v2x * v2x + v2y * v2y)
+
+
+    cdef bint check_collision(self, double point[2]):
+        if self.state != STARTED:
+            return False
+
+        return self._check_collision(point, 2.5)
+
+
+    cdef bint check_grazing(self, double point[2]):
+        #TODO: quadruple check!
+        if self.state == STOPPING and self.frame >= self.grazing_extra_duration:
+            return False
+        if self.state == STARTING and self.frame <= self.grazing_delay:
+            return False
+        if self.frame % 12 != 0:
+            return False
+
+        return self._check_collision(point, 96 + 2.5)
+
+
+    def get_bullets_pos(self):
+        #TODO: check
+        length = <double>min(self.end_offset - self.start_offset, self.max_length)
+        offset = self.end_offset - length
+        dx, dy = cos(self.angle), sin(self.angle)
+        while self.start_offset <= offset < self.end_offset:
+            yield (self.base_pos[0] + offset * dx, self.base_pos[1] + offset * dy)
+            offset += 48.
+
+
+    cpdef cancel(self):
+        self.grazing_extra_duration = 0
+        if self.state != STOPPING:
+            self.frame = 0
+            self.state = STOPPING
+
+
+    cpdef update(self):
+        if self.anmrunner is not None and not self.anmrunner.run_frame():
+            self.anmrunner = None
+
+        self.end_offset += self.speed
+
+        length = <double>min(self.end_offset - self.start_offset, self.max_length) # TODO
+        if self.state == STARTING:
+            if self.frame == self.start_duration:
+                self.frame = 0
+                self.state = STARTED
+            else:
+                width = self.width * float(self.frame) / self.start_duration #TODO
+        if self.state == STARTED:
+            width = self.width #TODO
+            if self.frame == self.duration:
+                self.frame = 0
+                self.state = STOPPING
+        if self.state == STOPPING:
+            if self.frame == self.stop_duration:
+                width = 0.
+                self.removed = True
+            else:
+                width = self.width * (1. - float(self.frame) / self.stop_duration) #TODO
+
+        offset = self.end_offset - length / 2.
+        self.x = self.base_pos[0] + offset * cos(self.angle)
+        self.y = self.base_pos[1] + offset * sin(self.angle)
+        self.sprite.visible = (width > 0 and length > 0)
+        self.sprite.width_override = width
+        self.sprite.height_override = length
+
+        self.sprite.update_orientation(pi/2. - self.angle, True)
+        self.sprite.changed = True #TODO
+
+        self.frame += 1
+
+
+cdef class PlayerLaser(Element):
+    def __init__(self, LaserType laser_type, unsigned long sprite_idx_offset,
+                 tuple hitbox, unsigned long damage, double angle,
+                 double offset, unsigned long duration, Element origin):
+        Element.__init__(self)
+
+        self._laser_type = laser_type
+        self.origin = origin
+
+        self.hitbox[:] = [hitbox[0], hitbox[1]]
+
+        self.frame = 0
+        self.duration = duration
+
+        self.sprite_idx_offset = sprite_idx_offset
+        self.angle = angle
+        self.offset = offset
+        self.damage = damage
+
+        self.set_anim()
+
+
+    cdef void set_anim(self, long sprite_idx_offset=-1):
+        if sprite_idx_offset >= 0:
+            self.sprite_idx_offset = sprite_idx_offset
+
+        lt = self._laser_type
+        self.sprite = Sprite()
+        self.anmrunner = ANMRunner(lt.anm, lt.anim_index,
+                                   self.sprite, self.sprite_idx_offset)
+        #self.sprite.blendfunc = 1 #XXX
+
+
+    cdef void cancel(self):
+        self.anmrunner.interrupt(1)
+
+
+    cdef void update(self):
+        if self.anmrunner is not None and not self.anmrunner.run_frame():
+            self.anmrunner = None
+            self.removed = True
+
+        length = self.origin.y
+        if self.frame == self.duration:
+            self.cancel()
+
+        self.sprite.visible = (length > 0)
+        self.sprite.height_override = length
+        self.sprite.changed = True #TODO
+
+        self.x = self.origin.x + self.offset * cos(self.angle)
+        self.y = self.origin.y / 2. + self.offset * sin(self.angle)
+
+        self.frame += 1
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/lasertype.pxd
@@ -0,0 +1,4 @@
+cdef class LaserType:
+    cdef long anim_index, launch_sprite_idx
+    cdef tuple launch_anim_offsets
+    cdef object anm
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/lasertype.py
@@ -0,0 +1,8 @@
+class LaserType(object):
+    def __init__(self, anm, anim_index,
+                 launch_sprite_idx=140,
+                 launch_anim_offsets=(0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 0)):
+        self.anm = anm
+        self.anim_index = anim_index
+        self.launch_sprite_idx = launch_sprite_idx
+        self.launch_anim_offsets = launch_anim_offsets
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/orb.pxd
@@ -0,0 +1,10 @@
+from pytouhou.game.element cimport Element
+from pytouhou.game.sprite cimport Sprite
+from pytouhou.game.player cimport PlayerState
+
+cdef class Orb(Element):
+    cdef public double offset_x, offset_y
+    cdef PlayerState player_state
+    cdef object fire
+
+    cpdef update(self)
--- a/pytouhou/game/orb.py
+++ b/pytouhou/game/orb.py
@@ -12,36 +12,23 @@
 ## GNU General Public License for more details.
 ##
 
-
-from pytouhou.game.sprite import Sprite
 from pytouhou.vm.anmrunner import ANMRunner
 
 
-class Orb(object):
-    __slots__ = ('_sprite', '_anmrunner', 'offset_x', 'offset_y', 'player_state',
-                 'fire')
+class Orb(Element):
+    def __init__(self, anm, index, player_state):
+        Element.__init__(self)
 
-    def __init__(self, anm_wrapper, index, player_state, fire_func):
-        self._sprite = Sprite()
-        self._anmrunner = ANMRunner(anm_wrapper, index, self._sprite)
-        self._anmrunner.run_frame()
+        self.sprite = Sprite()
+        self.anmrunner = ANMRunner(anm, index, self.sprite)
 
         self.offset_x = 0
         self.offset_y = 0
 
         self.player_state = player_state
-        self.fire = fire_func
-
-
-    @property
-    def x(self):
-        return self.player_state.x + self.offset_x
-
-
-    @property
-    def y(self):
-        return self.player_state.y + self.offset_y
 
 
     def update(self):
-        self._anmrunner.run_frame()
+        self.anmrunner.run_frame()
+        self.x = self.player_state.x + self.offset_x
+        self.y = self.player_state.y + self.offset_y
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/player.pxd
@@ -0,0 +1,27 @@
+from pytouhou.game.element cimport Element
+from pytouhou.game.game cimport Game
+
+cdef class PlayerState:
+    cdef public double x, y
+    cdef public bint touchable, focused
+    cdef public long character, score, effective_score, lives, bombs, power
+    cdef public long graze, points
+
+    cdef long invulnerable_time, power_bonus, continues, continues_used, miss,
+    cdef long bombs_used
+
+
+cdef class Player(Element):
+    cdef public PlayerState state
+    cdef public long death_time
+    cdef public Game _game
+
+    cdef object anm
+    cdef tuple speeds
+    cdef long fire_time, bomb_time, direction
+
+    cdef void set_anim(self, index) except *
+    cdef void play_sound(self, str name) except *
+    cdef void collide(self) except *
+    cdef void fire(self) except *
+    cpdef update(self, long keystate)
rename from pytouhou/game/player.py
rename to pytouhou/game/player.pyx
--- a/pytouhou/game/player.py
+++ b/pytouhou/game/player.pyx
@@ -12,22 +12,35 @@
 ## GNU General Public License for more details.
 ##
 
+from libc.math cimport M_PI as pi
 
-from pytouhou.game.sprite import Sprite
+from pytouhou.game.sprite cimport Sprite
 from pytouhou.vm.anmrunner import ANMRunner
-from pytouhou.game.bullettype import BulletType
-
-from math import pi
+from pytouhou.game.bullettype cimport BulletType
+from pytouhou.game.bullet cimport Bullet
+from pytouhou.game.lasertype cimport LaserType
+from pytouhou.game.laser cimport PlayerLaser
 
 
-class PlayerState(object):
-    def __init__(self, character=0, score=0, power=0, lives=0, bombs=0):
+class GameOver(Exception):
+    pass
+
+
+cdef class PlayerState:
+    def __init__(self, long character=0, long score=0, long power=0,
+                 long lives=2, long bombs=3, long continues=0):
         self.character = character # ReimuA/ReimuB/MarisaA/MarisaB/...
 
         self.score = score
+        self.effective_score = score
         self.lives = lives
         self.bombs = bombs
         self.power = power
+        self.continues = continues
+
+        self.continues_used = 0
+        self.miss = 0
+        self.bombs_used = 0
 
         self.graze = 0
         self.points = 0
@@ -42,61 +55,45 @@ class PlayerState(object):
         self.power_bonus = 0 # Never goes over 30.
 
 
-class Player(object):
-    def __init__(self, state, game, anm_wrapper, hitbox_size=2.5, graze_hitbox_size=42., speeds=None):
-        self._sprite = None
-        self._anmrunner = None
-        self._game = game
-        self.anm_wrapper = anm_wrapper
-
-        self.speeds = speeds
+cdef class Player(Element):
+    def __init__(self, PlayerState state, Game game, anm):
+        Element.__init__(self)
 
-        self.hitbox_size = hitbox_size
-        self.hitbox_half_size = self.hitbox_size / 2.
-        self.graze_hitbox_size = graze_hitbox_size
-        self.graze_hitbox_half_size = self.graze_hitbox_size / 2.
+        self._game = game
+        self.anm = anm
 
-        self.bullet_type = BulletType(anm_wrapper, 64, 96, 0, 0, 0, hitbox_size=4)
-        self.bullet_launch_interval = 5
-        self.bullet_speed = 12.
-        self.bullet_launch_angle = -pi/2
+        self.speeds = (self.sht.horizontal_vertical_speed,
+                       self.sht.diagonal_speed,
+                       self.sht.horizontal_vertical_focused_speed,
+                       self.sht.diagonal_focused_speed)
+
         self.fire_time = 0
 
         self.state = state
-        self.direction = None
+        self.direction = 0
 
         self.set_anim(0)
 
         self.death_time = 0
 
 
-    @property
-    def x(self):
-        return self.state.x
+    cdef void set_anim(self, index):
+        self.sprite = Sprite()
+        self.anmrunner = ANMRunner(self.anm, index, self.sprite)
 
 
-    @property
-    def y(self):
-        return self.state.y
-
-
-    def objects(self):
-        return []
+    cdef void play_sound(self, str name):
+        self._game.sfx_player.play('%s.wav' % name)
 
 
-    def set_anim(self, index):
-        self._sprite = Sprite()
-        self._anmrunner = ANMRunner(self.anm_wrapper, index, self._sprite)
-        self._anmrunner.run_frame()
-
-
-    def collide(self):
+    cdef void collide(self):
         if not self.state.invulnerable_time and not self.death_time and self.state.touchable: # Border Between Life and Death
             self.death_time = self._game.frame
-            self._game.new_death((self.state.x, self.state.y), 2)
+            self._game.new_effect((self.state.x, self.state.y), 17)
             self._game.modify_difficulty(-1600)
-            for i in range(16):
-                self._game.new_particle((self.state.x, self.state.y), 2, 4., 256) #TODO: find the real size and range.
+            self.play_sound('pldead00')
+            for i in xrange(16):
+                self._game.new_particle((self.state.x, self.state.y), 11, 256) #TODO: find the real size and range.
 
 
     def start_focusing(self):
@@ -107,15 +104,84 @@ class Player(object):
         self.state.focused = False
 
 
-    def update(self, keystate):
+    cdef void fire(self):
+        cdef double x, y
+        cdef long shot_power
+
+        sht = self.focused_sht if self.state.focused else self.sht
+
+        # Don’t use min() since sht.shots could be an empty dict.
+        power = 999
+        for shot_power in sht.shots:
+            if self.state.power < shot_power:
+                power = power if power < shot_power else shot_power
+
+        bullets = self._game.players_bullets
+        lasers = self._game.players_lasers
+        nb_bullets_max = self._game.nb_bullets_max
+
+        if self.fire_time % 5 == 0:
+            self.play_sound('plst00')
+
+        for shot in sht.shots[power]:
+            origin = self.orbs[shot.orb - 1] if shot.orb else self.state
+            shot_type = <unsigned char>shot.type
+
+            if shot_type == 3:
+                if self.fire_time != 30:
+                    continue
+
+                #TODO: number can do very surprising things, like removing any
+                # bullet creation from enemies with 3. For now, crash when not
+                # an actual laser number.
+                number = <long>shot.delay
+                if lasers[number] is not None:
+                    continue
+
+                laser_type = LaserType(self.anm, shot.sprite % 256, 68)
+                lasers[number] = PlayerLaser(laser_type, 0, shot.hitbox, shot.damage, shot.angle, shot.speed, shot.interval, origin)
+                continue
+
+            if (self.fire_time + shot.delay) % shot.interval != 0:
+                continue
+
+            if nb_bullets_max != 0 and len(bullets) == nb_bullets_max:
+                break
+
+            x = origin.x + shot.pos[0]
+            y = origin.y + shot.pos[1]
+
+            #TODO: find a better way to do that.
+            bullet_type = BulletType(self.anm, shot.sprite % 256,
+                                     shot.sprite % 256 + 32, #TODO: find the real cancel anim
+                                     0, 0, 0, 0.)
+            #TODO: Type 1 (homing bullets)
+            if shot_type == 2:
+                #TODO: triple-check acceleration!
+                bullets.append(Bullet((x, y), bullet_type, 0,
+                                      shot.angle, shot.speed,
+                                      (-1, 0, 0, 0, 0.15, -pi/2., 0., 0.),
+                                      16, self, self._game, player_bullet=True,
+                                      damage=shot.damage, hitbox=shot.hitbox))
+            else:
+                bullets.append(Bullet((x, y), bullet_type, 0,
+                                      shot.angle, shot.speed,
+                                      (0, 0, 0, 0, 0., 0., 0., 0.),
+                                      0, self, self._game, player_bullet=True,
+                                      damage=shot.damage, hitbox=shot.hitbox))
+
+
+    cpdef update(self, long keystate):
+        cdef double dx, dy
+
         if self.death_time == 0 or self._game.frame - self.death_time > 60:
             speed, diag_speed = self.speeds[2:] if self.state.focused else self.speeds[:2]
             try:
-                dx, dy = {16: (0.0, -speed), 32: (0.0, speed), 64: (-speed, 0.0), 128: (speed, 0.0),
+                dx, dy = {16: (0., -speed), 32: (0., speed), 64: (-speed, 0.), 128: (speed, 0.),
                           16|64: (-diag_speed, -diag_speed), 16|128: (diag_speed, -diag_speed),
                           32|64: (-diag_speed, diag_speed), 32|128:  (diag_speed, diag_speed)}[keystate & (16|32|64|128)]
             except KeyError:
-                dx, dy = 0.0, 0.0
+                dx, dy = 0., 0.
 
             if dx < 0 and self.direction != -1:
                 self.set_anim(1)
@@ -123,13 +189,26 @@ class Player(object):
             elif dx > 0 and self.direction != +1:
                 self.set_anim(3)
                 self.direction = +1
-            elif dx == 0 and self.direction is not None:
+            elif dx == 0 and self.direction != 0:
                 self.set_anim({-1: 2, +1: 4}[self.direction])
-                self.direction = None
+                self.direction = 0
 
             self.state.x += dx
             self.state.y += dy
 
+            #XXX
+            self.x = self.state.x
+            self.y = self.state.y
+
+            if self.state.x < 8.:
+                self.state.x = 8.
+            if self.state.x > self._game.width - 8:
+                self.state.x = self._game.width - 8.
+            if self.state.y < 16.:
+                self.state.y = 16.
+            if self.state.y > self._game.height - 16:
+                self.state.y = self._game.height -16.
+
             if not self.state.focused and keystate & 4:
                 self.start_focusing()
             elif self.state.focused and not keystate & 4:
@@ -139,12 +218,12 @@ class Player(object):
                 self.state.invulnerable_time -= 1
 
                 m = self.state.invulnerable_time % 8
-                if m == 0:
-                    self._sprite.color = (255, 255, 255)
-                    self._sprite._changed = True
-                elif m == 2:
-                    self._sprite.color = (64, 64, 64)
-                    self._sprite._changed = True
+                if m == 7 or self.state.invulnerable_time == 0:
+                    self.sprite.color = (255, 255, 255)
+                    self.sprite.changed = True
+                elif m == 1:
+                    self.sprite.color = (64, 64, 64)
+                    self.sprite.changed = True
 
             if keystate & 1 and self.fire_time == 0:
                 self.fire_time = 30
@@ -152,57 +231,89 @@ class Player(object):
                 self.fire()
                 self.fire_time -= 1
 
+            if keystate & 2 and self.bomb_time == 0:
+                self._game.set_player_bomb()
+                self.bomb_time = 240
+            if self.bomb_time > 0:
+                self.bomb_time -= 1
+                if self.bomb_time == 0:
+                    self._game.unset_player_bomb()
+
         if self.death_time:
             time = self._game.frame - self.death_time
             if time == 6: # too late, you are dead :(
                 self.state.touchable = False
-                self.state.lives -= 1
                 if self.state.power > 16:
                     self.state.power -= 16
                 else:
                     self.state.power = 0
+                self._game.cancel_player_lasers()
 
-                self._game.drop_bonus(self.state.x, self.state.y, 2,
-                                      end_pos=(self._game.prng.rand_double() * 288 + 48, # 102h.exe@0x41f3dc
-                                               self._game.prng.rand_double() * 192 - 64))        # @0x41f3
-                for i in range(5):
-                    self._game.drop_bonus(self.state.x, self.state.y, 0,
-                                          end_pos=(self._game.prng.rand_double() * 288 + 48,
-                                                   self._game.prng.rand_double() * 192 - 64))
+                self.state.miss += 1
+                self.state.lives -= 1
+                if self.state.lives < 0:
+                    #TODO: display a menu to ask the players if they want to continue.
+                    if self.state.continues == 0:
+                        raise GameOver
+
+                    # Don’t decrement if it’s infinite.
+                    if self.state.continues >= 0:
+                        self.state.continues -= 1
+                    self.state.continues_used += 1
+
+                    for i in xrange(5):
+                        self._game.drop_bonus(self.state.x, self.state.y, 4,
+                                              end_pos=(self._game.prng.rand_double() * 288 + 48,
+                                                       self._game.prng.rand_double() * 192 - 64))
+                    self.state.score = 0
+                    self.state.effective_score = 0
+                    self.state.lives = 2 #TODO: use the right default.
+                    self.state.bombs = 3 #TODO: use the right default.
+                    self.state.power = 0
+
+                    self.state.graze = 0
+                    self.state.points = 0
+                else:
+                    self._game.drop_bonus(self.state.x, self.state.y, 2,
+                                          end_pos=(self._game.prng.rand_double() * 288 + 48, # 102h.exe@0x41f3dc
+                                                   self._game.prng.rand_double() * 192 - 64))        # @0x41f3
+                    for i in xrange(5):
+                        self._game.drop_bonus(self.state.x, self.state.y, 0,
+                                              end_pos=(self._game.prng.rand_double() * 288 + 48,
+                                                       self._game.prng.rand_double() * 192 - 64))
 
             elif time == 7:
-                self._sprite.mirrored = False
-                self._sprite.blendfunc = 0
-                self._sprite.rescale = 0.75, 1.5
-                self._sprite.fade(26, 96, lambda x: x)
-                self._sprite.scale_in(26, 0.00, 2.5, lambda x: x)
+                self.sprite.mirrored = False
+                self.sprite.blendfunc = 0
+                self.sprite.rescale = 0.75, 1.5
+                self.sprite.fade(26, 96)
+                self.sprite.scale_in(26, 0., 2.5)
+
+            #TODO: the next two branches could be happening at the same frame.
+            elif time == 31:
+                self._game.cancel_bullets()
 
             elif time == 32:
                 self.state.x = float(self._game.width) / 2. #TODO
                 self.state.y = float(self._game.width) #TODO
-                self.direction = None
+                self.direction = 0
 
-                self._sprite = Sprite()
-                self._anmrunner = ANMRunner(self.anm_wrapper, 0, self._sprite)
-                self._sprite.alpha = 128
-                self._sprite.rescale = 0.0, 2.5
-                self._sprite.fade(30, 255, lambda x: x)
-                self._sprite.blendfunc = 1
-                self._sprite.scale_in(30, 1., 1., lambda x: x)
-                self._anmrunner.run_frame()
+                self.sprite = Sprite()
+                self.anmrunner = ANMRunner(self.anm, 0, self.sprite)
+                self.sprite.alpha = 128
+                self.sprite.rescale = 0., 2.5
+                self.sprite.fade(30, 255)
+                self.sprite.blendfunc = 1
+                self.sprite.scale_in(30, 1., 1.)
 
             elif time == 61: # respawned
                 self.state.touchable = True
                 self.state.invulnerable_time = 240
-                self._sprite.blendfunc = 0
-                self._sprite._changed = True
+                self.sprite.blendfunc = 0
+                self.sprite.changed = True
 
-            if time > 30:
-                for bullet in self._game.bullets:
-                    bullet.cancel()
-
-            if time > 90: # start the bullet hell again
+            elif time == 91: # start the bullet hell again
                 self.death_time = 0
 
-        self._anmrunner.run_frame()
+        self.anmrunner.run_frame()
 
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/sprite.pxd
@@ -0,0 +1,24 @@
+from pytouhou.utils.interpolator cimport Interpolator
+
+cdef class Sprite:
+    cdef public long blendfunc, frame
+    cdef public double width_override, height_override, angle
+    cdef public bint removed, changed, visible, force_rotation
+    cdef public bint automatic_orientation, allow_dest_offset, mirrored
+    cdef public bint corner_relative_placement
+    cdef public Interpolator scale_interpolator, fade_interpolator
+    cdef public Interpolator offset_interpolator, rotation_interpolator
+    cdef public Interpolator color_interpolator
+    cdef public tuple texcoords, dest_offset, texoffsets, rescale, scale_speed
+    cdef public tuple rotations_3d, rotations_speed_3d, color
+    cdef public unsigned char alpha
+    cdef public object anm, _rendering_data
+
+    cpdef fade(self, unsigned long duration, alpha, formula=*)
+    cpdef scale_in(self, unsigned long duration, sx, sy, formula=*)
+    cpdef move_in(self, unsigned long duration, x, y, z, formula=*)
+    cpdef rotate_in(self, unsigned long duration, rx, ry, rz, formula=*)
+    cpdef change_color_in(self, unsigned long duration, r, g, b, formula=*)
+    cpdef update_orientation(self, double angle_base=*, bint force_rotation=*)
+    cpdef Sprite copy(self)
+    cpdef update(self)
--- a/pytouhou/game/sprite.py
+++ b/pytouhou/game/sprite.py
@@ -12,22 +12,12 @@
 ## GNU General Public License for more details.
 ##
 
-
-from pytouhou.utils.interpolator import Interpolator
-
-
 class Sprite(object):
-    __slots__ = ('anm', '_removed', '_changed', 'width_override', 'height_override',
-                 'angle', 'force_rotation', 'scale_interpolator', 'fade_interpolator',
-                 'offset_interpolator', 'automatic_orientation', 'blendfunc',
-                 'texcoords', 'dest_offset', 'allow_dest_offset', 'texoffsets',
-                 'mirrored', 'rescale', 'scale_speed', 'rotations_3d',
-                 'rotations_speed_3d', 'corner_relative_placement', 'frame',
-                 'color', 'alpha', '_rendering_data')
     def __init__(self, width_override=0, height_override=0):
         self.anm = None
-        self._removed = False
-        self._changed = True
+        self.removed = False
+        self.changed = True
+        self.visible = True
 
         self.width_override = width_override
         self.height_override = height_override
@@ -37,6 +27,8 @@ class Sprite(object):
         self.scale_interpolator = None
         self.fade_interpolator = None
         self.offset_interpolator = None
+        self.rotation_interpolator = None
+        self.color_interpolator = None
 
         self.automatic_orientation = False
 
@@ -59,30 +51,116 @@ class Sprite(object):
         self._rendering_data = None
 
 
-    def fade(self, duration, alpha, formula):
-        if not self.fade_interpolator:
-            self.fade_interpolator = Interpolator((self.alpha,), self.frame,
-                                                  (alpha,), self.frame + duration,
+    def fade(self, duration, alpha, formula=None):
+        self.fade_interpolator = Interpolator((self.alpha,), self.frame,
+                                              (alpha,), self.frame + duration,
+                                              formula)
+
+
+    def scale_in(self, duration, sx, sy, formula=None):
+        self.scale_interpolator = Interpolator(self.rescale, self.frame,
+                                               (sx, sy), self.frame + duration,
+                                               formula)
+
+
+    def move_in(self, duration, x, y, z, formula=None):
+        self.offset_interpolator = Interpolator(self.dest_offset, self.frame,
+                                                (x, y, z), self.frame + duration,
+                                                formula)
+
+
+    def rotate_in(self, duration, rx, ry, rz, formula=None):
+        self.rotation_interpolator = Interpolator(self.rotations_3d, self.frame,
+                                                  (rx, ry, rz), self.frame + duration,
                                                   formula)
 
 
-    def scale_in(self, duration, sx, sy, formula):
-        if not self.scale_interpolator:
-            self.scale_interpolator = Interpolator(self.rescale, self.frame,
-                                                   (sx, sy), self.frame + duration,
-                                                   formula)
-
-
-    def move_in(self, duration, x, y, z, formula):
-        if not self.offset_interpolator:
-            self.offset_interpolator = Interpolator(self.dest_offset, self.frame,
-                                                    (x, y, z), self.frame + duration,
-                                                    formula)
+    def change_color_in(self, duration, r, g, b, formula=None):
+        self.color_interpolator = Interpolator(self.color, self.frame,
+                                               (r, g, b), self.frame + duration,
+                                               formula)
 
 
     def update_orientation(self, angle_base=0., force_rotation=False):
         if (self.angle != angle_base or self.force_rotation != force_rotation):
             self.angle = angle_base
             self.force_rotation = force_rotation
-            self._changed = True
+            self.changed = True
+
+
+    def copy(self):
+        sprite = Sprite(self.width_override, self.height_override)
+
+        sprite.blendfunc = self.blendfunc
+        sprite.frame = self.frame
+        sprite.angle = self.angle
+
+        sprite.removed = self.removed
+        sprite.changed = self.changed
+        sprite.visible = self.visible
+        sprite.force_rotation = self.force_rotation
+        sprite.automatic_orientation = self.automatic_orientation
+        sprite.allow_dest_offset = self.allow_dest_offset
+        sprite.mirrored = self.mirrored
+        sprite.corner_relative_placement = self.corner_relative_placement
+
+        sprite.scale_interpolator = self.scale_interpolator
+        sprite.fade_interpolator = self.fade_interpolator
+        sprite.offset_interpolator = self.offset_interpolator
+        sprite.rotation_interpolator = self.rotation_interpolator
+        sprite.color_interpolator = self.color_interpolator
+
+        sprite.texcoords = self.texcoords
+        sprite.dest_offset = self.dest_offset
+        sprite.texoffsets = self.texoffsets
+        sprite.rescale = self.rescale
+        sprite.scale_speed = self.scale_speed
+        sprite.rotations_3d = self.rotations_3d
+        sprite.rotations_speed_3d = self.rotations_speed_3d
+        sprite.color = self.color
+
+        sprite.alpha = self.alpha
+        sprite.anm = self.anm
+        sprite._rendering_data = self._rendering_data
+
+        return sprite
 
+
+    def update(self):
+        self.frame += 1
+
+        if self.rotations_speed_3d != (0., 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.changed = True
+        elif self.rotation_interpolator:
+            self.rotation_interpolator.update(self.frame)
+            self.rotations_3d = self.rotation_interpolator.values
+            self.changed = True
+
+        if self.scale_speed != (0., 0.):
+            rx, ry = self.rescale
+            rsx, rsy = self.scale_speed
+            self.rescale = rx + rsx, ry + rsy
+            self.changed = True
+
+        if self.fade_interpolator:
+            self.fade_interpolator.update(self.frame)
+            self.alpha = self.fade_interpolator.values[0]
+            self.changed = True
+
+        if self.scale_interpolator:
+            self.scale_interpolator.update(self.frame)
+            self.rescale = self.scale_interpolator.values
+            self.changed = True
+
+        if self.offset_interpolator:
+            self.offset_interpolator.update(self.frame)
+            self.dest_offset = self.offset_interpolator.values
+            self.changed = True
+
+        if self.color_interpolator:
+            self.color_interpolator.update(self.frame)
+            self.color = self.color_interpolator.values
+            self.changed = True
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/text.pxd
@@ -0,0 +1,81 @@
+from pytouhou.game.element cimport Element
+from pytouhou.game.sprite cimport Sprite
+from pytouhou.utils.interpolator cimport Interpolator
+
+cdef class Glyph(Element):
+    pass
+
+
+cdef class Widget(Element):
+    cdef public object update
+    cdef public bint changed
+
+    cdef unsigned long frame
+    cdef object back_anm
+
+    #def update(self)
+
+
+cdef class GlyphCollection(Widget):
+    cdef Sprite ref_sprite
+    cdef object anm
+    cdef list glyphes
+    cdef long xspacing
+
+    cpdef set_length(self, unsigned long length)
+    cpdef set_sprites(self, list sprite_indexes)
+    cpdef set_color(self, text=*, color=*)
+    cpdef set_alpha(self, unsigned char alpha)
+
+
+cdef class Text(GlyphCollection):
+    cdef bytes text
+    cdef long shift, timeout, duration, start
+    cdef Interpolator fade_interpolator
+    cdef unsigned char alpha
+
+    cpdef set_text(self, bytes text)
+    #def timeout_update(self)
+    #def move_timeout_update(self)
+    #def fadeout_timeout_update(self)
+    cdef void fade(self, unsigned long duration, unsigned char alpha, formula=*) except *
+    cpdef set_timeout(self, long timeout, str effect=*, long duration=*, long start=*)
+
+
+cdef class Counter(GlyphCollection):
+    cdef long value
+
+    cpdef set_value(self, long value)
+
+
+cdef class Gauge(Element):
+    cdef public long value, max_length, maximum
+
+    cpdef set_value(self, long value)
+    cpdef update(self)
+
+
+cdef class NativeText(Element):
+    cdef public object update
+
+    cdef unicode text
+    cdef long width, height
+    cdef unsigned char alpha
+    cdef bint shadow
+    cdef bytes align #TODO: use a proper enum.
+    cdef unsigned long frame, timeout, duration
+    cdef long start
+    cdef double to[2], end[2]
+    cdef list gradient
+    cdef Interpolator fade_interpolator, offset_interpolator
+    cdef object texture
+
+    #def normal_update(self)
+    #def timeout_update(self)
+    #def move_timeout_update(self)
+    #def move_ex_timeout_update(self)
+    #def fadeout_timeout_update(self)
+
+    cdef void fade(self, unsigned long duration, unsigned char alpha, formula=*) except *
+    cdef void move_in(self, unsigned long duration, double x, double y, formula=*) except *
+    cpdef set_timeout(self, long timeout, str effect=*, long duration=*, long start=*, to=*, end=*)
new file mode 100644
--- /dev/null
+++ b/pytouhou/game/text.py
@@ -0,0 +1,315 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2011 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.element import Element
+from pytouhou.game.sprite import Sprite
+from pytouhou.vm.anmrunner import ANMRunner
+from pytouhou.utils.interpolator import Interpolator
+
+
+class Glyph(Element):
+    def __init__(self, sprite, pos):
+        Element.__init__(self, pos)
+        self.sprite = sprite
+
+
+class Widget(Element):
+    def __init__(self, pos, back_anm=None, back_script=22):
+        Element.__init__(self, pos)
+        self.changed = True
+        self.frame = 0
+
+        # Set up the backround sprite
+        self.back_anm = back_anm
+        if back_anm:
+            self.sprite = Sprite()
+            self.anmrunner = ANMRunner(back_anm, back_script, self.sprite)
+
+    def normal_update(self):
+        if self.changed:
+            if self.anmrunner is not None and not self.anmrunner.run_frame():
+                self.anmrunner = None
+            self.changed = False
+        self.frame += 1
+
+
+
+class GlyphCollection(Widget):
+    def __init__(self, pos, anm, back_anm=None, ref_script=0,
+                 xspacing=14, back_script=22):
+        Widget.__init__(self, pos, back_anm, back_script)
+
+        self.ref_sprite = Sprite()
+        self.anm = anm
+        self.glyphes = []
+        self.xspacing = xspacing
+
+        # Set up ref sprite
+        ANMRunner(anm, ref_script, self.ref_sprite)
+        self.ref_sprite.corner_relative_placement = True #TODO: perhaps not right
+
+
+    def set_length(self, length):
+        current_length = len(self.glyphes)
+        if length > current_length:
+            self.glyphes.extend([Glyph(self.ref_sprite.copy(),
+                                       (self.x + self.xspacing * i, self.y))
+                                 for i in range(current_length, length)])
+            self.objects = [self] + self.glyphes
+        elif length < current_length:
+            self.glyphes[:] = self.glyphes[:length]
+            self.objects = [self] + self.glyphes
+
+
+    def set_sprites(self, sprite_indexes):
+        self.set_length(len(sprite_indexes))
+        for glyph, idx in zip(self.glyphes, sprite_indexes):
+            glyph.sprite.anm = self.anm
+            glyph.sprite.texcoords = self.anm.sprites[idx]
+            glyph.sprite.changed = True
+
+
+    def set_color(self, text=None, color=None):
+        if text is not None:
+            colors = {'white': (255, 255, 255), 'yellow': (255, 255, 0),
+                      'blue': (192, 192, 255), 'darkblue': (160, 128, 255),
+                      'purple': (224, 128, 255), 'red': (255, 64, 0)}
+            color = colors[text]
+        else:
+            assert color is not None
+        self.ref_sprite.color = color
+        for glyph in self.glyphes:
+            glyph.sprite.color = color
+
+
+    def set_alpha(self, alpha):
+        self.ref_sprite.alpha = alpha
+        for glyph in self.glyphes:
+            glyph.sprite.alpha = alpha
+
+
+
+class Text(GlyphCollection):
+    def __init__(self, pos, ascii_anm, back_anm=None, text='',
+                 xspacing=14, shift=21, back_script=22, align='left'):
+        GlyphCollection.__init__(self, pos, ascii_anm, back_anm,
+                                 xspacing=xspacing, back_script=back_script)
+        self.text = b''
+        self.shift = shift
+
+        if align == 'center':
+            self.x -= xspacing * len(text) // 2
+        elif align == 'right':
+            self.x -= xspacing * len(text)
+        else:
+            assert align == 'left'
+
+        self.set_text(text)
+
+
+    def set_text(self, text):
+        if text == self.text:
+            return
+
+        self.set_sprites([ord(c) - self.shift for c in text])
+        self.text = text
+        self.changed = True
+
+
+    def timeout_update(self):
+        GlyphCollection.normal_update(self)
+        if self.frame == self.timeout:
+            self.removed = True
+
+
+    def move_timeout_update(self):
+        if self.frame % 2:
+            for glyph in self.glyphes:
+                glyph.y -= 1
+        self.timeout_update()
+
+
+    def fadeout_timeout_update(self):
+        if self.frame >= self.start:
+            if self.frame == self.start:
+                self.fade(self.duration, 255)
+            elif self.frame == self.timeout - self.duration:
+                self.fade(self.duration, 0)
+            self.fade_interpolator.update(self.frame)
+            self.alpha = int(self.fade_interpolator.values[0])
+            for glyph in self.glyphes:
+                glyph.sprite.alpha = self.alpha
+                glyph.sprite.changed = True
+        self.timeout_update()
+
+
+    def fade(self, duration, alpha, formula=None):
+        self.fade_interpolator = Interpolator((self.alpha,), self.frame,
+                                              (alpha,), self.frame + duration,
+                                              formula)
+
+
+    def set_timeout(self, timeout, effect=None, duration=0, start=0):
+        self.timeout = timeout + start
+        if effect == 'move':
+            self.update = self.move_timeout_update
+        elif effect == 'fadeout':
+            self.alpha = 0
+            for glyph in self.glyphes:
+                glyph.sprite.alpha = 0
+            self.update = self.fadeout_timeout_update
+            self.duration = duration
+            self.start = start
+        else:
+            self.update = self.timeout_update
+
+
+
+class Counter(GlyphCollection):
+    def __init__(self, pos, anm, back_anm=None, script=0,
+                 xspacing=16, value=0, back_script=22):
+        GlyphCollection.__init__(self, pos, anm,
+                                 back_anm=back_anm, ref_script=script,
+                                 xspacing=xspacing, back_script=back_script)
+
+        self.value = value
+        self.set_value(value)
+
+
+    def set_value(self, value):
+        if value < 0:
+            value = 0
+        if value == self.value:
+            return
+
+        self.set_length(value)
+        self.value = value
+        self.changed = True
+
+
+
+class Gauge(Element):
+    def __init__(self, pos, anm, max_length=280, maximum=1, value=0):
+        Element.__init__(self, pos)
+        self.sprite = Sprite()
+        self.anmrunner = ANMRunner(anm, 21, self.sprite)
+        self.sprite.corner_relative_placement = True #TODO: perhaps not right
+
+        self.max_length = max_length
+        self.maximum = maximum
+
+        self.set_value(value)
+
+
+    def set_value(self, value):
+        self.value = value
+        self.sprite.width_override = self.max_length * value / self.maximum
+        self.sprite.changed = True #TODO
+
+
+    def update(self):
+        #XXX
+        if self.value == 0:
+            self.sprite.visible = False
+        else:
+            self.sprite.visible = True
+        if self.anmrunner is not None and not self.anmrunner.run_frame():
+            self.anmrunner = None
+
+
+
+class NativeText(Element):
+    def __init__(self, pos, text, gradient=None, alpha=255, shadow=False, align='left'):
+        self.removed = False
+        self.x, self.y = pos
+        self.text = text
+        self.alpha = alpha
+        self.shadow = shadow
+        self.align = align
+        self.frame = 0
+
+        self.gradient = gradient or [(255, 255, 255), (255, 255, 255),
+                                     (128, 128, 255), (128, 128, 255)]
+
+        self.update = self.normal_update
+
+
+    def normal_update(self):
+        self.frame += 1
+
+
+    def timeout_update(self):
+        self.normal_update()
+        if self.frame == self.timeout:
+            self.removed = True
+
+
+    def move_timeout_update(self):
+        if self.frame % 2:
+            self.y -= 1
+        self.timeout_update()
+
+
+    def move_ex_timeout_update(self):
+        if self.frame >= self.start:
+            if self.frame == self.start:
+                self.move_in(self.duration, self.to[0], self.to[1])
+            elif self.frame == self.timeout - self.duration:
+                self.move_in(self.duration, self.end[0], self.end[1])
+            if self.offset_interpolator:
+                self.offset_interpolator.update(self.frame)
+                self.x, self.y = self.offset_interpolator.values
+        self.timeout_update()
+
+
+    def fadeout_timeout_update(self):
+        if self.frame >= self.start:
+            if self.frame == self.start:
+                self.fade(self.duration, 255)
+            elif self.frame == self.timeout - self.duration:
+                self.fade(self.duration, 0)
+            self.fade_interpolator.update(self.frame)
+            self.alpha = int(self.fade_interpolator.values[0])
+        self.timeout_update()
+
+
+    def fade(self, duration, alpha, formula=None):
+        self.fade_interpolator = Interpolator((self.alpha,), self.frame,
+                                              (alpha,), self.frame + duration,
+                                              formula)
+
+
+    def move_in(self, duration, x, y, formula=None):
+        self.offset_interpolator = Interpolator((self.x, self.y), self.frame,
+                                                (x, y), self.frame + duration,
+                                                formula)
+
+
+    def set_timeout(self, timeout, effect=None, duration=0, start=0, to=None, end=None):
+        self.timeout = timeout + start
+        if effect == 'move':
+            self.update = self.move_timeout_update
+        elif effect == 'move_ex':
+            self.update = self.move_ex_timeout_update
+            self.duration = duration
+            self.start = start
+            self.to[:] = [to[0], to[1]]
+            self.end[:] = [end[0], end[1]]
+        elif effect == 'fadeout':
+            self.alpha = 0
+            self.update = self.fadeout_timeout_update
+            self.duration = duration
+            self.start = start
+        else:
+            self.update = self.timeout_update
--- a/pytouhou/games/eosd.py
+++ b/pytouhou/games/eosd.py
@@ -16,56 +16,267 @@ from pytouhou.utils.interpolator import 
 
 from pytouhou.game.game import Game
 from pytouhou.game.bullettype import BulletType
+from pytouhou.game.lasertype import LaserType
 from pytouhou.game.itemtype import ItemType
 from pytouhou.game.player import Player
-from pytouhou.game.bullet import Bullet
 from pytouhou.game.orb import Orb
+from pytouhou.game.effect import Effect
+from pytouhou.game.text import Text, Counter, Gauge, NativeText
+from pytouhou.game.background import Background
 
-from math import pi
+from pytouhou.vm.eclrunner import ECLMainRunner
 
 
-SQ2 = 2. ** 0.5 / 2.
+class EoSDCommon(object):
+    def __init__(self, resource_loader):
+        self.etama = resource_loader.get_multi_anm(('etama3.anm', 'etama4.anm'))
+        self.bullet_types = [BulletType(self.etama[0], 0, 11, 14, 15, 16, hitbox_size=2,
+                                        type_id=0),
+                             BulletType(self.etama[0], 1, 12, 17, 18, 19, hitbox_size=3,
+                                        type_id=1),
+                             BulletType(self.etama[0], 2, 12, 17, 18, 19, hitbox_size=2,
+                                        type_id=2),
+                             BulletType(self.etama[0], 3, 12, 17, 18, 19, hitbox_size=3,
+                                        type_id=3),
+                             BulletType(self.etama[0], 4, 12, 17, 18, 19, hitbox_size=2.5,
+                                        type_id=4),
+                             BulletType(self.etama[0], 5, 12, 17, 18, 19, hitbox_size=2,
+                                        type_id=5),
+                             BulletType(self.etama[0], 6, 13, 20, 20, 20, hitbox_size=8,
+                                        launch_anim_offsets=(0, 1, 1, 2, 2, 3, 4, 0),
+                                        type_id=6),
+                             BulletType(self.etama[0], 7, 13, 20, 20, 20, hitbox_size=5.5,
+                                        launch_anim_offsets=(1, 1, 1, 1),
+                                        type_id=7),
+                             BulletType(self.etama[0], 8, 13, 20, 20, 20, hitbox_size=4.5,
+                                        launch_anim_offsets=(0, 1, 1, 2, 2, 3, 4, 0),
+                                        type_id=8),
+                             BulletType(self.etama[1], 0, 1, 2, 2, 2, hitbox_size=16,
+                                        launch_anim_offsets=(0, 1, 2, 3),
+                                        type_id=9)]
+
+        self.laser_types = [LaserType(self.etama[0], 9),
+                            LaserType(self.etama[0], 10)]
+
+        self.item_types = [ItemType(self.etama[0], 0, 7), #Power
+                           ItemType(self.etama[0], 1, 8), #Point
+                           ItemType(self.etama[0], 2, 9), #Big power
+                           ItemType(self.etama[0], 3, 10), #Bomb
+                           ItemType(self.etama[0], 4, 11), #Full power
+                           ItemType(self.etama[0], 5, 12), #1up
+                           ItemType(self.etama[0], 6, 13)] #Star
+
+        self.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.characters = resource_loader.get_eosd_characters()
+        self.interface = EoSDInterface(resource_loader)
+
 
 
 class EoSDGame(Game):
-    def __init__(self, resource_loader, player_states, stage, rank, difficulty, **kwargs):
-        etama3 = resource_loader.get_anm_wrapper(('etama3.anm',))
-        etama4 = resource_loader.get_anm_wrapper(('etama4.anm',))
-        bullet_types = [BulletType(etama3, 0, 11, 14, 15, 16, hitbox_size=4),
-                        BulletType(etama3, 1, 12, 17, 18, 19, hitbox_size=6),
-                        BulletType(etama3, 2, 12, 17, 18, 19, hitbox_size=4),
-                        BulletType(etama3, 3, 12, 17, 18, 19, hitbox_size=6),
-                        BulletType(etama3, 4, 12, 17, 18, 19, hitbox_size=5),
-                        BulletType(etama3, 5, 12, 17, 18, 19, hitbox_size=4),
-                        BulletType(etama3, 6, 13, 20, 20, 20, hitbox_size=16),
-                        BulletType(etama3, 7, 13, 20, 20, 20, hitbox_size=11),
-                        BulletType(etama3, 8, 13, 20, 20, 20, hitbox_size=9),
-                        BulletType(etama4, 0, 1, 2, 2, 2, hitbox_size=32)]
-        #TODO: hitbox
-        item_types = [ItemType(etama3, 0, 7), #Power
-                      ItemType(etama3, 1, 8), #Point
-                      ItemType(etama3, 2, 9), #Big power
-                      ItemType(etama3, 3, 10), #Bomb
-                      ItemType(etama3, 4, 11), #Full power
-                      ItemType(etama3, 5, 12), #1up
-                      ItemType(etama3, 6, 13)] #Star
+    def __init__(self, resource_loader, player_states, stage, rank, difficulty,
+                 common, nb_bullets_max=640, width=384, height=448, prng=None,
+                 hints=None):
+
+        self.etama = common.etama #XXX
+        try:
+            self.enm_anm = resource_loader.get_multi_anm(('stg%denm.anm' % stage,
+                                                          'stg%denm2.anm' % stage))
+        except KeyError:
+            self.enm_anm = resource_loader.get_anm('stg%denm.anm' % stage)
+        ecl = resource_loader.get_ecl('ecldata%d.ecl' % stage)
+        self.ecl_runners = [ECLMainRunner(main, ecl.subs, self) for main in ecl.mains]
+
+        self.spellcard_effect_anm = resource_loader.get_single_anm('eff0%d.anm' % stage)
+
+        player_face = player_states[0].character // 2
+        self.msg = resource_loader.get_msg('msg%d.dat' % stage)
+        msg_anm = [resource_loader.get_multi_anm(('face0%da.anm' % player_face,
+                                                  'face0%db.anm' % player_face,
+                                                  'face0%dc.anm' % player_face)),
+                   resource_loader.get_multi_anm(common.enemy_face[stage - 1])]
+
+        self.msg_anm = [[], []]
+        for i, anms in enumerate(msg_anm):
+            for anm in anms:
+                for sprite in anm.sprites.values():
+                    self.msg_anm[i].append((anm, sprite))
+
+        players = [EoSDPlayer(state, self, resource_loader, common.characters[state.character]) for state in player_states]
+
+        # Load stage data
+        self.std = resource_loader.get_stage('stage%d.std' % stage)
+
+        background_anm = resource_loader.get_single_anm('stg%dbg.anm' % stage)
+        self.background = Background(self.std, background_anm)
+
+        common.interface.start_stage(self, stage)
+        self.native_texts = [common.interface.stage_name, common.interface.song_name]
+
+        self.resource_loader = resource_loader #XXX: currently used for texture preload in pytouhou.ui.gamerunner. Wipe it!
+
+        Game.__init__(self, players, stage, rank, difficulty,
+                      common.bullet_types, common.laser_types,
+                      common.item_types, nb_bullets_max, width, height, prng,
+                      common.interface, hints)
+
+
+
+class EoSDInterface(object):
+    def __init__(self, resource_loader):
+        self.game = None
+        front = resource_loader.get_single_anm('front.anm')
+        self.ascii_anm = resource_loader.get_single_anm('ascii.anm')
+
+        self.width = 640
+        self.height = 480
+        self.game_pos = (32, 16)
+
+        self.highscore = 1000000 #TODO: read score.dat
+        self.items = ([Effect((0, 32 * i), 6, front) for i in range(15)] +
+                      [Effect((416 + 32 * i, 32 * j), 6, front) for i in range(7) for j in range(15)] +
+                      [Effect((32 + 32 * i, 0), 7, front) for i in range(12)] +
+                      [Effect((32 + 32 * i, 464), 8, front) for i in range(12)] +
+                      [Effect((0, 0), 5, front)] +
+                      [Effect((0, 0), i, front) for i in range(5) + range(9, 16)])
+        for item in self.items:
+            item.sprite.allow_dest_offset = True #XXX
+
+        self.level_start = []
+
+        self.labels = {
+            'highscore': Text((500, 58), self.ascii_anm, front, text='0'),
+            'score': Text((500, 82), self.ascii_anm, front, text='0'),
+            'player': Counter((500, 122), front, front, script=16, value=0),
+            'bombs': Counter((500, 146), front, front, script=17, value=0),
+            'power': Text((500, 186), self.ascii_anm, front, text='0'),
+            'graze': Text((500, 206), self.ascii_anm, front, text='0'),
+            'points': Text((500, 226), self.ascii_anm, front, text='0'),
+            'framerate': Text((512, 464), self.ascii_anm, front),
+            'debug?': Text((0, 464), self.ascii_anm, front),
+
+            # Only when there is a boss.
+            'boss_lives': Text((80, 16), self.ascii_anm),
+            'timeout': Text((384, 16), self.ascii_anm),
+        }
+        self.labels['boss_lives'].set_color('yellow')
 
-        eosd_characters = [ReimuA, ReimuB, MarisaA, MarisaB]
-        players = []
-        for player in player_states:
-            players.append(eosd_characters[player.character](player, self, resource_loader))
+        self.boss_items = [
+            Effect((0, 0), 19, front), # Enemy
+            Gauge((100, 24), front), # Gauge
+            Gauge((100, 24), front), # Spellcard gauge
+        ]
+        for item in self.boss_items:
+            item.sprite.allow_dest_offset = True #XXX
+
+
+    def start_stage(self, game, stage):
+        self.game = game
+        if stage < 6:
+            text = 'STAGE %d' % stage
+        elif stage == 6:
+            text = 'FINAL STAGE'
+        elif stage == 7:
+            text = 'EXTRA STAGE'
+
+        self.stage_name = NativeText((192, 200), unicode(game.std.name), shadow=True, align='center')
+        self.stage_name.set_timeout(240, effect='fadeout', duration=60, start=120)
+
+        self.set_song_name(game.std.bgms[0][0])
+
+        self.level_start = [Text((16+384/2, 200), self.ascii_anm, text=text, align='center')] #TODO: find the exact location.
+        self.level_start[0].set_timeout(240, effect='fadeout', duration=60, start=120)
+        self.level_start[0].set_color('yellow')
+
+
+    def set_song_name(self, name):
+        #TODO: use the correct animation.
+        self.song_name = NativeText((384, 432), u'♪ ' + name, shadow=True, align='right')
+        self.song_name.set_timeout(240, effect='fadeout', duration=60, start=120)
+
+
+    def set_boss_life(self):
+        if not self.game.boss:
+            return
+        self.boss_items[1].maximum = self.game.boss._enemy.life or 1
+        self.boss_items[2].maximum = self.game.boss._enemy.life or 1
+
+
+    def set_spell_life(self):
+        self.boss_items[2].set_value(self.game.boss._enemy.low_life_trigger if self.game.boss else 0)
+
+
+    def update(self):
+        for elem in self.items:
+            elem.update()
 
-        Game.__init__(self, resource_loader, players, stage, rank, difficulty,
-                      bullet_types, item_types, nb_bullets_max=640, **kwargs)
+        for elem in self.level_start:
+            elem.update()
+            if elem.removed: #XXX
+                self.level_start = []
+
+        player_state = self.game.players[0].state
+
+        self.highscore = max(self.highscore, player_state.effective_score)
+        self.labels['highscore'].set_text('%09d' % self.highscore)
+        self.labels['score'].set_text('%09d' % player_state.effective_score)
+        self.labels['power'].set_text('%d' % player_state.power)
+        self.labels['graze'].set_text('%d' % player_state.graze)
+        self.labels['points'].set_text('%d' % player_state.points)
+        self.labels['player'].set_value(player_state.lives)
+        self.labels['bombs'].set_value(player_state.bombs)
+
+        if self.game.boss:
+            boss = self.game.boss._enemy
+
+            life_gauge = self.boss_items[1]
+            life_gauge.set_value(boss.life)
+
+            spell_gauge = self.boss_items[2]
+            spell_gauge.sprite.color = (255, 192, 192)
+            if boss.life < spell_gauge.value:
+                spell_gauge.set_value(boss.life)
+
+            for item in self.boss_items:
+                item.update()
+
+            self.labels['boss_lives'].set_text('%d' % boss.remaining_lives)
+            self.labels['boss_lives'].changed = True
+
+            timeout = min((boss.timeout - boss.frame) // 60, 99)
+            timeout_label = self.labels['timeout']
+            if timeout >= 20:
+                timeout_label.set_color('blue')
+            elif timeout >= 10:
+                timeout_label.set_color('darkblue')
+            else:
+                if timeout >= 5:
+                    timeout_label.set_color('purple')
+                else:
+                    timeout_label.set_color('red')
+                if (boss.timeout - boss.frame) % 60 == 0 and boss.timeout != 0:
+                    self.game.sfx_player.play('timeout.wav', volume=1.)
+            timeout_label.set_text('%02d' % (timeout if timeout >= 0 else 0))
+            timeout_label.changed = True
 
 
 
 class EoSDPlayer(Player):
-    def __init__(self, state, game, anm_wrapper, speeds=None, hitbox_size=2.5, graze_hitbox_size=42.):
-        Player.__init__(self, state, game, anm_wrapper, speeds=speeds)
+    def __init__(self, state, game, resource_loader, character):
+        self.sht = character[0]
+        self.focused_sht = character[1]
+        self.anm = resource_loader.get_single_anm('player0%d.anm' % (state.character // 2))
 
-        self.orbs = [Orb(self.anm_wrapper, 128, self.state, self.orb_fire),
-                     Orb(self.anm_wrapper, 129, self.state, self.orb_fire)]
+        Player.__init__(self, state, game, self.anm)
+
+        self.orbs = [Orb(self.anm, 128, self.state),
+                     Orb(self.anm, 129, self.state)]
 
         self.orbs[0].offset_x = -24
         self.orbs[1].offset_x = 24
@@ -92,8 +303,9 @@ class EoSDPlayer(Player):
         self.state.focused = False
 
 
+    @property
     def objects(self):
-        return self.orbs if self.state.power >= 8 else []
+        return [self] + (self.orbs if self.state.power >= 8 else [])
 
 
     def update(self, keystate):
@@ -113,212 +325,3 @@ class EoSDPlayer(Player):
 
         for orb in self.orbs:
             orb.update()
-
-
-    def orb_fire(self, orb):
-        pass
-
-
-
-class Reimu(EoSDPlayer):
-    def __init__(self, state, game, resource_loader):
-        anm_wrapper = resource_loader.get_anm_wrapper(('player00.anm',))
-        self.bullet_angle = pi/30 #TODO: check
-
-        EoSDPlayer.__init__(self, state, game, anm_wrapper, speeds=(4., 4. * SQ2, 2., 2. * SQ2))
-
-
-    def fire(self):
-        if self.fire_time % self.bullet_launch_interval == 0:
-            if self.state.power < 16:
-                bullets_per_shot = 1
-            elif self.state.power < 48:
-                bullets_per_shot = 2
-            elif self.state.power < 96:
-                bullets_per_shot = 3
-            elif self.state.power < 128:
-                bullets_per_shot = 4
-            else:
-                bullets_per_shot = 5
-
-            bullets = self._game.players_bullets
-            nb_bullets_max = self._game.nb_bullets_max
-
-            bullet_angle = self.bullet_launch_angle - self.bullet_angle * (bullets_per_shot - 1) / 2.
-            for bullet_nb in range(bullets_per_shot):
-                if nb_bullets_max is not None and len(bullets) == nb_bullets_max:
-                    break
-
-                bullets.append(Bullet((self.x, self.y), self.bullet_type, 0,
-                                      bullet_angle, self.bullet_speed,
-                                      (0, 0, 0, 0, 0., 0., 0., 0.),
-                                      0, self, self._game, damage=48, player_bullet=True))
-                bullet_angle += self.bullet_angle
-
-        for orb in self.orbs:
-            orb.fire(orb)
-
-
-
-class ReimuA(Reimu):
-    def __init__(self, state, game, resource_loader):
-        Reimu.__init__(self, state, game, resource_loader)
-
-        self.bulletA_type = BulletType(self.anm_wrapper, 65, 97, 0, 0, 0, hitbox_size=4) #TODO: verify the hitbox, damage is 14.
-        self.bulletA_speed = 12.
-
-
-    def fire(self):
-        Reimu.fire(self)
-
-        if self.state.power < 8:
-            return
-
-        else:
-            pass #TODO
-
-
-
-class ReimuB(Reimu):
-    def __init__(self, state, game, resource_loader):
-        Reimu.__init__(self, state, game, resource_loader)
-
-        self.bulletB_type = BulletType(self.anm_wrapper, 66, 98, 0, 0, 0, hitbox_size=4) #TODO: verify the hitbox.
-        self.bulletB_speed = 22.
-
-
-    def fire_spine(self, orb, offset_x):
-        bullets = self._game.players_bullets
-        nb_bullets_max = self._game.nb_bullets_max
-
-        if nb_bullets_max is not None and len(bullets) == nb_bullets_max:
-            return
-
-        bullets.append(Bullet((orb.x + offset_x, orb.y), self.bulletB_type, 0,
-                              self.bullet_launch_angle, self.bulletB_speed,
-                              (0, 0, 0, 0, 0., 0., 0., 0.),
-                              0, self, self._game, damage=12, player_bullet=True))
-
-    def orb_fire(self, orb):
-        if self.state.power < 8:
-            return
-
-        elif self.state.power < 16:
-            if self.fire_time % 15 == 0:
-                self.fire_spine(orb, 0)
-
-        elif self.state.power < 32:
-            if self.fire_time % 10 == 0:
-                self.fire_spine(orb, 0)
-
-        elif self.state.power < 48:
-            if self.fire_time % 8 == 0:
-                self.fire_spine(orb, 0)
-
-        elif self.state.power < 96:
-            if self.fire_time % 8 == 0:
-                self.fire_spine(orb, -8)
-            if self.fire_time % 5 == 0:
-                self.fire_spine(orb, 8)
-
-        elif self.state.power < 128:
-            if self.fire_time % 5 == 0:
-                self.fire_spine(orb, -12)
-            if self.fire_time % 10 == 0:
-                self.fire_spine(orb, 0)
-            if self.fire_time % 3 == 0:
-                self.fire_spine(orb, 12)
-
-        else:
-            if self.fire_time % 3 == 0:
-                self.fire_spine(orb, -12)
-                self.fire_spine(orb, 12)
-            if self.fire_time % 5 == 0:
-                self.fire_spine(orb, 0)
-
-
-
-class Marisa(EoSDPlayer):
-    def __init__(self, state, game, resource_loader):
-        anm_wrapper = resource_loader.get_anm_wrapper(('player01.anm',))
-        self.bullet_angle = pi/40 #TODO: check
-
-        EoSDPlayer.__init__(self, state, game, anm_wrapper, speeds=(5., 5. * SQ2, 2.5, 2.5 * SQ2))
-
-
-    def fire(self):
-        if self.fire_time % self.bullet_launch_interval == 0:
-            if self.state.power < 32:
-                bullets_per_shot = 1
-            elif self.state.power < 96:
-                bullets_per_shot = 2
-            elif self.state.power < 128:
-                bullets_per_shot = 3
-            else:
-                bullets_per_shot = 5
-
-            bullets = self._game.players_bullets
-            nb_bullets_max = self._game.nb_bullets_max
-
-            bullet_angle = self.bullet_launch_angle - self.bullet_angle * (bullets_per_shot - 1) / 2.
-            for bullet_nb in range(bullets_per_shot):
-                if nb_bullets_max is not None and len(bullets) == nb_bullets_max:
-                    break
-
-                bullets.append(Bullet((self.x, self.y), self.bullet_type, 0,
-                                      bullet_angle, self.bullet_speed,
-                                      (0, 0, 0, 0, 0., 0., 0., 0.),
-                                      0, self, self._game, damage=48, player_bullet=True))
-                bullet_angle += self.bullet_angle
-
-
-
-class MarisaA(Marisa):
-    def __init__(self, state, game, resource_loader):
-        Marisa.__init__(self, state, game, resource_loader)
-
-        #TODO: verify the hitbox and damages.
-        self.bulletA_types = [BulletType(self.anm_wrapper, 65, 0, 0, 0, 0, hitbox_size=4), # damage is 40.
-                              BulletType(self.anm_wrapper, 66, 0, 0, 0, 0, hitbox_size=4),
-                              BulletType(self.anm_wrapper, 67, 0, 0, 0, 0, hitbox_size=4),
-                              BulletType(self.anm_wrapper, 68, 0, 0, 0, 0, hitbox_size=4)]
-        self.bulletA_speed_interpolator = None
-
-
-    def fire(self):
-        Marisa.fire(self)
-
-        if self.state.power < 8:
-            return
-
-        else:
-            pass #TODO
-
-
-
-class MarisaB(Marisa):
-    def __init__(self, state, game, resource_loader):
-        Marisa.__init__(self, state, game, resource_loader)
-
-        #TODO:  power   damages period
-        #       8       240     120
-        #       16      390     170
-        #       32      480     ???
-        #       48      510     ???
-        #       64      760     ???
-        #       80      840     ???
-        #       96      1150    270
-        #       128     1740    330
-        # The duration of the laser is period - 42.
-        # The damages are given for one laser shot on one enemy for its entire duration.
-
-
-    def fire(self):
-        Marisa.fire(self)
-
-        if self.state.power < 8:
-            return
-
-        else:
-            pass #TODO
-
--- a/pytouhou/games/pcb.py
+++ b/pytouhou/games/pcb.py
@@ -16,58 +16,61 @@ from pytouhou.utils.interpolator import 
 
 from pytouhou.game.game import Game
 from pytouhou.game.bullettype import BulletType
+from pytouhou.game.bullettype import LaserType
 from pytouhou.game.itemtype import ItemType
 from pytouhou.game.player import Player
-from pytouhou.game.bullet import Bullet
 from pytouhou.game.orb import Orb
 
 from math import pi
 
 
 class PCBGame(Game):
-    def __init__(self, resource_loader, player_states, stage, rank, difficulty, **kwargs):
-        etama3 = resource_loader.get_anm_wrapper(('etama3.anm',))
-        etama4 = resource_loader.get_anm_wrapper(('etama4.anm',))
-        bullet_types = [BulletType(etama3, 0, 11, 14, 15, 16, hitbox_size=4),
-                        BulletType(etama3, 1, 12, 17, 18, 19, hitbox_size=6),
-                        BulletType(etama3, 2, 12, 17, 18, 19, hitbox_size=4),
-                        BulletType(etama3, 3, 12, 17, 18, 19, hitbox_size=6),
-                        BulletType(etama3, 4, 12, 17, 18, 19, hitbox_size=5),
-                        BulletType(etama3, 5, 12, 17, 18, 19, hitbox_size=4),
-                        BulletType(etama3, 6, 13, 20, 20, 20, hitbox_size=16),
-                        BulletType(etama3, 7, 13, 20, 20, 20, hitbox_size=11),
-                        BulletType(etama3, 8, 13, 20, 20, 20, hitbox_size=9),
-                        BulletType(etama4, 0, 1, 2, 2, 2, hitbox_size=32)]
-        #TODO: hitbox
-        item_types = [ItemType(etama3, 0, 7), #Power
-                      ItemType(etama3, 1, 8), #Point
-                      ItemType(etama3, 2, 9), #Big power
-                      ItemType(etama3, 3, 10), #Bomb
-                      ItemType(etama3, 4, 11), #Full power
-                      ItemType(etama3, 5, 12), #1up
-                      ItemType(etama3, 6, 13)] #Star
+    def __init__(self, resource_loader, player_states, stage, rank, difficulty,
+                 bullet_types=None, laser_types=None, item_types=None,
+                 nb_bullets_max=640, width=384, height=448, prng=None):
+        if not bullet_types:
+            etama3 = resource_loader.get_anm_wrapper(('etama3.anm',))
+            etama4 = resource_loader.get_anm_wrapper(('etama4.anm',))
+            bullet_types = [BulletType(etama3, 0, 11, 14, 15, 16, hitbox_size=4),
+                            BulletType(etama3, 1, 12, 17, 18, 19, hitbox_size=6),
+                            BulletType(etama3, 2, 12, 17, 18, 19, hitbox_size=4),
+                            BulletType(etama3, 3, 12, 17, 18, 19, hitbox_size=6),
+                            BulletType(etama3, 4, 12, 17, 18, 19, hitbox_size=5),
+                            BulletType(etama3, 5, 12, 17, 18, 19, hitbox_size=4),
+                            BulletType(etama3, 6, 13, 20, 20, 20, hitbox_size=16),
+                            BulletType(etama3, 7, 13, 20, 20, 20, hitbox_size=11),
+                            BulletType(etama3, 8, 13, 20, 20, 20, hitbox_size=9),
+                            BulletType(etama4, 0, 1, 2, 2, 2, hitbox_size=32)]
 
-        players = []
-        for player in player_states:
-            players.append(PCBPlayer(player, self, resource_loader))
+        if not laser_types:
+            laser_types = [] #TODO
+
+        if not item_types:
+            item_types = [ItemType(etama3, 0, 7), #Power
+                          ItemType(etama3, 1, 8), #Point
+                          ItemType(etama3, 2, 9), #Big power
+                          ItemType(etama3, 3, 10), #Bomb
+                          ItemType(etama3, 4, 11), #Full power
+                          ItemType(etama3, 5, 12), #1up
+                          ItemType(etama3, 6, 13)] #Star
+
+        players = [PCBPlayer(state, self, resource_loader) for state in player_states]
 
         Game.__init__(self, resource_loader, players, stage, rank, difficulty,
-                      bullet_types, item_types, nb_bullets_max=640, **kwargs)
+                      bullet_types, laser_types, item_types, nb_bullets_max,
+                      width, height, prng)
 
 
 
 class PCBPlayer(Player):
-    def __init__(self, state, game, resource_loader, speed=4., hitbox_size=2.5, graze_hitbox_size=42.):
+    def __init__(self, state, game, resource_loader):
         number = '%d%s' % (state.character // 2, 'b' if state.character % 2 else 'a')
         self.sht = resource_loader.get_sht('ply0%s.sht' % number)
         self.focused_sht = resource_loader.get_sht('ply0%ss.sht' % number)
         anm_wrapper = resource_loader.get_anm_wrapper(('player0%d.anm' % (state.character // 2),))
+        self.anm_wrapper = anm_wrapper
 
-        Player.__init__(self, state, game, anm_wrapper,
-                        speeds=(self.sht.horizontal_vertical_speed,
-                                self.sht.diagonal_speed,
-                                self.sht.horizontal_vertical_focused_speed,
-                                self.sht.diagonal_focused_speed))
+        Player.__init__(self, state, game, anm_wrapper)
 
         self.orbs = [Orb(self.anm_wrapper, 128, self.state, None),
                      Orb(self.anm_wrapper, 129, self.state, None)]
@@ -119,24 +122,3 @@ class PCBPlayer(Player):
         for orb in self.orbs:
             orb.update()
 
-
-    def fire(self):
-        sht = self.focused_sht if self.state.focused else self.sht
-        power = min(power for power in sht.shots if self.state.power < power)
-
-        bullets = self._game.players_bullets
-        nb_bullets_max = self._game.nb_bullets_max
-
-        for shot in sht.shots[power]:
-            if self.fire_time % shot.interval == 0:
-                if nb_bullets_max is not None and len(bullets) == nb_bullets_max:
-                    break
-
-                origin = self.orbs[shot.orb - 1] if shot.orb else self
-                x = origin.x + shot.pos[0]
-                y = origin.y + shot.pos[1]
-
-                bullets.append(Bullet((x, y), self.bullet_type, 0,
-                                      shot.angle, shot.speed,
-                                      (0, 0, 0, 0, 0., 0., 0., 0.),
-                                      0, self, self._game, player_bullet=True, damage=shot.damage, hitbox=shot.hitbox))
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/pytouhou/lib/_sdl.pxd
@@ -0,0 +1,194 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2013 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.
+##
+
+cdef extern from "SDL.h" nogil:
+    ctypedef unsigned int Uint32
+    ctypedef unsigned short Uint16
+    ctypedef unsigned char Uint8
+
+    int SDL_INIT_VIDEO
+
+    int SDL_Init(Uint32 flags)
+    void SDL_Quit()
+
+
+IF UNAME_SYSNAME == "Windows":
+    cdef extern from "SDL_main.h" nogil:
+        void SDL_SetMainReady()
+
+
+cdef extern from "SDL_error.h" nogil:
+    const char *SDL_GetError()
+
+
+cdef extern from "SDL_video.h" nogil:
+    ctypedef enum SDL_GLattr:
+        SDL_GL_CONTEXT_MAJOR_VERSION
+        SDL_GL_CONTEXT_MINOR_VERSION
+        SDL_GL_DOUBLEBUFFER
+        SDL_GL_DEPTH_SIZE
+
+    ctypedef enum SDL_WindowFlags:
+        SDL_WINDOWPOS_CENTERED
+        SDL_WINDOW_OPENGL
+        SDL_WINDOW_SHOWN
+        SDL_WINDOW_RESIZABLE
+
+    ctypedef struct SDL_Window:
+        pass
+
+    ctypedef void *SDL_GLContext
+
+    int SDL_GL_SetAttribute(SDL_GLattr attr, int value)
+    SDL_Window *SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint32 flags)
+    SDL_GLContext SDL_GL_CreateContext(SDL_Window *window)
+    void SDL_GL_SwapWindow(SDL_Window *window)
+    void SDL_GL_DeleteContext(SDL_GLContext context)
+    void SDL_DestroyWindow(SDL_Window *window)
+
+    void SDL_SetWindowSize(SDL_Window *window, int w, int h)
+
+
+cdef extern from "SDL_scancode.h" nogil:
+    ctypedef enum SDL_Scancode:
+        SDL_SCANCODE_Z
+        SDL_SCANCODE_X
+        SDL_SCANCODE_LSHIFT
+        SDL_SCANCODE_UP
+        SDL_SCANCODE_DOWN
+        SDL_SCANCODE_LEFT
+        SDL_SCANCODE_RIGHT
+        SDL_SCANCODE_LCTRL
+        SDL_SCANCODE_ESCAPE
+
+
+cdef extern from "SDL_events.h" nogil:
+    ctypedef enum SDL_EventType:
+        SDL_KEYDOWN
+        SDL_QUIT
+        SDL_WINDOWEVENT
+
+    ctypedef struct SDL_Keysym:
+        SDL_Scancode scancode
+
+    ctypedef struct SDL_KeyboardEvent:
+        Uint32 type
+        SDL_Keysym keysym
+
+    ctypedef enum SDL_WindowEventID:
+        SDL_WINDOWEVENT_RESIZED
+
+    ctypedef struct SDL_WindowEvent:
+        Uint32 type
+        SDL_WindowEventID event
+        int data1
+        int data2
+
+    ctypedef union SDL_Event:
+        Uint32 type
+        SDL_KeyboardEvent key
+        SDL_WindowEvent window
+
+    int SDL_PollEvent(SDL_Event *event)
+
+
+cdef extern from "SDL_keyboard.h" nogil:
+    const Uint8 *SDL_GetKeyboardState(int *numkeys)
+
+
+cdef extern from "SDL_timer.h" nogil:
+    Uint32 SDL_GetTicks()
+    void SDL_Delay(Uint32 ms)
+
+
+cdef extern from "SDL_rect.h" nogil:
+    ctypedef struct SDL_Rect:
+        int x, y
+        int w, h
+
+
+cdef extern from "SDL_surface.h" nogil:
+    ctypedef struct SDL_Surface:
+        int w, h
+        unsigned char *pixels
+
+    void SDL_FreeSurface(SDL_Surface *surface)
+    int SDL_BlitSurface(SDL_Surface *src, const SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect)
+    SDL_Surface *SDL_CreateRGBSurface(Uint32 flags, int width, int height, int depth, Uint32 Rmask, Uint32 Gmask, Uint32 Bmask, Uint32 Amask)
+
+
+cdef extern from "SDL_rwops.h" nogil:
+    ctypedef struct SDL_RWops:
+        pass
+
+    SDL_RWops *SDL_RWFromConstMem(const void *mem, int size)
+    int SDL_RWclose(SDL_RWops *context)
+
+
+cdef extern from "SDL_image.h" nogil:
+    int IMG_INIT_PNG
+
+    int IMG_Init(int flags)
+    void IMG_Quit()
+    SDL_Surface *IMG_LoadPNG_RW(SDL_RWops *src)
+
+
+cdef extern from "SDL_mixer.h" nogil:
+    ctypedef enum:
+        MIX_DEFAULT_FORMAT
+
+    ctypedef struct Mix_Music:
+        pass
+
+    ctypedef struct Mix_Chunk:
+        pass
+
+    int Mix_Init(int flags)
+    void Mix_Quit()
+
+    int Mix_OpenAudio(int frequency, Uint16 format_, int channels, int chunksize)
+    void Mix_CloseAudio()
+
+    int Mix_AllocateChannels(int numchans)
+
+    Mix_Music *Mix_LoadMUS(const char *filename)
+    Mix_Chunk *Mix_LoadWAV_RW(SDL_RWops *src, int freesrc)
+
+    void Mix_FreeMusic(Mix_Music *music)
+    void Mix_FreeChunk(Mix_Chunk *chunk)
+
+    int Mix_PlayMusic(Mix_Music *music, int loops)
+    #int Mix_SetLoopPoints(Mix_Music *music, double start, double end)
+
+    int Mix_Volume(int channel, int volume)
+    int Mix_VolumeChunk(Mix_Chunk *chunk, int volume)
+    int Mix_VolumeMusic(int volume)
+
+    int Mix_PlayChannel(int channel, Mix_Chunk *chunk, int loops)
+
+
+cdef extern from "SDL_pixels.h" nogil:
+    ctypedef struct SDL_Color:
+        Uint8 r, g, b, a
+
+
+cdef extern from "SDL_ttf.h" nogil:
+    ctypedef struct TTF_Font:
+        pass
+
+    int TTF_Init()
+    void TTF_Quit()
+    TTF_Font *TTF_OpenFont(const char *filename, int ptsize)
+    void TTF_CloseFont(TTF_Font *font)
+    SDL_Surface *TTF_RenderUTF8_Blended(TTF_Font *font, const char *text, SDL_Color fg)
new file mode 100644
--- /dev/null
+++ b/pytouhou/lib/opengl.pxd
@@ -0,0 +1,157 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2013 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.
+##
+
+
+IF USE_GLEW:
+    cdef extern from 'GL/glew.h' nogil:
+        GLenum glewInit()
+
+
+cdef extern from 'GL/gl.h' nogil:
+    ctypedef unsigned int GLuint
+    ctypedef int GLint
+    ctypedef float GLfloat
+    ctypedef float GLclampf
+    ctypedef char GLboolean
+    ctypedef char GLchar
+    ctypedef unsigned int GLsizei
+    ctypedef unsigned int GLsizeiptr
+    ctypedef unsigned int GLbitfield
+    ctypedef void GLvoid
+
+    ctypedef enum GLenum:
+        GL_ARRAY_BUFFER
+        GL_STATIC_DRAW
+        GL_DYNAMIC_DRAW
+        GL_UNSIGNED_BYTE
+        GL_UNSIGNED_SHORT
+        GL_INT
+        GL_FLOAT
+        GL_SRC_ALPHA
+        GL_ONE_MINUS_SRC_ALPHA
+        GL_ONE
+        GL_ZERO
+        GL_TEXTURE_2D
+        GL_TRIANGLES
+        GL_DEPTH_TEST
+        GL_QUADS
+
+        GL_TEXTURE_MIN_FILTER
+        GL_TEXTURE_MAG_FILTER
+        GL_LINEAR
+        GL_BGRA
+        GL_RGBA
+        GL_RGB
+        GL_LUMINANCE
+        GL_UNSIGNED_SHORT_5_6_5
+        GL_UNSIGNED_SHORT_4_4_4_4_REV
+
+        GL_COLOR_BUFFER_BIT
+        GL_SCISSOR_TEST
+        GL_MODELVIEW
+        GL_FOG
+
+        GL_DEPTH_BUFFER_BIT
+        GL_PROJECTION
+        GL_FOG_MODE
+        GL_FOG_START
+        GL_FOG_END
+        GL_FOG_COLOR
+
+        GL_BLEND
+        GL_PERSPECTIVE_CORRECTION_HINT
+        GL_FOG_HINT
+        GL_NICEST
+        GL_COLOR_ARRAY
+        GL_VERTEX_ARRAY
+        GL_TEXTURE_COORD_ARRAY
+
+        GL_VERTEX_SHADER
+        GL_FRAGMENT_SHADER
+        GL_INFO_LOG_LENGTH
+        GL_COMPILE_STATUS
+        GL_LINK_STATUS
+
+        GL_FRAMEBUFFER
+        GL_COLOR_ATTACHMENT0
+        GL_RENDERBUFFER
+        GL_DEPTH_COMPONENT
+        GL_DEPTH_ATTACHMENT
+        GL_FRAMEBUFFER_COMPLETE
+
+    void glVertexPointer(GLint size, GLenum type_, GLsizei stride, GLvoid *pointer)
+    void glTexCoordPointer(GLint size, GLenum type_, GLsizei stride, GLvoid *pointer)
+    void glColorPointer(GLint size, GLenum type_, GLsizei stride, GLvoid *pointer)
+    void glVertexAttribPointer(GLuint index, GLint size, GLenum type_, GLboolean normalized, GLsizei stride, const GLvoid *pointer)
+    void glEnableVertexAttribArray(GLuint index)
+
+    void glBlendFunc(GLenum sfactor, GLenum dfactor)
+    void glDrawArrays(GLenum mode, GLint first, GLsizei count)
+    void glDrawElements(GLenum mode, GLsizei count, GLenum type_, const GLvoid *indices)
+    void glEnable(GLenum cap)
+    void glDisable(GLenum cap)
+
+    void glGenBuffers(GLsizei n, GLuint * buffers)
+    void glDeleteBuffers(GLsizei n, const GLuint * buffers)
+    void glBindBuffer(GLenum target, GLuint buffer_)
+    void glBufferData(GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage)
+
+    void glGenTextures(GLsizei n, GLuint *textures)
+    void glDeleteTextures(GLsizei n, const GLuint *textures)
+    void glBindTexture(GLenum target, GLuint texture)
+    void glTexParameteri(GLenum target, GLenum pname, GLint param)
+    void glTexImage2D(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLsizei height, GLint border, GLenum format_, GLenum type_, const GLvoid *data)
+
+    void glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha) #XXX
+    void glClear(GLbitfield mask)
+    void glViewport(GLint x, GLint y, GLsizei width, GLsizei height)
+    void glScissor(GLint x, GLint y, GLsizei width, GLsizei height)
+    void glMatrixMode(GLenum mode)
+    void glLoadIdentity()
+    void glLoadMatrixf(const GLfloat * m)
+
+    void glFogi(GLenum pname, GLint param)
+    void glFogf(GLenum pname, GLfloat param)
+    void glFogfv(GLenum pname, const GLfloat * params)
+
+    void glHint(GLenum target, GLenum mode)
+    void glEnableClientState(GLenum cap)
+
+    GLuint glCreateProgram()
+    GLuint glCreateShader(GLenum shaderType)
+    void glLinkProgram(GLuint program)
+    void glUseProgram(GLuint program)
+    void glGetProgramiv(GLuint program, GLenum pname, GLint *params)
+    void glGetProgramInfoLog(GLuint program, GLsizei maxLength, GLsizei *length, GLchar *infoLog)
+
+    void glShaderSource(GLuint shader, GLsizei count, const GLchar **string, const GLint *length)
+    void glCompileShader(GLuint shader)
+    void glGetShaderiv(GLuint shader, GLenum pname, GLint *params)
+    void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsizei *length, GLchar *infoLog)
+    void glAttachShader(GLuint program, GLuint shader)
+
+    GLint glGetUniformLocation(GLuint program, const GLchar *name)
+    void glBindAttribLocation(GLuint program, GLuint index, const GLchar *name)
+    void glUniform1fv(GLint location, GLsizei count, const GLfloat *value)
+    void glUniform4fv(GLint location, GLsizei count, const GLfloat *value)
+    void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value)
+
+    void glGenFramebuffers(GLsizei n, GLuint *ids)
+    void glBindFramebuffer(GLenum target, GLuint framebuffer)
+    void glFramebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level)
+    void glGenRenderbuffers(GLsizei n, GLuint *renderbuffers)
+    void glBindRenderbuffer(GLenum target, GLuint renderbuffer)
+    void glRenderbufferStorage(GLenum target, GLenum internalformat, GLsizei width, GLsizei height)
+    void glFramebufferRenderbuffer(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer)
+    GLenum glCheckFramebufferStatus(GLenum target)
new file mode 100644
--- /dev/null
+++ b/pytouhou/lib/sdl.pxd
@@ -0,0 +1,98 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2013 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 _sdl cimport *
+
+
+cdef SDL_GLattr GL_CONTEXT_MAJOR_VERSION
+cdef SDL_GLattr GL_CONTEXT_MINOR_VERSION
+cdef SDL_GLattr GL_DOUBLEBUFFER
+cdef SDL_GLattr GL_DEPTH_SIZE
+
+cdef SDL_WindowFlags WINDOWPOS_CENTERED
+cdef SDL_WindowFlags WINDOW_OPENGL
+cdef SDL_WindowFlags WINDOW_SHOWN
+cdef SDL_WindowFlags WINDOW_RESIZABLE
+
+#TODO: should be SDL_Scancode, but Cython doesn’t allow enum for array indexing.
+cdef long SCANCODE_Z
+cdef long SCANCODE_X
+cdef long SCANCODE_LSHIFT
+cdef long SCANCODE_UP
+cdef long SCANCODE_DOWN
+cdef long SCANCODE_LEFT
+cdef long SCANCODE_RIGHT
+cdef long SCANCODE_LCTRL
+cdef long SCANCODE_ESCAPE
+
+cdef SDL_WindowEventID WINDOWEVENT_RESIZED
+
+cdef SDL_EventType KEYDOWN
+cdef SDL_EventType QUIT
+cdef SDL_EventType WINDOWEVENT
+
+
+cdef class Window:
+    cdef SDL_Window *window
+    cdef SDL_GLContext context
+
+    cdef void gl_create_context(self) except *
+    cdef void gl_swap_window(self) nogil
+    cdef void set_window_size(self, int width, int height) nogil
+
+
+cdef class Surface:
+    cdef SDL_Surface *surface
+
+    cdef void blit(self, Surface other) except *
+    cdef void set_alpha(self, Surface alpha_surface) nogil
+
+
+cdef class Music:
+    cdef Mix_Music *music
+
+    cdef void play(self, int loops) nogil
+    cdef void set_loop_points(self, double start, double end) nogil
+
+
+cdef class Chunk:
+    cdef Mix_Chunk *chunk
+
+    cdef void play(self, int channel, int loops) nogil
+    cdef void set_volume(self, float volume) nogil
+
+
+cdef class Font:
+    cdef TTF_Font *font
+
+    cdef Surface render(self, unicode text)
+
+
+cdef void init(Uint32 flags) except *
+cdef void img_init(Uint32 flags) except *
+cdef void mix_init(int flags) except *
+cdef void ttf_init() except *
+cdef void gl_set_attribute(SDL_GLattr attr, int value) except *
+cdef list poll_events()
+cdef const Uint8* get_keyboard_state() nogil
+cdef Surface load_png(file_)
+cdef Surface create_rgb_surface(int width, int height, int depth, Uint32 rmask=*, Uint32 gmask=*, Uint32 bmask=*, Uint32 amask=*)
+cdef void mix_open_audio(int frequency, Uint16 format_, int channels, int chunksize) except *
+cdef void mix_allocate_channels(int numchans) except *
+cdef int mix_volume(int channel, float volume) nogil
+cdef int mix_volume_music(float volume) nogil
+cdef Music load_music(const char *filename)
+cdef Chunk load_chunk(file_)
+cdef Uint32 get_ticks() nogil
+cdef void delay(Uint32 ms) nogil
new file mode 100644
--- /dev/null
+++ b/pytouhou/lib/sdl.pyx
@@ -0,0 +1,284 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2013 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.
+##
+
+GL_CONTEXT_MAJOR_VERSION = SDL_GL_CONTEXT_MAJOR_VERSION
+GL_CONTEXT_MINOR_VERSION = SDL_GL_CONTEXT_MINOR_VERSION
+GL_DOUBLEBUFFER = SDL_GL_DOUBLEBUFFER
+GL_DEPTH_SIZE = SDL_GL_DEPTH_SIZE
+
+WINDOWPOS_CENTERED = SDL_WINDOWPOS_CENTERED
+WINDOW_OPENGL = SDL_WINDOW_OPENGL
+WINDOW_SHOWN = SDL_WINDOW_SHOWN
+WINDOW_RESIZABLE = SDL_WINDOW_RESIZABLE
+
+SCANCODE_Z = SDL_SCANCODE_Z
+SCANCODE_X = SDL_SCANCODE_X
+SCANCODE_LSHIFT = SDL_SCANCODE_LSHIFT
+SCANCODE_UP = SDL_SCANCODE_UP
+SCANCODE_DOWN = SDL_SCANCODE_DOWN
+SCANCODE_LEFT = SDL_SCANCODE_LEFT
+SCANCODE_RIGHT = SDL_SCANCODE_RIGHT
+SCANCODE_LCTRL = SDL_SCANCODE_LCTRL
+SCANCODE_ESCAPE = SDL_SCANCODE_ESCAPE
+
+WINDOWEVENT_RESIZED = SDL_WINDOWEVENT_RESIZED
+
+KEYDOWN = SDL_KEYDOWN
+QUIT = SDL_QUIT
+WINDOWEVENT = SDL_WINDOWEVENT
+
+
+class SDLError(Exception):
+    pass
+
+
+class SDL(object):
+    def __init__(self, sound=True):
+        self.sound = sound
+
+    def __enter__(self):
+        IF UNAME_SYSNAME == "Windows":
+            SDL_SetMainReady()
+        init(SDL_INIT_VIDEO)
+        img_init(IMG_INIT_PNG)
+        ttf_init()
+
+        if self.sound:
+            mix_init(0)
+            mix_open_audio(44100, MIX_DEFAULT_FORMAT, 2, 4096)
+            mix_allocate_channels(MAX_CHANNELS) #TODO: make it dependent on the SFX number.
+
+    def __exit__(self, *args):
+        if self.sound:
+            Mix_CloseAudio()
+            Mix_Quit()
+
+        TTF_Quit()
+        IMG_Quit()
+        SDL_Quit()
+
+
+cdef class Window:
+    def __init__(self, const char *title, int x, int y, int w, int h, Uint32 flags):
+        self.window = SDL_CreateWindow(title, x, y, w, h, flags)
+        if self.window == NULL:
+            raise SDLError(SDL_GetError())
+
+    def __dealloc__(self):
+        if self.context != NULL:
+            SDL_GL_DeleteContext(self.context)
+        if self.window != NULL:
+            SDL_DestroyWindow(self.window)
+
+    cdef void gl_create_context(self) except *:
+        self.context = SDL_GL_CreateContext(self.window)
+        if self.context == NULL:
+            raise SDLError(SDL_GetError())
+
+    cdef void gl_swap_window(self) nogil:
+        SDL_GL_SwapWindow(self.window)
+
+    cdef void set_window_size(self, int width, int height) nogil:
+        SDL_SetWindowSize(self.window, width, height)
+
+
+cdef class Surface:
+    def __dealloc__(self):
+        if self.surface != NULL:
+            SDL_FreeSurface(self.surface)
+
+    property pixels:
+        def __get__(self):
+            return bytes(self.surface.pixels[:self.surface.w * self.surface.h * 4])
+
+    cdef void blit(self, Surface other):
+        if SDL_BlitSurface(other.surface, NULL, self.surface, NULL) < 0:
+            raise SDLError(SDL_GetError())
+
+    cdef void set_alpha(self, Surface alpha_surface) nogil:
+        nb_pixels = self.surface.w * self.surface.h
+        image = self.surface.pixels
+        alpha = alpha_surface.surface.pixels
+
+        for i in xrange(nb_pixels):
+            # Only use the red value, assume the others are equal.
+            image[3+4*i] = alpha[3*i]
+
+
+cdef class Music:
+    def __dealloc__(self):
+        if self.music != NULL:
+            Mix_FreeMusic(self.music)
+
+    cdef void play(self, int loops) nogil:
+        Mix_PlayMusic(self.music, loops)
+
+    cdef void set_loop_points(self, double start, double end) nogil:
+        #Mix_SetLoopPoints(self.music, start, end)
+        pass
+
+
+cdef class Chunk:
+    def __dealloc__(self):
+        if self.chunk != NULL:
+            Mix_FreeChunk(self.chunk)
+
+    cdef void play(self, int channel, int loops) nogil:
+        Mix_PlayChannel(channel, self.chunk, loops)
+
+    cdef void set_volume(self, float volume) nogil:
+        Mix_VolumeChunk(self.chunk, int(volume * 128))
+
+
+cdef class Font:
+    def __init__(self, const char *filename, int ptsize):
+        self.font = TTF_OpenFont(filename, ptsize)
+        if self.font == NULL:
+            raise SDLError(SDL_GetError())
+
+    def __dealloc__(self):
+        if self.font != NULL:
+            TTF_CloseFont(self.font)
+
+    cdef Surface render(self, unicode text):
+        cdef SDL_Color white
+        white = SDL_Color(255, 255, 255, 255)
+        surface = Surface()
+        string = text.encode('utf-8')
+        surface.surface = TTF_RenderUTF8_Blended(self.font, string, white)
+        if surface.surface == NULL:
+            raise SDLError(SDL_GetError())
+        return surface
+
+
+cdef void init(Uint32 flags) except *:
+    if SDL_Init(flags) < 0:
+        raise SDLError(SDL_GetError())
+
+
+cdef void img_init(Uint32 flags) except *:
+    if IMG_Init(flags) != flags:
+        raise SDLError(SDL_GetError())
+
+
+cdef void mix_init(int flags) except *:
+    if Mix_Init(flags) != flags:
+        raise SDLError(SDL_GetError())
+
+
+cdef void ttf_init() except *:
+    if TTF_Init() < 0:
+        raise SDLError(SDL_GetError())
+
+
+cdef void quit() nogil:
+    SDL_Quit()
+
+
+cdef void img_quit() nogil:
+    IMG_Quit()
+
+
+cdef void mix_quit() nogil:
+    Mix_Quit()
+
+
+cdef void ttf_quit() nogil:
+    TTF_Quit()
+
+
+cdef void gl_set_attribute(SDL_GLattr attr, int value) except *:
+    if SDL_GL_SetAttribute(attr, value) < 0:
+        raise SDLError(SDL_GetError())
+
+
+cdef list poll_events():
+    cdef SDL_Event event
+    ret = []
+    while SDL_PollEvent(&event):
+        if event.type == SDL_KEYDOWN:
+            ret.append((event.type, event.key.keysym.scancode))
+        elif event.type == SDL_QUIT:
+            ret.append((event.type,))
+        elif event.type == SDL_WINDOWEVENT:
+            ret.append((event.type, event.window.event, event.window.data1, event.window.data2))
+    return ret
+
+
+cdef const Uint8* get_keyboard_state() nogil:
+    return SDL_GetKeyboardState(NULL)
+
+
+cdef Surface load_png(file_):
+    data = file_.read()
+    rwops = SDL_RWFromConstMem(<char*>data, len(data))
+    surface = Surface()
+    surface.surface = IMG_LoadPNG_RW(rwops)
+    SDL_RWclose(rwops)
+    if surface.surface == NULL:
+        raise SDLError(SDL_GetError())
+    return surface
+
+
+cdef Surface create_rgb_surface(int width, int height, int depth, Uint32 rmask=0, Uint32 gmask=0, Uint32 bmask=0, Uint32 amask=0):
+    surface = Surface()
+    surface.surface = SDL_CreateRGBSurface(0, width, height, depth, rmask, gmask, bmask, amask)
+    if surface.surface == NULL:
+        raise SDLError(SDL_GetError())
+    return surface
+
+
+cdef void mix_open_audio(int frequency, Uint16 format_, int channels, int chunksize) except *:
+    if Mix_OpenAudio(frequency, format_, channels, chunksize) < 0:
+        raise SDLError(SDL_GetError())
+
+
+cdef void mix_allocate_channels(int numchans) except *:
+    if Mix_AllocateChannels(numchans) != numchans:
+        raise SDLError(SDL_GetError())
+
+
+cdef int mix_volume(int channel, float volume) nogil:
+    return Mix_Volume(channel, int(volume * 128))
+
+
+cdef int mix_volume_music(float volume) nogil:
+    return Mix_VolumeMusic(int(volume * 128))
+
+
+cdef Music load_music(const char *filename):
+    music = Music()
+    music.music = Mix_LoadMUS(filename)
+    if music.music == NULL:
+        raise SDLError(SDL_GetError())
+    return music
+
+
+cdef Chunk load_chunk(file_):
+    cdef SDL_RWops *rwops
+    chunk = Chunk()
+    data = file_.read()
+    rwops = SDL_RWFromConstMem(<char*>data, len(data))
+    chunk.chunk = Mix_LoadWAV_RW(rwops, 1)
+    if chunk.chunk == NULL:
+        raise SDLError(SDL_GetError())
+    return chunk
+
+
+cdef Uint32 get_ticks() nogil:
+    return SDL_GetTicks()
+
+
+cdef void delay(Uint32 ms) nogil:
+    SDL_Delay(ms)
deleted file mode 100644
--- a/pytouhou/resource/anmwrapper.py
+++ /dev/null
@@ -1,17 +0,0 @@
-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
--- a/pytouhou/resource/loader.py
+++ b/pytouhou/resource/loader.py
@@ -1,14 +1,65 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2012 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.
+##
+
+import os
+from glob import glob
+from itertools import chain
 from io import BytesIO
 
+from pytouhou.formats import WrongFormatError
 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.formats.anm0 import ANM0
 from pytouhou.formats.msg import MSG
 from pytouhou.formats.sht import SHT
+from pytouhou.formats.exe import SHT as EoSDSHT, InvalidExeException
+from pytouhou.formats.music import Track
+from pytouhou.formats.fmt import FMT
+
+from pytouhou.utils.helpers import get_logger
+
+logger = get_logger(__name__)
+
+
+
+class Directory(object):
+    def __init__(self, path):
+        self.path = path
 
 
-from pytouhou.resource.anmwrapper import AnmWrapper
+    def __enter__(self):
+        return self
+
+
+    def __exit__(self, type, value, traceback):
+        return False
+
+
+    def list_files(self):
+        file_list = []
+        for path in os.listdir(self.path):
+            if os.path.isfile(os.path.join(self.path, path)):
+                file_list.append(path)
+        return file_list
+
+
+    def extract(self, name):
+        with open(os.path.join(self.path, str(name)), 'rb') as file:
+            contents = file.read()
+        return contents
+
 
 
 class ArchiveDescription(object):
@@ -21,6 +72,9 @@ class ArchiveDescription(object):
 
 
     def open(self):
+        if self.format_class is Directory:
+            return self.format_class(self.path)
+
         file = open(self.path, 'rb')
         instance = self.format_class.read(file)
         return instance
@@ -28,6 +82,10 @@ class ArchiveDescription(object):
 
     @classmethod
     def get_from_path(cls, path):
+        if os.path.isdir(path):
+            instance = Directory(path)
+            file_list = instance.list_files()
+            return cls(path, Directory, file_list)
         with open(path, 'rb') as file:
             magic = file.read(4)
             file.seek(0)
@@ -39,20 +97,30 @@ class ArchiveDescription(object):
 
 
 class Loader(object):
-    def __init__(self):
+    def __init__(self, game_dir=None):
+        self.exe_files = []
+        self.game_dir = game_dir
         self.known_files = {}
-        self.instanced_ecls = {}
-        self.instanced_anms = {}
-        self.instanced_stages = {}
-        self.instanced_msgs = {}
-        self.instanced_shts = {}
+        self.instanced_anms = {} #TODO: remove it someday.
 
 
-    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 scan_archives(self, paths_lists):
+        for paths in paths_lists:
+            def _expand_paths():
+                for path in paths.split(os.path.pathsep):
+                    if self.game_dir and not os.path.isabs(path):
+                        path = os.path.join(self.game_dir, path)
+                    yield glob(path)
+            paths = list(chain(*_expand_paths()))
+            if not paths:
+                raise IOError
+            path = paths[0]
+            if os.path.splitext(path)[1] == '.exe':
+                self.exe_files.extend(paths)
+            else:
+                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):
@@ -70,49 +138,60 @@ class Loader(object):
     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
+            self.instanced_anms[name] = ANM0.read(file)
         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]
+        file = self.get_file(name)
+        return Stage.read(file) #TODO: modular
 
 
     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]
+        file = self.get_file(name)
+        return ECL.read(file) #TODO: modular
 
 
     def get_msg(self, name):
-        if name not in self.instanced_msgs:
-            file = self.get_file(name)
-            self.instanced_msgs[name] = MSG.read(file) #TODO: modular
-        return self.instanced_msgs[name]
+        file = self.get_file(name)
+        return MSG.read(file) #TODO: modular
 
 
     def get_sht(self, name):
-        if name not in self.instanced_shts:
-            file = self.get_file(name)
-            self.instanced_shts[name] = SHT.read(file) #TODO: modular
-        return self.instanced_shts[name]
+        file = self.get_file(name)
+        return SHT.read(file) #TODO: modular
 
 
-    def get_anm_wrapper(self, names):
-        return AnmWrapper(self.get_anm(name) for name in names)
+    def get_eosd_characters(self):
+        #TODO: Move to pytouhou.games.eosd?
+        for path in self.exe_files:
+            try:
+                with open(path, 'rb') as file:
+                    characters = EoSDSHT.read(file)
+                return characters
+            except InvalidExeException:
+                pass
+        logger.error("Required game exe not found!")
 
 
-    def get_anm_wrapper2(self, names):
-        anims = []
-        try:
-            for name in names:
-                anims.append(self.get_anm(name))
-        except KeyError:
-            pass
+    def get_track(self, name):
+        posname = name.replace('bgm/', '').replace('.mid', '.pos')
+        file = self.get_file(posname)
+        return Track.read(file) #TODO: modular
+
+
+    def get_fmt(self, name):
+        file = self.get_file(name)
+        return FMT.read(file) #TODO: modular
+
 
-        return AnmWrapper(anims)
+    def get_single_anm(self, name):
+        """Hack for EoSD, since it doesn’t support multi-entries ANMs."""
+        anm = self.get_anm(name)
+        assert len(anm) == 1
+        return anm[0]
 
+
+    def get_multi_anm(self, names):
+        """Hack for EoSD, since it doesn’t support multi-entries ANMs."""
+        return sum((self.get_anm(name) for name in names), [])
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/anmrenderer.pyx
@@ -0,0 +1,172 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2011 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.lib.opengl cimport \
+         (glClearColor, glClear, GL_COLOR_BUFFER_BIT)
+
+from pytouhou.game.sprite import Sprite
+from pytouhou.vm.anmrunner import ANMRunner
+
+from pytouhou.utils.helpers import get_logger
+from pytouhou.utils.maths cimport perspective, setup_camera
+
+from .renderer import Renderer
+from .shaders.eosd import GameShader
+
+from pytouhou.lib cimport sdl
+
+
+logger = get_logger(__name__)
+
+
+class ANMRenderer(Renderer):
+    def __init__(self, window, resource_loader, anm, index=0, sprites=False):
+        self.use_fixed_pipeline = window.use_fixed_pipeline #XXX
+
+        Renderer.__init__(self, resource_loader)
+
+        self.window = window
+        self.texture_manager.load(resource_loader.instanced_anms.values())
+
+        self._anm = anm
+        self.sprites = sprites
+        self.clear_color = (0., 0., 0., 1.)
+        self.force_allow_dest_offset = False
+        self.index_items()
+        self.load(index)
+        self.objects = [self]
+
+        self.width = 384
+        self.height = 448
+
+        self.x = self.width / 2
+        self.y = self.height / 2
+
+
+    def start(self, width=384, height=448):
+        self.window.set_size(width, height)
+
+        # Switch to game projection
+        proj = perspective(30, float(width) / float(height),
+                           101010101./2010101., 101010101./10101.)
+        view = setup_camera(0, 0, 1)
+
+        shader = GameShader()
+
+        mvp = view * proj
+        shader.bind()
+        shader.uniform_matrix('mvp', mvp)
+
+
+    def load(self, index=None):
+        if index is None:
+            index = self.num
+        self.sprite = Sprite()
+        if self.sprites:
+            self.sprite.anm = self._anm
+            self.sprite.texcoords = self._anm.sprites[index]
+            print('Loaded sprite %d' % index)
+        else:
+            self.anmrunner = ANMRunner(self._anm, index, self.sprite)
+            print('Loading anim %d, handled events: %r' % (index, self.anmrunner.script.interrupts.keys()))
+        self.num = index
+
+
+    def change(self, diff):
+        keys = self.items.keys()
+        keys.sort()
+        index = (keys.index(self.num) + diff) % len(keys)
+        item = keys[index]
+        self.load(item)
+
+
+    def index_items(self):
+        self.items = {}
+        if self.sprites:
+            self.items = self._anm.sprites
+        else:
+            self.items = self._anm.scripts
+
+
+    def toggle_sprites(self):
+        self.sprites = not(self.sprites)
+        self.index_items()
+        self.load(0)
+
+
+    def toggle_clear_color(self):
+        if self.clear_color[0] == 0.:
+            self.clear_color = (1., 1., 1., 1.)
+        else:
+            self.clear_color = (0., 0., 0., 1.)
+
+
+    def update(self):
+        sdl.SCANCODE_C = 6
+        sdl.SCANCODE_TAB = 43
+        sdl.SCANCODE_SPACE = 44
+        sdl.SCANCODE_F1 = 58
+        sdl.SCANCODE_F12 = 69
+        for event in sdl.poll_events():
+            type_ = event[0]
+            if type_ == sdl.KEYDOWN:
+                scancode = event[1]
+                if scancode == sdl.SCANCODE_Z:
+                    self.load()
+                elif scancode == sdl.SCANCODE_X:
+                    self.x, self.y = {(192, 224): (0, 0),
+                                      (0, 0): (-224, 0),
+                                      (-224, 0): (192, 224)}[(self.x, self.y)]
+                elif scancode == sdl.SCANCODE_C:
+                    self.force_allow_dest_offset = not self.force_allow_dest_offset
+                    self.load()
+                elif scancode == sdl.SCANCODE_LEFT:
+                    self.change(-1)
+                elif scancode == sdl.SCANCODE_RIGHT:
+                    self.change(+1)
+                elif scancode == sdl.SCANCODE_TAB:
+                    self.toggle_sprites()
+                elif scancode == sdl.SCANCODE_SPACE:
+                    self.toggle_clear_color()
+                elif sdl.SCANCODE_F1 <= scancode <= sdl.SCANCODE_F12:
+                    interrupt = scancode - sdl.SCANCODE_F1 + 1
+                    keys = sdl.get_keyboard_state()
+                    if keys[sdl.SCANCODE_LSHIFT]:
+                        interrupt += 12
+                    if not self.sprites:
+                        self.anmrunner.interrupt(interrupt)
+                elif scancode == sdl.SCANCODE_ESCAPE:
+                    return False
+            elif type_ == sdl.QUIT:
+                return False
+            elif type_ == sdl.WINDOWEVENT:
+                event_ = event[1]
+                if event_ == sdl.WINDOWEVENT_RESIZED:
+                    self.window.set_size(event[2], event[3])
+
+        if not self.sprites:
+            self.anmrunner.run_frame()
+
+        if self.force_allow_dest_offset:
+            self.sprite.allow_dest_offset = True
+
+        glClearColor(self.clear_color[0], self.clear_color[1], self.clear_color[2], self.clear_color[3])
+        glClear(GL_COLOR_BUFFER_BIT)
+        if not self.sprite.removed:
+            self.render_elements([self])
+        return True
+
+
+    def finish(self):
+        pass
--- a/pytouhou/ui/background.pxd
+++ b/pytouhou/ui/background.pxd
@@ -1,1 +1,17 @@
-cpdef object get_background_rendering_data(object background)
+from pytouhou.lib.opengl cimport GLuint
+
+cdef struct Vertex:
+    float x, y, z
+    float u, v
+    unsigned char r, g, b, a
+
+
+cdef class BackgroundRenderer:
+    cdef GLuint texture
+    cdef unsigned short blendfunc, nb_vertices
+    cdef Vertex *vertex_buffer
+    cdef unsigned int use_fixed_pipeline, vbo
+    cdef object background
+
+    cdef void render_background(self) except *
+    cdef void load(self, background) except *
--- a/pytouhou/ui/background.pyx
+++ b/pytouhou/ui/background.pyx
@@ -1,6 +1,6 @@
 # -*- encoding: utf-8 -*-
 ##
-## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
+## Copyright (C) 2013 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
@@ -12,42 +12,92 @@
 ## GNU General Public License for more details.
 ##
 
-#TODO: lots of things
+from libc.stdlib cimport malloc, free, realloc
 
-from struct import pack
-from itertools import chain
+from pytouhou.lib.opengl cimport \
+         (glVertexPointer, glTexCoordPointer, glColorPointer,
+          glVertexAttribPointer, glEnableVertexAttribArray, glBlendFunc,
+          glBindTexture, glBindBuffer, glBufferData, GL_ARRAY_BUFFER,
+          GL_STATIC_DRAW, GL_UNSIGNED_BYTE, GL_FLOAT, GL_SRC_ALPHA,
+          GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_TEXTURE_2D, glGenBuffers,
+          glEnable, glDisable, GL_DEPTH_TEST, glDrawArrays, GL_QUADS)
 
 from .sprite cimport get_sprite_rendering_data
 
-cpdef object get_background_rendering_data(object background):
-    cdef float x, y, z, ox, oy, oz, ox2, oy2, oz2
-    cdef list vertices, uvs, colors
+
+cdef class BackgroundRenderer:
+    def __cinit__(self):
+        # Allocate buffers
+        self.vertex_buffer = <Vertex*> malloc(65536 * sizeof(Vertex))
+
+
+    def __dealloc__(self):
+        free(self.vertex_buffer)
+
+
+    def __init__(self, use_fixed_pipeline):
+        self.use_fixed_pipeline = use_fixed_pipeline
+
+        if not use_fixed_pipeline:
+            glGenBuffers(1, &self.vbo)
 
-    #TODO: do not cache the results, and use view frustum culling
-    try:
-        return background._rendering_data
-    except AttributeError:
-        pass
 
-    vertices = []
-    uvs = []
-    colors = []
+    cdef void render_background(self):
+        if self.use_fixed_pipeline:
+            glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &self.vertex_buffer[0].x)
+            glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &self.vertex_buffer[0].u)
+            glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(Vertex), &self.vertex_buffer[0].r)
+        else:
+            glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
+
+            #TODO: find a way to use offsetof() instead of those ugly hardcoded values.
+            glVertexAttribPointer(0, 3, GL_FLOAT, False, sizeof(Vertex), <void*>0)
+            glEnableVertexAttribArray(0)
+            glVertexAttribPointer(1, 2, GL_FLOAT, False, sizeof(Vertex), <void*>12)
+            glEnableVertexAttribArray(1)
+            glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, True, sizeof(Vertex), <void*>20)
+            glEnableVertexAttribArray(2)
 
-    for ox, oy, oz, model_id, model in background.object_instances:
-        for ox2, oy2, oz2, width_override, height_override, sprite in model:
-            #TODO: view frustum culling
-            key, (vertices2, uvs2, colors2) = get_sprite_rendering_data(sprite)
-            vertices.extend([(x + ox + ox2, y + oy + oy2, z + oz + oz2)
-                                for x, y, z in vertices2])
-            uvs.extend(uvs2)
-            colors.extend(colors2)
+        glEnable(GL_DEPTH_TEST)
+        glBlendFunc(GL_SRC_ALPHA, (GL_ONE_MINUS_SRC_ALPHA, GL_ONE)[self.blendfunc])
+        glBindTexture(GL_TEXTURE_2D, self.texture)
+        glDrawArrays(GL_QUADS, 0, self.nb_vertices)
+        glDisable(GL_DEPTH_TEST)
+
+        if not self.use_fixed_pipeline:
+            glBindBuffer(GL_ARRAY_BUFFER, 0)
+
+
+    cdef void load(self, background):
+        cdef float ox, oy, oz, ox2, oy2, oz2
+        cdef unsigned short nb_vertices = 0
+        cdef Vertex* vertex_buffer
+
+        self.background = background
+
+        vertex_buffer = self.vertex_buffer
 
-    nb_vertices = len(vertices)
-    vertices_s = pack(str(3 * nb_vertices) + 'f', *chain(*vertices))
-    uvs_s = pack(str(2 * nb_vertices) + 'f', *uvs)
-    colors_s = pack(str(4 * nb_vertices) + 'B', *colors)
+        for ox, oy, oz, model_id, model in background.object_instances:
+            for ox2, oy2, oz2, width_override, height_override, sprite in model:
+                #TODO: view frustum culling
+                key, (vertices, uvs, colors) = get_sprite_rendering_data(sprite)
+                x1, x2, x3, x4, y1, y2, y3, y4, z1, z2, z3, z4 = vertices
+                left, right, bottom, top = uvs
+                r, g, b, a = colors
 
-    background._rendering_data = [(key, (nb_vertices, vertices_s, uvs_s, colors_s))]
+                vertex_buffer[nb_vertices] = Vertex(x1 + ox + ox2, y1 + oy + oy2, z1 + oz + oz2, left, bottom, r, g, b, a)
+                vertex_buffer[nb_vertices+1] = Vertex(x2 + ox + ox2, y2 + oy + oy2, z2 + oz + oz2, right, bottom, r, g, b, a)
+                vertex_buffer[nb_vertices+2] = Vertex(x3 + ox + ox2, y3 + oy + oy2, z3 + oz + oz2, right, top, r, g, b, a)
+                vertex_buffer[nb_vertices+3] = Vertex(x4 + ox + ox2, y4 + oy + oy2, z4 + oz + oz2, left, top, r, g, b, a)
+
+                nb_vertices += 4
 
-    return background._rendering_data
+        self.texture = key % MAX_TEXTURES
+        self.blendfunc = key // MAX_TEXTURES
+        self.nb_vertices = nb_vertices
+        self.vertex_buffer = <Vertex*> realloc(vertex_buffer, nb_vertices * sizeof(Vertex))
 
+        if not self.use_fixed_pipeline:
+            glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
+            glBufferData(GL_ARRAY_BUFFER, nb_vertices * sizeof(Vertex), &self.vertex_buffer[0], GL_STATIC_DRAW)
+            glBindBuffer(GL_ARRAY_BUFFER, 0)
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/gamerenderer.pxd
@@ -0,0 +1,19 @@
+from pytouhou.utils.matrix cimport Matrix
+from pytouhou.game.game cimport Game
+from .background cimport BackgroundRenderer
+from .renderer cimport Renderer, Framebuffer
+from .shader cimport Shader
+from .window cimport Window
+
+cdef class GameRenderer(Renderer):
+    cdef Matrix game_mvp, interface_mvp, proj
+    cdef Shader game_shader, background_shader, interface_shader, passthrough_shader
+    cdef Framebuffer framebuffer
+    cdef BackgroundRenderer background_renderer
+
+    cdef void load_background(self, background) except *
+    cdef void start(self, Game game) except *
+    cdef void render(self, Game game, Window window) except *
+    cdef void render_game(self, Game game) except *
+    cdef void render_text(self, texts) except *
+    cdef void render_interface(self, interface, game_boss) except *
--- a/pytouhou/ui/gamerenderer.pyx
+++ b/pytouhou/ui/gamerenderer.pyx
@@ -12,76 +12,227 @@
 ## GNU General Public License for more details.
 ##
 
-
 from itertools import chain
 
-from pyglet.gl import *
+from pytouhou.lib.opengl cimport \
+         (glClear, glMatrixMode, glLoadIdentity, glLoadMatrixf, glDisable,
+          glEnable, glFogi, glFogf, glFogfv, GL_PROJECTION, GL_MODELVIEW,
+          GL_FOG, GL_FOG_MODE, GL_LINEAR, GL_FOG_START, GL_FOG_END,
+          GL_FOG_COLOR, GL_COLOR_BUFFER_BIT, GLfloat, glViewport, glScissor,
+          GL_SCISSOR_TEST, GL_DEPTH_BUFFER_BIT)
 
-from .renderer cimport Renderer
-from .background cimport get_background_rendering_data
+from pytouhou.utils.maths cimport perspective, setup_camera, ortho_2d
+from pytouhou.game.text cimport NativeText, GlyphCollection
+from .shaders.eosd import GameShader, BackgroundShader, PassthroughShader
 
+from collections import namedtuple
+Rect = namedtuple('Rect', 'x y w h')
+Color = namedtuple('Color', 'r g b a')
 
 
 cdef class GameRenderer(Renderer):
-    cdef public game
-    cdef public background
+    def __init__(self, resource_loader, use_fixed_pipeline):
+        self.use_fixed_pipeline = use_fixed_pipeline #XXX
+
+        Renderer.__init__(self, resource_loader)
+
+        if not self.use_fixed_pipeline:
+            self.game_shader = GameShader()
+            self.background_shader = BackgroundShader()
+            self.interface_shader = self.game_shader
+            self.passthrough_shader = PassthroughShader()
+
+            self.framebuffer = Framebuffer(0, 0, 640, 480)
 
 
-    def __init__(self, resource_loader, game=None, background=None):
-        Renderer.__init__(self, resource_loader)
+    cdef void load_background(self, background):
+        if background is not None:
+            self.background_renderer = BackgroundRenderer(self.use_fixed_pipeline)
+            self.background_renderer.load(background)
+        else:
+            self.background_renderer = None
+
 
-        self.game = game
-        self.background = background
+    cdef void start(self, Game game):
+        self.proj = perspective(30, float(game.width) / float(game.height),
+                                101010101./2010101., 101010101./10101.)
+        game_view = setup_camera(0, 0, 1)
+        self.game_mvp = game_view * self.proj
+        self.interface_mvp = ortho_2d(0., float(game.interface.width), float(game.interface.height), 0.)
+
+
+    cdef void render(self, Game game, Window window):
+        if not self.use_fixed_pipeline:
+            self.framebuffer.bind()
+
+        self.render_game(game)
+        self.render_text(game.texts + game.native_texts)
+        self.render_interface(game.interface, game.boss)
+
+        if not self.use_fixed_pipeline:
+            self.passthrough_shader.bind()
+            self.passthrough_shader.uniform_matrix('mvp', self.interface_mvp)
+            self.render_framebuffer(self.framebuffer, window)
 
 
-    def render(self):
-        glClear(GL_DEPTH_BUFFER_BIT)
+    cdef void render_game(self, Game game):
+        cdef long game_x, game_y
+        cdef float x, y, z, dx, dy, dz, fog_data[4], fog_start, fog_end
+        cdef unsigned char fog_r, fog_g, fog_b
+        cdef Matrix mvp
 
-        back = self.background
-        game = self.game
-        texture_manager = self.texture_manager
+        game_x, game_y = game.interface.game_pos
+        glViewport(game_x, game_y, game.width, game.height)
+        glClear(GL_DEPTH_BUFFER_BIT)
+        glScissor(game_x, game_y, game.width, game.height)
+        glEnable(GL_SCISSOR_TEST)
 
-        if game is not None and game.effect is not None:
-            self.setup_camera(0, 0, 1)
+        if self.use_fixed_pipeline:
+            glMatrixMode(GL_PROJECTION)
+            glLoadIdentity()
 
-            glDisable(GL_FOG)
-            self.render_elements([game.effect])
-            glEnable(GL_FOG)
-        elif back is not None:
-            fog_b, fog_g, fog_r, fog_start, fog_end = back.fog_interpolator.values
+        if game is not None and game.spellcard_effect is not None:
+            if self.use_fixed_pipeline:
+                glMatrixMode(GL_MODELVIEW)
+                glLoadMatrixf(self.game_mvp.data)
+                glDisable(GL_FOG)
+            else:
+                self.game_shader.bind()
+                self.game_shader.uniform_matrix('mvp', self.game_mvp)
+
+            self.render_elements([game.spellcard_effect])
+        elif self.background_renderer is not None:
+            back = self.background_renderer.background
             x, y, z = back.position_interpolator.values
             dx, dy, dz = back.position2_interpolator.values
+            fog_b, fog_g, fog_r, fog_start, fog_end = back.fog_interpolator.values
 
-            glFogi(GL_FOG_MODE, GL_LINEAR)
-            glFogf(GL_FOG_START, fog_start)
-            glFogf(GL_FOG_END,  fog_end)
-            glFogfv(GL_FOG_COLOR, (GLfloat * 4)(fog_r / 255., fog_g / 255., fog_b / 255., 1.))
+            # Those two lines may come from the difference between Direct3D and
+            # OpenGL’s distance handling.  The first one seem to calculate fog
+            # from the eye, while the second does that starting from the near
+            # plane.
+            #TODO: investigate, and use a variable to keep the near plane
+            # distance at a single place.
+            fog_start -= 101010101./2010101.
+            fog_end -= 101010101./2010101.
 
-            self.setup_camera(dx, dy, dz)
-            glTranslatef(-x, -y, -z)
+            model = Matrix()
+            model.data[12] = -x
+            model.data[13] = -y
+            model.data[14] = -z
+            view = setup_camera(dx, dy, dz)
+            mvp = model * view * self.proj
 
-            glEnable(GL_DEPTH_TEST)
-            for (texture_key, blendfunc), (nb_vertices, vertices, uvs, colors) in get_background_rendering_data(back):
-                glBlendFunc(GL_SRC_ALPHA, (GL_ONE_MINUS_SRC_ALPHA, GL_ONE)[blendfunc])
-                glBindTexture(GL_TEXTURE_2D, texture_manager[texture_key].id)
-                glVertexPointer(3, GL_FLOAT, 0, vertices)
-                glTexCoordPointer(2, GL_FLOAT, 0, uvs)
-                glColorPointer(4, GL_UNSIGNED_BYTE, 0, colors)
-                glDrawArrays(GL_QUADS, 0, nb_vertices)
-            glDisable(GL_DEPTH_TEST)
+            if self.use_fixed_pipeline:
+                glMatrixMode(GL_MODELVIEW)
+                glLoadMatrixf(mvp.data)
+
+                glEnable(GL_FOG)
+                glFogi(GL_FOG_MODE, GL_LINEAR)
+                glFogf(GL_FOG_START, fog_start)
+                glFogf(GL_FOG_END,  fog_end)
+
+                fog_data[0] = fog_r / 255.
+                fog_data[1] = fog_g / 255.
+                fog_data[2] = fog_b / 255.
+                fog_data[3] = 1.
+                glFogfv(GL_FOG_COLOR, fog_data)
+            else:
+                self.background_shader.bind()
+                self.background_shader.uniform_matrix('mvp', mvp)
+
+                self.background_shader.uniform_1('fog_scale', 1. / (fog_end - fog_start))
+                self.background_shader.uniform_1('fog_end', fog_end)
+                self.background_shader.uniform_4('fog_color', fog_r / 255., fog_g / 255., fog_b / 255., 1.)
+
+            self.background_renderer.render_background()
         else:
             glClear(GL_COLOR_BUFFER_BIT)
 
         if game is not None:
-            self.setup_camera(0, 0, 1)
+            if self.use_fixed_pipeline:
+                glMatrixMode(GL_MODELVIEW)
+                glLoadMatrixf(self.game_mvp.data)
+                glDisable(GL_FOG)
+            else:
+                self.game_shader.bind()
+                self.game_shader.uniform_matrix('mvp', self.game_mvp)
 
-            glDisable(GL_FOG)
-            self.render_elements(game.enemies)
+            self.render_elements([enemy for enemy in game.enemies if enemy.visible])
             self.render_elements(game.effects)
             self.render_elements(chain(game.players_bullets,
+                                       game.lasers_sprites(),
                                        game.players,
-                                       *(player.objects() for player in game.players)))
-            self.render_elements(chain(game.bullets, game.cancelled_bullets, game.items))
-            #TODO: display item indicators
-            glEnable(GL_FOG)
+                                       game.msg_sprites()))
+            self.render_elements(chain(game.bullets, game.lasers,
+                                       game.cancelled_bullets, game.items,
+                                       game.labels))
+
+        if game.msg_runner is not None:
+            rect = Rect(48, 368, 288, 48)
+            color1 = Color(0, 0, 0, 192)
+            color2 = Color(0, 0, 0, 128)
+            self.render_quads([rect], [(color1, color1, color2, color2)], 0)
+
+        glDisable(GL_SCISSOR_TEST)
+
+
+    cdef void render_text(self, texts):
+        cdef NativeText label
+
+        if self.font_manager is None:
+            return
+
+        labels = [label for label in texts if label is not None]
+        self.font_manager.load(labels)
+
+        black = Color(0, 0, 0, 255)
+
+        for label in labels:
+            if label is None:
+                continue
+
+            rect = Rect(label.x, label.y, label.width, label.height)
+            gradient = [Color(*color, a=label.alpha) for color in label.gradient]
 
+            if label.shadow:
+                shadow_rect = Rect(label.x + 1, label.y + 1, label.width, label.height)
+                shadow = [black._replace(a=label.alpha)] * 4
+                self.render_quads([shadow_rect, rect], [shadow, gradient], label.texture)
+            else:
+                self.render_quads([rect], [gradient], label.texture)
+
+
+    cdef void render_interface(self, interface, game_boss):
+        cdef GlyphCollection label
+
+        elements = []
+
+        if self.use_fixed_pipeline:
+            glMatrixMode(GL_MODELVIEW)
+            glLoadMatrixf(self.interface_mvp.data)
+            glDisable(GL_FOG)
+        else:
+            self.interface_shader.bind()
+            self.interface_shader.uniform_matrix('mvp', self.interface_mvp)
+        glViewport(0, 0, interface.width, interface.height)
+
+        items = [item for item in interface.items if item.anmrunner and item.anmrunner.running]
+        labels = interface.labels.values()
+
+        if items:
+            # Redraw all the interface
+            elements.extend(items)
+        else:
+            # Redraw only changed labels
+            labels = [label for label in labels if label.changed]
+
+        elements.extend(interface.level_start)
+
+        if game_boss is not None:
+            elements.extend(interface.boss_items)
+
+        elements.extend(labels)
+        self.render_elements(elements)
+        for label in labels:
+            label.changed = False
rename from pytouhou/ui/gamerunner.py
rename to pytouhou/ui/gamerunner.pyx
--- a/pytouhou/ui/gamerunner.py
+++ b/pytouhou/ui/gamerunner.pyx
@@ -12,152 +12,134 @@
 ## GNU General Public License for more details.
 ##
 
-import pyglet
-import traceback
+from pytouhou.lib cimport sdl
 
-from pyglet.gl import (glMatrixMode, glLoadIdentity, glEnable,
-                       glHint, glEnableClientState, glViewport,
-                       gluPerspective, gluOrtho2D,
-                       GL_MODELVIEW, GL_PROJECTION,
-                       GL_TEXTURE_2D, GL_BLEND, GL_FOG,
-                       GL_PERSPECTIVE_CORRECTION_HINT, GL_FOG_HINT, GL_NICEST,
-                       GL_COLOR_ARRAY, GL_VERTEX_ARRAY, GL_TEXTURE_COORD_ARRAY)
-
-from pytouhou.utils.helpers import get_logger
-
-from .gamerenderer import GameRenderer
+from .window cimport Window, Runner
+from .gamerenderer cimport GameRenderer
+from .music import MusicPlayer, SFXPlayer, NullPlayer
 
 
-logger = get_logger(__name__)
+cdef class GameRunner(Runner):
+    cdef object game, background, con
+    cdef GameRenderer renderer
+    cdef Window window
+    cdef object replay_level, save_keystates
+    cdef bint skip
+
+    def __init__(self, Window window, resource_loader, bint skip=False,
+                 con=None):
+        self.renderer = GameRenderer(resource_loader, window.use_fixed_pipeline)
+
+        self.window = window
+        self.replay_level = None
+        self.skip = skip
+        self.con = con
+
+        self.width = window.width #XXX
+        self.height = window.height #XXX
 
 
-class GameRunner(pyglet.window.Window, GameRenderer):
-    def __init__(self, resource_loader, game=None, background=None, replay=None,
-                 con=None):
-        GameRenderer.__init__(self, resource_loader, game, background)
+    def load_game(self, game=None, background=None, bgms=None, replay=None, save_keystates=None):
+        self.game = game
+        self.background = background
+
+        self.renderer.texture_manager.load(game.resource_loader.instanced_anms.values())
+        self.renderer.load_background(background)
 
-        width, height = (game.width, game.height) if game else (None, None)
-        pyglet.window.Window.__init__(self, width=width, height=height,
-                                      caption='PyTouhou', resizable=False)
+        self.set_input(replay)
+        if replay and replay.levels[game.stage - 1]:
+            game.players[0].state.lives = self.replay_level.lives
+            game.players[0].state.power = self.replay_level.power
+            game.players[0].state.bombs = self.replay_level.bombs
+            game.difficulty = self.replay_level.difficulty
+
+        self.save_keystates = save_keystates
 
-        self.con = con
-        self.replay_level = None
-        if not replay or not replay.levels[game.stage-1]:
-            self.keys = pyglet.window.key.KeyStateHandler()
-            self.push_handlers(self.keys)
+        null_player = NullPlayer()
+        if bgms:
+            game.music = MusicPlayer(game.resource_loader, bgms)
+            game.music.play(0)
         else:
-            self.keys = 0
-            self.replay_level = replay.levels[game.stage-1]
+            game.music = null_player
+
+        game.sfx_player = SFXPlayer(game.resource_loader) if not self.skip else null_player
+
 
-        self.fps_display = pyglet.clock.ClockDisplay()
+    def set_input(self, replay=None):
+        if not replay or not replay.levels[self.game.stage-1]:
+            self.replay_level = None
+        else:
+            self.replay_level = replay.levels[self.game.stage-1]
+            self.keys = self.replay_level.iter_keystates()
 
 
-    def start(self, width=None, height=None):
-        width = width or (self.game.width if self.game else 640)
-        height = height or (self.game.height if self.game else 480)
-
-        if (width, height) != (self.width, self.height):
-            self.set_size(width, height)
-
-        # Initialize OpenGL
-        glEnable(GL_BLEND)
-        glEnable(GL_TEXTURE_2D)
-        glEnable(GL_FOG)
-        glHint(GL_FOG_HINT, GL_NICEST)
-        glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
-        glEnableClientState(GL_COLOR_ARRAY)
-        glEnableClientState(GL_VERTEX_ARRAY)
-        glEnableClientState(GL_TEXTURE_COORD_ARRAY)
+    cdef void start(self) except *:
+        cdef long width, height
+        width = self.game.interface.width if self.game is not None else 640
+        height = self.game.interface.height if self.game is not None else 480
+        if width != self.width or height != self.height:
+            self.window.set_size(width, height)
 
-        # Use our own loop to ensure 60 (for now, 120) fps
-        pyglet.clock.set_fps_limit(60)
-        while not self.has_exit:
-            pyglet.clock.tick()
-            self.dispatch_events()
-            self.update()
-            self.on_draw()
-            self.flip()
-
-
-    def on_resize(self, width, height):
-        glViewport(0, 0, width, height)
-
-
-    def _event_text_symbol(self, ev):
-        # XXX: Ugly workaround to a pyglet bug on X11
-        #TODO: fix that bug in pyglet
-        try:
-            return pyglet.window.Window._event_text_symbol(self, ev)
-        except Exception as exc:
-            logger.warn('Pyglet error: %s', traceback.format_exc(exc))
-            return None, None
+        self.renderer.start(self.game)
 
 
-    def on_key_press(self, symbol, modifiers):
-        if symbol == pyglet.window.key.ESCAPE:
-            self.has_exit = True
-        # XXX: Fullscreen will be enabled the day pyglet stops sucking
-        elif symbol == pyglet.window.key.F11:
-            self.set_fullscreen(not self.fullscreen)
+    cdef bint update(self) except *:
+        cdef long keystate
 
-
-    def update(self):
         if self.background:
             self.background.update(self.game.frame)
+        for event in sdl.poll_events():
+            type_ = event[0]
+            if type_ == sdl.KEYDOWN:
+                scancode = event[1]
+                if scancode == sdl.SCANCODE_ESCAPE:
+                    return False #TODO: implement the pause.
+            elif type_ == sdl.QUIT:
+                return False
+            elif type_ == sdl.WINDOWEVENT:
+                event_ = event[1]
+                if event_ == sdl.WINDOWEVENT_RESIZED:
+                    self.window.set_size(event[2], event[3])
         if self.game:
-            if not self.replay_level:
+            if self.replay_level is None:
                 #TODO: allow user settings
+                keys = sdl.get_keyboard_state()
                 keystate = 0
-                if self.keys[pyglet.window.key.W]:
+                if keys[sdl.SCANCODE_Z]:
                     keystate |= 1
-                if self.keys[pyglet.window.key.X]:
+                if keys[sdl.SCANCODE_X]:
                     keystate |= 2
-                #TODO: on some configurations, LSHIFT is Shift_L when pressed
-                # and ISO_Prev_Group when released, confusing the hell out of pyglet
-                # and leading to a always-on LSHIFT...
-                if self.keys[pyglet.window.key.LSHIFT]:
+                if keys[sdl.SCANCODE_LSHIFT]:
                     keystate |= 4
-                if self.keys[pyglet.window.key.UP]:
+                if keys[sdl.SCANCODE_UP]:
                     keystate |= 16
-                if self.keys[pyglet.window.key.DOWN]:
+                if keys[sdl.SCANCODE_DOWN]:
                     keystate |= 32
-                if self.keys[pyglet.window.key.LEFT]:
+                if keys[sdl.SCANCODE_LEFT]:
                     keystate |= 64
-                if self.keys[pyglet.window.key.RIGHT]:
+                if keys[sdl.SCANCODE_RIGHT]:
                     keystate |= 128
-                if self.keys[pyglet.window.key.LCTRL]:
+                if keys[sdl.SCANCODE_LCTRL]:
                     keystate |= 256
             else:
-                keystate = 0
-                for frame, _keystate, unknown in self.replay_level.keys:
-                    if self.game.frame < frame:
-                        break
-                    else:
-                        keystate = _keystate
+                try:
+                    keystate = self.keys.next()
+                except StopIteration:
+                    keystate = 0
+                    if self.skip:
+                        self.set_input()
+                        self.skip = False
+                        self.game.sfx_player = SFXPlayer(self.game.resource_loader)
+
+            if self.save_keystates is not None:
+                self.save_keystates.append(keystate)
 
             if self.con:
                 self.con.run_iter(self.game, keystate)
             else:
                 self.game.run_iter([keystate])
 
-
-    def on_draw(self):
-        # Switch to game projection
-        #TODO: move that to GameRenderer?
-        glMatrixMode(GL_PROJECTION)
-        glLoadIdentity()
-        gluPerspective(30, float(self.width) / float(self.height),
-                       101010101./2010101., 101010101./10101.)
-
-        GameRenderer.render(self)
-
-        # Get back to standard orthographic projection
-        glMatrixMode(GL_PROJECTION)
-        glLoadIdentity()
-        glMatrixMode(GL_MODELVIEW)
-        glLoadIdentity()
-
-        #TODO: draw interface
-        gluOrtho2D(0., float(self.game.width), 0., float(self.game.height))
-        self.fps_display.draw()
-
+            self.game.interface.labels['framerate'].set_text('%.2ffps' % self.window.get_fps())
+        if not self.skip:
+            self.renderer.render(self.game, self.window)
+        return True
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/music.pyx
@@ -0,0 +1,98 @@
+# -*- 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 os.path import join
+from glob import glob
+from pytouhou.lib cimport sdl
+from pytouhou.utils.helpers import get_logger
+
+logger = get_logger(__name__)
+
+
+class MusicPlayer(object):
+    def __init__(self, resource_loader, bgms):
+        self.bgms = []
+        for bgm in bgms:
+            if not bgm:
+                self.bgms.append(None)
+                continue
+            posname = bgm[1].replace('bgm/', '').replace('.mid', '.pos')
+            try:
+                track = resource_loader.get_track(posname)
+            except KeyError:
+                self.bgms.append(None)
+                logger.warn(u'Music description “%s” not found.', posname)
+                continue
+            globname = join(resource_loader.game_dir, bgm[1].encode('ascii')).replace('.mid', '.*')
+            filenames = glob(globname)
+            for filename in reversed(filenames):
+                try:
+                    source = sdl.load_music(filename)
+                except sdl.SDLError as error:
+                    logger.debug(u'Music file “%s” unreadable: %s', filename, error)
+                    continue
+                else:
+                    source.set_loop_points(track.start / 44100., track.end / 44100.) #TODO: retrieve the sample rate from the actual track.
+                    self.bgms.append(source)
+                    logger.debug(u'Music file “%s” opened.', filename)
+                    break
+            else:
+                self.bgms.append(None)
+                logger.warn(u'No working music file for “%s”, disabling bgm.', globname)
+
+    def play(self, index):
+        cdef sdl.Music bgm
+        bgm = self.bgms[index]
+        if bgm:
+            bgm.play(-1)
+
+
+class SFXPlayer(object):
+    def __init__(self, loader, volume=.42):
+        self.loader = loader
+        self.channels = {}
+        self.sounds = {}
+        self.volume = volume
+        self.next_channel = 0
+
+    def get_channel(self, name):
+        if name not in self.channels:
+            self.channels[name] = self.next_channel
+            self.next_channel += 1
+        return self.channels[name]
+
+    def get_sound(self, name):
+        if name not in self.sounds:
+            wave_file = self.loader.get_file(name)
+            chunk = sdl.load_chunk(wave_file)
+            chunk.set_volume(self.volume)
+            self.sounds[name] = chunk
+        return self.sounds[name]
+
+    def play(self, name, volume=None):
+        cdef sdl.Chunk sound
+        sound = self.get_sound(name)
+        channel = self.get_channel(name)
+        if volume:
+            sdl.mix_volume(channel, volume)
+        sound.play(channel, 0)
+
+
+class NullPlayer(object):
+    def __init__(self, loader=None, bgms=None):
+        pass
+
+    def play(self, name, volume=None):
+        pass
--- a/pytouhou/ui/renderer.pxd
+++ b/pytouhou/ui/renderer.pxd
@@ -1,12 +1,36 @@
+from cpython cimport PyObject
+from .window cimport Window
+from pytouhou.lib.opengl cimport GLuint
+
 cdef struct Vertex:
     int x, y, z
     float u, v
     unsigned char r, g, b, a
 
 
+cdef struct PassthroughVertex:
+    int x, y
+    float u, v
+
+
 cdef class Renderer:
-    cdef public texture_manager
+    cdef public texture_manager, font_manager
+    cdef GLuint vbo, framebuffer_vbo
     cdef Vertex *vertex_buffer
 
-    cpdef render_elements(self, elements)
-    cpdef setup_camera(self, dx, dy, dz)
+    cdef bint use_fixed_pipeline #XXX
+
+    cdef unsigned short *indices[2][MAX_TEXTURES]
+    cdef unsigned short last_indices[2 * MAX_TEXTURES]
+    cdef PyObject *elements[640*3]
+
+    cdef void render_elements(self, elements) except *
+    cdef void render_quads(self, rects, colors, texture) except *
+    cdef void render_framebuffer(self, Framebuffer fb, Window window) except *
+
+
+cdef class Framebuffer:
+    cdef GLuint fbo, texture, rbo
+    cdef int x, y, width, height
+
+    cpdef bind(self)
--- a/pytouhou/ui/renderer.pyx
+++ b/pytouhou/ui/renderer.pyx
@@ -13,81 +13,273 @@
 ##
 
 from libc.stdlib cimport malloc, free
-
-import ctypes
-
-from struct import pack
+from libc.string cimport memset
+from os.path import join
 
-from pyglet.gl import *
+from pytouhou.lib.opengl cimport \
+         (glVertexPointer, glTexCoordPointer, glColorPointer,
+          glVertexAttribPointer, glEnableVertexAttribArray, glBlendFunc,
+          glBindTexture, glDrawElements, glBindBuffer, glBufferData,
+          GL_ARRAY_BUFFER, GL_DYNAMIC_DRAW, GL_UNSIGNED_BYTE,
+          GL_UNSIGNED_SHORT, GL_INT, GL_FLOAT, GL_SRC_ALPHA,
+          GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO, GL_TEXTURE_2D, GL_TRIANGLES,
+          glGenBuffers, glBindFramebuffer, glViewport, glDeleteBuffers,
+          glGenTextures, glTexParameteri, glTexImage2D, glGenRenderbuffers,
+          glBindRenderbuffer, glRenderbufferStorage, glGenFramebuffers,
+          glFramebufferTexture2D, glFramebufferRenderbuffer,
+          glCheckFramebufferStatus, GL_FRAMEBUFFER, GL_TEXTURE_MIN_FILTER,
+          GL_LINEAR, GL_TEXTURE_MAG_FILTER, GL_RGBA, GL_RENDERBUFFER,
+          GL_DEPTH_COMPONENT, GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT,
+          GL_FRAMEBUFFER_COMPLETE, glClear, GL_COLOR_BUFFER_BIT,
+          GL_DEPTH_BUFFER_BIT)
 
+from pytouhou.lib.sdl import SDLError
+
+from pytouhou.game.element cimport Element
 from .sprite cimport get_sprite_rendering_data
-from .texture cimport TextureManager
+from .texture import TextureManager, FontManager
+
+from pytouhou.utils.helpers import get_logger
+
+logger = get_logger(__name__)
 
 
-MAX_ELEMENTS = 10000
+DEF MAX_ELEMENTS = 640*4*3
+
+
+cdef long find_objects(Renderer self, object elements) except -1:
+    # Don’t type element as Element, or else the overriding of objects won’t work.
+    cdef Element obj
+    cdef long i = 0
+    for element in elements:
+        for obj in element.objects:
+            sprite = obj.sprite
+            if sprite and sprite.visible:
+                # warning: no reference is preserved on the object—assuming the object will not die accidentally
+                self.elements[i] = <PyObject*>obj
+                i += 1
+                if i >= 640*3-4:
+                    return i
+    return i
 
 
 cdef class Renderer:
     def __cinit__(self):
-        # Allocate buffers
         self.vertex_buffer = <Vertex*> malloc(MAX_ELEMENTS * sizeof(Vertex))
 
 
     def __dealloc__(self):
         free(self.vertex_buffer)
 
+        if not self.use_fixed_pipeline:
+            glDeleteBuffers(1, &self.framebuffer_vbo)
+            glDeleteBuffers(1, &self.vbo)
+
 
     def __init__(self, resource_loader):
-        self.texture_manager = TextureManager(resource_loader)
+        self.texture_manager = TextureManager(resource_loader, self)
+        font_name = join(resource_loader.game_dir, 'font.ttf')
+        try:
+            self.font_manager = FontManager(font_name, 16, self)
+        except SDLError:
+            self.font_manager = None
+            logger.error('Font file “%s” not found, disabling text rendering altogether.', font_name)
+
+        if not self.use_fixed_pipeline:
+            glGenBuffers(1, &self.vbo)
+            glGenBuffers(1, &self.framebuffer_vbo)
+
+
+    def add_texture(self, int texture):
+        for i in xrange(2):
+            self.indices[i][texture] = <unsigned short*> malloc(65536 * sizeof(unsigned short))
+
+
+    def remove_texture(self, int texture):
+        for i in xrange(2):
+            free(self.indices[i][texture])
+
+
+    cdef void render_elements(self, elements):
+        cdef int key
+        cdef int x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4, ox, oy
+        cdef float left, right, bottom, top
+        cdef unsigned char r, g, b, a
+
+        nb_vertices = 0
+        memset(self.last_indices, 0, sizeof(self.last_indices))
+
+        nb_elements = find_objects(self, elements)
+        for element_idx in xrange(nb_elements):
+            element = <object>self.elements[element_idx]
+            sprite = element.sprite
+            ox, oy = element.x, element.y
+            key, (vertices, uvs, colors) = get_sprite_rendering_data(sprite)
+
+            blendfunc = key // MAX_TEXTURES
+            texture = key % MAX_TEXTURES
+
+            rec = self.indices[blendfunc][texture]
+            next_indice = self.last_indices[key]
+
+            # Pack data in buffer
+            x1, x2, x3, x4, y1, y2, y3, y4, z1, z2, z3, z4 = vertices
+            left, right, bottom, top = uvs
+            r, g, b, a = colors
+            self.vertex_buffer[nb_vertices] = Vertex(x1 + ox, y1 + oy, z1, left, bottom, r, g, b, a)
+            self.vertex_buffer[nb_vertices+1] = Vertex(x2 + ox, y2 + oy, z2, right, bottom, r, g, b, a)
+            self.vertex_buffer[nb_vertices+2] = Vertex(x3 + ox, y3 + oy, z3, right, top, r, g, b, a)
+            self.vertex_buffer[nb_vertices+3] = Vertex(x4 + ox, y4 + oy, z4, left, top, r, g, b, a)
+
+            # Add indices
+            rec[next_indice] = nb_vertices
+            rec[next_indice+1] = nb_vertices + 1
+            rec[next_indice+2] = nb_vertices + 2
+            rec[next_indice+3] = nb_vertices + 2
+            rec[next_indice+4] = nb_vertices + 3
+            rec[next_indice+5] = nb_vertices
+            self.last_indices[key] += 6
+
+            nb_vertices += 4
+
+        if nb_vertices == 0:
+            return
+
+        if self.use_fixed_pipeline:
+            glVertexPointer(3, GL_INT, sizeof(Vertex), &self.vertex_buffer[0].x)
+            glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &self.vertex_buffer[0].u)
+            glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(Vertex), &self.vertex_buffer[0].r)
+        else:
+            glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
+            glBufferData(GL_ARRAY_BUFFER, nb_vertices * sizeof(Vertex), &self.vertex_buffer[0], GL_DYNAMIC_DRAW)
+
+            #TODO: find a way to use offsetof() instead of those ugly hardcoded values.
+            glVertexAttribPointer(0, 3, GL_INT, False, sizeof(Vertex), <void*>0)
+            glEnableVertexAttribArray(0)
+            glVertexAttribPointer(1, 2, GL_FLOAT, False, sizeof(Vertex), <void*>12)
+            glEnableVertexAttribArray(1)
+            glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, True, sizeof(Vertex), <void*>20)
+            glEnableVertexAttribArray(2)
+
+        for key in xrange(2 * MAX_TEXTURES):
+            nb_indices = self.last_indices[key]
+            if not nb_indices:
+                continue
+
+            blendfunc = key // MAX_TEXTURES
+            texture = key % MAX_TEXTURES
+
+            glBlendFunc(GL_SRC_ALPHA, (GL_ONE_MINUS_SRC_ALPHA, GL_ONE)[blendfunc])
+            glBindTexture(GL_TEXTURE_2D, texture)
+            glDrawElements(GL_TRIANGLES, nb_indices, GL_UNSIGNED_SHORT, self.indices[blendfunc][texture])
+
+        if not self.use_fixed_pipeline:
+            glBindBuffer(GL_ARRAY_BUFFER, 0)
 
 
-    cpdef render_elements(self, elements):
-        cdef unsigned short nb_vertices = 0
-
-        indices_by_texture = {}
+    cdef void render_quads(self, rects, colors, texture):
+        # There is nothing that batch more than two quads on the same texture, currently.
+        cdef Vertex buf[8]
+        cdef unsigned short indices[12]
+        indices[:] = [0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4]
 
-        for element in elements:
-            sprite = element._sprite
-            if sprite:
-                ox, oy = element.x, element.y
-                key, (vertices, uvs, colors) = get_sprite_rendering_data(sprite)
-                rec = indices_by_texture.setdefault(key, [])
+        length = len(rects)
+        assert length == len(colors)
+
+        for i, r in enumerate(rects):
+            c1, c2, c3, c4 = colors[i]
+
+            buf[4*i] = Vertex(r.x, r.y, 0, 0, 0, c1.r, c1.g, c1.b, c1.a)
+            buf[4*i+1] = Vertex(r.x + r.w, r.y, 0, 1, 0, c2.r, c2.g, c2.b, c2.a)
+            buf[4*i+2] = Vertex(r.x + r.w, r.y + r.h, 0, 1, 1, c3.r, c3.g, c3.b, c3.a)
+            buf[4*i+3] = Vertex(r.x, r.y + r.h, 0, 0, 1, c4.r, c4.g, c4.b, c4.a)
 
-                # Pack data in buffer
-                (x1, y1, z1), (x2, y2, z2), (x3, y3, z3), (x4, y4, z4) = vertices
-                r1, g1, b1, a1, r2, g2, b2, a2, r3, g3, b3, a3, r4, g4, b4, a4 = colors
-                u1, v1, u2, v2, u3, v3, u4, v4 = uvs
-                self.vertex_buffer[nb_vertices] = Vertex(x1 + ox, y1 + oy, z1, u1, v1, r1, g1, b1, a1)
-                self.vertex_buffer[nb_vertices+1] = Vertex(x2 + ox, y2 + oy, z2, u2, v2, r2, g2, b2, a2)
-                self.vertex_buffer[nb_vertices+2] = Vertex(x3 + ox, y3 + oy, z3, u3, v3, r3, g3, b3, a3)
-                self.vertex_buffer[nb_vertices+3] = Vertex(x4 + ox, y4 + oy, z4, u4, v4, r4, g4, b4, a4)
-
-                # Add indices
-                index = nb_vertices
-                rec.extend((index, index + 1, index + 2, index + 3))
+        if self.use_fixed_pipeline:
+            glVertexPointer(3, GL_INT, sizeof(Vertex), &buf[0].x)
+            glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &buf[0].u)
+            glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(Vertex), &buf[0].r)
+        else:
+            glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
+            glBufferData(GL_ARRAY_BUFFER, 4 * length * sizeof(Vertex), buf, GL_DYNAMIC_DRAW)
 
-                nb_vertices += 4
+            #TODO: find a way to use offsetof() instead of those ugly hardcoded values.
+            glVertexAttribPointer(0, 3, GL_INT, False, sizeof(Vertex), <void*>0)
+            glEnableVertexAttribArray(0)
+            glVertexAttribPointer(1, 2, GL_FLOAT, False, sizeof(Vertex), <void*>12)
+            glEnableVertexAttribArray(1)
+            glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, True, sizeof(Vertex), <void*>20)
+            glEnableVertexAttribArray(2)
 
-        for (texture_key, blendfunc), indices in indices_by_texture.items():
-            glVertexPointer(3, GL_INT, 24, <long> &self.vertex_buffer[0].x)
-            glTexCoordPointer(2, GL_FLOAT, 24, <long> &self.vertex_buffer[0].u)
-            glColorPointer(4, GL_UNSIGNED_BYTE, 24, <long> &self.vertex_buffer[0].r)
+        glBindTexture(GL_TEXTURE_2D, texture)
+        glDrawElements(GL_TRIANGLES, 6 * length, GL_UNSIGNED_SHORT, indices)
 
-            nb_indices = len(indices)
-            indices = pack(str(nb_indices) + 'H', *indices)
-            glBlendFunc(GL_SRC_ALPHA, (GL_ONE_MINUS_SRC_ALPHA, GL_ONE)[blendfunc])
-            glBindTexture(GL_TEXTURE_2D, self.texture_manager[texture_key].id)
-            glDrawElements(GL_QUADS, nb_indices, GL_UNSIGNED_SHORT, indices)
+        if not self.use_fixed_pipeline:
+            glBindBuffer(GL_ARRAY_BUFFER, 0)
 
 
-    cpdef setup_camera(self, dx, dy, dz):
-            glMatrixMode(GL_MODELVIEW)
-            glLoadIdentity()
-            # Some explanations on the magic constants:
-            # 192. = 384. / 2. = width / 2.
-            # 224. = 448. / 2. = height / 2.
-            # 835.979370 = 224./math.tan(math.radians(15)) = (height/2.)/math.tan(math.radians(fov/2))
-            # This is so that objects on the (O, x, y) plane use pixel coordinates
-            gluLookAt(192., 224., - 835.979370 * dz,
-                      192. + dx, 224. - dy, 0., 0., -1., 0.)
+    cdef void render_framebuffer(self, Framebuffer fb, Window window):
+        cdef PassthroughVertex[4] buf
+        cdef unsigned short indices[6]
+        indices[:] = [0, 1, 2, 2, 3, 0]
+
+        assert not self.use_fixed_pipeline
+
+        glBindFramebuffer(GL_FRAMEBUFFER, 0)
+        glViewport(window.x, window.y, window.width, window.height)
+        glBlendFunc(GL_ONE, GL_ZERO)
+        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+
+        glBindBuffer(GL_ARRAY_BUFFER, self.framebuffer_vbo)
+
+        #TODO: find a way to use offsetof() instead of those ugly hardcoded values.
+        glVertexAttribPointer(0, 2, GL_INT, False, sizeof(PassthroughVertex), <void*>0)
+        glEnableVertexAttribArray(0)
+        glVertexAttribPointer(1, 2, GL_FLOAT, False, sizeof(PassthroughVertex), <void*>8)
+        glEnableVertexAttribArray(1)
+
+        buf[0] = PassthroughVertex(fb.x, fb.y, 0, 1)
+        buf[1] = PassthroughVertex(fb.x + fb.width, fb.y, 1, 1)
+        buf[2] = PassthroughVertex(fb.x + fb.width, fb.y + fb.height, 1, 0)
+        buf[3] = PassthroughVertex(fb.x, fb.y + fb.height, 0, 0)
+        glBufferData(GL_ARRAY_BUFFER, 4 * sizeof(PassthroughVertex), buf, GL_DYNAMIC_DRAW)
+
+        glBindTexture(GL_TEXTURE_2D, fb.texture)
+        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices)
+        glBindTexture(GL_TEXTURE_2D, 0)
+
+        glBindBuffer(GL_ARRAY_BUFFER, 0)
+
 
+cdef class Framebuffer:
+    def __init__(self, int x, int y, int width, int height):
+        self.x = x
+        self.y = y
+        self.width = width
+        self.height = height
+
+        glGenTextures(1, &self.texture)
+        glBindTexture(GL_TEXTURE_2D, self.texture)
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
+        glTexImage2D(GL_TEXTURE_2D, 0,
+                     GL_RGBA,
+                     width, height,
+                     0,
+                     GL_RGBA, GL_UNSIGNED_BYTE,
+                     NULL)
+        glBindTexture(GL_TEXTURE_2D, 0)
+
+        glGenRenderbuffers(1, &self.rbo)
+        glBindRenderbuffer(GL_RENDERBUFFER, self.rbo)
+        glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height)
+        glBindRenderbuffer(GL_RENDERBUFFER, 0)
+
+        glGenFramebuffers(1, &self.fbo)
+        glBindFramebuffer(GL_FRAMEBUFFER, self.fbo)
+        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self.texture, 0)
+        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, self.rbo)
+        assert glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE
+        glBindFramebuffer(GL_FRAMEBUFFER, 0)
+
+    cpdef bind(self):
+        glBindFramebuffer(GL_FRAMEBUFFER, self.fbo)
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/shader.pxd
@@ -0,0 +1,15 @@
+from pytouhou.lib.opengl cimport GLuint, GLint, GLchar, GLenum, GLfloat
+from pytouhou.utils.matrix cimport Matrix
+
+cdef class Shader:
+    cdef GLuint handle
+    cdef bint linked
+    cdef dict location_cache
+
+    cdef void create_shader(self, const GLchar *string, GLenum shader_type) except *
+    cdef void link(self) except *
+    cdef GLint get_uniform_location(self, name) except -1
+    cdef void bind(self) nogil
+    cdef void uniform_1(self, name, GLfloat val) except *
+    cdef void uniform_4(self, name, GLfloat a, GLfloat b, GLfloat c, GLfloat d) except *
+    cdef void uniform_matrix(self, name, Matrix mat) except *
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/shader.pyx
@@ -0,0 +1,142 @@
+# -*- encoding: utf-8 -*-
+#
+# Copyright Tristam Macdonald 2008.
+# Copyright Emmanuel Gil Peyrot 2012.
+#
+# Distributed under the Boost Software License, Version 1.0
+# (see http://www.boost.org/LICENSE_1_0.txt)
+#
+# Source: https://swiftcoder.wordpress.com/2008/12/19/simple-glsl-wrapper-for-pyglet/
+#
+
+from pytouhou.lib.opengl cimport \
+         (glCreateProgram, glCreateShader, GL_VERTEX_SHADER,
+          GL_FRAGMENT_SHADER, glShaderSource, glCompileShader, glGetShaderiv,
+          GL_COMPILE_STATUS, GL_INFO_LOG_LENGTH, glGetShaderInfoLog,
+          glAttachShader, glLinkProgram, glGetProgramiv, glGetProgramInfoLog,
+          GL_LINK_STATUS, glUseProgram, glGetUniformLocation, glUniform1fv,
+          glUniform4fv, glUniformMatrix4fv, glBindAttribLocation)
+
+from libc.stdlib cimport malloc, free
+
+
+class GLSLException(Exception):
+    pass
+
+
+cdef class Shader:
+    # vert and frag take arrays of source strings the arrays will be
+    # concattenated into one string by OpenGL
+    def __init__(self, vert=None, frag=None):
+        # create the program handle
+        self.handle = glCreateProgram()
+        # we are not linked yet
+        self.linked = False
+
+        # cache the uniforms location
+        self.location_cache = {}
+
+        # create the vertex shader
+        self.create_shader(vert[0], GL_VERTEX_SHADER)
+        # create the fragment shader
+        self.create_shader(frag[0], GL_FRAGMENT_SHADER)
+
+        #TODO: put those elsewhere.
+        glBindAttribLocation(self.handle, 0, 'in_position')
+        glBindAttribLocation(self.handle, 1, 'in_texcoord')
+        glBindAttribLocation(self.handle, 2, 'in_color')
+
+        # attempt to link the program
+        self.link()
+
+    cdef void create_shader(self, const GLchar *string, GLenum shader_type):
+        cdef GLint temp
+        cdef const GLchar **strings = &string
+
+        # create the shader handle
+        shader = glCreateShader(shader_type)
+
+        # upload the source strings
+        glShaderSource(shader, 1, strings, NULL)
+
+        # compile the shader
+        glCompileShader(shader)
+
+        # retrieve the compile status
+        glGetShaderiv(shader, GL_COMPILE_STATUS, &temp)
+
+        # if compilation failed, print the log
+        if not temp:
+            # retrieve the log length
+            glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &temp)
+            # create a buffer for the log
+            temp_buf = <GLchar*>malloc(temp * sizeof(GLchar))
+            # retrieve the log text
+            glGetShaderInfoLog(shader, temp, NULL, temp_buf)
+            buf = temp_buf[:temp]
+            free(temp_buf)
+            # print the log to the console
+            raise GLSLException(buf)
+        else:
+            # all is well, so attach the shader to the program
+            glAttachShader(self.handle, shader)
+
+    cdef void link(self):
+        cdef GLint temp
+
+        # link the program
+        glLinkProgram(self.handle)
+
+        # retrieve the link status
+        glGetProgramiv(self.handle, GL_LINK_STATUS, &temp)
+
+        # if linking failed, print the log
+        if not temp:
+            #   retrieve the log length
+            glGetProgramiv(self.handle, GL_INFO_LOG_LENGTH, &temp)
+            # create a buffer for the log
+            temp_buf = <GLchar*>malloc(temp * sizeof(GLchar))
+            # retrieve the log text
+            glGetProgramInfoLog(self.handle, temp, NULL, temp_buf)
+            buf = temp_buf[:temp]
+            free(temp_buf)
+            # print the log to the console
+            raise GLSLException(buf)
+        else:
+            # all is well, so we are linked
+            self.linked = True
+
+    cdef GLint get_uniform_location(self, name):
+        if name not in self.location_cache:
+            loc = glGetUniformLocation(self.handle, name)
+            if loc == -1:
+                raise GLSLException('Undefined {} uniform.'.format(name))
+            self.location_cache[name] = loc
+        return self.location_cache[name]
+
+    cdef void bind(self) nogil:
+        # bind the program
+        glUseProgram(self.handle)
+
+    # upload a floating point uniform
+    # this program must be currently bound
+    cdef void uniform_1(self, name, GLfloat val):
+        glUniform1fv(self.get_uniform_location(name), 1, &val)
+
+    # upload a vec4 uniform
+    cdef void uniform_4(self, name, GLfloat a, GLfloat b, GLfloat c, GLfloat d):
+        cdef GLfloat vals[4]
+        vals[0] = a
+        vals[1] = b
+        vals[2] = c
+        vals[3] = d
+        glUniform4fv(self.get_uniform_location(name), 1, vals)
+
+    # upload a uniform matrix
+    # works with matrices stored as lists,
+    # as well as euclid matrices
+    cdef void uniform_matrix(self, name, Matrix mat):
+        # obtain the uniform location
+        loc = self.get_uniform_location(name)
+        # uplaod the 4x4 floating point matrix
+        glUniformMatrix4fv(loc, 1, False, mat.data)
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/shaders/eosd.py
@@ -0,0 +1,123 @@
+# -*- 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.ui.shader import Shader
+
+
+class GameShader(Shader):
+    def __init__(self):
+        Shader.__init__(self, ['''
+            #version 120
+
+            attribute vec3 in_position;
+            attribute vec2 in_texcoord;
+            attribute vec4 in_color;
+
+            uniform mat4 mvp;
+
+            varying vec2 texcoord;
+            varying vec4 color;
+
+            void main()
+            {
+                gl_Position = mvp * vec4(in_position, 1.0);
+                texcoord = in_texcoord;
+                color = in_color;
+            }
+        '''], ['''
+            #version 120
+
+            varying vec2 texcoord;
+            varying vec4 color;
+
+            uniform sampler2D color_map;
+
+            void main()
+            {
+                gl_FragColor = texture2D(color_map, texcoord) * color;
+            }
+        '''])
+
+
+class BackgroundShader(Shader):
+    def __init__(self):
+        Shader.__init__(self, ['''
+            #version 120
+
+            attribute vec3 in_position;
+            attribute vec2 in_texcoord;
+            attribute vec4 in_color;
+
+            uniform mat4 mvp;
+
+            varying vec2 texcoord;
+            varying vec4 color;
+
+            void main()
+            {
+                gl_Position = mvp * vec4(in_position, 1.0);
+                texcoord = in_texcoord;
+                color = in_color;
+            }
+        '''], ['''
+            #version 120
+
+            varying vec2 texcoord;
+            varying vec4 color;
+
+            uniform sampler2D color_map;
+            uniform float fog_scale;
+            uniform float fog_end;
+            uniform vec4 fog_color;
+
+            void main()
+            {
+                vec4 temp_color = texture2D(color_map, texcoord) * color;
+                float depth = gl_FragCoord.z / gl_FragCoord.w;
+                float fog_density = clamp((fog_end - depth) * fog_scale, 0.0f, 1.0f);
+                gl_FragColor = vec4(mix(fog_color, temp_color, fog_density).rgb, temp_color.a);
+            }
+        '''])
+
+
+class PassthroughShader(Shader):
+    def __init__(self):
+        Shader.__init__(self, ['''
+            #version 120
+
+            attribute vec2 in_position;
+            attribute vec2 in_texcoord;
+
+            uniform mat4 mvp;
+
+            varying vec2 texcoord;
+
+            void main()
+            {
+                gl_Position = mvp * vec4(in_position, 0.0, 1.0);
+                texcoord = in_texcoord;
+            }
+        '''], ['''
+            #version 120
+
+            varying vec2 texcoord;
+
+            uniform sampler2D color_map;
+
+            void main()
+            {
+                gl_FragColor = texture2D(color_map, texcoord);
+            }
+        '''])
--- a/pytouhou/ui/sprite.pxd
+++ b/pytouhou/ui/sprite.pxd
@@ -1,1 +1,3 @@
-cpdef object get_sprite_rendering_data(object sprite)
+from pytouhou.game.sprite cimport Sprite
+
+cpdef object get_sprite_rendering_data(Sprite sprite)
--- a/pytouhou/ui/sprite.pyx
+++ b/pytouhou/ui/sprite.pyx
@@ -13,21 +13,25 @@
 ##
 
 
-from math import pi
+from libc.math cimport M_PI as pi
 
 from pytouhou.utils.matrix cimport Matrix
 
 
-cpdef object get_sprite_rendering_data(object sprite):
+cpdef object get_sprite_rendering_data(Sprite sprite):
     cdef Matrix vertmat
+    cdef double tx, ty, tw, th, sx, sy, rx, ry, rz, tox, toy
+    cdef object tmp1, tmp2
 
-    if not sprite._changed:
+    if not sprite.changed:
         return sprite._rendering_data
 
-    vertmat = Matrix([[-.5,     .5,     .5,    -.5],
-                      [-.5,    -.5,     .5,     .5],
-                      [ .0,     .0,     .0,     .0],
-                      [ 1.,     1.,     1.,     1.]])
+    tmp1 = .5
+    tmp2 = -.5
+    vertmat = Matrix([tmp2, tmp1, tmp1, tmp2,
+                      tmp2, tmp2, tmp1, tmp1,
+                      0,    0,    0,    0,
+                      1,    1,    1,    1])
 
     tx, ty, tw, th = sprite.texcoords
     sx, sy = sprite.rescale
@@ -44,33 +48,29 @@ cpdef object get_sprite_rendering_data(o
     elif sprite.force_rotation:
         rz += sprite.angle
 
-    if (rx, ry, rz) != (0., 0., 0.):
-        if rx:
-            vertmat.rotate_x(-rx)
-        if ry:
-            vertmat.rotate_y(ry)
-        if rz:
-            vertmat.rotate_z(-rz) #TODO: minus, really?
-    if sprite.corner_relative_placement: # Reposition
-        vertmat.translate(width / 2., height / 2., 0.)
+    if rx:
+        vertmat.rotate_x(-rx)
+    if ry:
+        vertmat.rotate_y(ry)
+    if rz:
+        vertmat.rotate_z(-rz) #TODO: minus, really?
     if sprite.allow_dest_offset:
         vertmat.translate(sprite.dest_offset[0], sprite.dest_offset[1], sprite.dest_offset[2])
+    if sprite.corner_relative_placement: # Reposition
+        vertmat.translate(width / 2, height / 2, 0)
 
-    x_1 = 1. / sprite.anm.size[0]
-    y_1 = 1. / sprite.anm.size[1]
+    x_1 = 1 / <double>sprite.anm.size[0]
+    y_1 = 1 / <double>sprite.anm.size[1]
     tox, toy = sprite.texoffsets
-    uvs = [tx * x_1 + tox,         1. - (ty * y_1 + toy),
-           (tx + tw) * x_1 + tox,  1. - (ty * y_1 + toy),
-           (tx + tw) * x_1 + tox,  1. - ((ty + th) * y_1 + toy),
-           tx * x_1 + tox,         1. - ((ty + th) * y_1 + toy)]
+    uvs = (tx * x_1 + tox,
+           (tx + tw) * x_1 + tox,
+           ty * y_1 + toy,
+           (ty + th) * y_1 + toy)
 
-    (x1, x2 , x3, x4), (y1, y2, y3, y4), (z1, z2, z3, z4), _ = vertmat.data
-
-    key = (sprite.anm.first_name, sprite.anm.secondary_name), sprite.blendfunc
+    key = MAX_TEXTURES * sprite.blendfunc + <long>sprite.anm.texture
     r, g, b = sprite.color
-    values = ((x1, y1, z1), (x2, y2, z2), (x3, y3, z3), (x4, y4, z4)), uvs, [r, g, b, sprite.alpha] * 4
+    values = tuple([x for x in vertmat.data[:12]]), uvs, (r, g, b, sprite.alpha)
     sprite._rendering_data = key, values
-    sprite._changed = False
+    sprite.changed = False
 
     return sprite._rendering_data
-
deleted file mode 100644
--- a/pytouhou/ui/texture.pxd
+++ /dev/null
@@ -1,3 +0,0 @@
-cdef class TextureManager:
-    cdef public object loader
-    cdef public dict textures
--- a/pytouhou/ui/texture.pyx
+++ b/pytouhou/ui/texture.pyx
@@ -12,54 +12,131 @@
 ## GNU General Public License for more details.
 ##
 
-import pyglet
-from pyglet.gl import (glTexParameteri,
-                       GL_TEXTURE_MIN_FILTER, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
+from pytouhou.lib.opengl cimport \
+         (glTexParameteri, GL_TEXTURE_MIN_FILTER, GL_TEXTURE_MAG_FILTER,
+          GL_LINEAR, GL_BGRA, GL_RGBA, GL_RGB, GL_LUMINANCE, GL_UNSIGNED_BYTE,
+          GL_UNSIGNED_SHORT_5_6_5, GL_UNSIGNED_SHORT_4_4_4_4_REV,
+          glGenTextures, glBindTexture, glTexImage2D, GL_TEXTURE_2D, GLuint,
+          glDeleteTextures)
+
+from pytouhou.lib.sdl cimport load_png, create_rgb_surface, Font
+from pytouhou.formats.thtx import Texture #TODO: perhaps define that elsewhere?
+from pytouhou.game.text cimport NativeText
+
 import os
 
 
-cdef class TextureManager:
-    def __init__(self, loader=None):
+class TextureId(int):
+    def __del__(self):
+        cdef GLuint texture = self
+        glDeleteTextures(1, &texture)
+        self.renderer.remove_texture(self)
+
+
+class TextureManager(object):
+    def __init__(self, loader=None, renderer=None):
         self.loader = loader
-        self.textures = {}
+        self.renderer = renderer
+
+
+    def load(self, anm_list):
+        for anm in sorted(anm_list, key=lambda x: x[0].first_name.endswith('ascii.png')):
+            for entry in anm:
+                if not hasattr(entry, 'texture'):
+                    texture = decode_png(self.loader, entry.first_name, entry.secondary_name)
+                    entry.texture = load_texture(texture)
+                elif not isinstance(entry.texture, TextureId):
+                    entry.texture = load_texture(entry.texture)
+                self.renderer.add_texture(entry.texture)
+                entry.texture.renderer = self.renderer
 
 
-    def __getitem__(self, key):
-        if not key in self.textures:
-            self.textures[key] = self.load_texture(key)
-        return self.textures[key]
+cdef class FontManager:
+    cdef Font font
+    cdef object renderer
+
+    def __init__(self, fontname, fontsize=16, renderer=None):
+        self.font = Font(fontname, fontsize)
+        self.renderer = renderer
 
 
-    def preload(self, anm_wrapper):
-        try:
-            anms = anm_wrapper.anm_files
-        except AttributeError:
-            anms = anm_wrapper
+    def load(self, label_list):
+        cdef NativeText label
+
+        for label in label_list:
+            if label.texture is None:
+                surface = self.font.render(label.text)
+                label.width, label.height = surface.surface.w, surface.surface.h
 
-        for anm in anms:
-            key = anm.first_name, anm.secondary_name
-            texture = self[key]
+                if label.align == 'center':
+                    label.x -= label.width // 2
+                elif label.align == 'right':
+                    label.x -= label.width
+                else:
+                    assert label.align == 'left'
+
+                texture = Texture(label.width, label.height, -4, surface.pixels)
+                label.texture = load_texture(texture)
+                label.texture.renderer = self.renderer
 
 
-    def load_texture(self, key):
-        first_name, secondary_name = key
+cdef decode_png(loader, first_name, secondary_name):
+    image_file = load_png(loader.get_file(os.path.basename(first_name)))
+    width, height = image_file.surface.w, image_file.surface.h
+
+    # Support only 32 bits RGBA. Paletted surfaces are awful to work with.
+    #TODO: verify it doesn’t blow up on big-endian systems.
+    new_image = create_rgb_surface(width, height, 32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000)
+    new_image.blit(image_file)
 
-        image_file = pyglet.image.load(first_name, file=self.loader.get_file(os.path.basename(first_name)))
+    if secondary_name:
+        alpha_file = load_png(loader.get_file(os.path.basename(secondary_name)))
+        assert (width == alpha_file.surface.w and height == alpha_file.surface.h)
+
+        new_alpha_file = create_rgb_surface(width, height, 24)
+        new_alpha_file.blit(alpha_file)
 
-        if secondary_name:
-            alpha_file = pyglet.image.load(secondary_name, file=self.loader.get_file(os.path.basename(secondary_name)))
-            assert (image_file.width, image_file.height) == (alpha_file.width, image_file.height)
+        new_image.set_alpha(new_alpha_file)
+
+    return Texture(width, height, -4, new_image.pixels)
+
+
+cdef load_texture(thtx):
+    cdef GLuint texture
 
-            data = image_file.get_data('RGB', image_file.width * 3)
-            alpha_data = alpha_file.get_data('RGB', image_file.width * 3)
-            image_file = pyglet.image.ImageData(image_file.width, image_file.height, 'RGBA', b''.join(data[i*3:i*3+3] + alpha_data[i*3] for i in range(image_file.width * image_file.height)))
-
-            #TODO: improve perfs somehow
+    if thtx.fmt == 1:
+        format_ = GL_BGRA
+        type_ = GL_UNSIGNED_BYTE
+        composants = GL_RGBA
+    elif thtx.fmt == 3:
+        format_ = GL_RGB
+        type_ = GL_UNSIGNED_SHORT_5_6_5
+        composants = GL_RGB
+    elif thtx.fmt == 5:
+        format_ = GL_BGRA
+        type_ = GL_UNSIGNED_SHORT_4_4_4_4_REV
+        composants = GL_RGBA
+    elif thtx.fmt == 7:
+        format_ = GL_LUMINANCE
+        type_ = GL_UNSIGNED_BYTE
+        composants = GL_LUMINANCE
+    elif thtx.fmt == -4: #XXX: non-standard
+        format_ = GL_RGBA
+        type_ = GL_UNSIGNED_BYTE
+        composants = GL_RGBA
+    else:
+        raise Exception('Unknown texture type')
 
-        texture = image_file.get_texture()
+    glGenTextures(1, &texture)
+    glBindTexture(GL_TEXTURE_2D, texture)
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
 
-        glTexParameteri(texture.target, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
-        glTexParameteri(texture.target, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
+    glTexImage2D(GL_TEXTURE_2D, 0,
+                 composants,
+                 thtx.width, thtx.height,
+                 0,
+                 format_, type_,
+                 <char*>thtx.data)
 
-        return texture
-
+    return TextureId(texture)
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/window.pxd
@@ -0,0 +1,33 @@
+from pytouhou.lib cimport sdl
+
+
+cdef class Clock:
+    cdef long _target_fps, _ref_tick, _ref_frame, _fps_tick, _fps_frame
+    cdef double _rate
+
+    cdef void set_target_fps(self, long fps) nogil
+    cdef double get_fps(self) nogil
+    cdef void tick(self) nogil except *
+
+
+cdef class Runner:
+    cdef long width, height
+
+    cdef void start(self) except *
+    cdef void finish(self) except *
+    cdef bint update(self) except *
+
+
+cdef class Window:
+    cdef sdl.Window win
+    cdef long fps_limit
+    cdef public long x, y, width, height
+    cdef public bint use_fixed_pipeline
+    cdef Runner runner
+    cdef Clock clock
+
+    cdef void set_size(self, int width, int height) nogil
+    cpdef set_runner(self, Runner runner=*)
+    cpdef run(self)
+    cdef bint run_frame(self) except? False
+    cdef double get_fps(self) nogil
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/window.pyx
@@ -0,0 +1,176 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2013 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.
+##
+
+cimport cython
+
+from pytouhou.lib.opengl cimport \
+         (glEnable, glHint, glEnableClientState, GL_TEXTURE_2D, GL_BLEND,
+          GL_PERSPECTIVE_CORRECTION_HINT, GL_FOG_HINT, GL_NICEST,
+          GL_COLOR_ARRAY, GL_VERTEX_ARRAY, GL_TEXTURE_COORD_ARRAY)
+
+IF USE_GLEW:
+    from pytouhou.lib.opengl cimport glewInit
+
+
+cdef class Clock:
+    def __init__(self, long fps=-1):
+        self._target_fps = 0
+        self._ref_tick = 0
+        self._ref_frame = 0
+        self._fps_tick = 0
+        self._fps_frame = 0
+        self._rate = 0
+        self.set_target_fps(fps)
+
+
+    cdef void set_target_fps(self, long fps) nogil:
+        self._target_fps = fps
+        self._ref_tick = 0
+        self._fps_tick = 0
+
+
+    cdef double get_fps(self) nogil:
+        return self._rate
+
+
+    cdef void tick(self) nogil except *:
+        current = sdl.get_ticks()
+
+        if not self._ref_tick:
+            self._ref_tick = current
+            self._ref_frame = 0
+
+        if self._fps_frame >= (self._target_fps if self._target_fps > 0 else 60):
+            self._rate = self._fps_frame * 1000. / (current - self._fps_tick)
+            self._fps_tick = current
+            self._fps_frame = 0
+            # If we are relying on vsync, but vsync doesn't work or is higher
+            # than 60 fps, limit ourselves to 60 fps.
+            if self._target_fps < 0 and self._rate > 64.:
+                self._target_fps = 60
+
+        self._ref_frame += 1
+        self._fps_frame += 1
+
+        target_tick = self._ref_tick
+        if self._target_fps:
+            target_tick += <long>(self._ref_frame * 1000 / self._target_fps)
+
+        if current <= target_tick:
+            sdl.delay(target_tick - current)
+        else:
+            self._ref_tick = current
+            self._ref_frame = 0
+
+
+
+cdef class Runner:
+    cdef void start(self) except *:
+        pass
+
+    cdef void finish(self) except *:
+        pass
+
+    cdef bint update(self) except *:
+        return False
+
+
+
+cdef class Window:
+    def __init__(self, tuple size=None, bint double_buffer=True, long fps_limit=-1,
+                 bint fixed_pipeline=False, bint sound=True):
+        self.fps_limit = fps_limit
+        self.use_fixed_pipeline = fixed_pipeline
+        self.runner = None
+
+        sdl.gl_set_attribute(sdl.GL_CONTEXT_MAJOR_VERSION, 2)
+        sdl.gl_set_attribute(sdl.GL_CONTEXT_MINOR_VERSION, 1)
+        sdl.gl_set_attribute(sdl.GL_DOUBLEBUFFER, int(double_buffer))
+        sdl.gl_set_attribute(sdl.GL_DEPTH_SIZE, 24)
+
+        self.width, self.height = size if size is not None else (640, 480)
+
+        flags = sdl.WINDOW_OPENGL | sdl.WINDOW_SHOWN
+        if not self.use_fixed_pipeline:
+            flags |= sdl.WINDOW_RESIZABLE
+
+        self.win = sdl.Window('PyTouhou',
+                              sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED,
+                              self.width, self.height,
+                              flags)
+        self.win.gl_create_context()
+
+        IF USE_GLEW:
+            if glewInit() != 0:
+                raise Exception('GLEW init fail!')
+
+        # Initialize OpenGL
+        glEnable(GL_BLEND)
+        if self.use_fixed_pipeline:
+            glEnable(GL_TEXTURE_2D)
+            glHint(GL_FOG_HINT, GL_NICEST)
+            glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
+            glEnableClientState(GL_COLOR_ARRAY)
+            glEnableClientState(GL_VERTEX_ARRAY)
+            glEnableClientState(GL_TEXTURE_COORD_ARRAY)
+
+        self.clock = Clock(self.fps_limit)
+
+
+    @cython.cdivision(True)
+    cdef void set_size(self, int width, int height) nogil:
+        self.win.set_window_size(width, height)
+
+        runner_width = float(self.runner.width)
+        runner_height = float(self.runner.height)
+
+        scale = min(width / runner_width,
+                    height / runner_height)
+
+        self.width = int(runner_width * scale)
+        self.height = int(runner_height * scale)
+
+        self.x = (width - self.width) // 2
+        self.y = (height - self.height) // 2
+
+
+    cpdef set_runner(self, Runner runner=None):
+        self.runner = runner
+        if runner is not None:
+            runner.start()
+
+
+    cpdef run(self):
+        try:
+            while self.run_frame():
+                pass
+        finally:
+            self.runner.finish()
+
+
+    cdef bint run_frame(self) except? False:
+        cdef bint running = False
+        if self.runner is not None:
+            running = self.runner.update()
+        self.win.gl_swap_window()
+        self.clock.tick()
+        return running
+
+
+    cdef double get_fps(self) nogil:
+        return self.clock.get_fps()
+
+
+    def del_runner(self):
+        self.runner = None
--- a/pytouhou/utils/bitstream.pyx
+++ b/pytouhou/utils/bitstream.pyx
@@ -13,11 +13,13 @@
 ##
 
 cdef class BitStream:
-    cdef public io
-    cdef public int bits
-    cdef public int byte
+    cdef public object io
+    cdef unsigned int bits
+    cdef unsigned char byte
+    cdef bytes bytes
 
-    def __init__(BitStream self, io):
+
+    def __init__(self, io):
         self.io = io
         self.bits = 0
         self.byte = 0
@@ -31,37 +33,46 @@ cdef class BitStream:
         return self.io.__exit__(type, value, traceback)
 
 
-    def seek(BitStream self, offset, whence=0):
+    def seek(self, offset, whence=0):
         self.io.seek(offset, whence)
         self.byte = 0
         self.bits = 0
 
 
-    def tell(BitStream self):
+    def tell(self):
         return self.io.tell()
 
 
-    def tell2(BitStream self):
+    def tell2(self):
         return self.io.tell(), self.bits
 
 
-    cpdef unsigned char read_bit(BitStream self):
+    cpdef unsigned char read_bit(self) except? -1:
         if not self.bits:
-            self.byte = ord(self.io.read(1))
+            self.bytes = self.io.read(1)
+            self.byte = (<unsigned char*> self.bytes)[0]
             self.bits = 8
         self.bits -= 1
         return (self.byte >> self.bits) & 0x01
 
 
-    def read(BitStream self, nb_bits):
-        cdef unsigned int value
-        value = 0
-        for i in range(nb_bits - 1, -1, -1):
-            value |= self.read_bit() << i
-        return value
+    cpdef unsigned int read(self, unsigned int nb_bits) except? -1:
+        cdef unsigned int value = 0, read = 0
+        cdef unsigned int nb_bits2 = nb_bits
+
+        while nb_bits2:
+            if not self.bits:
+                self.bytes = self.io.read(1)
+                self.byte = (<unsigned char*> self.bytes)[0]
+                self.bits = 8
+            read = self.bits if nb_bits2 > self.bits else nb_bits2
+            nb_bits2 -= read
+            self.bits -= read
+            value |= (self.byte >> self.bits) << nb_bits2
+        return value & ((1 << nb_bits) - 1)
 
 
-    cpdef write_bit(BitStream self, bit):
+    cpdef write_bit(self, bit):
         if self.bits == 8:
             self.io.write(chr(self.byte))
             self.bits = 0
@@ -71,12 +82,12 @@ cdef class BitStream:
         self.bits += 1
 
 
-    def write(BitStream self, bits, nb_bits):
+    def write(self, bits, nb_bits):
         for i in range(nb_bits):
             self.write_bit(bits >> (nb_bits - 1 - i) & 0x01)
 
 
-    def flush(BitStream self):
+    def flush(self):
         self.io.write(chr(self.byte))
         self.bits = 0
         self.byte = 0
new file mode 100644
--- /dev/null
+++ b/pytouhou/utils/interpolator.pxd
@@ -0,0 +1,11 @@
+cdef class Interpolator:
+    cdef unsigned long start_frame, end_frame, _frame
+    cdef long _length
+    cdef double *_values, *start_values, *end_values
+    cdef object _formula
+
+    cpdef set_interpolation_start(self, unsigned long frame, tuple values)
+    cpdef set_interpolation_end(self, unsigned long frame, tuple values)
+    cpdef set_interpolation_end_frame(self, unsigned long end_frame)
+    cpdef set_interpolation_end_values(self, tuple values)
+    cpdef update(self, unsigned long frame)
--- a/pytouhou/utils/interpolator.pyx
+++ b/pytouhou/utils/interpolator.pyx
@@ -12,50 +12,79 @@
 ## GNU General Public License for more details.
 ##
 
+from libc.stdlib cimport malloc, free
 
-class Interpolator(object):
-    __slots__ = ('values', 'start_values', 'end_values', 'start_frame', 'end_frame', '_frame', '_formula')
-    def __init__(self, values=(), start_frame=0, end_values=(), end_frame=0, formula=None):
-        self.values = tuple(values)
-        self.start_values = tuple(values)
-        self.end_values = tuple(end_values)
+
+cdef class Interpolator:
+    def __init__(self, tuple values, unsigned long start_frame=0, tuple end_values=None,
+                 unsigned long end_frame=0, formula=None):
+        self._length = len(values)
+        self._values = <double*>malloc(self._length * sizeof(double))
+        self.start_values = <double*>malloc(self._length * sizeof(double))
+        self.end_values = <double*>malloc(self._length * sizeof(double))
+        for i in xrange(self._length):
+            self._values[i] = values[i]
+            self.start_values[i] = self._values[i]
+        if end_values is not None:
+            for i in xrange(self._length):
+                self.end_values[i] = end_values[i]
         self.start_frame = start_frame
         self.end_frame = end_frame
         self._frame = 0
-        self._formula = formula or (lambda x: x)
+        self._formula = formula
+
+
+    def __dealloc__(self):
+        free(self.end_values)
+        free(self.start_values)
+        free(self._values)
+
+
+    property values:
+        def __get__(self):
+            return tuple([self._values[i] for i in xrange(self._length)])
 
 
     def __nonzero__(self):
         return self._frame < self.end_frame
 
 
-    def set_interpolation_start(self, frame, values):
-        self.start_values = tuple(values)
+    cpdef set_interpolation_start(self, unsigned long frame, tuple values):
+        for i in xrange(self._length):
+            self.start_values[i] = values[i]
         self.start_frame = frame
 
 
-    def set_interpolation_end(self, frame, values):
-        self.end_values = tuple(values)
+    cpdef set_interpolation_end(self, unsigned long frame, tuple values):
+        for i in xrange(self._length):
+            self.end_values[i] = values[i]
         self.end_frame = frame
 
 
-    def set_interpolation_end_frame(self, end_frame):
+    cpdef set_interpolation_end_frame(self, unsigned long end_frame):
         self.end_frame = end_frame
 
 
-    def set_interpolation_end_values(self, values):
-        self.end_values = tuple(values)
+    cpdef set_interpolation_end_values(self, tuple values):
+        for i in xrange(self._length):
+            self.end_values[i] = values[i]
 
 
-    def update(self, frame):
+    cpdef update(self, unsigned long frame):
+        cdef double coeff
+
         self._frame = frame
-        if frame >= self.end_frame - 1: #XXX: skip the last interpolation step
+        if frame + 1 >= self.end_frame: #XXX: skip the last interpolation step
             # This bug is replicated from the original game
-            self.values = self.end_values
-            self.start_values = self.end_values
+            for i in xrange(self._length):
+                self._values[i] = self.end_values[i]
+                self.start_values[i] = self.end_values[i]
             self.start_frame = frame
         else:
-            coeff = self._formula(float(frame - self.start_frame) / float(self.end_frame - self.start_frame))
-            self.values = [start_value + coeff * (end_value - start_value)
-                           for (start_value, end_value) in zip(self.start_values, self.end_values)]
-
+            coeff = float(frame - self.start_frame) / float(self.end_frame - self.start_frame)
+            if self._formula is not None:
+                coeff = self._formula(coeff)
+            for i in xrange(self._length):
+                start_value = self.start_values[i]
+                end_value = self.end_values[i]
+                self._values[i] = start_value + coeff * (end_value - start_value)
rename from pytouhou/utils/lzss.py
rename to pytouhou/utils/lzss.pyx
--- a/pytouhou/utils/lzss.py
+++ b/pytouhou/utils/lzss.pyx
@@ -12,28 +12,49 @@
 ## GNU General Public License for more details.
 ##
 
-def decompress(bitstream, size, dictionary_size=0x2000,
-               offset_size=13, length_size=4, minimum_match_length=3):
-    out_data = []
-    dictionary = [0] * dictionary_size
-    dictionary_head = 1
-    while len(out_data) < size:
-        flag = bitstream.read_bit()
-        if flag:
+from libc.stdlib cimport calloc, malloc, free
+
+
+cpdef bytes decompress(object bitstream,
+                       Py_ssize_t size,
+                       unsigned int dictionary_size=0x2000,
+                       unsigned int offset_size=13,
+                       unsigned int length_size=4,
+                       unsigned int minimum_match_length=3):
+    cdef unsigned int i, ptr, dictionary_head, offset, length
+    cdef unsigned char byte
+    cdef char *out_data, *dictionary
+    cdef bytes _out_data
+
+    out_data = <char*> malloc(size)
+    dictionary = <char*> calloc(dictionary_size, 1)
+    dictionary_head, ptr = 1, 0
+
+    while ptr < size:
+        if bitstream.read_bit():
             # The `flag` bit is set, indicating the upcoming chunk of data is a literal
             # Add it to the uncompressed file, and store it in the dictionary
             byte = bitstream.read(8)
             dictionary[dictionary_head] = byte
             dictionary_head = (dictionary_head + 1) % dictionary_size
-            out_data.append(byte)
+            out_data[ptr] = byte
+            ptr += 1
         else:
             # The `flag` bit is not set, the upcoming chunk is a (offset, length) tuple
             offset = bitstream.read(offset_size)
             length = bitstream.read(length_size) + minimum_match_length
-            if (offset, length) == (0, 0):
+            if ptr + length > size:
+                raise Exception
+            if offset == 0 and length == 0:
                 break
             for i in range(offset, offset + length):
-                out_data.append(dictionary[i % dictionary_size])
+                out_data[ptr] = dictionary[i % dictionary_size]
                 dictionary[dictionary_head] = dictionary[i % dictionary_size]
                 dictionary_head = (dictionary_head + 1) % dictionary_size
-    return b''.join(chr(byte) for byte in out_data)
+                ptr += 1
+
+    _out_data = out_data[:size]
+    free(out_data)
+    free(dictionary)
+    return _out_data
+
new file mode 100644
--- /dev/null
+++ b/pytouhou/utils/maths.pxd
@@ -0,0 +1,5 @@
+from .matrix cimport Matrix
+
+cdef Matrix ortho_2d(float left, float right, float bottom, float top)
+cdef Matrix perspective(float fovy, float aspect, float zNear, float zFar)
+cdef Matrix setup_camera(float dx, float dy, float dz)
new file mode 100644
--- /dev/null
+++ b/pytouhou/utils/maths.pyx
@@ -0,0 +1,75 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2013 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 libc.math cimport tan, M_PI as pi
+
+from .vector cimport Vector, normalize, cross, dot
+
+
+cdef double radians(double degrees) nogil:
+    return degrees * pi / 180
+
+
+cdef Matrix ortho_2d(float left, float right, float bottom, float top):
+    cdef float *data
+
+    mat = Matrix()
+    data = mat.data
+    data[4*0+0] = 2 / (right - left)
+    data[4*1+1] = 2 / (top - bottom)
+    data[4*2+2] = -1
+    data[4*3+0] = -(right + left) / (right - left)
+    data[4*3+1] = -(top + bottom) / (top - bottom)
+    return mat
+
+
+cdef Matrix look_at(Vector eye, Vector center, Vector up):
+    f = normalize(center.sub(eye))
+    u = normalize(up)
+    s = normalize(cross(f, u))
+    u = cross(s, f)
+
+    return Matrix([s.x, u.x, -f.x, 0,
+                   s.y, u.y, -f.y, 0,
+                   s.z, u.z, -f.z, 0,
+                   -dot(s, eye), -dot(u, eye), dot(f, eye), 1])
+
+
+cdef Matrix perspective(float fovy, float aspect, float z_near, float z_far):
+    cdef float *data
+
+    top = tan(radians(fovy / 2)) * z_near
+    bottom = -top
+    left = -top * aspect
+    right = top * aspect
+
+    mat = Matrix()
+    data = mat.data
+    data[4*0+0] = (2 * z_near) / (right - left)
+    data[4*1+1] = (2 * z_near) / (top - bottom)
+    data[4*2+2] = -(z_far + z_near) / (z_far - z_near)
+    data[4*2+3] = -1
+    data[4*3+2] = -(2 * z_far * z_near) / (z_far - z_near)
+    data[4*3+3] = 0
+    return mat
+
+
+cdef Matrix setup_camera(float dx, float dy, float dz):
+    # Some explanations on the magic constants:
+    # 192. = 384. / 2. = width / 2.
+    # 224. = 448. / 2. = height / 2.
+    # 835.979370 = 224./math.tan(math.radians(15)) = (height/2.)/math.tan(math.radians(fov/2))
+    # This is so that objects on the (O, x, y) plane use pixel coordinates
+    return look_at(Vector(192., 224., - 835.979370 * dz),
+                   Vector(192. + dx, 224. - dy, 0.), Vector(0., -1., 0.))
--- a/pytouhou/utils/matrix.pxd
+++ b/pytouhou/utils/matrix.pxd
@@ -1,10 +1,10 @@
 cdef class Matrix:
-    cdef public list data
+    cdef float data[16]
 
-    cpdef flip(Matrix self)
-    cpdef scale(Matrix self, x, y, z)
-    cpdef scale2d(Matrix self, x, y)
-    cpdef translate(Matrix self, x, y, z)
-    cpdef rotate_x(Matrix self, angle)
-    cpdef rotate_y(Matrix self, angle)
-    cpdef rotate_z(Matrix self, angle)
+    cdef void flip(self) nogil
+    cdef void scale(self, float x, float y, float z) nogil
+    cdef void scale2d(self, float x, float y) nogil
+    cdef void translate(self, float x, float y, float z) nogil
+    cdef void rotate_x(self, float angle) nogil
+    cdef void rotate_y(self, float angle) nogil
+    cdef void rotate_z(self, float angle) nogil
--- a/pytouhou/utils/matrix.pyx
+++ b/pytouhou/utils/matrix.pyx
@@ -13,66 +13,119 @@
 ##
 
 from libc.math cimport sin, cos
+from libc.stdlib cimport malloc, free
 
 
 cdef class Matrix:
-    def __init__(Matrix self, data=None):
-        self.data = data or [[0] * 4 for i in xrange(4)]
+    def __init__(self, data=None):
+        if data is None:
+            data = [1, 0, 0, 0,
+                    0, 1, 0, 0,
+                    0, 0, 1, 0,
+                    0, 0, 0, 1]
+        for i in xrange(4):
+            for j in xrange(4):
+                self.data[i*4+j] = data[4*i+j]
 
 
-    cpdef flip(Matrix self):
-        data = self.data
-        a, b, c, d = data[0]
-        data[0] = [-a, -b, -c, -d]
+    def __mul__(Matrix self, Matrix other):
+        cdef float *d1, *d2, *d3
+
+        out = Matrix()
+        d1 = self.data
+        d2 = other.data
+        d3 = out.data
+        for i in xrange(4):
+            for j in xrange(4):
+                d3[4*i+j] = 0
+                for k in xrange(4):
+                    d3[4*i+j] += d1[4*i+k] * d2[4*k+j]
+        return out
 
 
-    cpdef scale(Matrix self, x, y, z):
-        d1 = self.data
-        d1[0] = [a * x for a in d1[0]]
-        d1[1] = [a * y for a in d1[1]]
-        d1[2] = [a * z for a in d1[2]]
+    cdef void flip(self) nogil:
+        cdef float *data
+
+        data = self.data
+        for i in xrange(4):
+            data[i] = -data[i]
 
 
-    cpdef scale2d(Matrix self, x, y):
+    cdef void scale(self, float x, float y, float z) nogil:
+        cdef float *data, coordinate[3]
+
         data = self.data
-        d1a, d1b, d1c, d1d = data[0]
-        d2a, d2b, d2c, d2d = data[1]
-        data[0] = [d1a * x, d1b * x, d1c * x, d1d * x]
-        data[1] = [d2a * y, d2b * y, d2c * y, d2d * y]
+        coordinate[0] = x
+        coordinate[1] = y
+        coordinate[2] = z
+
+        for i in xrange(3):
+            for j in xrange(4):
+                data[4*i+j] *= coordinate[i]
+
+
+    cdef void scale2d(self, float x, float y) nogil:
+        cdef float *data
+
+        data = self.data
+        for i in xrange(4):
+            data[  i] *= x
+            data[4+i] *= y
 
 
-    cpdef translate(Matrix self, x, y, z):
+    cdef void translate(self, float x, float y, float z) nogil:
+        cdef float *data, coordinate[3], item[3]
+
         data = self.data
-        a, b, c = data[3][:3]
-        a, b, c = a * x, b * y, c * z
-        d1a, d1b, d1c, d1d = data[0]
-        d2a, d2b, d2c, d2d = data[1]
-        d3a, d3b, d3c, d3d = data[2]
-        data[0] = [d1a + a, d1b + a, d1c + a, d1d + a]
-        data[1] = [d2a + b, d2b + b, d2c + b, d2d + b]
-        data[2] = [d3a + c, d3b + c, d3c + c, d3d + c]
+        coordinate[0] = x
+        coordinate[1] = y
+        coordinate[2] = z
+        for i in xrange(3):
+            item[i] = data[12+i] * coordinate[i]
+
+        for i in xrange(3):
+            for j in xrange(4):
+                data[4*i+j] += item[i]
 
 
-    cpdef rotate_x(Matrix self, angle):
-        d1 = self.data
+    cdef void rotate_x(self, float angle) nogil:
+        cdef float cos_a, sin_a
+        cdef float lines[8], *data
+
+        data = self.data
         cos_a = cos(angle)
         sin_a = sin(angle)
-        d1[1], d1[2] = ([cos_a * d1[1][i] - sin_a * d1[2][i] for i in range(4)],
-                        [sin_a * d1[1][i] + cos_a * d1[2][i] for i in range(4)])
+        for i in xrange(8):
+            lines[i] = data[i+4]
+        for i in xrange(4):
+            data[4+i] = cos_a * lines[i] - sin_a * lines[4+i]
+            data[8+i] = sin_a * lines[i] + cos_a * lines[4+i]
 
 
-    cpdef rotate_y(Matrix self, angle):
-        d1 = self.data
+    cdef void rotate_y(self, float angle) nogil:
+        cdef float cos_a, sin_a
+        cdef float lines[8], *data
+
+        data = self.data
         cos_a = cos(angle)
         sin_a = sin(angle)
-        d1[0], d1[2] = ([cos_a * d1[0][i] + sin_a * d1[2][i] for i in range(4)],
-                        [- sin_a * d1[0][i] + cos_a * d1[2][i] for i in range(4)])
+        for i in xrange(4):
+            lines[i] = data[i]
+            lines[i+4] = data[i+8]
+        for i in xrange(4):
+            data[  i] =  cos_a * lines[i] + sin_a * lines[4+i]
+            data[8+i] = -sin_a * lines[i] + cos_a * lines[4+i]
 
 
-    cpdef rotate_z(Matrix self, angle):
-        d1 = self.data
+    cdef void rotate_z(self, float angle) nogil:
+        cdef float cos_a, sin_a
+        cdef float lines[8], *data
+
+        data = self.data
         cos_a = cos(angle)
         sin_a = sin(angle)
-        d1[0], d1[1] = ([cos_a * d1[0][i] - sin_a * d1[1][i] for i in range(4)],
-                        [sin_a * d1[0][i] + cos_a * d1[1][i] for i in range(4)])
-
+        for i in xrange(8):
+            lines[i] = data[i]
+        for i in xrange(4):
+            data[  i] = cos_a * lines[i] - sin_a * lines[4+i]
+            data[4+i] = sin_a * lines[i] + cos_a * lines[4+i]
new file mode 100644
--- /dev/null
+++ b/pytouhou/utils/pe.py
@@ -0,0 +1,137 @@
+# -*- 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 struct import Struct, unpack
+from collections import namedtuple
+
+
+class PEStructs:
+    _IMAGE_FILE_HEADER = namedtuple('_IMAGE_FILE_HEADER',
+                                    ('Machine',
+                                     'NumberOfSections',
+                                     'TimeDateStamp',
+                                     'PointerToSymbolTable',
+                                     'NumberOfSymbols',
+                                     'SizeOfOptionalHeader',
+                                     'Characteristics'))
+    @classmethod
+    def read_image_file_header(cls, file):
+        format = Struct('<HHIIIHH')
+        return cls._IMAGE_FILE_HEADER(*format.unpack(file.read(format.size)))
+
+    _IMAGE_OPTIONAL_HEADER = namedtuple('_IMAGE_OPTIONAL_HEADER',
+                                        ('Magic',
+                                         'MajorLinkerVersion', 'MinorLinkerVersion',
+                                         'SizeOfCode', 'SizeOfInitializedData',
+                                         'SizeOfUninitializedData',
+                                         'AddressOfEntryPoint', 'BaseOfCode',
+                                         'BaseOfData', 'ImageBase',
+                                         'SectionAlignement', 'FileAlignement',
+                                         'MajorOperatingSystemVersion',
+                                         'MinorOperatingSystemVersion',
+                                         'MajorImageVersion',
+                                         'MinorImageVersion',
+                                         'MajorSubsystemVersion',
+                                         'MinorSubsystemVersion',
+                                         'Win32VersionValue',
+                                         'SizeOfImage',
+                                         'SizeOfHeaders',
+                                         'CheckSum',
+                                         'Subsystem',
+                                         'DllCharacteristics',
+                                         'SizeOfStackReserve',
+                                         'SizeOfStackCommit',
+                                         'SizeOfHeapReserve',
+                                         'SizeOfHeapCommit',
+                                         'LoaderFlags',
+                                         'NumberOfRvaAndSizes',
+                                         'DataDirectory'))
+    _IMAGE_DATA_DIRECTORY = namedtuple('_IMAGE_DATA_DIRECTORY',
+                                       ('VirtualAddress', 'Size'))
+    @classmethod
+    def read_image_optional_header(cls, file):
+        format = Struct('<HBBIIIIIIIIIHHHHHHIIIIHHIIIIII')
+        directory_format = Struct('<II')
+        directory = []
+        partial_header = format.unpack(file.read(format.size))
+        directory = [cls._IMAGE_DATA_DIRECTORY(*directory_format.unpack(file.read(directory_format.size))) for i in xrange(16)]
+        return cls._IMAGE_OPTIONAL_HEADER(*(partial_header + (directory,)))
+
+    _IMAGE_SECTION_HEADER = namedtuple('_IMAGE_SECTION_HEADER',
+                                       ('Name', 'VirtualSize',
+                                        'VirtualAddress',
+                                        'SizeOfRawData', 'PointerToRawData',
+                                        'PointerToRelocations',
+                                        'PointerToLinenumbers',
+                                        'NumberOfRelocations',
+                                        'NumberOfLinenumbers',
+                                        'Characteristics'))
+    @classmethod
+    def read_image_section_header(cls, file):
+        format = Struct('<8sIIIIIIHHI')
+        return cls._IMAGE_SECTION_HEADER(*format.unpack(file.read(format.size)))
+
+
+
+class PEFile(object):
+    def __init__(self, file):
+        self.file = file
+
+        self.image_base = 0
+        self.sections = []
+
+        file.seek(0x3c)
+        pe_offset, = unpack('<I', file.read(4))
+
+        file.seek(pe_offset)
+        pe_sig = file.read(4)
+        assert pe_sig == b'PE\0\0'
+
+        pe_file_header = PEStructs.read_image_file_header(file)
+        pe_optional_header = PEStructs.read_image_optional_header(file)
+
+        # Read image base
+        self.image_base = pe_optional_header.ImageBase
+
+        self.sections = [PEStructs.read_image_section_header(file)
+                            for i in xrange(pe_file_header.NumberOfSections)]
+
+
+    def seek_to_va(self, va):
+        self.file.seek(self.va_to_offset(va))
+
+
+    def offset_to_rva(self, offset):
+        for section in self.sections:
+            if 0 <= (offset - section.PointerToRawData) < section.SizeOfRawData:
+                #TODO: is that okay?
+                return offset - section.PointerToRawData + section.VirtualAddress
+        raise IndexError #TODO
+
+
+    def offset_to_va(self, offset):
+        return self.offset_to_rva(offset) + self.image_base
+
+
+    def rva_to_offset(self, rva):
+        for section in self.sections:
+            if 0 <= (rva - section.VirtualAddress) < section.SizeOfRawData:
+                #TODO: is that okay?
+                return rva - section.VirtualAddress + section.PointerToRawData
+        raise IndexError #TODO
+
+
+    def va_to_offset(self, va):
+        return self.rva_to_offset(va - self.image_base)
+
new file mode 100644
--- /dev/null
+++ b/pytouhou/utils/vector.pxd
@@ -0,0 +1,8 @@
+cdef class Vector:
+    cdef float x, y, z
+
+    cdef Vector sub(self, Vector other)
+
+cdef Vector cross(Vector vec1, Vector vec2)
+cdef float dot(Vector vec1, Vector vec2)
+cdef Vector normalize(Vector vec)
new file mode 100644
--- /dev/null
+++ b/pytouhou/utils/vector.pyx
@@ -0,0 +1,49 @@
+# -*- 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 libc.math cimport sqrt
+
+
+cdef class Vector:
+    def __init__(self, float x, float y, float z):
+        self.x = x
+        self.y = y
+        self.z = z
+
+
+    cdef Vector sub(self, Vector other):
+        cdef float x, y, z
+
+        x = self.x - other.x
+        y = self.y - other.y
+        z = self.z - other.z
+
+        return Vector(x, y, z)
+
+
+cdef Vector cross(Vector vec1, Vector vec2):
+    return Vector(vec1.y * vec2.z - vec2.y * vec1.z,
+                  vec1.z * vec2.x - vec2.z * vec1.x,
+                  vec1.x * vec2.y - vec2.x * vec1.y)
+
+
+cdef float dot(Vector vec1, Vector vec2):
+    return vec1.x * vec2.x + vec2.y * vec1.y + vec1.z * vec2.z
+
+
+cdef Vector normalize(Vector vec):
+    cdef float normal
+
+    normal = 1 / sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z)
+    return Vector(vec.x * normal, vec.y * normal, vec.z * normal)
--- a/pytouhou/vm/anmrunner.py
+++ b/pytouhou/vm/anmrunner.py
@@ -13,7 +13,7 @@
 ##
 
 
-from random import randrange
+from random import randrange, random
 
 from pytouhou.utils.helpers import get_logger
 from pytouhou.vm.common import MetaRegistry, instruction
@@ -23,30 +23,60 @@ logger = get_logger(__name__)
 
 class ANMRunner(object):
     __metaclass__ = MetaRegistry
-    __slots__ = ('_anm_wrapper', '_sprite', '_running',
-                 'sprite_index_offset',
-                 'script', 'instruction_pointer', 'frame')
+    __slots__ = ('_anm', '_sprite', 'running', 'sprite_index_offset', 'script',
+                 'instruction_pointer', 'frame', 'waiting', 'handlers',
+                 'variables', 'version', 'timeout')
+
+    #TODO: check!
+    formulae = {0: lambda x: x,
+                1: lambda x: x ** 2,
+                2: lambda x: x ** 3,
+                3: lambda x: x ** 4,
+                4: lambda x: 2 * x - x ** 2,
+                5: lambda x: 2 * x - x ** 3,
+                6: lambda x: 2 * x - x ** 4,
+                7: lambda x: x,
+                255: lambda x: x} #XXX
+
+    def __init__(self, anm, script_id, sprite, sprite_index_offset=0):
+        self._anm = anm
+        self._sprite = sprite
+        self.running = True
+        self.waiting = False
+
+        self.script = anm.scripts[script_id]
+        self.version = anm.version
+        self.handlers = self._handlers[{0: 6, 2: 7}[anm.version]]
+        self.frame = 0
+        self.timeout = -1
+        self.instruction_pointer = 0
+        self.variables = [0,  0,  0,  0,
+                          0., 0., 0., 0.,
+                          0,  0,  0,  0]
+
+        self.sprite_index_offset = sprite_index_offset
+        self.run_frame()
+        self.sprite_index_offset = 0
 
 
-    def __init__(self, anm_wrapper, script_id, sprite, sprite_index_offset=0):
-        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
-
-        self.sprite_index_offset = sprite_index_offset
+    def interrupt(self, interrupt):
+        new_ip = self.script.interrupts.get(interrupt, None)
+        if new_ip is None:
+            new_ip = self.script.interrupts.get(-1, None)
+        if new_ip is None:
+            return False
+        self.instruction_pointer = new_ip
+        self.frame, opcode, args = self.script[self.instruction_pointer]
+        self.waiting = False
+        self._sprite.visible = True
+        return True
 
 
     def run_frame(self):
-        if not self._running:
+        if not self.running:
             return False
 
-        sprite = self._sprite
-
-        while self._running:
+        while self.running and not self.waiting:
             frame, opcode, args = self.script[self.instruction_pointer]
 
             if frame > self.frame:
@@ -56,71 +86,69 @@ class ANMRunner(object):
 
             if frame == self.frame:
                 try:
-                    callback = self._handlers[opcode]
+                    callback = self.handlers[opcode]
                 except KeyError:
                     logger.warn('unhandled opcode %d (args: %r)', opcode, args)
                 else:
+                    logger.debug('[%d - %04d] anm_%d%r', id(self),
+                                 self.frame, opcode, args)
                     callback(self, *args)
-                    sprite._changed = True
-        self.frame += 1
+                    self._sprite.changed = True
 
-        # Update sprite
-        sprite.frame += 1
+        if not self.waiting:
+            self.frame += 1
+        elif self.timeout == self._sprite.frame: #TODO: check if it’s happening at the correct frame.
+            self.waiting = False
 
-        if sprite.rotations_speed_3d != (0., 0., 0.):
-            ax, ay, az = sprite.rotations_3d
-            sax, say, saz = sprite.rotations_speed_3d
-            sprite.rotations_3d = ax + sax, ay + say, az + saz
-            sprite._changed = True
+        self._sprite.update()
 
-        if sprite.scale_speed != (0., 0.):
-            rx, ry = sprite.rescale
-            rsx, rsy = sprite.scale_speed
-            sprite.rescale = rx + rsx, ry + rsy
-            sprite._changed = True
+        return self.running
 
-        if sprite.fade_interpolator:
-            sprite.fade_interpolator.update(sprite.frame)
-            sprite.alpha = int(sprite.fade_interpolator.values[0])
-            sprite._changed = True
 
-        if sprite.scale_interpolator:
-            sprite.scale_interpolator.update(sprite.frame)
-            sprite.rescale = sprite.scale_interpolator.values
-            sprite._changed = True
+    def _setval(self, variable_id, value):
+        if self.version == 2:
+            if 10000 <= variable_id <= 10011:
+                self.variables[int(variable_id-10000)] = value
+
 
-        if sprite.offset_interpolator:
-            sprite.offset_interpolator.update(sprite.frame)
-            sprite.dest_offset = sprite.offset_interpolator.values
-            sprite._changed = True
-
-        return self._running
+    def _getval(self, value):
+        if self.version == 2:
+            if 10000 <= value <= 10011:
+                return self.variables[int(value-10000)]
+        return value
 
 
     @instruction(0)
+    @instruction(1, 7)
     def remove(self):
-        self._sprite._removed = True
-        self._running = False
+        self._sprite.removed = True
+        self.running = False
 
 
     @instruction(1)
+    @instruction(3, 7)
     def load_sprite(self, sprite_index):
-        self._sprite.anm, self._sprite.texcoords = self._anm_wrapper.get_sprite(sprite_index + self.sprite_index_offset)
+        #TODO: version 2 only: do not crash when assigning a non-existant sprite.
+        self._sprite.anm, self._sprite.texcoords = self._anm, self._anm.sprites[sprite_index + self.sprite_index_offset]
 
 
     @instruction(2)
+    @instruction(7, 7)
     def set_scale(self, sx, sy):
-        self._sprite.rescale = sx, sy
+        self._sprite.rescale = self._getval(sx), self._getval(sy)
 
 
     @instruction(3)
+    @instruction(8, 7)
     def set_alpha(self, alpha):
         self._sprite.alpha = alpha % 256 #TODO
 
 
     @instruction(4)
+    @instruction(9, 7)
     def set_color(self, b, g, r):
-        self._sprite.color = (r, g, b)
+        if not self._sprite.fade_interpolator:
+            self._sprite.color = (r, g, b)
 
 
     @instruction(5)
@@ -131,26 +159,31 @@ class ANMRunner(object):
 
 
     @instruction(7)
+    @instruction(10, 7)
     def toggle_mirrored(self):
         self._sprite.mirrored = not self._sprite.mirrored
 
 
     @instruction(9)
+    @instruction(12, 7)
     def set_rotations_3d(self, rx, ry, rz):
-        self._sprite.rotations_3d = rx, ry, rz
+        self._sprite.rotations_3d = self._getval(rx), self._getval(ry), self._getval(rz)
 
 
     @instruction(10)
+    @instruction(13, 7)
     def set_rotations_speed_3d(self, srx, sry, srz):
-        self._sprite.rotations_speed_3d = srx, sry, srz
+        self._sprite.rotations_speed_3d = self._getval(srx), self._getval(sry), self._getval(srz)
 
 
     @instruction(11)
+    @instruction(14, 7)
     def set_scale_speed(self, ssx, ssy):
         self._sprite.scale_speed = ssx, ssy
 
 
     @instruction(12)
+    @instruction(15, 7)
     def fade(self, new_alpha, duration):
         self._sprite.fade(duration, new_alpha, lambda x: x) #TODO: formula
 
@@ -166,9 +199,9 @@ class ANMRunner(object):
 
 
     @instruction(15)
-    @instruction(21) #TODO
+    @instruction(2, 7)
     def keep_still(self):
-        self._running = False
+        self.running = False
 
     @instruction(16)
     def load_random_sprite(self, min_idx, amp):
@@ -177,36 +210,67 @@ class ANMRunner(object):
 
 
     @instruction(17)
+    @instruction(6, 7)
     def move(self, x, y, z):
         self._sprite.dest_offset = (x, y, z)
 
 
     @instruction(18)
+    @instruction(17, 7)
     def move_in_linear(self, x, y, z, duration):
         self._sprite.move_in(duration, x, y, z, lambda x: x)
 
 
     @instruction(19)
+    @instruction(18, 7)
     def move_in_decel(self, x, y, z, duration):
         self._sprite.move_in(duration, x, y, z, lambda x: 2. * x - x ** 2)
 
 
     @instruction(20)
+    @instruction(19, 7)
     def move_in_accel(self, x, y, z, duration):
         self._sprite.move_in(duration, x, y, z, lambda x: x ** 2)
 
 
+    @instruction(21)
+    @instruction(20, 7)
+    def wait(self):
+        """Wait for an interrupt.
+        """
+        self.waiting = True
+
+
+    @instruction(22)
+    @instruction(21, 7)
+    def interrupt_label(self, interrupt):
+        """Noop"""
+        pass
+
+
     @instruction(23)
+    @instruction(22, 7)
     def set_corner_relative_placement(self):
         self._sprite.corner_relative_placement = True #TODO
 
 
+    @instruction(24)
+    @instruction(23, 7)
+    def wait_ex(self):
+        """Hide the sprite and wait for an interrupt.
+        """
+        self._sprite.visible = False
+        self.waiting = True
+
+
     @instruction(25)
+    @instruction(24, 7)
     def set_allow_dest_offset(self, value):
         self._sprite.allow_dest_offset = bool(value)
 
 
     @instruction(26)
+    @instruction(25, 7)
     def set_automatic_orientation(self, value):
         """If true, rotate by pi-angle around the z axis.
         """
@@ -214,18 +278,134 @@ class ANMRunner(object):
 
 
     @instruction(27)
+    @instruction(26, 7)
     def shift_texture_x(self, dx):
         tox, toy = self._sprite.texoffsets
         self._sprite.texoffsets = tox + dx, toy
 
 
     @instruction(28)
+    @instruction(27, 7)
     def shift_texture_y(self, dy):
         tox, toy = self._sprite.texoffsets
         self._sprite.texoffsets = tox, toy + dy
 
 
+    @instruction(29)
+    @instruction(28, 7)
+    def set_visible(self, visible):
+        self._sprite.visible = bool(visible & 1)
+
+
     @instruction(30)
+    @instruction(29, 7)
     def scale_in(self, sx, sy, duration):
         self._sprite.scale_in(duration, sx, sy, lambda x: x) #TODO: formula
 
+
+# Now are the instructions new to anm2.
+
+
+    @instruction(0, 7)
+    def noop(self):
+        pass
+
+
+    @instruction(4, 7)
+    def jump_bis(self, instruction_pointer, frame):
+        self.instruction_pointer = instruction_pointer
+        self.frame = frame
+
+
+    @instruction(5, 7)
+    def jump_ex(self, variable_id, instruction_pointer, frame):
+        """If the given variable is non-zero, decrease it by 1 and jump to a
+        relative offset in the same subroutine.
+        """
+        counter_value = self._getval(variable_id) - 1
+        if counter_value > 0:
+            self._setval(variable_id, counter_value)
+            self.instruction_pointer = instruction_pointer
+            self.frame = frame
+
+
+    @instruction(16, 7)
+    def set_blendfunc(self, value):
+        self._sprite.blendfunc = bool(value & 1)
+
+
+    @instruction(32, 7)
+    def move_in_bis(self, duration, formula, x, y, z):
+        self._sprite.move_in(duration, x, y, z, self.formulae[formula])
+
+
+    @instruction(33, 7)
+    def change_color_in(self, duration, formula, r, g, b):
+        self._sprite.change_color_in(duration, r, g, b, self.formulae[formula])
+
+
+    @instruction(34, 7)
+    def fade_bis(self, duration, formula, new_alpha):
+        self._sprite.fade(duration, new_alpha, self.formulae[formula])
+
+
+    @instruction(35, 7)
+    def rotate_in_bis(self, duration, formula, rx, ry, rz):
+        self._sprite.rotate_in(duration, rx, ry, rz, self.formulae[formula])
+
+
+    @instruction(36, 7)
+    def scale_in_bis(self, duration, formula, sx, sy):
+        self._sprite.scale_in(duration, sx, sy, self.formulae[formula])
+
+
+    @instruction(37, 7)
+    @instruction(38, 7)
+    def set_variable(self, variable_id, value):
+        self._setval(variable_id, value)
+
+
+    @instruction(42, 7)
+    def decrement(self, variable_id, value):
+        self._setval(variable_id, self._getval(variable_id) - self._getval(value))
+
+
+    @instruction(50, 7)
+    def add(self, variable_id, a, b):
+        self._setval(variable_id, self._getval(a) + self._getval(b))
+
+
+    @instruction(52, 7)
+    def substract(self, variable_id, a, b):
+        self._setval(variable_id, self._getval(a) - self._getval(b))
+
+
+    @instruction(55, 7)
+    def divide_int(self, variable_id, a, b):
+        self._setval(variable_id, self._getval(a) // self._getval(b))
+
+
+    @instruction(59, 7)
+    def set_random_int(self, variable_id, amp):
+        #TODO: use the game's PRNG?
+        self._setval(variable_id, randrange(amp))
+
+
+    @instruction(60, 7)
+    def set_random_float(self, variable_id, amp):
+        #TODO: use the game's PRNG?
+        self._setval(variable_id, amp * random())
+
+
+    @instruction(69, 7)
+    def branch_if_not_equal(self, variable_id, value, instruction_pointer, frame):
+        if self._getval(variable_id) != value:
+            self.instruction_pointer = instruction_pointer
+            self.frame = frame
+            assert self.frame == self.script[self.instruction_pointer][0]
+
+
+    @instruction(79, 7)
+    def wait_duration(self, duration):
+        self.timeout = self._sprite.frame + duration
+        self.waiting = True
--- a/pytouhou/vm/common.py
+++ b/pytouhou/vm/common.py
@@ -17,22 +17,19 @@ 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
+            if hasattr(item, '_instruction_ids'):
+                for version, instruction_ids in item._instruction_ids.iteritems():
+                    for id_ in instruction_ids:
+                        instruction_handlers.setdefault(version, {})[id_] = item
         classdict['_handlers'] = instruction_handlers
         return type.__new__(mcs, name, bases, classdict)
 
 
 
-def instruction(instruction_id):
+def instruction(instruction_id, version=6):
     def _decorator(func):
         if not hasattr(func, '_instruction_ids'):
-            func._instruction_ids = set()
-        func._instruction_ids.add(instruction_id)
+            func._instruction_ids = {}
+        func._instruction_ids.setdefault(version, set()).add(instruction_id)
         return func
     return _decorator
--- a/pytouhou/vm/eclrunner.py
+++ b/pytouhou/vm/eclrunner.py
@@ -13,7 +13,7 @@
 ##
 
 
-from math import atan2, cos, sin, pi
+from math import atan2, cos, sin, pi, hypot
 
 from pytouhou.utils.helpers import get_logger
 
@@ -25,43 +25,45 @@ logger = get_logger(__name__)
 
 class ECLMainRunner(object):
     __metaclass__ = MetaRegistry
-    __slots__ = ('_ecl', '_game', 'processes', 'frame',
-                 'instruction_pointer')
+    __slots__ = ('_main', '_subs', '_game', 'frame',
+                 'instruction_pointer', 'boss_wait', 'handlers')
 
-    def __init__(self, ecl, game):
-        self._ecl = ecl
+    def __init__(self, main, subs, game):
+        self._main = main
+        self._subs = subs
         self._game = game
+        self.handlers = self._handlers[6]
         self.frame = 0
-
-        self.processes = []
+        self.boss_wait = False
 
         self.instruction_pointer = 0
 
 
     def run_iter(self):
+        if not self._game.boss:
+            self.boss_wait = False
+
         while True:
             try:
-                frame, sub, instr_type, args = self._ecl.main[self.instruction_pointer]
+                frame, sub, instr_type, args = self._main[self.instruction_pointer]
             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.boss_wait:
                 break
             else:
                 self.instruction_pointer += 1
 
             if frame == self.frame:
                 try:
-                    callback = self._handlers[instr_type]
+                    callback = self.handlers[instr_type]
                 except KeyError:
                     logger.warn('unhandled main 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())
-
-        if not self._game.spellcard:
+        if not (self._game.msg_wait or self.boss_wait):
             self.frame += 1
 
 
@@ -72,11 +74,11 @@ class ECLMainRunner(object):
             if y < -990: #102h.exe@0x41184b
                 y = self._game.prng.rand_double() * 416
             if z < -990: #102h.exe@0x411881
-                y = self._game.prng.rand_double() * 800
-        enemy = self._game.new_enemy((x, y), life, instr_type, bonus_dropped, die_score)
-        process = ECLRunner(self._ecl, sub, enemy, self._game)
-        self.processes.append(process)
-        process.run_iteration()
+                z = self._game.prng.rand_double() * 800
+        enemy = self._game.new_enemy((x, y, z), life, instr_type,
+                                     bonus_dropped, die_score)
+        enemy.process = ECLRunner(self._subs, sub, enemy, self._game, self._pop_enemy) #TODO
+        enemy.process.run_iteration()
 
 
     @instruction(0)
@@ -89,133 +91,100 @@ 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
+        self._game.msg_wait = False
+        if boss._enemy.boss_callback > -1:
+            boss.switch_to_sub(boss._enemy.boss_callback)
+            boss._enemy.boss_callback = -1
+        else:
+            raise Exception #TODO
+
+
+    @instruction(12)
+    def wait_for_boss_death(self, sub, instr_type):
+        self.boss_wait = True
+
+
 
 
 class ECLRunner(object):
     __metaclass__ = MetaRegistry
-    __slots__ = ('_ecl', '_enemy', '_game', 'variables', 'sub', 'frame',
-                 'instruction_pointer', 'comparison_reg', 'stack')
+    __slots__ = ('_subs', '_enemy', '_game', '_pop_enemy', 'variables', 'sub',
+                 'frame', 'instruction_pointer', 'comparison_reg', 'stack',
+                 'running', 'handlers')
 
-    def __init__(self, ecl, sub, enemy, game):
+    def __init__(self, subs, sub, enemy, game, pop_enemy):
         # Things not supposed to change
-        self._ecl = ecl
+        self._subs = subs
         self._enemy = enemy
         self._game = game
+        self._pop_enemy = pop_enemy
+        self.handlers = self._handlers[6]
+
+        self.running = True
 
         # Things supposed to change (and be put in the stack)
         self.variables = [0,  0,  0,  0,
                           0., 0., 0., 0.,
                           0,  0,  0,  0]
         self.comparison_reg = 0
-        self.sub = sub
-        self.frame = 0
-        self.instruction_pointer = 0
+        self.switch_to_sub(sub)
 
         self.stack = []
 
 
-    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
-
-            enm.die_anim()
-
-            if death_flags < 4:
-                if enm._bonus_dropped >= 0:
-                    enm.drop_particles(7, 0)
-                    self._game.drop_bonus(enm.x, enm.y, enm._bonus_dropped)
-                elif enm._bonus_dropped == -1:
-                    if self._game.deaths_count % 3 == 0:
-                        enm.drop_particles(10, 0)
-                        self._game.drop_bonus(enm.x, enm.y, self._game.bonus_list[self._game.next_bonus])
-                        self._game.next_bonus = (self._game.next_bonus + 1) % 32
-                    else:
-                        enm.drop_particles(4, 0)
-                    self._game.deaths_count += 1
-                else:
-                    enm.drop_particles(4, 0)
-
-                if death_flags == 0:
-                    enm._removed = True
-                    return
-
-                if death_flags == 1:
-                    enm.touchable = False
-                elif death_flags == 2:
-                    pass # Just that?
-                elif death_flags == 3:
-                    enm.damageable = False
-                    enm.life = 1
-                    enm.death_flags = 0
-            else:
-                pass #TODO: sparks
-
-            if death_flags != 0 and enm.death_callback is not None:
-                self.frame = 0
-                self.sub = enm.death_callback
-                self.instruction_pointer = 0
-                enm.death_callback = None
-        elif enm.life <= enm.low_life_trigger and enm.low_life_callback is not None:
-            self.frame = 0
-            self.sub = enm.low_life_callback
-            self.instruction_pointer = 0
-            enm.low_life_callback = None
-        elif enm.timeout and enm.frame == enm.timeout:
-            enm.frame = 0
-            if enm.timeout_callback is not None:
-                self.frame = 0
-                self.sub = enm.timeout_callback
-                self.instruction_pointer = 0
-                enm.timeout_callback = None
-            else:
-                enm.life = 0
-        #TODO: other callbacks (low life, etc.)
+    def switch_to_sub(self, sub, preserve_stack=False):
+        if not preserve_stack:
+            self.stack = []
+        self.running = True
+        self.frame = 0
+        self.sub = sub
+        self.instruction_pointer = 0
 
 
     def run_iteration(self):
-        # First, if enemy is dead, return
-        if self._enemy._removed:
-            return False
-
-        # Then, check for callbacks
-        self.handle_callbacks()
-
-        # Now, process script
-        while True:
+        # Process script
+        while self.running:
             try:
-                frame, instr_type, rank_mask, param_mask, args = self._ecl.subs[self.sub][self.instruction_pointer]
+                frame, instr_type, rank_mask, param_mask, args = self._subs[self.sub][self.instruction_pointer]
             except IndexError:
-                return False
+                self.running = False
+                break
 
             if frame > self.frame:
                 break
             else:
                 self.instruction_pointer += 1
 
-
-            #TODO: skip bad ranks
             if not rank_mask & (0x100 << self._game.rank):
                 continue
 
-
             if frame == self.frame:
                 try:
-                    callback = self._handlers[instr_type]
+                    callback = self.handlers[instr_type]
                 except KeyError:
-                    logger.warn('unhandled opcode %d (args: %r)', instr_type, args)
+                    logger.warn('[%d %r - %04d] unhandled opcode %d (args: %r)',
+                                id(self), [self.sub] + [e[0] for e in self.stack],
+                                self.frame, instr_type, args)
                 else:
+                    logger.debug('[%d %r - %04d] ins_%d%r', id(self),
+                                 [self.sub] + [e[0] for e in self.stack],
+                                 self.frame, instr_type, args)
                     callback(self, *args)
-                    logger.debug('executed opcode %d (args: %r)', instr_type, args)
 
         self.frame += 1
-        return True
 
 
     def _getval(self, value):
@@ -234,10 +203,10 @@ class ECLRunner(object):
                 return self._enemy.z
             elif value == -10018:
                 player = self._enemy.select_player()
-                return player.x
+                return player.state.x
             elif value == -10019:
                 player = self._enemy.select_player()
-                return player.y
+                return player.state.y
             elif value == -10021:
                 return self._enemy.get_player_angle()
             elif value == -10022:
@@ -279,7 +248,7 @@ class ECLRunner(object):
     @instruction(1)
     def destroy(self, arg):
         #TODO: arg?
-        self._enemy._removed = True
+        self._enemy.removed = True
 
 
     @instruction(2)
@@ -445,11 +414,9 @@ class ECLRunner(object):
     def call(self, sub, param1, param2):
         self.stack.append((self.sub, self.frame, self.instruction_pointer,
                            list(self.variables), self.comparison_reg))
-        self.sub = sub
-        self.frame = 0
-        self.instruction_pointer = 0
         self.variables[0] = param1
         self.variables[4] = param2
+        self.switch_to_sub(sub, preserve_stack=True)
 
 
     @instruction(36)
@@ -470,22 +437,26 @@ class ECLRunner(object):
 
     @instruction(45)
     def set_angle_speed(self, angle, speed):
-        self._enemy.angle, self._enemy.speed = angle, speed
+        self._enemy.update_mode = 0
+        self._enemy.angle, self._enemy.speed = self._getval(angle), self._getval(speed)
 
 
     @instruction(46)
     def set_rotation_speed(self, speed):
-        self._enemy.rotation_speed = speed
+        self._enemy.update_mode = 0
+        self._enemy.rotation_speed = self._getval(speed)
 
 
     @instruction(47)
     def set_speed(self, speed):
-        self._enemy.speed = speed
+        self._enemy.update_mode = 0
+        self._enemy.speed = self._getval(speed)
 
 
     @instruction(48)
     def set_acceleration(self, acceleration):
-        self._enemy.acceleration = acceleration
+        self._enemy.update_mode = 0
+        self._enemy.acceleration = self._getval(acceleration)
 
 
     @instruction(49)
@@ -519,6 +490,7 @@ class ECLRunner(object):
     @instruction(51)
     def target_player(self, unknown, speed):
         #TODO: unknown
+        self._enemy.update_mode = 0
         self._enemy.speed = speed
         self._enemy.angle = self._enemy.get_player_angle()
 
@@ -684,7 +656,7 @@ class ECLRunner(object):
 
     @instruction(77)
     def set_bullet_interval_ex(self, value):
-        self._enemy.set_bullet_launch_interval(value, self._game.prng.rand_double()) #TODO: check
+        self._enemy.set_bullet_launch_interval(value, self._game.prng.rand_uint32())
 
 
     @instruction(78)
@@ -712,46 +684,112 @@ class ECLRunner(object):
         self._game.change_bullets_into_star_items()
 
 
+    @instruction(85)
+    def new_laser(self, laser_type, sprite_idx_offset, angle, speed,
+                  start_offset, end_offset, max_length, width,
+                  start_duration, duration, end_duration,
+                  grazing_delay, grazing_extra_duration, unknown):
+        self._enemy.new_laser(85, laser_type, sprite_idx_offset, self._getval(angle), speed,
+                              start_offset, end_offset, max_length, width,
+                              start_duration, duration, end_duration,
+                              grazing_delay, grazing_extra_duration, unknown)
+
+
+    @instruction(86)
+    def new_laser_towards_player(self, laser_type, sprite_idx_offset, angle, speed,
+                                 start_offset, end_offset, max_length, width,
+                                 start_duration, duration, end_duration,
+                                 grazing_delay, grazing_extra_duration, unknown):
+        self._enemy.new_laser(86, laser_type, sprite_idx_offset, self._getval(angle), speed,
+                              start_offset, end_offset, max_length, width,
+                              start_duration, duration, end_duration,
+                              grazing_delay, grazing_extra_duration, unknown)
+
+
+    @instruction(87)
+    def set_upcoming_laser_id(self, laser_id):
+        self._enemy.current_laser_id = laser_id
+
+
+    @instruction(88)
+    def alter_laser_angle(self, laser_id, delta):
+        try:
+            laser = self._enemy.laser_by_id[laser_id]
+        except KeyError:
+            pass #TODO
+        else:
+            laser.angle += self._getval(delta)
+
+
+    @instruction(90)
+    def reposition_laser(self, laser_id, ox, oy, oz):
+        try:
+            laser = self._enemy.laser_by_id[laser_id]
+        except KeyError:
+            pass #TODO
+        else:
+            laser.set_base_pos(self._enemy.x + ox, self._enemy.y + oy)
+
+
+    @instruction(92)
+    def cancel_laser(self, laser_id):
+        try:
+            laser = self._enemy.laser_by_id[laser_id]
+        except KeyError:
+            pass #TODO
+        else:
+            laser.cancel()
+
+
     @instruction(93)
-    def set_spellcard(self, unknown, number, name):
+    def set_spellcard(self, face, number, name):
         #TODO: display it on the game.
-        #TODO: make the enemies more resistants (and find how).
+        self._enemy.difficulty_coeffs = (-.5, .5, 0, 0, 0, 0)
         self._game.change_bullets_into_star_items()
-        self._game.spellcard = number
-        self._game.enable_effect()
+        self._game.spellcard = (number, name, face)
+        self._game.enable_spellcard_effect()
 
 
     @instruction(94)
     def end_spellcard(self):
         #TODO: return everything back to normal
         #TODO: give the spellcard bonus.
-        if self._game.spellcard:
+        if self._game.spellcard is not None:
             self._game.change_bullets_into_star_items()
         self._game.spellcard = None
-        self._game.disable_effect()
+        self._game.disable_spellcard_effect()
 
 
     @instruction(95)
     def pop_enemy(self, sub, x, y, z, life, bonus_dropped, die_score):
-        self._game.ecl_runner._pop_enemy(sub, 0, self._getval(x), self._getval(y), 0, life, bonus_dropped, die_score)
+        self._pop_enemy(sub, 0, self._getval(x),
+                                self._getval(y),
+                                self._getval(z),
+                                life, bonus_dropped, die_score)
 
 
     @instruction(96)
     def kill_enemies(self):
-        for enemy in self._game.enemies:
-            if enemy.touchable and not enemy.boss:
-                enemy.life = 0
+        self._game.kill_enemies()
 
 
     @instruction(97)
-    def set_anim(self, sprite_index):
-        self._enemy.set_anim(sprite_index)
+    def set_anim(self, script):
+        self._enemy.set_anim(script)
 
 
     @instruction(98)
     def set_multiple_anims(self, default, end_left, end_right, left, right):
-        self._enemy.movement_dependant_sprites = end_left, end_right, left, right
-        self._enemy.set_anim(default)
+        if left == -1:
+            self._enemy.movement_dependant_sprites = None
+        else:
+            self._enemy.movement_dependant_sprites = end_left, end_right, left, right
+            self._enemy.set_anim(default)
+
+
+    @instruction(99)
+    def set_aux_anm(self, number, script):
+        self._enemy.set_aux_anm(number, script)
 
 
     @instruction(100)
@@ -766,7 +804,8 @@ 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
+            self._game.interface.set_boss_life()
         elif value == -1:
             self._enemy.boss = False
             self._game.boss = None
@@ -776,8 +815,7 @@ class ECLRunner(object):
 
     @instruction(103)
     def set_hitbox(self, width, height, depth):
-        self._enemy.hitbox = (width, height)
-        self._enemy.hitbox_half_size = (width / 2., height / 2.)
+        self._enemy.set_hitbox(width, height)
 
 
     @instruction(104)
@@ -793,6 +831,11 @@ class ECLRunner(object):
         self._enemy.damageable = bool(damageable & 1)
 
 
+    @instruction(106)
+    def play_sound(self, index):
+        self._enemy.play_sound(index)
+
+
     @instruction(107)
     def set_death_flags(self, death_flags):
         self._enemy.death_flags = death_flags
@@ -805,8 +848,6 @@ class ECLRunner(object):
 
     @instruction(109)
     def memory_write(self, value, index):
-        #TODO
-        #XXX: this is a hack to display bosses although we don't handle MSG :)
         if index == 0:
             self._enemy.boss_callback = value
         else:
@@ -816,6 +857,7 @@ class ECLRunner(object):
     @instruction(111)
     def set_life(self, value):
         self._enemy.life = value
+        self._game.interface.set_boss_life()
 
 
     @instruction(112)
@@ -829,7 +871,11 @@ class ECLRunner(object):
 
     @instruction(113)
     def set_low_life_trigger(self, value):
+        #TODO: the enemy's life bar fills in 100 frames.
+        # During those frames, the ECL doesn't seem to be executed.
+        # However, the ECL isn't directly paused by this instruction itself.
         self._enemy.low_life_trigger = value
+        self._game.interface.set_spell_life()
 
 
     @instruction(114)
@@ -839,6 +885,7 @@ class ECLRunner(object):
 
     @instruction(115)
     def set_timeout(self, timeout):
+        self._enemy.frame = 0
         self._enemy.timeout = timeout
 
 
@@ -856,14 +903,50 @@ class ECLRunner(object):
         self._enemy.touchable = bool(value)
 
 
+    @instruction(118)
+    def drop_particles(self, anim, number, a, b, c, d):
+        #TODO: find the utility of the other values.
+
+        if number == 0 or number > 640: #TODO: remove that hardcoded 640, and verify it.
+            number = 640
+
+        if anim == -1:
+            return
+        if 0 <= anim <= 2:
+            self._game.new_effect((self._enemy.x, self._enemy.y), anim + 3, number=number)
+        elif anim == 3:
+            self._game.new_particle((self._enemy.x, self._enemy.y), 6, 256, number=number) #TODO: make it go back a bit at the end.
+        elif 4 <= anim <= 15:
+            self._game.new_particle((self._enemy.x, self._enemy.y), anim + 5, 192, number=number)
+        elif anim == 16:
+            self._game.new_effect((self._enemy.x, self._enemy.y), 0, self._game.spellcard_effect_anm, number=number)
+        elif anim == 17:
+            self._game.new_particle((self._enemy.x, self._enemy.y), anim - 10, 640, number=number, reverse=True, duration=60)
+        elif anim == 18:
+            self._game.new_particle((self._enemy.x, self._enemy.y), anim - 10, 640, number=number, reverse=True, duration=240)
+        elif anim == 19:
+            self._game.new_effect((self._enemy.x, self._enemy.y), anim - 10, number=number)
+        else:
+            raise Exception #TODO
+
+
     @instruction(119)
     def drop_some_bonus(self, number):
-        bonus = 0 if self._enemy.select_player().state.power < 128 else 1
-        for i in range(number):
-            #TODO: find the formula in the binary.
-            self._game.drop_bonus(self._enemy.x - 64 + self._game.prng.rand_uint16() % 128,
-                                  self._enemy.y - 64 + self._game.prng.rand_uint16() % 128,
-                                  bonus)
+        if self._enemy.select_player().state.power < 128:
+            if number > 0:
+                #TODO: find the real formula in the binary.
+                self._game.drop_bonus(self._enemy.x - 64 + self._game.prng.rand_double() * 128,
+                                      self._enemy.y - 64 + self._game.prng.rand_double() * 128,
+                                      2)
+            for i in xrange(number - 1):
+                self._game.drop_bonus(self._enemy.x - 64 + self._game.prng.rand_double() * 128,
+                                      self._enemy.y - 64 + self._game.prng.rand_double() * 128,
+                                      0)
+        else:
+            for i in xrange(number):
+                self._game.drop_bonus(self._enemy.x - 64 + self._game.prng.rand_double() * 128,
+                                      self._enemy.y - 64 + self._game.prng.rand_double() * 128,
+                                      1)
 
 
     @instruction(120)
@@ -876,40 +959,148 @@ class ECLRunner(object):
     def call_special_function(self, function, arg):
         if function == 0: # Cirno
             if arg == 0:
+                self._game.new_effect((self._enemy.x, self._enemy.y), 17)
                 for bullet in self._game.bullets:
                     bullet.speed = bullet.angle = 0.
-                    bullet.delta = (0., 0.)
+                    bullet.dx, bullet.dy = 0., 0.
                     bullet.set_anim(sprite_idx_offset=15) #TODO: check
             else:
+                self._game.new_effect((self._enemy.x, self._enemy.y), 17)
                 for bullet in self._game.bullets:
-                    bullet.speed = 2.0 #TODO
-                    bullet.angle = self._game.prng.rand_double() * pi #TODO
-                    bullet.delta = (cos(bullet.angle) * bullet.speed, sin(bullet.angle) * bullet.speed)
+                    bullet.flags = 16 #TODO: check
+                    angle = pi + self._game.prng.rand_double() * 2. * pi
+                    bullet.attributes[4:6] = [0.01, angle] #TODO: check
+                    bullet.attributes[0] = -1 #TODO: check
+                    bullet.set_anim(sprite_idx_offset=15) #TODO: check
         elif function == 1: # Cirno
             offset = (self._game.prng.rand_uint16() % arg - arg / 2,
                       self._game.prng.rand_uint16() % arg - arg / 2)
             self._enemy.fire(offset=offset)
+        elif function == 3: # Patchouli’s dual sign spellcards
+            values = [[0, 3, 1],
+                      [2, 3, 4],
+                      [1, 4, 0],
+                      [4, 2, 3]]
+            character = self._enemy.select_player().state.character
+            self.variables[1:4] = values[character]
+        elif function == 4:
+            if arg == 1:
+                self._game.time_stop = True
+            else:
+                self._game.time_stop = False
+        elif function == 7: # Remilia’s laser webs
+            base_angle = self._game.prng.rand_double() * 2 * pi
+            for i in xrange(16):
+                delta = [+pi / 4., -pi / 4.][i % 2]
+                ox, oy = self._enemy.bullet_launch_offset
+                length = 32. #TODO: check
+
+                # Inner circle
+                angle = base_angle + i * pi / 8.
+                ox, oy = ox + cos(angle) * length, oy + sin(angle) * length
+                length = 112. #TODO: check
+                if arg == 0:
+                    self._enemy.new_laser(85, 1, 1, angle, 0., 0., length, length, 30.,
+                                          100, 80, 15, #TODO: check
+                                          0, 0, 0, offset=(ox, oy))
+                else:
+                    self._enemy.fire(offset=(ox, oy))
+
+                # Middle circle
+                ox, oy = ox + cos(angle) * length, oy + sin(angle) * length
+                angle += delta
+
+                if arg == 0:
+                    self._enemy.new_laser(85, 1, 1, angle, 0., 0., length, length, 30.,
+                                          100, 80, 15, #TODO: check
+                                          0, 0, 0, offset=(ox, oy))
+                else:
+                    self._enemy.fire(offset=(ox, oy))
+
+                # Outer circle
+                ox, oy = ox + cos(angle) * length, oy + sin(angle) * length
+                angle += delta
+                length = 400. #TODO: check
+
+                if arg == 0:
+                    self._enemy.new_laser(85, 1, 1, angle, 0., 0., length, length, 30.,
+                                          100, 80, 15, #TODO: check
+                                          0, 0, 0, offset=(ox, oy))
+                else:
+                    self._enemy.fire(offset=(ox, oy))
+        elif function == 8: # Remilia’s magic
+            bullet_attributes = [70, 1, 1, 1, 1, 0., 0., 0., 0.7, 0]
+            n = 0
+            for bullet in self._game.bullets:
+                if bullet._bullet_type.type_id < 5:
+                    continue
+                n += 1
+                bullet_attributes[8] = bullet.angle
+                self._enemy.fire(launch_pos=(bullet.x, bullet.y),
+                                 bullet_attributes=bullet_attributes)
+            self._setval(-10004, n)
+        elif function == 9:
+            self._game.new_effect((self._enemy.x, self._enemy.y), 17)
+            base_angle = pi + 2. * self._game.prng.rand_double() * pi
+            for bullet in self._game.bullets:
+                if bullet._bullet_type.type_id < 5 and bullet.speed == 0.:
+                    bullet.flags = 16 #TODO: check
+                    distance = hypot(bullet.x - self._enemy.x, bullet.y - self._enemy.y)
+                    angle = base_angle
+                    angle += distance /80. #TODO: This is most probably wrong
+                    bullet.attributes[4:6] = [0.01, angle] #TODO: check
+                    bullet.attributes[0] = -1 #TODO: check
+                    bullet.set_anim(sprite_idx_offset=1) #TODO: check
+        elif function == 11:
+            self._game.new_effect((self._enemy.x, self._enemy.y), 17)
+            self._game.prng.rand_double() #TODO: what is it for?
+            for bullet in self._game.bullets: #TODO Bullet order is WRONG
+                if bullet._bullet_type.type_id < 5 and bullet.speed == 0.:
+                    bullet.flags = 16 #TODO: check
+                    angle = pi + self._game.prng.rand_double() * 2. * pi
+                    bullet.attributes[4:6] = [0.01, angle] #TODO: check
+                    bullet.attributes[0] = -1 #TODO: check
+                    bullet.set_anim(sprite_idx_offset=1) #TODO: check
         elif function == 13:
             if self._enemy.bullet_attributes is None:
                 return
 
-            if self._enemy.frame % 6:
+            frame = self._getval(-10004)
+            self._setval(-10004, frame + 1)
+
+            if frame % 6 != 0:
                 return
 
             (type_, anim, sprite_idx_offset, bullets_per_shot, number_of_shots,
              speed, speed2, launch_angle, angle, flags) = self._enemy.bullet_attributes
             for i in range(arg):
-                _angle = i*2*pi/arg
-                _angle2 = _angle + self._getval(-10007)
+                _angle = i*2*pi/arg + self._getval(-10007)
                 _distance = self._getval(-10008)
-                launch_pos = (192 + cos(_angle2) * _distance,
-                              224 + sin(_angle2) * _distance)
+                launch_pos = (192 + cos(_angle) * _distance,
+                              224 + sin(_angle) * _distance)
                 bullet_attributes = (type_, anim, sprite_idx_offset,
                                      bullets_per_shot, number_of_shots,
                                      speed, speed2,
                                      self._getval(-10006) + _angle, angle, flags)
                 self._enemy.fire(launch_pos=launch_pos,
                                  bullet_attributes=bullet_attributes)
+        elif function == 14: # Laevateinn
+            if arg == 0:
+                self.variables[4] = 0
+                for laser in self._enemy.laser_by_id.values():
+                    self.variables[4] += 1
+                    for pos in laser.get_bullets_pos():
+                        self._enemy.fire(launch_pos=pos)
+            else:
+                pass #TODO: check
+        elif function == 16: # QED: Ripples of 495 years
+            if arg == 0:
+                self.variables[9] = 40 + self._enemy.life // 25
+                self.variables[7] = 2. - self._enemy.life / 6000.
+            else:
+                #TODO: I'm really not sure about that...
+                self.variables[6] = self._game.prng.rand_double() * (self._game.width - 64.) + 32.
+                self.variables[7] = self._game.prng.rand_double() * (self._game.width / 2. - 64.) + 32.
         else:
             logger.warn("Unimplemented special function %d!", function)
 
@@ -931,14 +1122,27 @@ class ECLRunner(object):
         self._enemy.remaining_lives = lives
 
 
-    @instruction(132)
-    def set_visible(self, value):
-        self._enemy._visible = bool(value)
-        if self._enemy._sprite:
-            self._enemy._sprite._removed = bool(value)
+    @instruction(128)
+    def interrupt(self, event):
+        self._enemy.anmrunner.interrupt(event)
+
+
+    @instruction(129)
+    def interrupt_aux(self, number, event):
+        self._enemy.aux_anm[number].anmrunner.interrupt(event)
 
 
     @instruction(131)
     def set_difficulty_coeffs(self, speed_a, speed_b, nb_a, nb_b, shots_a, shots_b):
         self._enemy.difficulty_coeffs = (speed_a, speed_b, nb_a, nb_b, shots_a, shots_b)
 
+
+    @instruction(132)
+    def set_invisible(self, value):
+        self._enemy.visible = not bool(value)
+
+
+    @instruction(133)
+    def copy_callbacks(self):
+        self._enemy.timeout_callback = self._enemy.death_callback
+
new file mode 100644
--- /dev/null
+++ b/pytouhou/vm/msgrunner.py
@@ -0,0 +1,160 @@
+# -*- 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
+
+logger = get_logger(__name__)
+
+
+class NextStage(Exception):
+    pass
+
+
+class MSGRunner(object):
+    __metaclass__ = MetaRegistry
+    __slots__ = ('_msg', '_game', 'frame', 'sleep_time', 'allow_skip',
+                 'skipping', 'frozen', 'ended', 'instruction_pointer',
+                 'handlers')
+
+    def __init__(self, msg, script, game):
+        self._msg = msg.msgs[script + 10 * (game.players[0].state.character // 2)]
+        self._game = game
+        self.handlers = self._handlers[6]
+        self.frame = 0
+        self.sleep_time = 0
+        self.allow_skip = True
+        self.skipping = False
+        self.frozen = False
+        self.ended = False
+
+        self.instruction_pointer = 0
+
+
+    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
+
+        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
+        self._game.texts = [None] * 4 + self._game.texts[4:]
+
+
+    @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._game.new_face(side, effect)
+
+
+    @instruction(2)
+    def change_face(self, side, index):
+        face = self._game.faces[side]
+        if face:
+            face.load(index)
+
+
+    @instruction(3)
+    def display_text(self, side, index, text):
+        if index == 0:
+            self._game.texts[0] = None
+            self._game.texts[1] = None
+        self._game.texts[index] = self._game.new_native_text((64, 372 + index * 24), text)
+        self._game.texts[index].set_timeout(-1, effect='fadeout', duration=15)
+
+
+    @instruction(4)
+    def pause(self, duration):
+        if not (self.skipping and self.allow_skip):
+            self.sleep_time = duration
+
+
+    @instruction(5)
+    def animate(self, side, effect):
+        face = self._game.faces[side]
+        if face:
+            face.animate(effect)
+
+
+    @instruction(6)
+    def spawn_enemy_sprite(self):
+        self._game.msg_wait = False
+
+
+    @instruction(7)
+    def change_music(self, track):
+        self._game.music.play(track)
+
+
+    @instruction(8)
+    def display_description(self, side, index, text):
+        assert side == 1  # It shouldn’t crash when it’s something else.
+        self._game.texts[2 + index] = self._game.new_native_text((336, 320 + index * 18), text, align='right')
+
+
+    @instruction(10)
+    def freeze(self):
+        self.frozen = True
+
+
+    @instruction(11)
+    def next_stage(self):
+        raise NextStage
+
+
+    @instruction(13)
+    def set_allow_skip(self, boolean):
+        self.allow_skip = bool(boolean)
--- a/setup.py
+++ b/setup.py
@@ -1,81 +1,85 @@
 # -*- encoding: utf-8 -*-
 
-import os, sys
+import os
+import sys
 from distutils.core import setup
 from distutils.extension import Extension
-from distutils.command.build_scripts import build_scripts
-from distutils.dep_util import newer
-from distutils import log
+from subprocess import check_output
 
 # Cython is needed
 try:
-    from Cython.Distutils import build_ext
+    from Cython.Build import cythonize
 except ImportError:
     print('You don’t seem to have Cython installed. Please get a '
-          'copy from www.cython.org and install it')
+          'copy from http://www.cython.org/ and install it.')
     sys.exit(1)
 
 
+COMMAND = 'pkg-config'
+SDL_LIBRARIES = ['sdl2', 'SDL2_image', 'SDL2_mixer', 'SDL2_ttf']
+
 packages = []
 extension_names = []
 extensions = []
 
 
-
-# The installed script shouldn't call pyximport, strip references to it
-class BuildScripts(build_scripts):
-    def copy_scripts(self):
-        self.mkpath('scripts')
-        for script in (os.path.basename(script) for script in self.scripts):
-            outfile = os.path.join('scripts', script)
-            if not self.force and not newer(script, outfile):
-                log.debug("not copying %s (up-to-date)", script)
-            elif not self.dry_run:
-                with open(script, 'r') as file, open(outfile, 'w') as out:
-                    for line in file:
-                        if not 'pyximport' in line:
-                            out.write(line)
-
-        build_scripts.copy_scripts(self)
-
+def get_arguments(arg, libraries):
+    try:
+        return check_output([COMMAND, arg] + libraries).split()
+    except OSError:
+        print('You don’t seem to have pkg-config installed. Please get a copy '
+              'from http://pkg-config.freedesktop.org/ and install it.\n'
+              'If you prefer to use it from somewhere else, just modify the '
+              'setup.py script.')
+        sys.exit(1)
 
 
 for directory, _, files in os.walk('pytouhou'):
     package = directory.replace(os.path.sep, '.')
     packages.append(package)
     for filename in files:
-        if filename.endswith('.pyx'):
+        if filename.endswith('.pyx') or filename.endswith('.py') and not filename == '__init__.py':
             extension_name = '%s.%s' % (package, os.path.splitext(filename)[0])
             extension_names.append(extension_name)
+            if extension_name == 'pytouhou.lib.sdl':
+                compile_args = get_arguments('--cflags', SDL_LIBRARIES)
+                link_args = get_arguments('--libs', SDL_LIBRARIES)
+            elif extension_name.startswith('pytouhou.ui.'): #XXX
+                compile_args = get_arguments('--cflags', ['gl'] + SDL_LIBRARIES)
+                link_args = get_arguments('--libs', ['gl'] + SDL_LIBRARIES)
+            else:
+                compile_args = None
+                link_args = None
             extensions.append(Extension(extension_name,
-                                        [os.path.join(directory, filename)]))
-
+                                        [os.path.join(directory, filename)],
+                                        extra_compile_args=compile_args,
+                                        extra_link_args=link_args))
 
 
 # TODO: find a less-intrusive, cleaner way to do this...
 try:
     from cx_Freeze import setup, Executable
 except ImportError:
+    is_windows = False
     extra = {}
 else:
-    extra = {
-             'options': {'build_exe': {'includes': extension_names}},
-             'executables': [Executable(script='scripts/eosd', base='Win32GUI')]
-            }
-
+    is_windows = True
+    extra = {'options': {'build_exe': {'includes': extension_names}},
+             'executables': [Executable(script='eosd', base='Win32GUI')]}
 
 
 setup(name='PyTouhou',
-      version="0.1",
+      version='0.1',
       author='Thibaut Girka',
       author_email='thib@sitedethib.com',
-      url='http://hg.sitedethib.com/touhou/',
+      url='http://pytouhou.linkmauve.fr/',
       license='GPLv3',
       packages=packages,
-      ext_modules=extensions,
-      scripts=['scripts/eosd'],
-      cmdclass={'build_ext': build_ext,
-                'build_scripts': BuildScripts},
-      **extra
-     )
-
+      ext_modules=cythonize(extensions, nthreads=4,
+                            compiler_directives={'infer_types': True,
+                                                 'infer_types.verbose': True},
+                            compile_time_env={'MAX_TEXTURES': 1024,
+                                              'MAX_CHANNELS': 26,
+                                              'USE_GLEW': is_windows}),
+      scripts=['eosd', 'anmviewer'],
+      **extra)