# HG changeset patch # User Thibaut Girka # Date 1314033780 -7200 # Node ID cbe1cb50f2fda3a5d8d4d8684b7074c42fe32a39 # Parent 8353c33d53d44367cc369281b11653ae43b4fcd4 Refactor ECLRunner/EnemyManager so that all VM stuff goes to ECLRunner diff --git a/eclviewer.py b/eclviewer.py --- a/eclviewer.py +++ b/eclviewer.py @@ -18,6 +18,7 @@ from pytouhou.game.sprite import AnmWrap from pytouhou.game.background import Background from pytouhou.game.enemymanager import EnemyManager from pytouhou.opengl.texture import TextureManager +from pytouhou.game.game import GameState import OpenGL OpenGL.FORWARD_COMPATIBLE_ONLY = True @@ -61,7 +62,7 @@ def main(path, stage_num): pass else: anims.append(enemies2_anim) - enemy_manager = EnemyManager(stage, AnmWrapper(anims), ecl) + enemy_manager = EnemyManager(stage, AnmWrapper(anims), ecl, GameState([], stage_num, 0, 16)) texture_manager.preload(anims) background_anim = Animations.read(BytesIO(archive.extract('stg%dbg.anm' % stage_num))) diff --git a/pytouhou/game/eclrunner.py b/pytouhou/game/eclrunner.py --- a/pytouhou/game/eclrunner.py +++ b/pytouhou/game/eclrunner.py @@ -1,59 +1,128 @@ +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): - def __init__(self, ecl, sub, frame=0, instruction_pointer=0, implementation=None): - self.ecl = ecl + __metaclass__ = MetaRegistry + __slots__ = ('_ecl', '_enemy', '_game_state', 'variables', 'sub', 'frame', + 'instruction_pointer', '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.sub = sub - self.frame = frame - self.instruction_pointer = instruction_pointer + self.frame = 0 + self.instruction_pointer = 0 self.stack = [] - self.implementation = {4: self.set_variable, - 5: self.set_variable, - 2: self.relative_jump, - 3: self.relative_jump_ex, - 20: self.add, - 21: self.substract, - 35: self.call, - 36: self.ret, - 109: self.memory_write} - if implementation: - self.implementation.update(implementation) + + 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 + try: + while frame <= self.frame: + frame, instr_type, rank_mask, param_mask, args = self._ecl.subs[self.sub][self.instruction_pointer] + + #TODO: skip bad ranks + + if frame == self.frame: + try: + callback = self._handlers[instr_type] + except KeyError: + print('Warning: unhandled opcode %d!' % instr_type) #TODO + else: + callback(self, *args) + frame, instr_type, rank_mask, param_mask, args = self._ecl.subs[self.sub][self.instruction_pointer] + if frame <= self.frame: + self.instruction_pointer += 1 + except IndexError: + return False + + self.frame += 1 + return True - def _get_value(self, value): #TODO: -10013 and beyond! - assert not -10025 <= value <= -10013 + def _getval(self, value): if -10012 <= value <= -10001: return self.variables[int(-10001-value)] + elif -10025 <= value <= -10013: + raise NotImplementedError #TODO else: return value - def add(self, variable_id, a, b): - #TODO: proper variable handling - #TODO: int vs float thing - self.variables[-10001-variable_id] = self._get_value(a) + self._get_value(b) + def _setval(self, variable_id, value): + if -10012 <= value <= -10001: + self.variables[int(-10001-variable_id)] = value + elif -10025 <= value <= -10013: + raise NotImplementedError #TODO + else: + raise IndexError #TODO + + + @instruction(1) + def destroy(self, arg): + #TODO: arg? + self._enemy._removed = True - def substract(self, variable_id, a, b): - #TODO: proper variable handling - #TODO: int vs float thing - self.variables[-10001-variable_id] = self._get_value(a) - self._get_value(b) - + @instruction(2) + def relative_jump(self, frame, instruction_pointer): + self.frame, self.instruction_pointer = frame, instruction_pointer - 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(3) + def relative_jump_ex(self, frame, instruction_pointer, variable_id): + if self.variables[-10001-variable_id]: + self.variables[-10001-variable_id] -= 1 + self.frame, self.instruction_pointer = frame, instruction_pointer + @instruction(4) + @instruction(5) + def set_variable(self, variable_id, value): + #TODO: -10013 and beyond! + self.variables[-10001-variable_id] = self._getval(value) + + + @instruction(35) def call(self, sub, param1, param2): self.stack.append((self.sub, self.frame, self.instruction_pointer, self.variables)) @@ -65,43 +134,139 @@ class ECLRunner(object): 0, 0, 0, 0] + @instruction(36) def ret(self): self.sub, self.frame, self.instruction_pointer, self.variables = self.stack.pop() - def set_variable(self, variable_id, value): - #TODO: -10013 and beyond! - self.variables[-10001-variable_id] = self._get_value(value) + @instruction(20) + def add(self, variable_id, a, b): + #TODO: proper variable handling + #TODO: int vs float thing + self.variables[-10001-variable_id] = self._getval(a) + self._getval(b) + + + @instruction(21) + def substract(self, variable_id, a, b): + #TODO: proper variable handling + #TODO: int vs float thing + self.variables[-10001-variable_id] = self._getval(a) - self._getval(b) + + + @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 - def relative_jump(self, frame, instruction_pointer): - self.frame, self.instruction_pointer = frame, instruction_pointer + @instruction(47) + def set_speed(self, speed): + self._enemy.speed = speed + + + @instruction(48) + def set_acceleration(self, acceleration): + self._enemy.acceleration = acceleration + + + @instruction(51) + def target_player(self, unknown, speed): + self._enemy.speed = speed #TODO: unknown + player_x, player_y = 192., 384.#TODO + self._enemy.angle = atan2(player_y - self._enemy.y, player_x - self._enemy.x) #TODO - def relative_jump_ex(self, frame, instruction_pointer, variable_id): - if self.variables[-10001-variable_id]: - self.variables[-10001-variable_id] -= 1 - self.frame, self.instruction_pointer = frame, instruction_pointer + @instruction(57) + def move_to(self, duration, x, y, z): + self._enemy.move_to(duration, x, y, z) + + + @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 - def update(self): - frame = self.frame - try: - while frame <= self.frame: - frame, instr_type, rank_mask, param_mask, args = self.ecl.subs[self.sub][self.instruction_pointer] + @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(103) + def set_hitbox(self, width, height, depth): + self._enemy.hitbox = (width, height) + + + @instruction(105) + def set_vulnerable(self, vulnerable): + self._enemy.vulnerable = bool(vulnerable & 1) + - if frame == self.frame: - try: - callback = self.implementation[instr_type] - except KeyError: - print('Warning: unhandled opcode %d!' % instr_type) #TODO - else: - callback(*args) - frame, instr_type, rank_mask, param_mask, args = self.ecl.subs[self.sub][self.instruction_pointer] - if frame <= self.frame: - self.instruction_pointer += 1 - except IndexError: - pass #TODO: script ended, destroy enemy + @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 + - self.frame += 1 + @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(126) + def set_remaining_lives(self, lives): + self._enemy.remaining_lives = lives + diff --git a/pytouhou/game/enemymanager.py b/pytouhou/game/enemymanager.py --- a/pytouhou/game/enemymanager.py +++ b/pytouhou/game/enemymanager.py @@ -13,16 +13,18 @@ random = Random(0x39f4) class Enemy(object): - def __init__(self, pos, life, _type, ecl_runner, anm_wrapper): - self.anm_wrapper = anm_wrapper - self.anm = None - self.ecl_runner = ecl_runner + def __init__(self, pos, life, _type, anm_wrapper): + self._anm_wrapper = anm_wrapper + self._anm = None + self._sprite = None + self._removed = False + self._type = _type + + self.frame = 0 + self.x, self.y = pos self.life = life self.max_life = life - self.type = _type - self.frame = 0 - self.sprite = None self.pending_bullets = [] self.bullet_attributes = None self.bullet_launch_offset = (0, 0) @@ -36,7 +38,7 @@ class Enemy(object): self.bullet_launch_interval = 0 self.delay_attack = False - self.death_sprite = None + self.death_anim = None self.movement_dependant_sprites = None self.direction = None self.interpolator = None #TODO @@ -47,57 +49,6 @@ class Enemy(object): self.hitbox = (0, 0) - self.ecl_runner.implementation.update({67: self.set_bullet_attributes, - 97: self.set_sprite, - 98: self.set_multiple_sprites, - 45: self.set_angle_speed, - 43: self.set_pos, - 46: self.set_rotation_speed, - 47: self.set_speed, - 48: self.set_acceleration, - 51: self.target_player, - 57: self.move_to, - 77: self.set_bullet_interval, - 78: self.set_delay_attack, - 79: self.set_no_delay_attack, - 81: self.set_bullet_launch_offset, - 100: self.set_death_sprite, - 103: self.set_hitbox, - 105: self.set_vulnerable, - 108: self.set_death_callback, - 113: self.set_low_life_trigger, - 114: self.set_low_life_callback, - 115: self.set_timeout, - 126: self.set_remaining_lives}) #TODO - - - def set_remaining_lives(self, lives): - self.remaining_lives = lives - - - def set_death_callback(self, sub): - self.death_callback = sub - - - def set_low_life_trigger(self, value): - self.low_life_trigger = value - - - def set_low_life_callback(self, sub): - self.low_life_callback = sub - - - def set_timeout(self, timeout): - self.timeout = timeout - - - def set_vulnerable(self, vulnerable): - self.vulnerable = bool(vulnerable & 1) - - - def set_bullet_launch_offset(self, x, y, z): - self.bullet_launch_offset = (x, y) - def set_bullet_attributes(self, bullet_anim, launch_anim, bullets_per_shot, number_of_shots, speed, unknown, launch_angle, @@ -110,37 +61,8 @@ class Enemy(object): #TODO: actually fire - def set_bullet_interval(self, value): - self.bullet_launch_interval = value - - - def set_delay_attack(self): - self.delay_attack = True - - - def set_no_delay_attack(self): - self.delay_attack = False - - - def set_death_sprite(self, sprite_index): - self.death_sprite = sprite_index % 256 #TODO - - - def set_hitbox(self, width, height, depth): - self.hitbox = (width, height) - - - def set_sprite(self, sprite_index): - self.anm, self.sprite = self.anm_wrapper.get_sprite(sprite_index) - - - def set_multiple_sprites(self, default, end_left, end_right, left, right): - self.movement_dependant_sprites = end_left, end_right, left, right - self.anm, self.sprite = self.anm_wrapper.get_sprite(default) - - - def set_angle_speed(self, angle, speed): - self.angle, self.speed = angle, speed + def set_anim(self, index): + self._anm, self._sprite = self._anm_wrapper.get_sprite(index) def set_pos(self, x, y, z): @@ -149,34 +71,16 @@ class Enemy(object): self.interpolator.set_interpolation_start(self.frame, (x, y)) - def set_rotation_speed(self, speed): - self.rotation_speed = speed - - - def set_speed(self, speed): - self.speed = speed - - - def set_acceleration(self, acceleration): - self.acceleration = acceleration - - - def target_player(self, unknown, speed): - self.speed = speed #TODO: unknown - player_x, player_y = 192., 384.#TODO - self.angle = atan2(player_y - self.y, player_x - self.x) - - def move_to(self, duration, x, y, z): self.interpolator.set_interpolation_end(self.frame + duration, (x, y)) def is_visible(self, screen_width, screen_height): - if not self.sprite: + if not self._sprite: return False - tx, ty, tw, th = self.sprite.texcoords - if self.sprite.corner_relative_placement: + tx, ty, tw, th = self._sprite.texcoords + if self._sprite.corner_relative_placement: raise Exception #TODO else: max_x = tw / 2. @@ -194,20 +98,18 @@ class Enemy(object): def get_objects_by_texture(self): objects_by_texture = {} - key = self.anm.first_name, self.anm.secondary_name + key = self._anm.first_name, self._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) + vertices = tuple((x + self.x, y + self.y, z) for x, y, z in self._sprite._vertices) objects_by_texture[key][1].extend(vertices) - objects_by_texture[key][2].extend(self.sprite._uvs) - objects_by_texture[key][3].extend(self.sprite._colors) + objects_by_texture[key][2].extend(self._sprite._uvs) + objects_by_texture[key][3].extend(self._sprite._colors) #TODO: effects/bullet launch return objects_by_texture def update(self, frame): - self.ecl_runner.update() - x, y = self.x, self.y if self.interpolator: self.interpolator.update(self.frame) @@ -217,7 +119,7 @@ class Enemy(object): self.angle += self.rotation_speed #TODO: units? Execution order? dx, dy = cos(self.angle) * self.speed, sin(self.angle) * self.speed - if self.type & 2: + if self._type & 2: x -= dx else: x += dx @@ -226,24 +128,24 @@ class Enemy(object): if self.movement_dependant_sprites: #TODO: is that really how it works? if x < self.x: - self.anm, self.sprite = self.anm_wrapper.get_sprite(self.movement_dependant_sprites[2]) + self.set_anim(self.movement_dependant_sprites[2]) self.direction = -1 elif x > self.x: - self.anm, self.sprite = self.anm_wrapper.get_sprite(self.movement_dependant_sprites[3]) + self.set_anim(self.movement_dependant_sprites[3]) self.direction = +1 elif self.direction is not None: - self.anm, self.sprite = self.anm_wrapper.get_sprite(self.movement_dependant_sprites[{-1: 0, +1:1}[self.direction]]) + self.set_anim(self.movement_dependant_sprites[{-1: 0, +1:1}[self.direction]]) self.direction = None self.x, self.y = x, y - if self.sprite: - changed = self.sprite.update() + 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: + self._sprite.update_vertices_uvs_colors() + elif not self._sprite.playing: visible = False - self.sprite = None + self._sprite = None else: visible = False @@ -254,13 +156,15 @@ class Enemy(object): class EnemyManager(object): - def __init__(self, stage, anm_wrapper, ecl): + def __init__(self, stage, anm_wrapper, ecl, game_state): + self._game_state = game_state self.stage = stage self.anm_wrapper = anm_wrapper self.main = [] self.ecl = ecl self.objects_by_texture = {} self.enemies = [] + self.processes = [] # Populate main for frame, sub, instr_type, args in ecl.main: @@ -270,12 +174,6 @@ class EnemyManager(object): self.main[-1][1].append((sub, instr_type, args)) - def make_enemy_deleter(self, enemy): - def _enemy_deleter(unknown): #TODO: unknown - self.enemies.remove(enemy) - return _enemy_deleter - - def update(self, frame): if self.main and self.main[0][0] == frame: for sub, instr_type, args in self.main.pop(0)[1]: @@ -288,11 +186,16 @@ class EnemyManager(object): y = random.rand_double() * 416 #102h.exe@0x41184b if z < -990: y = random.rand_double() * 800 #102h.exe@0x411881 - ecl_runner = ECLRunner(self.ecl, sub) - enemy = Enemy((x, y), life, instr_type, ecl_runner, self.anm_wrapper) - ecl_runner.implementation[1] = self.make_enemy_deleter(enemy) + enemy = Enemy((x, y), life, instr_type, self.anm_wrapper) + self.enemies.append(enemy) + self.processes.append(ECLRunner(self.ecl, sub, enemy, self._game_state)) + - self.enemies.append(enemy) + # Run processes + self.processes[:] = (process for process in self.processes if process.run_iteration()) + + # Filter of destroyed enemies + 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)] diff --git a/pytouhou/game/game.py b/pytouhou/game/game.py new file mode 100644 --- /dev/null +++ b/pytouhou/game/game.py @@ -0,0 +1,9 @@ +class GameState(object): + __slots__ = ('players', 'rank', 'difficulty', 'frame', 'stage', 'boss') + def __init__(self, players, stage, rank, difficulty): + self.stage = stage + self.players = players + self.rank = rank + self.difficulty = difficulty + self.boss = None + self.frame = 0