# HG changeset patch # User Emmanuel Gil Peyrot # Date 1380301307 -7200 # Node ID 2f53be1b2f60e0254a0c25668eeec6b47dd5075f # Parent ca22df9e70bc31ae830d975db5f3a279bed71e43# Parent c099802e24354c6384e50188011bee4ebf6f558e Merge netplay branch. diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -1,1 +1,8 @@ -.pyc +\.pyc$ +\.pyxbldc$ +\.c$ +\.o$ +\.so$ +\.pyd$ +^build$ +^scripts$ diff --git a/README b/README --- 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/ diff --git a/TODO b/TODO 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 diff --git a/anmviewer b/anmviewer 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 +## +## 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) diff --git a/data/ST/Makefile b/data/ST/Makefile --- 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 diff --git a/eosd b/eosd --- a/eosd +++ b/eosd @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 # -*- encoding: utf-8 -*- ## ## Copyright (C) 2011 Thibaut Girka @@ -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() diff --git a/music.py b/music.py 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 +## +## 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)) diff --git a/pcb b/pcb --- a/pcb +++ b/pcb @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 # -*- encoding: utf-8 -*- ## ## Copyright (C) 2011 Thibaut Girka @@ -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 diff --git a/pytouhou/formats/__init__.py b/pytouhou/formats/__init__.py --- 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 diff --git a/pytouhou/formats/anm0.py b/pytouhou/formats/anm0.py --- 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(' +## Copyright (C) 2011 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +from 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(' +## +## 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(' +## +## 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') diff --git a/pytouhou/formats/msg.py b/pytouhou/formats/msg.py --- 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 diff --git a/pytouhou/formats/music.py b/pytouhou/formats/music.py new file mode 100644 --- /dev/null +++ b/pytouhou/formats/music.py @@ -0,0 +1,29 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2012 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + + +from 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(' +## +## 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('> 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('= 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('> 5) & 7) + encrypted = clear ^ key + key += clear + file.write(chr(encrypted)) + diff --git a/pytouhou/formats/sht.py b/pytouhou/formats/sht.py --- 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('= 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(' +## +## 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 diff --git a/pytouhou/game/background.py b/pytouhou/game/background.py --- 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 + diff --git a/pytouhou/game/bullet.pxd b/pytouhou/game/bullet.pxd 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 * diff --git a/pytouhou/game/bullet.py b/pytouhou/game/bullet.pyx 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 diff --git a/pytouhou/game/bullettype.pxd b/pytouhou/game/bullettype.pxd 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 diff --git a/pytouhou/game/bullettype.py b/pytouhou/game/bullettype.py --- 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 - diff --git a/pytouhou/game/effect.pxd b/pytouhou/game/effect.pxd 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) diff --git a/pytouhou/game/effect.py b/pytouhou/game/effect.pyx 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 * game.prng.rand_double() - amp / 2, + self.y + amp * 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 diff --git a/pytouhou/game/element.pxd b/pytouhou/game/element.pxd 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 diff --git a/pytouhou/game/element.py b/pytouhou/game/element.py new file mode 100644 --- /dev/null +++ b/pytouhou/game/element.py @@ -0,0 +1,23 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2013 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +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 diff --git a/pytouhou/game/enemy.pxd b/pytouhou/game/enemy.pxd 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 * diff --git a/pytouhou/game/enemy.py b/pytouhou/game/enemy.pyx 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 * (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) diff --git a/pytouhou/game/face.py b/pytouhou/game/face.py new file mode 100644 --- /dev/null +++ b/pytouhou/game/face.py @@ -0,0 +1,47 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2012 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + + +from pytouhou.game.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() diff --git a/pytouhou/game/game.pxd b/pytouhou/game/game.pxd 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) diff --git a/pytouhou/game/game.py b/pytouhou/game/game.pyx 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 = 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 = 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 = 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) diff --git a/pytouhou/game/item.pxd b/pytouhou/game/item.pxd 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) diff --git a/pytouhou/game/item.py b/pytouhou/game/item.pyx 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 < -(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 - diff --git a/pytouhou/game/itemtype.pxd b/pytouhou/game/itemtype.pxd 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 diff --git a/pytouhou/game/itemtype.py b/pytouhou/game/itemtype.py --- 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] diff --git a/pytouhou/game/laser.pxd b/pytouhou/game/laser.pxd 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 * diff --git a/pytouhou/game/laser.pyx b/pytouhou/game/laser.pyx new file mode 100644 --- /dev/null +++ b/pytouhou/game/laser.pyx @@ -0,0 +1,253 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2012 Thibaut Girka +## +## 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 = 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 = 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 = 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 = 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 diff --git a/pytouhou/game/lasertype.pxd b/pytouhou/game/lasertype.pxd 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 diff --git a/pytouhou/game/lasertype.py b/pytouhou/game/lasertype.py 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 diff --git a/pytouhou/game/orb.pxd b/pytouhou/game/orb.pxd 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) diff --git a/pytouhou/game/orb.py b/pytouhou/game/orb.py --- 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 diff --git a/pytouhou/game/player.pxd b/pytouhou/game/player.pxd 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) diff --git a/pytouhou/game/player.py b/pytouhou/game/player.pyx 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 = 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 = 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() diff --git a/pytouhou/game/sprite.pxd b/pytouhou/game/sprite.pxd 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) diff --git a/pytouhou/game/sprite.py b/pytouhou/game/sprite.py --- 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 diff --git a/pytouhou/game/text.pxd b/pytouhou/game/text.pxd 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=*) diff --git a/pytouhou/game/text.py b/pytouhou/game/text.py new file mode 100644 --- /dev/null +++ b/pytouhou/game/text.py @@ -0,0 +1,315 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2011 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +from pytouhou.game.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 diff --git a/pytouhou/games/eosd.py b/pytouhou/games/eosd.py --- 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 - diff --git a/pytouhou/games/pcb.py b/pytouhou/games/pcb.py --- 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)) diff --git a/pytouhou/lib/__init__.py b/pytouhou/lib/__init__.py new file mode 100644 diff --git a/pytouhou/lib/_sdl.pxd b/pytouhou/lib/_sdl.pxd new file mode 100644 --- /dev/null +++ b/pytouhou/lib/_sdl.pxd @@ -0,0 +1,194 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2013 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +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) diff --git a/pytouhou/lib/opengl.pxd b/pytouhou/lib/opengl.pxd new file mode 100644 --- /dev/null +++ b/pytouhou/lib/opengl.pxd @@ -0,0 +1,157 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2013 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + + +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) diff --git a/pytouhou/lib/sdl.pxd b/pytouhou/lib/sdl.pxd new file mode 100644 --- /dev/null +++ b/pytouhou/lib/sdl.pxd @@ -0,0 +1,98 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2013 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +from _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 diff --git a/pytouhou/lib/sdl.pyx b/pytouhou/lib/sdl.pyx new file mode 100644 --- /dev/null +++ b/pytouhou/lib/sdl.pyx @@ -0,0 +1,284 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2013 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +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(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(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) diff --git a/pytouhou/resource/anmwrapper.py b/pytouhou/resource/anmwrapper.py 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 diff --git a/pytouhou/resource/loader.py b/pytouhou/resource/loader.py --- a/pytouhou/resource/loader.py +++ b/pytouhou/resource/loader.py @@ -1,14 +1,65 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2012 Thibaut Girka +## +## 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), []) diff --git a/pytouhou/ui/anmrenderer.pyx b/pytouhou/ui/anmrenderer.pyx new file mode 100644 --- /dev/null +++ b/pytouhou/ui/anmrenderer.pyx @@ -0,0 +1,172 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2011 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +from pytouhou.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 diff --git a/pytouhou/ui/background.pxd b/pytouhou/ui/background.pxd --- 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 * diff --git a/pytouhou/ui/background.pyx b/pytouhou/ui/background.pyx --- a/pytouhou/ui/background.pyx +++ b/pytouhou/ui/background.pyx @@ -1,6 +1,6 @@ # -*- encoding: utf-8 -*- ## -## Copyright (C) 2011 Thibaut Girka +## Copyright (C) 2013 Emmanuel Gil Peyrot ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -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 = 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), 0) + glEnableVertexAttribArray(0) + glVertexAttribPointer(1, 2, GL_FLOAT, False, sizeof(Vertex), 12) + glEnableVertexAttribArray(1) + glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, True, sizeof(Vertex), 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 = 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) diff --git a/pytouhou/ui/gamerenderer.pxd b/pytouhou/ui/gamerenderer.pxd 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 * diff --git a/pytouhou/ui/gamerenderer.pyx b/pytouhou/ui/gamerenderer.pyx --- 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 diff --git a/pytouhou/ui/gamerunner.py b/pytouhou/ui/gamerunner.pyx 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 diff --git a/pytouhou/ui/music.pyx b/pytouhou/ui/music.pyx new file mode 100644 --- /dev/null +++ b/pytouhou/ui/music.pyx @@ -0,0 +1,98 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2012 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + + +from 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 diff --git a/pytouhou/ui/renderer.pxd b/pytouhou/ui/renderer.pxd --- 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) diff --git a/pytouhou/ui/renderer.pyx b/pytouhou/ui/renderer.pyx --- 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] = obj + i += 1 + if i >= 640*3-4: + return i + return i cdef class Renderer: def __cinit__(self): - # Allocate buffers self.vertex_buffer = 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] = 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 = 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), 0) + glEnableVertexAttribArray(0) + glVertexAttribPointer(1, 2, GL_FLOAT, False, sizeof(Vertex), 12) + glEnableVertexAttribArray(1) + glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, True, sizeof(Vertex), 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), 0) + glEnableVertexAttribArray(0) + glVertexAttribPointer(1, 2, GL_FLOAT, False, sizeof(Vertex), 12) + glEnableVertexAttribArray(1) + glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, True, sizeof(Vertex), 20) + glEnableVertexAttribArray(2) - for (texture_key, blendfunc), indices in indices_by_texture.items(): - glVertexPointer(3, GL_INT, 24, &self.vertex_buffer[0].x) - glTexCoordPointer(2, GL_FLOAT, 24, &self.vertex_buffer[0].u) - glColorPointer(4, GL_UNSIGNED_BYTE, 24, &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), 0) + glEnableVertexAttribArray(0) + glVertexAttribPointer(1, 2, GL_FLOAT, False, sizeof(PassthroughVertex), 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) diff --git a/pytouhou/ui/shader.pxd b/pytouhou/ui/shader.pxd 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 * diff --git a/pytouhou/ui/shader.pyx b/pytouhou/ui/shader.pyx 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 = 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 = 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) diff --git a/pytouhou/ui/shaders/__init__.py b/pytouhou/ui/shaders/__init__.py new file mode 100644 diff --git a/pytouhou/ui/shaders/eosd.py b/pytouhou/ui/shaders/eosd.py 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 +## +## 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); + } + ''']) diff --git a/pytouhou/ui/sprite.pxd b/pytouhou/ui/sprite.pxd --- 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) diff --git a/pytouhou/ui/sprite.pyx b/pytouhou/ui/sprite.pyx --- 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 / sprite.anm.size[0] + y_1 = 1 / 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 + 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 - diff --git a/pytouhou/ui/texture.pxd b/pytouhou/ui/texture.pxd 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 diff --git a/pytouhou/ui/texture.pyx b/pytouhou/ui/texture.pyx --- 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_, + thtx.data) - return texture - + return TextureId(texture) diff --git a/pytouhou/ui/window.pxd b/pytouhou/ui/window.pxd 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 diff --git a/pytouhou/ui/window.pyx b/pytouhou/ui/window.pyx new file mode 100644 --- /dev/null +++ b/pytouhou/ui/window.pyx @@ -0,0 +1,176 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2013 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +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 += (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 diff --git a/pytouhou/utils/bitstream.pyx b/pytouhou/utils/bitstream.pyx --- 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 = ( 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 = ( 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 diff --git a/pytouhou/utils/interpolator.pxd b/pytouhou/utils/interpolator.pxd 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) diff --git a/pytouhou/utils/interpolator.pyx b/pytouhou/utils/interpolator.pyx --- 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 = malloc(self._length * sizeof(double)) + self.start_values = malloc(self._length * sizeof(double)) + self.end_values = 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) diff --git a/pytouhou/utils/lzss.py b/pytouhou/utils/lzss.pyx 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 = malloc(size) + dictionary = 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 + diff --git a/pytouhou/utils/maths.pxd b/pytouhou/utils/maths.pxd 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) diff --git a/pytouhou/utils/maths.pyx b/pytouhou/utils/maths.pyx new file mode 100644 --- /dev/null +++ b/pytouhou/utils/maths.pyx @@ -0,0 +1,75 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2013 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + +from 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.)) diff --git a/pytouhou/utils/matrix.pxd b/pytouhou/utils/matrix.pxd --- 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 diff --git a/pytouhou/utils/matrix.pyx b/pytouhou/utils/matrix.pyx --- 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] diff --git a/pytouhou/utils/pe.py b/pytouhou/utils/pe.py new file mode 100644 --- /dev/null +++ b/pytouhou/utils/pe.py @@ -0,0 +1,137 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2011 Thibaut Girka +## +## 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(' +## +## 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) diff --git a/pytouhou/vm/anmrunner.py b/pytouhou/vm/anmrunner.py --- 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 diff --git a/pytouhou/vm/common.py b/pytouhou/vm/common.py --- 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 diff --git a/pytouhou/vm/eclrunner.py b/pytouhou/vm/eclrunner.py --- 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 + diff --git a/pytouhou/vm/msgrunner.py b/pytouhou/vm/msgrunner.py new file mode 100644 --- /dev/null +++ b/pytouhou/vm/msgrunner.py @@ -0,0 +1,160 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2012 Emmanuel Gil Peyrot +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + + +from pytouhou.utils.helpers import get_logger + +from pytouhou.vm.common import MetaRegistry, instruction + +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) diff --git a/setup.py b/setup.py --- 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)