Mercurial > touhou
changeset 429:40d5f3083ebc
Implement PCB’s ANM2 format and vm.
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Sat, 03 Aug 2013 15:48:57 +0200 |
parents | f41a26971a19 |
children | c9433188ffdb |
files | pytouhou/formats/anm0.py pytouhou/game/sprite.py pytouhou/resource/anmwrapper.py pytouhou/resource/loader.py pytouhou/ui/texture.pyx pytouhou/vm/anmrunner.py |
diffstat | 6 files changed, 387 insertions(+), 98 deletions(-) [+] |
line wrap: on
line diff
--- a/pytouhou/formats/anm0.py +++ b/pytouhou/formats/anm0.py @@ -24,6 +24,7 @@ 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__) @@ -39,36 +40,84 @@ class Script(list): class ANM0(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: ('', '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)} + _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): @@ -82,67 +131,113 @@ class ANM0(object): @classmethod def read(cls, file): - nb_sprites, nb_scripts, zero1 = unpack('<III', file.read(12)) - width, height, format, unknown1 = unpack('<IIII', file.read(16)) - first_name_offset, unused, secondary_name_offset = unpack('<III', file.read(12)) - version, unknown2, thtxoffset, hasdata, nextoffset, zero2 = unpack('<IIIIII', file.read(24)) - if version != 0: - raise WrongFormatError(version) - assert (zero1, zero2) == (0, 0) + anm_list = [] + start_offset = 0 + while True: + file.seek(start_offset) + nb_sprites, nb_scripts, zero1 = unpack('<III', file.read(12)) + width, height, fmt, unknown1 = unpack('<IIII', file.read(16)) + first_name_offset, unused, secondary_name_offset = unpack('<III', file.read(12)) + version, unknown2, texture_offset, has_data, next_offset, unknown3 = unpack('<IIIIII', file.read(24)) - sprite_offsets = [unpack('<I', file.read(4))[0] for i in range(nb_sprites)] - script_offsets = [unpack('<II', file.read(8)) for i in range(nb_scripts)] + if version == 0: + assert zero1 == 0 + assert unknown3 == 0 + assert has_data == 0 + elif version == 2: + assert zero1 == 0 + assert secondary_name_offset == 0 + assert has_data == 1 # Can be false but we don’t support that yet. + else: + raise WrongFormatError(version) + + instructions = cls._instructions[version] - self = cls() + sprite_offsets = [unpack('<I', file.read(4))[0] for i in range(nb_sprites)] + script_offsets = [unpack('<II', file.read(8)) for i in range(nb_scripts)] - self.size = (width, height) + self = cls() + + self.size = (width, height) + self.version = version - # Names - if first_name_offset: - file.seek(first_name_offset) - self.first_name = read_string(file, 32, 'ascii') #TODO: 32, really? - if secondary_name_offset: - file.seek(secondary_name_offset) - self.secondary_name = read_string(file, 32, 'ascii') #TODO: 32, really? + # Names + if first_name_offset: + file.seek(start_offset + first_name_offset) + self.first_name = read_string(file, 32, 'ascii') #TODO: 32, really? + if secondary_name_offset: + file.seek(start_offset + secondary_name_offset) + self.secondary_name = read_string(file, 32, 'ascii') #TODO: 32, really? + + + # Sprites + for offset in sprite_offsets: + file.seek(start_offset + offset) + idx, x, y, width, height = unpack('<Iffff', file.read(20)) + self.sprites[idx] = x, y, width, height - # Sprites - file.seek(64) - self.sprites = {} - for offset in sprite_offsets: - file.seek(offset) - idx, x, y, width, height = unpack('<Iffff', file.read(20)) - self.sprites[idx] = x, y, width, height + # Scripts + for i, offset in script_offsets: + self.scripts[i] = Script() + instruction_offsets = [] + file.seek(start_offset + offset) + while True: + instruction_offsets.append(file.tell() - (start_offset + offset)) + if version == 0: + time, opcode, size = unpack('<HBB', file.read(4)) + elif version == 2: + opcode, size, time, mask = unpack('<HHHH', file.read(8)) + if opcode == 0xffff: + break + size -= 8 + data = file.read(size) + if opcode in instructions: + args = unpack('<%s' % instructions[opcode][0], data) + else: + args = (data,) + logger.warn('unknown opcode %d', opcode) + self.scripts[i].append((time, opcode, args)) + if version == 0 and opcode == 0: + break - # Scripts - self.scripts = {} - for i, offset in script_offsets: - self.scripts[i] = Script() - instruction_offsets = [] - file.seek(offset) - while True: - instruction_offsets.append(file.tell() - offset) - time, opcode, size = unpack('<HBB', file.read(4)) + # Translate offsets to instruction pointers and register interrupts + for instr_offset, (j, instr) in zip(instruction_offsets, enumerate(self.scripts[i])): + time, opcode, args = instr + if version == 0: + if opcode == 5: + args = (instruction_offsets.index(args[0]),) + elif opcode == 22: + interrupt = args[0] + self.scripts[i].interrupts[interrupt] = j + 1 + elif version == 2: + if opcode == 4: + args = (instruction_offsets.index(args[0]), args[1]) + elif opcode == 5: + args = (args[0], instruction_offsets.index(args[1]), args[2]) + elif opcode == 21: + interrupt = args[0] + self.scripts[i].interrupts[interrupt] = j + 1 + elif opcode == 69: + args = (args[0], args[1], instruction_offsets.index(args[2]), args[3]) + self.scripts[i][j] = time, opcode, args + + # Texture + if has_data: + file.seek(start_offset + texture_offset) + magic = file.read(4) + assert magic == b'THTX' + zero, fmt, width, height, size = unpack('<HHHHI', file.read(12)) + assert zero == 0 data = file.read(size) - if opcode in cls._instructions: - args = unpack('<%s' % cls._instructions[opcode][0], data) - else: - args = (data,) - logger.warn('unknown opcode %d', opcode) + self.texture = Texture(width, height, fmt, data) - self.scripts[i].append((time, opcode, args)) - if opcode == 0: - break + anm_list.append(self) - # Translate offsets to instruction pointers and register interrupts - for instr_offset, (j, instr) in zip(instruction_offsets, enumerate(self.scripts[i])): - time, opcode, args = instr - if opcode == 5: - args = (instruction_offsets.index(args[0]),) - elif opcode == 22: - interrupt = args[0] - self.scripts[i].interrupts[interrupt] = j + 1 - self.scripts[i][j] = time, opcode, args + if next_offset: + start_offset += next_offset + else: + break - return self + return anm_list
--- a/pytouhou/game/sprite.py +++ b/pytouhou/game/sprite.py @@ -18,10 +18,12 @@ from pytouhou.utils.interpolator import 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', + 'angle', 'force_rotation', 'scale_interpolator', + 'fade_interpolator', 'offset_interpolator', + 'rotation_interpolator', 'color_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', 'visible', '_rendering_data') def __init__(self, width_override=0, height_override=0): @@ -38,6 +40,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 @@ -78,6 +82,18 @@ class Sprite(object): formula) + def rotate_in(self, duration, rx, ry, rz, formula): + self.rotation_interpolator = Interpolator(self.rotations_3d, self.frame, + (rx, ry, rz), self.frame + duration, + formula) + + + def change_color_in(self, duration, r, g, b, formula): + 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
--- a/pytouhou/resource/anmwrapper.py +++ b/pytouhou/resource/anmwrapper.py @@ -12,7 +12,7 @@ ## GNU General Public License for more details. ## -from itertools import repeat +from itertools import repeat, chain class AnmWrapper(object): @@ -32,7 +32,7 @@ class AnmWrapper(object): if not offsets: offsets = repeat(0) # “offsets” defaults to zeroes - for anm, offset in zip(anm_files, offsets): + for anm, offset in zip(chain(*anm_files), offsets): for script_id, script in anm.scripts.iteritems(): self.scripts[script_id + offset] = (anm, script) #TODO: check for sprite_id, sprite in anm.sprites.iteritems():
--- a/pytouhou/resource/loader.py +++ b/pytouhou/resource/loader.py @@ -17,6 +17,7 @@ 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 @@ -140,7 +141,7 @@ class Loader(object): def get_anm(self, name): if name not in self.instanced_anms: file = self.get_file(name) - self.instanced_anms[name] = ANM0.read(file) #TODO: modular + self.instanced_anms[name] = ANM0.read(file) return self.instanced_anms[name]
--- a/pytouhou/ui/texture.pyx +++ b/pytouhou/ui/texture.pyx @@ -38,9 +38,12 @@ class TextureManager(object): def load(self, anm_list): for anm in anm_list: - if not hasattr(anm, 'texture'): - texture = decode_png(self.loader, anm.first_name, anm.secondary_name) - anm.texture = load_texture(texture) + 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) cdef decode_png(loader, first_name, secondary_name):
--- 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 @@ -25,19 +25,34 @@ class ANMRunner(object): __metaclass__ = MetaRegistry __slots__ = ('_anm_wrapper', '_sprite', 'running', 'sprite_index_offset', 'script', 'instruction_pointer', - 'frame', 'waiting', 'handlers') + '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_wrapper, script_id, sprite, sprite_index_offset=0): self._anm_wrapper = anm_wrapper self._sprite = sprite - self.handlers = self._handlers[6] self.running = True self.waiting = False anm, self.script = anm_wrapper.get_script(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 @@ -75,11 +90,15 @@ class ANMRunner(object): 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 if not self.waiting: self.frame += 1 + elif self.timeout == sprite.frame: #TODO: check if it’s happening at the correct frame. + self.waiting = False # Update sprite sprite.frame += 1 @@ -89,6 +108,10 @@ class ANMRunner(object): sax, say, saz = sprite.rotations_speed_3d sprite.rotations_3d = ax + sax, ay + say, az + saz sprite.changed = True + elif sprite.rotation_interpolator: + sprite.rotation_interpolator.update(sprite.frame) + sprite.rotations_3d = sprite.rotation_interpolator.values + sprite.changed = True if sprite.scale_speed != (0., 0.): rx, ry = sprite.rescale @@ -111,31 +134,55 @@ class ANMRunner(object): sprite.dest_offset = sprite.offset_interpolator.values sprite.changed = True + if sprite.color_interpolator: + sprite.color_interpolator.update(sprite.frame) + sprite.color = sprite.color_interpolator.values + sprite.changed = True + return self.running + def _setval(self, variable_id, value): + if self.version == 2: + if 10000 <= variable_id <= 10011: + self.variables[int(variable_id-10000)] = value + + + 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 @instruction(1) + @instruction(3, 7) def load_sprite(self, sprite_index): + #TODO: version 2 only: do not crash when assigning a non-existant sprite. self._sprite.anm, self._sprite.texcoords = self._anm_wrapper.get_sprite(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): if not self._sprite.fade_interpolator: self._sprite.color = (r, g, b) @@ -149,26 +196,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 @@ -184,6 +236,7 @@ class ANMRunner(object): @instruction(15) + @instruction(2, 7) def keep_still(self): self.running = False @@ -194,26 +247,31 @@ 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. """ @@ -221,17 +279,20 @@ class ANMRunner(object): @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. """ @@ -240,11 +301,13 @@ class ANMRunner(object): @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. """ @@ -252,23 +315,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