Mercurial > touhou
changeset 69:a142e57218a0
Refactor. Move VMs to pytouhou.vm.
| author | Thibaut Girka <thib@sitedethib.com> |
|---|---|
| date | Sat, 27 Aug 2011 10:58:54 +0200 |
| parents | a2459defd4b6 |
| children | 7c1f20407b3e |
| files | pytouhou/formats/anm0.py pytouhou/game/background.py pytouhou/game/eclrunner.py pytouhou/game/enemymanager.py pytouhou/game/sprite.py pytouhou/vm/__init__.py pytouhou/vm/anmrunner.py pytouhou/vm/common.py pytouhou/vm/eclrunner.py |
| diffstat | 8 files changed, 794 insertions(+), 684 deletions(-) [+] |
line wrap: on
line diff
--- a/pytouhou/formats/anm0.py Fri Aug 26 22:53:15 2011 +0200 +++ b/pytouhou/formats/anm0.py Sat Aug 27 10:58:54 2011 +0200 @@ -13,12 +13,34 @@ ## from struct import pack, unpack -from pytouhou.utils.helpers import read_string +from pytouhou.utils.helpers import read_string, get_logger + + +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'), + 15: ('', 'keep_still'), + 16: ('i', 'set_random_sprite'), + 23: ('', 'set_corner_relative_placement'), + 27: ('f', 'shift_texture_x'), + 28: ('f', 'shift_texture_y'), + 30: ('ffi', 'scale_in')} + + def __init__(self): self.size = (0, 0) self.first_name = None @@ -71,40 +93,24 @@ while True: #TODO instruction_offsets.append(file.tell() - offset) - time, instr_type, length = unpack('<HBB', file.read(4)) + time, opcode, length = unpack('<HBB', file.read(4)) data = file.read(length) - if instr_type == 1: # set_sprite - args = unpack('<I', data) - elif instr_type == 2: # set_scale - args = unpack('<ff', data) - elif instr_type == 3: # set_alpha - args = unpack('<I', data) - elif instr_type == 4: # set_color - args = unpack('<BBBx', data) - elif instr_type == 5: # jump - # Translate offset to instruction index - args = (instruction_offsets.index(unpack('<I', data)[0]),) - elif instr_type == 9: # set_3d_rotation - args = unpack('<fff', data) - elif instr_type == 10: # set_3d_rotation_speed - args = unpack('<fff', data) - elif instr_type == 11: # set_scale_speed - args = unpack('<ff', data) - elif instr_type == 12: # fade - args = unpack('<ii', data) - elif instr_type == 16: # set_random_sprite - args = unpack('<ii', data) - elif instr_type == 27: # shift_texture_x - args = unpack('<f', data) - elif instr_type == 28: # shift_texture_y - args = unpack('<f', data) - elif instr_type == 30: # scale_in - args = unpack('<ffi', data) + if opcode in cls._instructions: + args = unpack('<%s' % cls._instructions[opcode][0], data) else: args = (data,) - anm.scripts[i].append((time, instr_type, args)) - if instr_type == 0: + logger.warn('unknown opcode %d', opcode) + + anm.scripts[i].append((time, opcode, args)) + if opcode == 0: break + + # Translate offsets to instruction pointers + for instr_offset, (j, instr) in zip(instruction_offsets, enumerate(anm.scripts[i])): + time, opcode, args = instr + if opcode == 5: + args = (instruction_offsets.index(args[0]),) + anm.scripts[i][j] = time, opcode, args #TODO return anm
--- a/pytouhou/game/background.py Fri Aug 26 22:53:15 2011 +0200 +++ b/pytouhou/game/background.py Sat Aug 27 10:58:54 2011 +0200 @@ -19,6 +19,7 @@ from itertools import chain from pytouhou.utils.interpolator import Interpolator +from pytouhou.vm.anmrunner import ANMRunner from pytouhou.game.sprite import Sprite @@ -72,8 +73,11 @@ faces = [] for script_index, ox, oy, oz, width_override, height_override in obj.quads: #TODO: per-texture rendering - anm, sprite = self.anm_wrapper.get_sprite(script_index) - if sprite.update(): + sprite = Sprite() + anm_runner = ANMRunner(self.anm_wrapper, script_index, sprite) + anm_runner.run_frame() + sprite.update() + if sprite._changed: sprite.update_vertices_uvs_colors(width_override, height_override) uvs, vertices = sprite._uvs, tuple((x + ox, y + oy, z + oz) for x, y, z in sprite._vertices) colors = sprite._colors
--- a/pytouhou/game/eclrunner.py Fri Aug 26 22:53:15 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,546 +0,0 @@ -# -*- encoding: utf-8 -*- -## -## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com> -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published -## by the Free Software Foundation; version 3 only. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## - - -from math import atan2, cos, sin - -from pytouhou.utils.helpers import get_logger - -logger = get_logger(__name__) - - - -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 - classdict['_handlers'] = instruction_handlers - return type.__new__(mcs, name, bases, classdict) - - - -def instruction(instruction_id): - def _decorator(func): - if not hasattr(func, '_instruction_ids'): - func._instruction_ids = set() - func._instruction_ids.add(instruction_id) - return func - return _decorator - - - -class ECLRunner(object): - __metaclass__ = MetaRegistry - __slots__ = ('_ecl', '_enemy', '_game_state', 'variables', 'sub', 'frame', - 'instruction_pointer', 'comparison_reg', 'stack') - - def __init__(self, ecl, sub, enemy, game_state): - # Things not supposed to change - self._ecl = ecl - self._enemy = enemy - self._game_state = game_state - - # 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.stack = [] - - - def run_iteration(self): - # First, if enemy is dead, return - if self._enemy._removed: - return False - - # Then, check for callbacks - #TODO - - # Now, process script - frame = self.frame - while True: - try: - frame, instr_type, rank_mask, param_mask, args = self._ecl.subs[self.sub][self.instruction_pointer] - except IndexError: - return False - - if frame > self.frame: - break - else: - self.instruction_pointer += 1 - - - #TODO: skip bad ranks - if not rank_mask & (0x100 << self._game_state.rank): - continue - - - if frame == self.frame: - try: - callback = self._handlers[instr_type] - except KeyError: - logger.warn('unhandled opcode %d (args: %r)', instr_type, args) - else: - callback(self, *args) - - self.frame += 1 - return True - - - def _getval(self, value): - if -10012 <= value <= -10001: - return self.variables[int(-10001-value)] - elif -10025 <= value <= -10013: - if value == -10013: - return self._game_state.rank - elif value == -10014: - return self._game_state.difficulty - elif value == -10015: - return self._enemy.x - elif value == -10016: - return self._enemy.y - elif value == -10017: - return self._enemy.z - elif value == -10018: - player = self._enemy.select_player(self._game_state.players) - return player.x - elif value == -10019: - player = self._enemy.select_player(self._game_state.players) - return player.y - elif value == -10021: - player = self._enemy.select_player(self._game_state.players) - return self._enemy.get_player_angle(player) - elif value == -10022: - return self._enemy.frame - elif value == -10024: - return self._enemy.life - elif value == -10025: - return self._enemy.select_player(self._game_state.players).character - raise NotImplementedError(value) #TODO - else: - return value - - - def _setval(self, variable_id, value): - if -10012 <= variable_id <= -10001: - self.variables[int(-10001-variable_id)] = value - elif -10025 <= variable_id <= -10013: - if variable_id == -10015: - self._enemy.x = value - elif variable_id == -10016: - self._enemy.y = value - elif variable_id == -10017: - self._enemy.z = value - elif variable_id == -10022: - self._enemy.frame = value - elif variable_id == -10024: - self._enemy.life = value - else: - raise IndexError #TODO: proper exception - else: - raise IndexError #TODO: proper exception - - - @instruction(0) - def noop(self): - pass #TODO: Really? - - - @instruction(1) - def destroy(self, arg): - #TODO: arg? - self._enemy._removed = True - - - @instruction(2) - def relative_jump(self, frame, instruction_pointer): - """Jumps to a relative offset in the same subroutine. - - Warning: the relative offset has been translated to an instruction pointer - by the ECL parsing code (see pytouhou.formats.ecl). - """ - self.frame, self.instruction_pointer = frame, instruction_pointer - - - @instruction(3) - def relative_jump_ex(self, frame, instruction_pointer, variable_id): - """If the given variable is non-zero, decrease it by 1 and jump to a - relative offset in the same subroutine. - - Warning: the relative offset has been translated to an instruction pointer - by the ECL parsing code (see pytouhou.formats.ecl). - """ - counter_value = self._getval(variable_id) - if counter_value: - self._setval(variable_id, counter_value - 1) - self.frame, self.instruction_pointer = frame, instruction_pointer - - - @instruction(4) - @instruction(5) - def set_variable(self, variable_id, value): - self._setval(variable_id, self._getval(value)) - - - @instruction(6) - def set_random_int(self, variable_id, maxval): - """Set the specified variable to a random int in the [0, maxval) range. - """ - self._setval(variable_id, int(self._getval(maxval) * self._game_state.prng.rand_double())) - - - @instruction(8) - def set_random_float(self, variable_id, maxval): - """Set the specified variable to a random float in [0, maxval) range. - """ - self._setval(variable_id, self._getval(maxval) * self._game_state.prng.rand_double()) - - - @instruction(9) - def set_random_float2(self, variable_id, minval, amp): - self._setval(variable_id, self._getval(minval) + self._getval(amp) * self._game_state.prng.rand_double()) - - - @instruction(13) - def set_random_int2(self, variable_id, minval, amp): - self._setval(variable_id, int(self._getval(minval)) + int(self._getval(amp)) * self._game_state.prng.rand_double()) - - - @instruction(14) - @instruction(21) - def substract(self, variable_id, a, b): - #TODO: 14 takes only ints and 21 only floats. - # The original engine dereferences the variables in the type it waits for, so this isn't exactly the correct implementation, but the data don't contain such case. - self._setval(variable_id, self._getval(a) - self._getval(b)) - - - @instruction(15) - def multiply_int(self, variable_id, a, b): - #TODO: takes only ints. - self._setval(variable_id, self._getval(a) * self._getval(b)) - - - @instruction(16) - def divide_int(self, variable_id, a, b): - #TODO: takes only ints. - self._setval(variable_id, self._getval(a) // self._getval(b)) - - - @instruction(17) - def modulo(self, variable_id, a, b): - self._setval(variable_id, self._getval(a) % self._getval(b)) - - - @instruction(20) - def add_float(self, variable_id, a, b): - #TODO: takes only floats. - self._setval(variable_id, self._getval(a) + self._getval(b)) - - - @instruction(23) - def divide_float(self, variable_id, a, b): - #TODO: takes only floats. - self._setval(variable_id, self._getval(a) / self._getval(b)) - - - @instruction(27) - @instruction(28) - def compare(self, a, b): - #TODO: 27 takes only ints and 28 only floats. - a, b = self._getval(a), self._getval(b) - if a < b: - self.comparison_reg = -1 - elif a == b: - self.comparison_reg = 0 - else: - self.comparison_reg = 1 - - - @instruction(29) - def relative_jump_if_lower_than(self, frame, instruction_pointer): - if self.comparison_reg == -1: - self.relative_jump(frame, instruction_pointer) - - - @instruction(30) - def relative_jump_if_lower_or_equal(self, frame, instruction_pointer): - if self.comparison_reg != 1: - self.relative_jump(frame, instruction_pointer) - - - @instruction(31) - def relative_jump_if_equal(self, frame, instruction_pointer): - if self.comparison_reg == 0: - self.relative_jump(frame, instruction_pointer) - - - @instruction(32) - def relative_jump_if_greater_than(self, frame, instruction_pointer): - if self.comparison_reg == 1: - self.relative_jump(frame, instruction_pointer) - - - @instruction(33) - def relative_jump_if_greater_or_equal(self, frame, instruction_pointer): - if self.comparison_reg != -1: - self.relative_jump(frame, instruction_pointer) - - - @instruction(34) - def relative_jump_if_not_equal(self, frame, instruction_pointer): - if self.comparison_reg != 0: - self.relative_jump(frame, instruction_pointer) - - - @instruction(35) - def call(self, sub, param1, param2): - self.stack.append((self.sub, self.frame, self.instruction_pointer, - self.variables, self.comparison_reg)) - self.sub = sub - self.frame = 0 - self.instruction_pointer = 0 - self.variables = [param1, 0, 0, 0, - param2, 0., 0., 0., - 0, 0, 0, 0] - - - @instruction(36) - def ret(self): - self.sub, self.frame, self.instruction_pointer, self.variables, self.comparison_reg = self.stack.pop() - - - @instruction(39) - def call_if_equal(self, sub, param1, param2, a, b): - if self._getval(a) == self._getval(b): - self.call(sub, param1, param2) - - - @instruction(43) - def set_pos(self, x, y, z): - self._enemy.set_pos(x, y, z) - - - @instruction(45) - def set_angle_speed(self, angle, speed): - self._enemy.angle, self._enemy.speed = angle, speed - - - @instruction(46) - def set_rotation_speed(self, speed): - self._enemy.rotation_speed = speed - - - @instruction(47) - def set_speed(self, speed): - self._enemy.speed = speed - - - @instruction(48) - def set_acceleration(self, acceleration): - self._enemy.acceleration = acceleration - - - @instruction(50) - def set_random_angle(self, min_angle, max_angle): - if self._enemy.screen_box: - minx, miny, maxx, maxy = self._enemy.screen_box - else: - minx, miny, maxx, maxy = (0., 0., 0., 0.) - - angle = self._game_state.prng.rand_double() * (max_angle - min_angle) + min_angle - sa, ca = sin(angle), cos(angle) - - distx = min(96.0, (maxx - minx) / 2.) - disty = min(96.0, (maxy - miny) / 2.) - - if self._enemy.x > maxx - 96.0: - ca = -abs(ca) - elif self._enemy.x < minx + 96.0: - ca = abs(ca) - - if self._enemy.y > maxy - 48.0: - sa = -abs(sa) - elif self._enemy.y < miny + 48.0: - sa = abs(sa) - self._enemy.angle = atan2(sa, ca) - - - @instruction(51) - def target_player(self, unknown, speed): - #TODO: unknown - self._enemy.speed = speed - self._enemy.angle = self._enemy.get_player_angle(self._enemy.select_player(self._game_state.players)) - - - @instruction(57) - def move_to(self, duration, x, y, z): - self._enemy.move_to(duration, x, y, z, lambda x: 2. * x - x ** 2) - - - @instruction(59) - def move_to2(self, duration, x, y, z): - #TODO: not accurate - self._enemy.move_to(duration, x, y, z, lambda x: 1.0014 * x ** 2 - 0.0012 * x) - - - @instruction(61) - def stop_in(self, duration): - self._enemy.stop_in(duration) - - - @instruction(65) - def set_screen_box(self, xmin, ymin, xmax, ymax): - self._enemy.screen_box = xmin, ymin, xmax, ymax - - - @instruction(66) - def clear_screen_box(self): - self._enemy.screen_box = None - - - @instruction(67) - def set_bullet_attributes1(self, bullet_anim, launch_anim, bullets_per_shot, - number_of_shots, speed, unknown, launch_angle, - angle, flags): - self._enemy.set_bullet_attributes(1, bullet_anim, launch_anim, - bullets_per_shot, number_of_shots, - speed, unknown, launch_angle, angle, - flags) - - - @instruction(77) - def set_bullet_interval(self, value): - self._enemy.bullet_launch_interval = value - - - @instruction(78) - def set_delay_attack(self): - self._enemy.delay_attack = True - - - @instruction(79) - def set_no_delay_attack(self): - self._enemy.delay_attack = False - - - @instruction(81) - def set_bullet_launch_offset(self, x, y, z): - self._enemy.bullet_launch_offset = (x, y) - - - @instruction(97) - def set_anim(self, sprite_index): - self._enemy.set_anim(sprite_index) - - - @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) - - - @instruction(100) - def set_death_anim(self, sprite_index): - self._enemy.death_anim = sprite_index % 256 #TODO - - - @instruction(101) - def set_boss_mode(self, unknown): - #TODO: unknown - self._game_state.boss = self._enemy - - - @instruction(103) - def set_hitbox(self, width, height, depth): - self._enemy.hitbox = (width, height) - - - @instruction(105) - def set_damageable(self, vulnerable): - self._enemy.damageable = bool(vulnerable & 1) - - - @instruction(108) - def set_death_callback(self, sub): - self._enemy.death_callback = sub - - - @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.sub = value - self.frame = 0 - self.instruction_pointer = 0 - - - @instruction(111) - def set_life(self, value): - self._enemy.life = value - - - @instruction(112) - def set_ellapsed_time(self, value): - """Sets the enemy's frame counter. - This is used for timeouts, where the framecounter is compared to the - timeout value (what's displayed is (enemy.timeout - enemy.frame) // 60). - """ - self._enemy.frame = value - - - @instruction(113) - def set_low_life_trigger(self, value): - self._enemy.low_life_trigger = value - - - @instruction(114) - def set_low_life_callback(self, sub): - self._enemy.low_life_callback = sub - - - @instruction(115) - def set_timeout(self, timeout): - self._enemy.timeout = timeout - - - @instruction(116) - def set_timeout_callback(self, sub): - self._enemy.timeout_callback = sub - - - @instruction(117) - def set_touchable(self, value): - """Defines whether the enemy is “touchable”. - Bullets only collide with an enemy if it is “touchable”. - Likewise, ReimuA's homing attacks only target “touchable” enemies. - """ - self._enemy.touchable = bool(value) - - - @instruction(126) - def set_remaining_lives(self, lives): - self._enemy.remaining_lives = lives -
--- a/pytouhou/game/enemymanager.py Fri Aug 26 22:53:15 2011 +0200 +++ b/pytouhou/game/enemymanager.py Sat Aug 27 10:58:54 2011 +0200 @@ -18,7 +18,8 @@ import os from struct import unpack, pack from pytouhou.utils.interpolator import Interpolator -from pytouhou.game.eclrunner import ECLRunner +from pytouhou.vm.eclrunner import ECLRunner +from pytouhou.vm.anmrunner import ANMRunner from pytouhou.game.sprite import Sprite from math import cos, sin, atan2 @@ -26,8 +27,8 @@ class Enemy(object): def __init__(self, pos, life, _type, anm_wrapper): self._anm_wrapper = anm_wrapper - self._anm = None self._sprite = None + self._anmrunner = None self._removed = False self._type = _type self._was_visible = False @@ -91,7 +92,8 @@ def set_anim(self, index): - self._anm, self._sprite = self._anm_wrapper.get_sprite(index) + self._sprite = Sprite() + self._anmrunner = ANMRunner(self._anm_wrapper, index, self._sprite) def set_pos(self, x, y, z): @@ -139,7 +141,7 @@ def get_objects_by_texture(self): objects_by_texture = {} - key = self._anm.first_name, self._anm.secondary_name + key = self._sprite.anm.first_name, self._sprite.anm.secondary_name if not key in objects_by_texture: objects_by_texture[key] = (0, [], [], []) vertices = tuple((x + self.x, y + self.y, z) for x, y, z in self._sprite._vertices) @@ -150,7 +152,7 @@ return objects_by_texture - def update(self, frame): + def update(self): x, y = self.x, self.y if self.interpolator: self.interpolator.update(self.frame) @@ -195,20 +197,20 @@ self.x, self.y = x, y + + #TODO + if self._anmrunner and not self._anmrunner.run_frame(): + self._anmrunner = None + if self._sprite: - changed = self._sprite.update() - visible = self.is_visible(384, 448) - if changed and visible: - self._sprite.update_vertices_uvs_colors() - elif not self._sprite.playing: - visible = False + if self._sprite._removed: self._sprite = None - else: - visible = False - + else: + self._sprite.update() + if self._sprite._changed: + self._sprite.update_vertices_uvs_colors() self.frame += 1 - return visible @@ -255,7 +257,11 @@ self.enemies[:] = (enemy for enemy in self.enemies if not enemy._removed) # Update enemies - visible_enemies = [enemy for enemy in self.enemies if enemy.update(frame)] + for enemy in self.enemies: + enemy.update() + + # Filter out non-visible enemies + visible_enemies = [enemy for enemy in self.enemies if enemy.is_visible(384, 448)] #TODO for enemy in visible_enemies: enemy._was_visible = True @@ -265,12 +271,10 @@ enemy._removed = True self.enemies.remove(enemy) - #TODO: disable boss mode if it is dead/it has timeout if self._game_state.boss and self._game_state.boss._removed: self._game_state.boss = None - # Add enemies to vertices/uvs self.objects_by_texture = {} for enemy in visible_enemies:
--- a/pytouhou/game/sprite.py Fri Aug 26 22:53:15 2011 +0200 +++ b/pytouhou/game/sprite.py Sat Aug 27 10:58:54 2011 +0200 @@ -13,30 +13,35 @@ ## -from random import randrange -from struct import unpack - from pytouhou.utils.matrix import Matrix -from pytouhou.utils.helpers import get_logger - -logger = get_logger(__name__) class AnmWrapper(object): def __init__(self, anm_files): self.anm_files = list(anm_files) - def get_sprite(self, script_index): + + 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, Sprite(anm, script_index) + return anm, anm.scripts[script_index] + raise IndexError class Sprite(object): - def __init__(self, anm, script_index): - self.anm = anm - self.script_index = script_index + def __init__(self): + self.anm = None + self._removed = False + self._changed = False + self.texcoords = (0, 0, 0, 0) # x, y, width, height self.texoffsets = (0., 0.) self.mirrored = False @@ -45,9 +50,6 @@ self.rotations_3d = (0., 0., 0.) self.rotations_speed_3d = (0., 0., 0.) self.corner_relative_placement = False - self.instruction_pointer = 0 - self.keep_still = False - self.playing = True self.frame = 0 self.color = (255, 255, 255) self.alpha = 255 @@ -95,78 +97,11 @@ self._uvs, self._vertices = uvs, zip(d[0], d[1], d[2]) - def update(self): - if not self.playing: - return False - - changed = False - if not self.keep_still: - script = self.anm.scripts[self.script_index] - frame = self.frame - while True: - try: - frame, instr_type, args = script[self.instruction_pointer] - except IndexError: - self.playing = False - return False + if self.rotations_speed_3d != (0., 0., 0.) or self.scale_speed != (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.rescale = self.rescale[0] + self.scale_speed[0], self.rescale[1] + self.scale_speed[1] + self._changed = True - if frame > self.frame: - break - else: - self.instruction_pointer += 1 - if frame == self.frame: - changed = True - if instr_type == 0: - self.playing = False - return False - if instr_type == 1: - #TODO: handle out-of-anm sprites - self.texcoords = self.anm.sprites[args[0]] - elif instr_type == 2: - self.rescale = args - elif instr_type == 3: - self.alpha = args[0] % 256 #TODO - elif instr_type == 4: - b, g, r = args - self.color = (r, g, b) - elif instr_type == 5: - self.instruction_pointer, = args - self.frame = script[self.instruction_pointer][0] - elif instr_type == 7: - self.mirrored = not self.mirrored - elif instr_type == 9: - self.rotations_3d = args - elif instr_type == 10: - self.rotations_speed_3d = args - elif instr_type == 11: - self.scale_speed = args - elif instr_type == 16: - #TODO: handle out-of-anm sprites - #TODO: use the game's PRNG? - self.texcoords = self.anm.sprites[args[0] + randrange(args[1])] - elif instr_type == 23: - self.corner_relative_placement = True #TODO - elif instr_type == 27: - tox, toy = self.texoffsets - self.texoffsets = tox + args[0], toy - elif instr_type == 28: - tox, toy = self.texoffsets - self.texoffsets = tox, toy + args[0] - elif instr_type in (15, 21): - self.keep_still = True - break - else: - logger.warn('unhandled instruction %d (args: %r)', instr_type, args) - self.frame += 1 - - ax, ay, az = self.rotations_3d - sax, say, saz = self.rotations_speed_3d - self.rotations_3d = ax + sax, ay + say, az + saz - self.rescale = self.rescale[0] + self.scale_speed[0], self.rescale[1] + self.scale_speed[1] - - if self.rotations_speed_3d != (0., 0., 0.) or self.scale_speed != (0., 0.): - return True - - return changed -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pytouhou/vm/anmrunner.py Sat Aug 27 10:58:54 2011 +0200 @@ -0,0 +1,148 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + + +from random import randrange + +from pytouhou.utils.helpers import get_logger +from pytouhou.vm.common import MetaRegistry, instruction + +logger = get_logger(__name__) + + +class ANMRunner(object): + __metaclass__ = MetaRegistry + __slots__ = ('_anm_wrapper', '_sprite', '_running', + 'script', 'instruction_pointer', 'frame') + + + def __init__(self, anm_wrapper, script_id, sprite): + 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 + pass + + + def run_frame(self): + if self._sprite._removed: + return False + + while self._running: + try: + frame, instr_type, args = self.script[self.instruction_pointer] + except IndexError: + 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 opcode %d (args: %r)', instr_type, args) + else: + callback(self, *args) + self._sprite._changed = True + self.frame += 1 + return self._running + + + @instruction(0) + def remove(self): + self._sprite._removed = True + self._running = True + + + @instruction(1) + def load_sprite(self, sprite_index): + self._sprite.anm, self._sprite.texcoords = self._anm_wrapper.get_sprite(sprite_index) + + + @instruction(2) + def set_scale(self, sx, sy): + self._sprite.rescale = sx, sy + + + @instruction(3) + def set_alpha(self, alpha): + self._sprite.alpha = alpha % 256 #TODO + + + @instruction(4) + def set_color(self, b, g, r): + self._sprite.color = (r, g, b) + + + @instruction(5) + def jump(self, instruction_pointer): + #TODO: is that really how it works? + self.instruction_pointer = instruction_pointer + self.frame = self.script[self.instruction_pointer][0] + + + @instruction(7) + def toggle_mirrored(self): + self._sprite.mirrored = not self._sprite.mirrored + + + @instruction(9) + def set_rotations_3d(self, rx, ry, rz): + self._sprite.rotations_3d = rx, ry, rz + + + @instruction(10) + def set_rotations_speed_3d(self, srx, sry, srz): + self._sprite.rotations_speed_3d = srx, sry, srz + + + @instruction(11) + def set_scale_speed(self, ssx, ssy): + self._sprite.scale_speed = ssx, ssy + + + @instruction(16) + def load_random_sprite(self, min_idx, amp): + #TODO: use the game's PRNG? + self.load_sprite(min_idx + randrange(amp)) + + + @instruction(23) + def set_corner_relative_placement(self): + self._sprite.corner_relative_placement = True #TODO + + + @instruction(27) + def shift_texture_x(self, dx): + tox, toy = self._sprite.texoffsets + self._sprite.texoffsets = tox + dx, toy + + + @instruction(28) + def shift_texture_y(self, dy): + tox, toy = self._sprite.texoffsets + self._sprite.texoffsets = tox, toy + dy + + + @instruction(15) + @instruction(21) #TODO + def keep_still(self): + self._running = False +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pytouhou/vm/common.py Sat Aug 27 10:58:54 2011 +0200 @@ -0,0 +1,38 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + + +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 + classdict['_handlers'] = instruction_handlers + return type.__new__(mcs, name, bases, classdict) + + + +def instruction(instruction_id): + def _decorator(func): + if not hasattr(func, '_instruction_ids'): + func._instruction_ids = set() + func._instruction_ids.add(instruction_id) + return func + return _decorator
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pytouhou/vm/eclrunner.py Sat Aug 27 10:58:54 2011 +0200 @@ -0,0 +1,521 @@ +# -*- encoding: utf-8 -*- +## +## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## + + +from math import atan2, cos, sin + +from pytouhou.utils.helpers import get_logger + +from pytouhou.vm.common import MetaRegistry, instruction + +logger = get_logger(__name__) + + + +class ECLRunner(object): + __metaclass__ = MetaRegistry + __slots__ = ('_ecl', '_enemy', '_game_state', 'variables', 'sub', 'frame', + 'instruction_pointer', 'comparison_reg', 'stack') + + def __init__(self, ecl, sub, enemy, game_state): + # Things not supposed to change + self._ecl = ecl + self._enemy = enemy + self._game_state = game_state + + # 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.stack = [] + + + def run_iteration(self): + # First, if enemy is dead, return + if self._enemy._removed: + return False + + # Then, check for callbacks + #TODO + + # Now, process script + while True: + try: + frame, instr_type, rank_mask, param_mask, args = self._ecl.subs[self.sub][self.instruction_pointer] + except IndexError: + return False + + if frame > self.frame: + break + else: + self.instruction_pointer += 1 + + + #TODO: skip bad ranks + if not rank_mask & (0x100 << self._game_state.rank): + continue + + + if frame == self.frame: + try: + callback = self._handlers[instr_type] + except KeyError: + logger.warn('unhandled opcode %d (args: %r)', instr_type, args) + else: + callback(self, *args) + + self.frame += 1 + return True + + + def _getval(self, value): + if -10012 <= value <= -10001: + return self.variables[int(-10001-value)] + elif -10025 <= value <= -10013: + if value == -10013: + return self._game_state.rank + elif value == -10014: + return self._game_state.difficulty + elif value == -10015: + return self._enemy.x + elif value == -10016: + return self._enemy.y + elif value == -10017: + return self._enemy.z + elif value == -10018: + player = self._enemy.select_player(self._game_state.players) + return player.x + elif value == -10019: + player = self._enemy.select_player(self._game_state.players) + return player.y + elif value == -10021: + player = self._enemy.select_player(self._game_state.players) + return self._enemy.get_player_angle(player) + elif value == -10022: + return self._enemy.frame + elif value == -10024: + return self._enemy.life + elif value == -10025: + return self._enemy.select_player(self._game_state.players).character + raise NotImplementedError(value) #TODO + else: + return value + + + def _setval(self, variable_id, value): + if -10012 <= variable_id <= -10001: + self.variables[int(-10001-variable_id)] = value + elif -10025 <= variable_id <= -10013: + if variable_id == -10015: + self._enemy.x = value + elif variable_id == -10016: + self._enemy.y = value + elif variable_id == -10017: + self._enemy.z = value + elif variable_id == -10022: + self._enemy.frame = value + elif variable_id == -10024: + self._enemy.life = value + else: + raise IndexError #TODO: proper exception + else: + raise IndexError #TODO: proper exception + + + @instruction(0) + def noop(self): + pass #TODO: Really? + + + @instruction(1) + def destroy(self, arg): + #TODO: arg? + self._enemy._removed = True + + + @instruction(2) + def relative_jump(self, frame, instruction_pointer): + """Jumps to a relative offset in the same subroutine. + + Warning: the relative offset has been translated to an instruction pointer + by the ECL parsing code (see pytouhou.formats.ecl). + """ + self.frame, self.instruction_pointer = frame, instruction_pointer + + + @instruction(3) + def relative_jump_ex(self, frame, instruction_pointer, variable_id): + """If the given variable is non-zero, decrease it by 1 and jump to a + relative offset in the same subroutine. + + Warning: the relative offset has been translated to an instruction pointer + by the ECL parsing code (see pytouhou.formats.ecl). + """ + counter_value = self._getval(variable_id) + if counter_value: + self._setval(variable_id, counter_value - 1) + self.frame, self.instruction_pointer = frame, instruction_pointer + + + @instruction(4) + @instruction(5) + def set_variable(self, variable_id, value): + self._setval(variable_id, self._getval(value)) + + + @instruction(6) + def set_random_int(self, variable_id, maxval): + """Set the specified variable to a random int in the [0, maxval) range. + """ + self._setval(variable_id, int(self._getval(maxval) * self._game_state.prng.rand_double())) + + + @instruction(8) + def set_random_float(self, variable_id, maxval): + """Set the specified variable to a random float in [0, maxval) range. + """ + self._setval(variable_id, self._getval(maxval) * self._game_state.prng.rand_double()) + + + @instruction(9) + def set_random_float2(self, variable_id, minval, amp): + self._setval(variable_id, self._getval(minval) + self._getval(amp) * self._game_state.prng.rand_double()) + + + @instruction(13) + def set_random_int2(self, variable_id, minval, amp): + self._setval(variable_id, int(self._getval(minval)) + int(self._getval(amp)) * self._game_state.prng.rand_double()) + + + @instruction(14) + @instruction(21) + def substract(self, variable_id, a, b): + #TODO: 14 takes only ints and 21 only floats. + # The original engine dereferences the variables in the type it waits for, so this isn't exactly the correct implementation, but the data don't contain such case. + self._setval(variable_id, self._getval(a) - self._getval(b)) + + + @instruction(15) + def multiply_int(self, variable_id, a, b): + #TODO: takes only ints. + self._setval(variable_id, self._getval(a) * self._getval(b)) + + + @instruction(16) + def divide_int(self, variable_id, a, b): + #TODO: takes only ints. + self._setval(variable_id, self._getval(a) // self._getval(b)) + + + @instruction(17) + def modulo(self, variable_id, a, b): + self._setval(variable_id, self._getval(a) % self._getval(b)) + + + @instruction(20) + def add_float(self, variable_id, a, b): + #TODO: takes only floats. + self._setval(variable_id, self._getval(a) + self._getval(b)) + + + @instruction(23) + def divide_float(self, variable_id, a, b): + #TODO: takes only floats. + self._setval(variable_id, self._getval(a) / self._getval(b)) + + + @instruction(27) + @instruction(28) + def compare(self, a, b): + #TODO: 27 takes only ints and 28 only floats. + a, b = self._getval(a), self._getval(b) + if a < b: + self.comparison_reg = -1 + elif a == b: + self.comparison_reg = 0 + else: + self.comparison_reg = 1 + + + @instruction(29) + def relative_jump_if_lower_than(self, frame, instruction_pointer): + if self.comparison_reg == -1: + self.relative_jump(frame, instruction_pointer) + + + @instruction(30) + def relative_jump_if_lower_or_equal(self, frame, instruction_pointer): + if self.comparison_reg != 1: + self.relative_jump(frame, instruction_pointer) + + + @instruction(31) + def relative_jump_if_equal(self, frame, instruction_pointer): + if self.comparison_reg == 0: + self.relative_jump(frame, instruction_pointer) + + + @instruction(32) + def relative_jump_if_greater_than(self, frame, instruction_pointer): + if self.comparison_reg == 1: + self.relative_jump(frame, instruction_pointer) + + + @instruction(33) + def relative_jump_if_greater_or_equal(self, frame, instruction_pointer): + if self.comparison_reg != -1: + self.relative_jump(frame, instruction_pointer) + + + @instruction(34) + def relative_jump_if_not_equal(self, frame, instruction_pointer): + if self.comparison_reg != 0: + self.relative_jump(frame, instruction_pointer) + + + @instruction(35) + def call(self, sub, param1, param2): + self.stack.append((self.sub, self.frame, self.instruction_pointer, + self.variables, self.comparison_reg)) + self.sub = sub + self.frame = 0 + self.instruction_pointer = 0 + self.variables = [param1, 0, 0, 0, + param2, 0., 0., 0., + 0, 0, 0, 0] + + + @instruction(36) + def ret(self): + self.sub, self.frame, self.instruction_pointer, self.variables, self.comparison_reg = self.stack.pop() + + + @instruction(39) + def call_if_equal(self, sub, param1, param2, a, b): + if self._getval(a) == self._getval(b): + self.call(sub, param1, param2) + + + @instruction(43) + def set_pos(self, x, y, z): + self._enemy.set_pos(x, y, z) + + + @instruction(45) + def set_angle_speed(self, angle, speed): + self._enemy.angle, self._enemy.speed = angle, speed + + + @instruction(46) + def set_rotation_speed(self, speed): + self._enemy.rotation_speed = speed + + + @instruction(47) + def set_speed(self, speed): + self._enemy.speed = speed + + + @instruction(48) + def set_acceleration(self, acceleration): + self._enemy.acceleration = acceleration + + + @instruction(50) + def set_random_angle(self, min_angle, max_angle): + if self._enemy.screen_box: + minx, miny, maxx, maxy = self._enemy.screen_box + else: + minx, miny, maxx, maxy = (0., 0., 0., 0.) + + angle = self._game_state.prng.rand_double() * (max_angle - min_angle) + min_angle + sa, ca = sin(angle), cos(angle) + + distx = min(96.0, (maxx - minx) / 2.) + disty = min(96.0, (maxy - miny) / 2.) + + if self._enemy.x > maxx - 96.0: + ca = -abs(ca) + elif self._enemy.x < minx + 96.0: + ca = abs(ca) + + if self._enemy.y > maxy - 48.0: + sa = -abs(sa) + elif self._enemy.y < miny + 48.0: + sa = abs(sa) + self._enemy.angle = atan2(sa, ca) + + + @instruction(51) + def target_player(self, unknown, speed): + #TODO: unknown + self._enemy.speed = speed + self._enemy.angle = self._enemy.get_player_angle(self._enemy.select_player(self._game_state.players)) + + + @instruction(57) + def move_to(self, duration, x, y, z): + self._enemy.move_to(duration, x, y, z, lambda x: 2. * x - x ** 2) + + + @instruction(59) + def move_to2(self, duration, x, y, z): + #TODO: not accurate + self._enemy.move_to(duration, x, y, z, lambda x: 1.0014 * x ** 2 - 0.0012 * x) + + + @instruction(61) + def stop_in(self, duration): + self._enemy.stop_in(duration) + + + @instruction(65) + def set_screen_box(self, xmin, ymin, xmax, ymax): + self._enemy.screen_box = xmin, ymin, xmax, ymax + + + @instruction(66) + def clear_screen_box(self): + self._enemy.screen_box = None + + + @instruction(67) + def set_bullet_attributes1(self, bullet_anim, launch_anim, bullets_per_shot, + number_of_shots, speed, unknown, launch_angle, + angle, flags): + self._enemy.set_bullet_attributes(1, bullet_anim, launch_anim, + bullets_per_shot, number_of_shots, + speed, unknown, launch_angle, angle, + flags) + + + @instruction(77) + def set_bullet_interval(self, value): + self._enemy.bullet_launch_interval = value + + + @instruction(78) + def set_delay_attack(self): + self._enemy.delay_attack = True + + + @instruction(79) + def set_no_delay_attack(self): + self._enemy.delay_attack = False + + + @instruction(81) + def set_bullet_launch_offset(self, x, y, z): + self._enemy.bullet_launch_offset = (x, y) + + + @instruction(97) + def set_anim(self, sprite_index): + self._enemy.set_anim(sprite_index) + + + @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) + + + @instruction(100) + def set_death_anim(self, sprite_index): + self._enemy.death_anim = sprite_index % 256 #TODO + + + @instruction(101) + def set_boss_mode(self, unknown): + #TODO: unknown + self._game_state.boss = self._enemy + + + @instruction(103) + def set_hitbox(self, width, height, depth): + self._enemy.hitbox = (width, height) + + + @instruction(105) + def set_damageable(self, vulnerable): + self._enemy.damageable = bool(vulnerable & 1) + + + @instruction(108) + def set_death_callback(self, sub): + self._enemy.death_callback = sub + + + @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.sub = value + self.frame = 0 + self.instruction_pointer = 0 + + + @instruction(111) + def set_life(self, value): + self._enemy.life = value + + + @instruction(112) + def set_ellapsed_time(self, value): + """Sets the enemy's frame counter. + This is used for timeouts, where the framecounter is compared to the + timeout value (what's displayed is (enemy.timeout - enemy.frame) // 60). + """ + self._enemy.frame = value + + + @instruction(113) + def set_low_life_trigger(self, value): + self._enemy.low_life_trigger = value + + + @instruction(114) + def set_low_life_callback(self, sub): + self._enemy.low_life_callback = sub + + + @instruction(115) + def set_timeout(self, timeout): + self._enemy.timeout = timeout + + + @instruction(116) + def set_timeout_callback(self, sub): + self._enemy.timeout_callback = sub + + + @instruction(117) + def set_touchable(self, value): + """Defines whether the enemy is “touchable”. + Bullets only collide with an enemy if it is “touchable”. + Likewise, ReimuA's homing attacks only target “touchable” enemies. + """ + self._enemy.touchable = bool(value) + + + @instruction(126) + def set_remaining_lives(self, lives): + self._enemy.remaining_lives = lives +
