view pytouhou/vm/anmrunner.py @ 792:11bc22bad1bf default tip

python: Replace the image crate with png We weren’t using any of its features anyway, so the png crate is exactly what we need, without the many heavy dependencies of image. https://github.com/image-rs/image-png/pull/670 will eventually make it even faster to build.
author Link Mauve <linkmauve@linkmauve.fr>
date Sat, 17 Jan 2026 22:22:25 +0100
parents e15672733c93
children
line wrap: on
line source

# -*- 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, random

from pytouhou.utils.helpers import get_logger
from pytouhou.vm.common import MetaRegistry, instruction

logger = get_logger(__name__)


class ANMRunner(metaclass=MetaRegistry):
    __slots__ = ('_anm', '_sprite', 'running', 'sprite_index_offset', 'script',
                 'instruction_pointer', 'frame', 'waiting', 'handlers',
                 'variables', 'version', 'timeout')

    #TODO: check!
    formulae = {0: None,
                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: None,
                255: None} #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 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:
            return False

        while self.running and not self.waiting:
            frame, opcode, args = self.script[self.instruction_pointer]

            if frame > self.frame:
                break
            else:
                self.instruction_pointer += 1

            if frame == self.frame:
                try:
                    callback = self.handlers[opcode]
                except KeyError:
                    logger.debug('[%d - %04d] unhandled opcode %d (args: %r)',
                                 id(self), self.frame, opcode, args)
                else:
                    callback(self, *args)
                    self._sprite.changed = True

        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

        self._sprite.update()

        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, self._anm.sprites[sprite_index + self.sprite_index_offset]


    @instruction(2)
    @instruction(7, 7)
    def set_scale(self, 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)


    @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)
    @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 = 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 = 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)


    @instruction(13)
    def set_blendfunc_alphablend(self):
        self._sprite.blendfunc = 1


    @instruction(14)
    def set_blendfunc_add(self):
        self._sprite.blendfunc = 0 #TODO


    @instruction(15)
    @instruction(2, 7)
    def keep_still(self):
        self.running = False

    @instruction(16)
    def load_random_sprite(self, min_idx, amp):
        #TODO: use the game's PRNG?
        self.load_sprite(min_idx + randrange(amp))


    @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)


    @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.
        """
        self._sprite.automatic_orientation = bool(value)


    @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)


# 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