changeset 757:21b186be2590

Split the Rust version into multiple crates.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Tue, 05 Jan 2021 02:16:32 +0100
parents 4d91790cf8ab
children daa23a4ff24d
files Cargo.toml examples/anmrenderer.rs examples/common.rs examples/dump_ecl.rs examples/eclrenderer.rs examples/menu.rs examples/stagerunner.rs examples/stdrenderer.rs formats/Cargo.toml formats/src/bin/dump_ecl.rs formats/src/lib.rs formats/src/th06/anm0.rs formats/src/th06/ecl.rs formats/src/th06/mod.rs formats/src/th06/pbg3.rs formats/src/th06/std.rs interpreters/Cargo.toml interpreters/src/lib.rs interpreters/src/th06/anm0.rs interpreters/src/th06/ecl.rs interpreters/src/th06/enemy.rs interpreters/src/th06/interpolator.rs interpreters/src/th06/std.rs runners/Cargo.toml runners/src/bin/anmrenderer.rs runners/src/bin/eclrenderer.rs runners/src/bin/menu.rs runners/src/bin/stagerunner.rs runners/src/bin/stdrenderer.rs runners/src/common.rs src/lib.rs src/th06/anm0.rs src/th06/anm0_vm.rs src/th06/ecl.rs src/th06/ecl_vm.rs src/th06/enemy.rs src/th06/interpolator.rs src/th06/mod.rs src/th06/pbg3.rs src/th06/std.rs src/th06/std_vm.rs src/util/bitstream.rs src/util/lzss.rs src/util/math.rs src/util/mod.rs src/util/prng.rs utils/Cargo.toml utils/src/bitstream.rs utils/src/lib.rs utils/src/lzss.rs utils/src/math.rs utils/src/prng.rs
diffstat 52 files changed, 5797 insertions(+), 5745 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.toml	Tue Jan 05 01:14:52 2021 +0100
+++ b/Cargo.toml	Tue Jan 05 02:16:32 2021 +0100
@@ -1,20 +1,14 @@
-[package]
-name = "touhou"
-version = "0.1.0"
-authors = ["Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>"]
-edition = "2018"
-description = "A collection of tools to work with Touhou data"
-homepage = "https://pytouhou.linkmauve.fr"
-license = "GPL-3.0-or-later"
+[workspace]
 
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+members = [
+    "formats",
+    "interpreters",
+    "runners",
+    "utils",
+]
 
-[dependencies]
-nom = { version = "6", default-features = false, features = ["alloc"] }
-encoding_rs = "0.8"
-image = { version = "0.23", default-features = false, features = ["png", "jpeg"] }
-bitflags = "1"
-luminance = "0.39"
-luminance-glfw = { version = "0.12", default-features = false, features = ["log-errors"] }
-luminance-derive = "0.5"
-ears = "0.8"
+[patch.crates-io]
+touhou-formats = { path = "formats" }
+touhou-interpreters = { path = "interpreters" }
+touhou-runners = { path = "runners" }
+touhou-utils = { path = "utils" }
--- a/examples/anmrenderer.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,216 +0,0 @@
-use luminance::blending::{Equation, Factor};
-use luminance::context::GraphicsContext;
-use luminance::pipeline::{BoundTexture, PipelineState};
-use luminance::pixel::NormUnsigned;
-use luminance::render_state::RenderState;
-use luminance::shader::program::{Program, Uniform};
-use luminance::tess::{Mode, TessBuilder};
-use luminance::texture::Dim2;
-use luminance_derive::{Semantics, Vertex, UniformInterface};
-use luminance_glfw::{Action, Key, WindowEvent, GlfwSurface, Surface, WindowDim, WindowOpt};
-use touhou::th06::anm0::Anm0;
-use touhou::th06::anm0_vm::{AnmRunner, Sprite, Vertex as FakeVertex};
-use touhou::util::math::{perspective, setup_camera};
-use touhou::util::prng::Prng;
-use std::cell::RefCell;
-use std::rc::Rc;
-use std::env;
-use std::path::Path;
-
-#[path = "common.rs"]
-mod common;
-use common::{load_file_into_vec, load_anm_image, LoadedTexture};
-
-const VS: &str = r#"
-in ivec3 in_position;
-in vec2 in_texcoord;
-in vec4 in_color;
-
-uniform mat4 mvp;
-
-out vec2 texcoord;
-out vec4 color;
-
-void main()
-{
-    gl_Position = mvp * vec4(vec3(in_position), 1.0);
-    texcoord = vec2(in_texcoord);
-
-    // It’s already normalized from the u8 being passed.
-    color = in_color;
-}
-"#;
-
-const FS: &str = r#"
-in vec2 texcoord;
-in vec4 color;
-
-uniform sampler2D color_map;
-
-out vec4 frag_color;
-
-void main()
-{
-    frag_color = texture(color_map, texcoord) * color;
-}
-"#;
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Semantics)]
-pub enum Semantics {
-    #[sem(name = "in_position", repr = "[i16; 3]", wrapper = "VertexPosition")]
-    Position,
-
-    #[sem(name = "in_texcoord", repr = "[f32; 2]", wrapper = "VertexTexcoord")]
-    Texcoord,
-
-    #[sem(name = "in_color", repr = "[u8; 4]", wrapper = "VertexColor")]
-    Color,
-}
-
-#[repr(C)]
-#[derive(Clone, Copy, Debug, PartialEq, Vertex)]
-#[vertex(sem = "Semantics")]
-struct Vertex {
-    pos: VertexPosition,
-    uv: VertexTexcoord,
-    #[vertex(normalized = "true")]
-    rgba: VertexColor,
-}
-
-#[derive(UniformInterface)]
-struct ShaderInterface {
-    // the 'static lifetime acts as “anything” here
-    color_map: Uniform<&'static BoundTexture<'static, Dim2, NormUnsigned>>,
-
-    #[uniform(name = "mvp")]
-    mvp: Uniform<[[f32; 4]; 4]>,
-}
-
-fn fill_vertices_ptr(sprite: Rc<RefCell<Sprite>>, vertices: *mut Vertex) {
-    let mut fake_vertices = unsafe { std::mem::transmute::<*mut Vertex, &mut [FakeVertex; 4]>(vertices) };
-    sprite.borrow().fill_vertices(&mut fake_vertices, 0., 0., 0.);
-}
-
-fn fill_vertices(sprite: Rc<RefCell<Sprite>>, vertices: &mut [Vertex; 4]) {
-    let mut fake_vertices = unsafe { std::mem::transmute::<&mut [Vertex; 4], &mut [FakeVertex; 4]>(vertices) };
-    sprite.borrow().fill_vertices(&mut fake_vertices, 0., 0., 0.);
-}
-
-fn main() {
-    // Parse arguments.
-    let args: Vec<_> = env::args().collect();
-    if args.len() != 3 {
-        eprintln!("Usage: {} <ANM file> <script number>", args[0]);
-        return;
-    }
-    let anm_filename = Path::new(&args[1]);
-    let script: u8 = args[2].parse().expect("number");
-
-    // Open the ANM file.
-    let buf = load_file_into_vec(anm_filename).unwrap();
-    let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
-    let anm0 = anms.pop().unwrap();
-
-    if !anm0.scripts.contains_key(&script) {
-        eprintln!("This anm0 doesn’t contain a script named {}.", script);
-        return;
-    }
-
-    // Create the sprite.
-    let sprite = Rc::new(RefCell::new(Sprite::new()));
-
-    // TODO: seed this PRNG with a valid seed.
-    let prng = Rc::new(RefCell::new(Prng::new(0)));
-
-    let mut surface = GlfwSurface::new(WindowDim::Windowed(384, 448), "Touhou", WindowOpt::default()).unwrap();
-
-    // Open the image atlas matching this ANM.
-    let tex = load_anm_image(&mut surface, &anm0, anm_filename).expect("image loading");
-
-    // Create the AnmRunner from the ANM and the sprite.
-    let anms = Rc::new(RefCell::new([anm0]));
-    let mut anm_runner = AnmRunner::new(anms, script, sprite.clone(), Rc::downgrade(&prng), 0);
-
-    assert_eq!(std::mem::size_of::<Vertex>(), std::mem::size_of::<FakeVertex>());
-    let mut vertices: [Vertex; 4] = {
-        let data = std::mem::MaybeUninit::uninit();
-        unsafe { data.assume_init() }
-    };
-    fill_vertices(sprite.clone(), &mut vertices);
-
-    // set the uniform interface to our type so that we can read textures from the shader
-    let program =
-        Program::<Semantics, (), ShaderInterface>::from_strings(None, VS, None, FS).expect("program creation").ignore_warnings();
-
-    let mut tess = TessBuilder::new(&mut surface)
-        .add_vertices(vertices)
-        .set_mode(Mode::TriangleFan)
-        .build()
-        .unwrap();
-
-    let mut back_buffer = surface.back_buffer().unwrap();
-    let mut resize = false;
-
-    'app: loop {
-        for event in surface.poll_events() {
-            match event {
-                WindowEvent::Close | WindowEvent::Key(Key::Escape, _, Action::Release, _) => break 'app,
-
-                WindowEvent::FramebufferSize(..) => {
-                    resize = true;
-                }
-
-                _ => (),
-            }
-        }
-
-        if resize {
-            back_buffer = surface.back_buffer().unwrap();
-            resize = false;
-        }
-
-        {
-            let mut slice = tess
-                .as_slice_mut()
-                .unwrap();
-
-            anm_runner.run_frame();
-            fill_vertices_ptr(sprite.clone(), slice.as_mut_ptr());
-        }
-
-        // here, we need to bind the pipeline variable; it will enable us to bind the texture to the GPU
-        // and use it in the shader
-        surface
-            .pipeline_builder()
-            .pipeline(&back_buffer, &PipelineState::default(), |pipeline, mut shd_gate| {
-                // bind our fancy texture to the GPU: it gives us a bound texture we can use with the shader
-                let bound_tex = match &tex {
-                    LoadedTexture::Rgb(tex) => pipeline.bind_texture(tex),
-                    LoadedTexture::Rgba(tex) => pipeline.bind_texture(tex),
-                    LoadedTexture::RgbaArray(tex) => unreachable!(),
-                };
-
-                shd_gate.shade(&program, |iface, mut rdr_gate| {
-                    // update the texture; strictly speaking, this update doesn’t do much: it just tells the GPU
-                    // to use the texture passed as argument (no allocation or copy is performed)
-                    iface.color_map.update(&bound_tex);
-                    //let mvp = ortho_2d(0., 384., 448., 0.);
-                    let proj = perspective(0.5235987755982988, 384. / 448., 101010101./2010101., 101010101./10101.);
-                    let view = setup_camera(0., 0., 1.);
-                    let mvp = view * proj;
-                    //println!("{:#?}", mvp);
-                    // TODO: check how to pass by reference.
-                    iface.mvp.update(*mvp.borrow_inner());
-
-                    let render_state = RenderState::default()
-                        .set_blending((Equation::Additive, Factor::SrcAlpha, Factor::SrcAlphaComplement));
-
-                    rdr_gate.render(&render_state, |mut tess_gate| {
-                        tess_gate.render(&tess);
-                    });
-                });
-            });
-
-        surface.swap_buffers();
-    }
-}
--- a/examples/common.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,164 +0,0 @@
-use image::{GenericImageView, DynamicImage, GrayImage, ImageError};
-use luminance::pixel::{NormRGB8UI, NormRGBA8UI};
-use luminance::texture::{Dim2, Dim2Array, Sampler, Texture, GenMipmaps};
-use luminance_glfw::GlfwSurface;
-use touhou::th06::anm0::Anm0;
-use std::fs::File;
-use std::io::{self, BufReader, Read};
-use std::path::Path;
-
-pub fn load_file_into_vec<P: AsRef<Path>>(filename: P) -> io::Result<Vec<u8>> {
-    let file = File::open(filename)?;
-    let mut file = BufReader::new(file);
-    let mut buf = vec![];
-    file.read_to_end(&mut buf)?;
-    Ok(buf)
-}
-
-pub enum LoadedTexture {
-    Rgba(Texture<Dim2, NormRGBA8UI>),
-    Rgb(Texture<Dim2, NormRGB8UI>),
-    RgbaArray(Texture<Dim2Array, NormRGBA8UI>),
-}
-
-#[derive(Debug)]
-pub enum TextureLoadError {
-    CannotOpenRgb(String, ImageError),
-    CannotOpenAlpha(String, ImageError),
-    AlphaToGrayscale(String),
-}
-
-fn open_rgb_png(path: &Path) -> Result<DynamicImage, TextureLoadError> {
-    // load the texture into memory as a whole bloc (i.e. no streaming)
-    image::open(&path).map_err(|e| TextureLoadError::CannotOpenRgb(path.to_str().unwrap().to_owned(), e))
-}
-
-fn open_alpha_png(path: &Path) -> Result<DynamicImage, TextureLoadError> {
-    // load the texture into memory as a whole bloc (i.e. no streaming)
-    image::open(&path).map_err(|e| TextureLoadError::CannotOpenAlpha(path.to_str().unwrap().to_owned(), e))
-}
-
-fn merge_rgb_alpha(rgb: &DynamicImage, alpha: &GrayImage) -> Vec<(u8, u8, u8, u8)> {
-    rgb
-        .pixels()
-        .zip(alpha.pixels())
-        .map(|((_x, _y, rgb), luma)| (rgb[0], rgb[1], rgb[2], luma[0]))
-        .collect::<Vec<_>>()
-}
-
-pub fn load_from_data(data: &[u8]) -> Result<DynamicImage, ImageError> {
-    image::load_from_memory(data)
-}
-
-pub fn reupload_texture_from_rgb_image(tex: &mut Texture<Dim2, NormRGB8UI>, img: DynamicImage) -> Result<(), TextureLoadError> {
-    let texels = img
-        .pixels()
-        .map(|(_x, _y, rgb)| (rgb[0], rgb[1], rgb[2]))
-        .collect::<Vec<_>>();
-
-    // the first argument disables mipmap generation (we don’t care so far)
-    tex.upload(GenMipmaps::No, &texels).unwrap();
-
-    Ok(())
-}
-
-pub fn upload_texture_from_rgb_image(surface: &mut GlfwSurface, img: DynamicImage) -> Result<LoadedTexture, TextureLoadError> {
-    let (width, height) = img.dimensions();
-
-    // create the luminance texture; the third argument is the number of mipmaps we want (leave it
-    // to 0 for now) and the latest is a the sampler to use when sampling the texels in the
-    // shader (we’ll just use the default one)
-    let mut tex =
-        Texture::new(surface, [width, height], 0, Sampler::default()).expect("luminance texture creation");
-
-    reupload_texture_from_rgb_image(&mut tex, img)?;
-
-    Ok(LoadedTexture::Rgb(tex))
-}
-
-pub fn load_rgb_texture(surface: &mut GlfwSurface, path: &Path) -> Result<LoadedTexture, TextureLoadError> {
-    let img = open_rgb_png(&path)?;
-    upload_texture_from_rgb_image(surface, img)
-}
-
-fn load_rgb_a_pngs(surface: &mut GlfwSurface, rgb: &Path, alpha: &Path) -> Result<LoadedTexture, TextureLoadError> {
-    let img = open_alpha_png(&alpha)?;
-    let alpha = match img.grayscale() {
-        DynamicImage::ImageLuma8(img) => img,
-        _ => {
-            return Err(TextureLoadError::AlphaToGrayscale(alpha.to_str().unwrap().to_owned()))
-        }
-    };
-    let (width, height) = img.dimensions();
-    let img = open_rgb_png(&rgb)?;
-    assert_eq!((width, height), img.dimensions());
-    let texels = merge_rgb_alpha(&img, &alpha);
-
-    // create the luminance texture; the third argument is the number of mipmaps we want (leave it
-    // to 0 for now) and the latest is a the sampler to use when sampling the texels in the
-    // shader (we’ll just use the default one)
-    let tex =
-        Texture::new(surface, [width, height], 0, Sampler::default()).expect("luminance texture creation");
-
-    // the first argument disables mipmap generation (we don’t care so far)
-    tex.upload(GenMipmaps::No, &texels).unwrap();
-
-    Ok(LoadedTexture::Rgba(tex))
-}
-
-pub fn load_anm_image<P: AsRef<Path>>(mut surface: &mut GlfwSurface, anm0: &Anm0, anm_filename: P) -> Result<LoadedTexture, TextureLoadError> {
-    let anm_filename = anm_filename.as_ref();
-    let png_filename = anm_filename.with_file_name(Path::new(&anm0.png_filename).file_name().unwrap());
-    match anm0.alpha_filename {
-        Some(ref filename) => {
-            let alpha_filename = anm_filename.with_file_name(Path::new(filename).file_name().unwrap());
-            load_rgb_a_pngs(&mut surface, &png_filename, &alpha_filename)
-        },
-        None => {
-            load_rgb_texture(&mut surface, &png_filename)
-        }
-    }
-}
-
-fn load_array_texture(surface: &mut GlfwSurface, images: &[(&Path, &Path)]) -> Result<LoadedTexture, TextureLoadError> {
-    let mut decoded = vec![];
-    let dimensions = (256, 256);
-    for (rgb, alpha) in images {
-        let img = open_alpha_png(&alpha)?;
-        assert_eq!(dimensions, img.dimensions());
-        let alpha = match img.grayscale() {
-            DynamicImage::ImageLuma8(img) => img,
-            _ => {
-                return Err(TextureLoadError::AlphaToGrayscale(alpha.to_str().unwrap().to_owned()))
-            }
-        };
-        let img = open_rgb_png(&rgb)?;
-        assert_eq!(dimensions, img.dimensions());
-        let texels = merge_rgb_alpha(&img, &alpha);
-        decoded.push(texels);
-    }
-
-    // create the luminance texture; the third argument is the number of mipmaps we want (leave it
-    // to 0 for now) and the latest is a the sampler to use when sampling the texels in the
-    // shader (we’ll just use the default one)
-    let tex =
-        Texture::new(surface, ([dimensions.0, dimensions.1], images.len() as u32), 0, Sampler::default()).expect("luminance texture creation");
-
-    // the first argument disables mipmap generation (we don’t care so far)
-    tex.upload(GenMipmaps::No, &decoded.into_iter().flatten().collect::<Vec<_>>()).unwrap();
-
-    Ok(LoadedTexture::RgbaArray(tex))
-}
-
-pub fn load_multiple_anm_images<P: AsRef<Path>>(mut surface: &mut GlfwSurface, anms: &[Anm0], anm_filename: P) -> Result<LoadedTexture, TextureLoadError> {
-    let anm_filename = anm_filename.as_ref();
-    let mut paths = vec![];
-    for anm0 in anms.iter() {
-        let rgb_filename = anm_filename.with_file_name(Path::new(&anm0.png_filename).file_name().unwrap());
-        let filename = anm0.alpha_filename.as_ref().expect("Can’t not have alpha here!");
-        let alpha_filename = anm_filename.with_file_name(Path::new(filename).file_name().unwrap());
-        paths.push((rgb_filename, alpha_filename));
-    }
-    let paths: Vec<_> = paths.iter().map(|(rgb, alpha)| (rgb.as_ref(), alpha.as_ref())).collect();
-    load_array_texture(&mut surface, paths.as_slice())
-}
--- a/examples/dump_ecl.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-use touhou::th06::ecl::{Ecl, CallMain, CallSub, Rank};
-use std::env;
-use std::path::Path;
-
-#[path = "common.rs"]
-mod common;
-use common::{load_file_into_vec};
-
-fn format_rank(rank: &Rank) -> String {
-    format!("{}{}{}{}", if rank.contains(Rank::EASY) { 'E' } else { ' ' },
-                        if rank.contains(Rank::NORMAL) { 'N' } else { ' ' },
-                        if rank.contains(Rank::HARD) { 'H' } else { ' ' },
-                        if rank.contains(Rank::LUNATIC) { 'L' } else { ' ' })
-}
-
-fn print_sub_instruction(call: &CallSub) {
-    let CallSub { time, rank_mask, param_mask: _, instr } = call;
-    println!("    {:>5}: {}: {:?}", time, format_rank(rank_mask), instr);
-}
-
-fn print_main_instruction(call: &CallMain) {
-    let CallMain { time, sub, instr } = call;
-    println!("    {:>5}: sub {:>2}: {:?}", time, sub, instr);
-}
-
-fn main() {
-    // Parse arguments.
-    let args: Vec<_> = env::args().collect();
-    if args.len() != 2 {
-        eprintln!("Usage: {} <ECL file>", args[0]);
-        return;
-    }
-    let ecl_filename = Path::new(&args[1]);
-
-    // Open the ECL file.
-    let buf = load_file_into_vec(ecl_filename).unwrap();
-    let (_, ecl) = Ecl::from_slice(&buf).unwrap();
-
-    for (i, main) in ecl.mains.iter().enumerate() {
-        println!("Main {} {{", i);
-        for call in main.instructions.iter() {
-            print_main_instruction(call);
-        }
-        println!("}}");
-        println!();
-    }
-
-    for (i, sub) in ecl.subs.iter().enumerate() {
-        println!("Sub {} {{", i);
-        for call in sub.instructions.iter() {
-            print_sub_instruction(call);
-        }
-        println!("}}");
-        println!();
-    }
-}
--- a/examples/eclrenderer.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,237 +0,0 @@
-use luminance::blending::{Equation, Factor};
-use luminance::context::GraphicsContext;
-use luminance::pipeline::{BoundTexture, PipelineState};
-use luminance::pixel::NormUnsigned;
-use luminance::render_state::RenderState;
-use luminance::shader::program::{Program, Uniform};
-use luminance::tess::{Mode, TessBuilder};
-use luminance::texture::Dim2;
-use luminance_derive::{Semantics, Vertex, UniformInterface};
-use luminance_glfw::{Action, Key, WindowEvent, GlfwSurface, Surface, WindowDim, WindowOpt};
-use touhou::th06::anm0::Anm0;
-use touhou::th06::anm0_vm::{Sprite, Vertex as FakeVertex};
-use touhou::th06::ecl::{Ecl, Rank};
-use touhou::th06::ecl_vm::EclRunner;
-use touhou::th06::enemy::{Enemy, Game, Position};
-use touhou::util::math::{perspective, setup_camera};
-use touhou::util::prng::Prng;
-use std::cell::RefCell;
-use std::rc::Rc;
-use std::env;
-use std::path::Path;
-
-#[path = "common.rs"]
-mod common;
-use common::{load_file_into_vec, load_anm_image, LoadedTexture};
-
-const VS: &str = r#"
-in ivec3 in_position;
-in vec2 in_texcoord;
-in uvec4 in_color;
-
-uniform mat4 mvp;
-
-out vec2 texcoord;
-out vec4 color;
-
-void main()
-{
-    gl_Position = mvp * vec4(vec3(in_position), 1.0);
-    texcoord = vec2(in_texcoord);
-
-    // Normalized from the u8 being passed.
-    color = vec4(in_color) / 255.;
-}
-"#;
-
-const FS: &str = r#"
-in vec2 texcoord;
-in vec4 color;
-
-uniform sampler2D color_map;
-
-out vec4 frag_color;
-
-void main()
-{
-    frag_color = texture(color_map, texcoord) * color;
-}
-"#;
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Semantics)]
-pub enum Semantics {
-    #[sem(name = "in_position", repr = "[i16; 3]", wrapper = "VertexPosition")]
-    Position,
-
-    #[sem(name = "in_texcoord", repr = "[f32; 2]", wrapper = "VertexTexcoord")]
-    Texcoord,
-
-    #[sem(name = "in_color", repr = "[u8; 4]", wrapper = "VertexColor")]
-    Color,
-}
-
-#[repr(C)]
-#[derive(Clone, Copy, Debug, PartialEq, Vertex)]
-#[vertex(sem = "Semantics")]
-struct Vertex {
-    pos: VertexPosition,
-    uv: VertexTexcoord,
-    rgba: VertexColor,
-}
-
-#[derive(UniformInterface)]
-struct ShaderInterface {
-    // the 'static lifetime acts as “anything” here
-    color_map: Uniform<&'static BoundTexture<'static, Dim2, NormUnsigned>>,
-
-    #[uniform(name = "mvp")]
-    mvp: Uniform<[[f32; 4]; 4]>,
-}
-
-fn main() {
-    // Parse arguments.
-    let args: Vec<_> = env::args().collect();
-    if args.len() != 5 {
-        eprintln!("Usage: {} <ECL file> <ANM file> <easy|normal|hard|lunatic> <sub number>", args[0]);
-        return;
-    }
-    let ecl_filename = Path::new(&args[1]);
-    let anm_filename = Path::new(&args[2]);
-    let rank: Rank = args[3].parse().expect("rank");
-    let sub: u16 = args[4].parse().expect("number");
-
-    // Open the ECL file.
-    let buf = load_file_into_vec(ecl_filename).unwrap();
-    let (_, ecl) = Ecl::from_slice(&buf).unwrap();
-
-    // Open the ANM file.
-    let buf = load_file_into_vec(anm_filename).unwrap();
-    let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
-    let anm0 = anms.pop().unwrap();
-    let anm0 = Rc::new(RefCell::new([anm0.clone(), anm0]));
-
-    if ecl.subs.len() < sub as usize {
-        eprintln!("This ecl doesn’t contain a sub named {}.", sub);
-        return;
-    }
-
-    // Get the time since January 1970 as a seed for the PRNG.
-    let time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap();
-    let prng = Rc::new(RefCell::new(Prng::new(time.subsec_micros() as u16)));
-
-    // Create the Game god object.
-    let game = Game::new(prng, rank);
-    let game = Rc::new(RefCell::new(game));
-
-    // And the enemy object.
-    let enemy = Enemy::new(Position::new(0., 0.), 500, 0, 640, false, Rc::downgrade(&anm0), Rc::downgrade(&game));
-    let mut ecl_runner = EclRunner::new(&ecl, enemy.clone(), sub);
-
-    assert_eq!(std::mem::size_of::<Vertex>(), std::mem::size_of::<FakeVertex>());
-    let vertices: [Vertex; 4] = {
-        let data = std::mem::MaybeUninit::uninit();
-        unsafe { data.assume_init() }
-    };
-
-    let mut surface = GlfwSurface::new(WindowDim::Windowed(384, 448), "Touhou", WindowOpt::default()).unwrap();
-
-    // Open the image atlas matching this ANM.
-    let tex = load_anm_image(&mut surface, &anm0.borrow()[0], &anm_filename).expect("image loading");
-
-    // set the uniform interface to our type so that we can read textures from the shader
-    let program =
-        Program::<Semantics, (), ShaderInterface>::from_strings(None, VS, None, FS).expect("program creation").ignore_warnings();
-
-    let mut tess = TessBuilder::new(&mut surface)
-        .add_vertices(vertices)
-        .set_mode(Mode::TriangleFan)
-        .build()
-        .unwrap();
-
-    let mut back_buffer = surface.back_buffer().unwrap();
-    let mut resize = false;
-
-    'app: loop {
-        for event in surface.poll_events() {
-            match event {
-                WindowEvent::Close | WindowEvent::Key(Key::Escape, _, Action::Release, _) => break 'app,
-
-                WindowEvent::FramebufferSize(..) => {
-                    resize = true;
-                }
-
-                _ => (),
-            }
-        }
-
-        if resize {
-            back_buffer = surface.back_buffer().unwrap();
-            resize = false;
-        }
-
-        if ecl_runner.running == false {
-            break;
-        }
-
-        {
-            let mut slice = tess
-                .as_slice_mut()
-                .unwrap();
-
-            ecl_runner.run_frame();
-            {
-                let mut enemy = enemy.borrow_mut();
-                enemy.update();
-            }
-            let mut game = game.borrow_mut();
-            game.run_frame();
-            let sprites = game.get_sprites();
-            fill_vertices_ptr(sprites, slice.as_mut_ptr());
-        }
-
-        // here, we need to bind the pipeline variable; it will enable us to bind the texture to the GPU
-        // and use it in the shader
-        surface
-            .pipeline_builder()
-            .pipeline(&back_buffer, &PipelineState::default(), |pipeline, mut shd_gate| {
-                // bind our fancy texture to the GPU: it gives us a bound texture we can use with the shader
-                let bound_tex = match &tex {
-                    LoadedTexture::Rgb(tex) => pipeline.bind_texture(tex),
-                    LoadedTexture::Rgba(tex) => pipeline.bind_texture(tex),
-                    LoadedTexture::RgbaArray(tex) => unreachable!(),
-                };
-
-                shd_gate.shade(&program, |iface, mut rdr_gate| {
-                    // update the texture; strictly speaking, this update doesn’t do much: it just tells the GPU
-                    // to use the texture passed as argument (no allocation or copy is performed)
-                    iface.color_map.update(&bound_tex);
-                    //let mvp = ortho_2d(0., 384., 448., 0.);
-                    let proj = perspective(0.5235987755982988, 384. / 448., 101010101./2010101., 101010101./10101.);
-                    let view = setup_camera(0., 0., 1.);
-                    let mvp = view * proj;
-                    //println!("{:#?}", mvp);
-                    // TODO: check how to pass by reference.
-                    iface.mvp.update(*mvp.borrow_inner());
-
-                    let render_state = RenderState::default()
-                        .set_blending((Equation::Additive, Factor::SrcAlpha, Factor::SrcAlphaComplement));
-
-                    rdr_gate.render(&render_state, |mut tess_gate| {
-                        // render the tessellation to the surface the regular way and let the vertex shader’s
-                        // magic do the rest!
-                        tess_gate.render(&tess);
-                    });
-                });
-            });
-
-        surface.swap_buffers();
-    }
-}
-
-fn fill_vertices_ptr(sprites: Vec<(f32, f32, f32, Rc<RefCell<Sprite>>)>, vertices: *mut Vertex) {
-    let mut fake_vertices = unsafe { std::mem::transmute::<*mut Vertex, &mut [FakeVertex; 4]>(vertices) };
-    for (x, y, z, sprite) in sprites {
-        let sprite = sprite.borrow();
-        sprite.fill_vertices(&mut fake_vertices, x, y, z);
-    }
-}
--- a/examples/menu.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,222 +0,0 @@
-use ears::{Music, AudioController};
-use luminance::blending::{Equation, Factor};
-use luminance::context::GraphicsContext;
-use luminance::pipeline::{BoundTexture, PipelineState};
-use luminance::pixel::NormUnsigned;
-use luminance::render_state::RenderState;
-use luminance::shader::program::{Program, Uniform};
-use luminance::tess::{Mode, TessBuilder};
-use luminance::texture::Dim2;
-use luminance_derive::{Semantics, Vertex, UniformInterface};
-use luminance_glfw::{Action, Key, WindowEvent, GlfwSurface, Surface, WindowDim, WindowOpt};
-use touhou::th06::pbg3;
-use touhou::th06::anm0::Anm0;
-use touhou::th06::anm0_vm::{AnmRunner, Sprite, Vertex as FakeVertex};
-use touhou::util::math::{perspective, setup_camera, ortho_2d};
-use touhou::util::prng::Prng;
-use std::cell::RefCell;
-use std::rc::Rc;
-use std::env;
-use std::path::Path;
-
-#[path = "common.rs"]
-mod common;
-use common::LoadedTexture;
-
-const VS: &str = r#"
-in ivec3 in_position;
-in vec2 in_texcoord;
-in vec4 in_color;
-
-uniform mat4 mvp;
-
-out vec2 texcoord;
-out vec4 color;
-
-void main()
-{
-    gl_Position = mvp * vec4(vec3(in_position), 1.0);
-    texcoord = vec2(in_texcoord);
-
-    // It’s already normalized from the u8 being passed.
-    color = in_color;
-}
-"#;
-
-const FS: &str = r#"
-in vec2 texcoord;
-in vec4 color;
-
-uniform sampler2D color_map;
-
-out vec4 frag_color;
-
-void main()
-{
-    frag_color = texture(color_map, texcoord) * color;
-}
-"#;
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Semantics)]
-pub enum Semantics {
-    #[sem(name = "in_position", repr = "[i16; 3]", wrapper = "VertexPosition")]
-    Position,
-
-    #[sem(name = "in_texcoord", repr = "[f32; 2]", wrapper = "VertexTexcoord")]
-    Texcoord,
-
-    #[sem(name = "in_color", repr = "[u8; 4]", wrapper = "VertexColor")]
-    Color,
-}
-
-#[repr(C)]
-#[derive(Clone, Copy, Debug, PartialEq, Vertex)]
-#[vertex(sem = "Semantics")]
-struct Vertex {
-    pos: VertexPosition,
-    uv: VertexTexcoord,
-    #[vertex(normalized = "true")]
-    rgba: VertexColor,
-}
-
-#[derive(UniformInterface)]
-struct ShaderInterface {
-    // the 'static lifetime acts as “anything” here
-    color_map: Uniform<&'static BoundTexture<'static, Dim2, NormUnsigned>>,
-
-    #[uniform(name = "mvp")]
-    mvp: Uniform<[[f32; 4]; 4]>,
-}
-
-const DEFAULT_VERTICES: [Vertex; 4] = [
-    Vertex::new(VertexPosition::new([0, 0, 0]), VertexTexcoord::new([0., 0.]), VertexColor::new([255, 255, 255, 255])),
-    Vertex::new(VertexPosition::new([640, 0, 0]), VertexTexcoord::new([1., 0.]), VertexColor::new([255, 255, 255, 255])),
-    Vertex::new(VertexPosition::new([640, 480, 0]), VertexTexcoord::new([1., 1.]), VertexColor::new([255, 255, 255, 255])),
-    Vertex::new(VertexPosition::new([0, 480, 0]), VertexTexcoord::new([0., 1.]), VertexColor::new([255, 255, 255, 255])),
-];
-
-fn main() {
-    // Parse arguments.
-    let args: Vec<_> = env::args().collect();
-    if args.len() != 2 {
-        eprintln!("Usage: {} <unarchived directory>", args[0]);
-        return;
-    }
-    let directory = Path::new(&args[1]);
-
-    let in_dat = directory.join("IN.DAT");
-    // Since GLFW can be slow to create its window, let’s decode the splash screen in another
-    // thread in the meantime.
-    let jpeg_thread = std::thread::spawn(|| {
-        let mut in_pbg3 = pbg3::from_path_buffered(in_dat).expect("IN.DAT present");
-        let jpeg = in_pbg3.get_file("th06logo.jpg", true).expect("th06logo.jpg in IN.DAT");
-        let image = common::load_from_data(&jpeg).expect("th06logo.jpg decodable");
-        image
-    });
-
-    let music_filename = directory.join("bgm").join("th06_01.wav");
-    let music_filename = music_filename.to_str().expect("non-UTF-8 music filename");
-    let music = match Music::new(music_filename) {
-        Ok(mut music) => {
-            music.set_looping(true);
-            music.play();
-            music
-        }
-        Err(err) => {
-            eprintln!("Impossible to open or play music file: {}", err);
-            return;
-        }
-    };
-
-    let mut surface = GlfwSurface::new(WindowDim::Windowed(640, 480), "Touhou", WindowOpt::default()).expect("GLFW window");
-
-    let image = jpeg_thread.join().expect("image loading");
-    let background = common::upload_texture_from_rgb_image(&mut surface, image).expect("upload data to texture");
-
-    let mut background = match background {
-        LoadedTexture::Rgb(tex) => tex,
-        LoadedTexture::Rgba(tex) => unreachable!(),
-        LoadedTexture::RgbaArray(tex) => unreachable!(),
-    };
-
-    // set the uniform interface to our type so that we can read textures from the shader
-    let program =
-        Program::<Semantics, (), ShaderInterface>::from_strings(None, VS, None, FS).expect("program creation").ignore_warnings();
-
-    let mut tess = TessBuilder::new(&mut surface)
-        .add_vertices(DEFAULT_VERTICES)
-        .set_mode(Mode::TriangleFan)
-        .build()
-        .unwrap();
-
-    let tl_dat = directory.join("TL.DAT");
-    let mut tl_pbg3 = pbg3::from_path_buffered(tl_dat).expect("TL.DAT present");
-
-    let mut back_buffer = surface.back_buffer().unwrap();
-    let mut resize = false;
-    let mut frame = 0;
-    let mut z_pressed = false;
-    let mut x_pressed = false;
-
-    'app: loop {
-        for event in surface.poll_events() {
-            match event {
-                WindowEvent::Close | WindowEvent::Key(Key::Escape, _, Action::Release, _) => break 'app,
-
-                WindowEvent::Key(Key::Z, _, Action::Press, _) => z_pressed = true,
-                WindowEvent::Key(Key::X, _, Action::Press, _) => x_pressed = true,
-
-                WindowEvent::FramebufferSize(..) => {
-                    resize = true;
-                }
-
-                _ => (),
-            }
-        }
-
-        if resize {
-            back_buffer = surface.back_buffer().unwrap();
-            resize = false;
-        }
-
-        frame += 1;
-        if frame == 60 {
-            let jpeg = tl_pbg3.get_file("title00.jpg", true).expect("title00.jpg in TL.DAT");
-            let image = common::load_from_data(&jpeg).expect("th06logo.jpg decodable");
-            common::reupload_texture_from_rgb_image(&mut background, image).expect("upload data to texture");
-        }
-
-        if frame >= 60 && z_pressed {
-            let jpeg = tl_pbg3.get_file("select00.jpg", true).expect("select00.jpg in TL.DAT");
-            let image = common::load_from_data(&jpeg).expect("select00.jpg decodable");
-            common::reupload_texture_from_rgb_image(&mut background, image).expect("upload data to texture");
-        }
-
-        // here, we need to bind the pipeline variable; it will enable us to bind the texture to the GPU
-        // and use it in the shader
-        surface
-            .pipeline_builder()
-            .pipeline(&back_buffer, &PipelineState::default(), |pipeline, mut shd_gate| {
-                // bind our fancy texture to the GPU: it gives us a bound texture we can use with the shader
-                let tex = pipeline.bind_texture(&background);
-
-                shd_gate.shade(&program, |iface, mut rdr_gate| {
-                    // update the texture; strictly speaking, this update doesn’t do much: it just tells the GPU
-                    // to use the texture passed as argument (no allocation or copy is performed)
-                    iface.color_map.update(&tex);
-                    let mvp = ortho_2d(0., 640., 480., 0.);
-                    // TODO: check how to pass by reference.
-                    iface.mvp.update(*mvp.borrow_inner());
-
-                    let render_state = RenderState::default()
-                        .set_blending((Equation::Additive, Factor::SrcAlpha, Factor::SrcAlphaComplement));
-
-                    rdr_gate.render(&render_state, |mut tess_gate| {
-                        tess_gate.render(&tess);
-                    });
-                });
-            });
-
-        surface.swap_buffers();
-    }
-}
--- a/examples/stagerunner.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,261 +0,0 @@
-use luminance::blending::{Equation, Factor};
-use luminance::context::GraphicsContext;
-use luminance::pipeline::{BoundTexture, PipelineState};
-use luminance::pixel::NormUnsigned;
-use luminance::render_state::RenderState;
-use luminance::shader::program::{Program, Uniform};
-use luminance::tess::{Mode, TessBuilder};
-use luminance::texture::Dim2Array;
-use luminance_derive::{Semantics, Vertex, UniformInterface};
-use luminance_glfw::{Action, Key, WindowEvent, GlfwSurface, Surface, WindowDim, WindowOpt};
-use touhou::th06::anm0::Anm0;
-use touhou::th06::anm0_vm::Vertex as FakeVertex;
-use touhou::th06::ecl::{Ecl, Rank, MainInstruction};
-use touhou::th06::ecl_vm::EclRunner;
-use touhou::th06::enemy::{Enemy, Game, Position};
-use touhou::util::math::{perspective, setup_camera};
-use touhou::util::prng::Prng;
-use std::cell::RefCell;
-use std::rc::Rc;
-use std::env;
-use std::path::Path;
-
-#[path = "common.rs"]
-mod common;
-use common::{load_file_into_vec, load_multiple_anm_images, LoadedTexture};
-
-const VS: &str = r#"
-in ivec3 in_position;
-in uint in_layer;
-in vec2 in_texcoord;
-in uvec4 in_color;
-
-uniform mat4 mvp;
-
-flat out uint layer;
-out vec2 texcoord;
-out vec4 color;
-
-void main()
-{
-    gl_Position = mvp * vec4(vec3(in_position), 1.0);
-    texcoord = vec2(in_texcoord);
-
-    // Normalized from the u8 being passed.
-    color = vec4(in_color) / 255.;
-
-    layer = in_layer;
-}
-"#;
-
-const FS: &str = r#"
-flat in uint layer;
-in vec2 texcoord;
-in vec4 color;
-
-uniform sampler2DArray color_map;
-
-out vec4 frag_color;
-
-void main()
-{
-    frag_color = texture(color_map, vec3(texcoord, layer)) * color;
-}
-"#;
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Semantics)]
-pub enum Semantics {
-    #[sem(name = "in_position", repr = "[i16; 3]", wrapper = "VertexPosition")]
-    Position,
-
-    #[sem(name = "in_layer", repr = "u16", wrapper = "VertexLayer")]
-    Layer,
-
-    #[sem(name = "in_texcoord", repr = "[f32; 2]", wrapper = "VertexTexcoord")]
-    Texcoord,
-
-    #[sem(name = "in_color", repr = "[u8; 4]", wrapper = "VertexColor")]
-    Color,
-}
-
-#[repr(C)]
-#[derive(Clone, Copy, Debug, PartialEq, Vertex)]
-#[vertex(sem = "Semantics")]
-struct Vertex {
-    pos: VertexPosition,
-    layer: VertexLayer,
-    uv: VertexTexcoord,
-    rgba: VertexColor,
-}
-
-#[derive(UniformInterface)]
-struct ShaderInterface {
-    // the 'static lifetime acts as “anything” here
-    color_map: Uniform<&'static BoundTexture<'static, Dim2Array, NormUnsigned>>,
-
-    #[uniform(name = "mvp")]
-    mvp: Uniform<[[f32; 4]; 4]>,
-}
-
-fn main() {
-    // Parse arguments.
-    let args: Vec<_> = env::args().collect();
-    if args.len() != 4 {
-        eprintln!("Usage: {} <unarchived ST.DAT directory> <stage number> <easy|normal|hard|lunatic>", args[0]);
-        return;
-    }
-    let directory = Path::new(&args[1]);
-    let stage_number: u8 = args[2].parse().expect("stage");
-    let rank: Rank = args[3].parse().expect("rank");
-
-    // Open the ECL file.
-    let buf = load_file_into_vec(directory.join(format!("ecldata{}.ecl", stage_number))).unwrap();
-    let (_, ecl) = Ecl::from_slice(&buf).unwrap();
-    assert_eq!(ecl.mains.len(), 1);
-    let main = ecl.mains[0].clone();
-
-    // Open the ANM file.
-    let anm_filename = directory.join(format!("stg{}enm.anm", stage_number));
-    let buf = load_file_into_vec(&anm_filename).unwrap();
-    let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
-    let anm0 = anms.pop().unwrap();
-
-    // Open the second ANM file.
-    let anm2_filename = directory.join(format!("stg{}enm2.anm", stage_number));
-    let buf = load_file_into_vec(&anm2_filename).unwrap();
-    let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
-    let anm0_bis = anms.pop().unwrap();
-
-    let anms = [anm0, anm0_bis];
-
-    // Get the time since January 1970 as a seed for the PRNG.
-    let time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap();
-    let prng = Rc::new(RefCell::new(Prng::new(time.subsec_micros() as u16)));
-
-    // Create the Game god object.
-    let game = Game::new(prng, rank);
-    let game = Rc::new(RefCell::new(game));
-
-    assert_eq!(std::mem::size_of::<Vertex>(), std::mem::size_of::<FakeVertex>());
-    let vertices: [Vertex; 4] = {
-        let data = std::mem::MaybeUninit::uninit();
-        unsafe { data.assume_init() }
-    };
-
-    let mut surface = GlfwSurface::new(WindowDim::Windowed(384, 448), "Touhou", WindowOpt::default()).unwrap();
-
-    // Open the image atlas matching this ANM.
-    let tex = load_multiple_anm_images(&mut surface, &anms, &anm_filename).expect("image loading");
-    let anms = Rc::new(RefCell::new(anms));
-
-    // set the uniform interface to our type so that we can read textures from the shader
-    let program =
-        Program::<Semantics, (), ShaderInterface>::from_strings(None, VS, None, FS).expect("program creation").ignore_warnings();
-
-    let mut tess = TessBuilder::new(&mut surface)
-        .add_vertices(vertices)
-        .set_mode(Mode::TriangleFan)
-        .build()
-        .unwrap();
-
-    let mut back_buffer = surface.back_buffer().unwrap();
-    let mut resize = false;
-    let mut frame = 0;
-    let mut ecl_runners = vec![];
-
-    'app: loop {
-        for event in surface.poll_events() {
-            match event {
-                WindowEvent::Close | WindowEvent::Key(Key::Escape, _, Action::Release, _) => break 'app,
-
-                WindowEvent::FramebufferSize(..) => {
-                    resize = true;
-                }
-
-                _ => (),
-            }
-        }
-
-        if resize {
-            back_buffer = surface.back_buffer().unwrap();
-            resize = false;
-        }
-
-        for call in main.instructions.iter() {
-            if call.time == frame {
-                let sub = call.sub;
-                let instr = call.instr;
-                let (x, y, _z, life, bonus, score, mirror) = match instr {
-                    MainInstruction::SpawnEnemy(x, y, z, life, bonus, score) => (x, y, z, life, bonus, score, false),
-                    MainInstruction::SpawnEnemyMirrored(x, y, z, life, bonus, score) => (x, y, z, life, bonus, score, true),
-                    MainInstruction::SpawnEnemyRandom(x, y, z, life, bonus, score) => (x, y, z, life, bonus, score, false),
-                    MainInstruction::SpawnEnemyMirroredRandom(x, y, z, life, bonus, score) => (x, y, z, life, bonus, score, true),
-                    _ => continue,
-                };
-                let enemy = Enemy::new(Position::new(x, y), life, bonus, score, mirror, Rc::downgrade(&anms), Rc::downgrade(&game));
-                let runner = EclRunner::new(&ecl, enemy, sub);
-                ecl_runners.push(runner);
-            }
-        }
-
-        for runner in ecl_runners.iter_mut() {
-            runner.run_frame();
-            let mut enemy = runner.enemy.borrow_mut();
-            enemy.update();
-        }
-
-        // here, we need to bind the pipeline variable; it will enable us to bind the texture to the GPU
-        // and use it in the shader
-        surface
-            .pipeline_builder()
-            .pipeline(&back_buffer, &PipelineState::default(), |pipeline, mut shd_gate| {
-                // bind our fancy texture to the GPU: it gives us a bound texture we can use with the shader
-                let bound_tex = match &tex {
-                    LoadedTexture::Rgb(tex) => unreachable!(),
-                    LoadedTexture::Rgba(tex) => unreachable!(),
-                    LoadedTexture::RgbaArray(tex) => pipeline.bind_texture(tex),
-                };
-
-                shd_gate.shade(&program, |iface, mut rdr_gate| {
-                    // update the texture; strictly speaking, this update doesn’t do much: it just tells the GPU
-                    // to use the texture passed as argument (no allocation or copy is performed)
-                    iface.color_map.update(&bound_tex);
-                    //let mvp = ortho_2d(0., 384., 448., 0.);
-                    let proj = perspective(0.5235987755982988, 384. / 448., 101010101./2010101., 101010101./10101.);
-                    let view = setup_camera(0., 0., 1.);
-                    let mvp = view * proj;
-                    //println!("{:#?}", mvp);
-                    // TODO: check how to pass by reference.
-                    iface.mvp.update(*mvp.borrow_inner());
-
-                    let render_state = RenderState::default()
-                        .set_depth_test(None)
-                        .set_blending((Equation::Additive, Factor::SrcAlpha, Factor::SrcAlphaComplement));
-
-                    rdr_gate.render(&render_state, |mut tess_gate| {
-                        let mut game = game.borrow_mut();
-                        game.run_frame();
-
-                        for (x, y, z, sprite) in game.get_sprites() {
-                            {
-                                let mut slice = tess
-                                    .as_slice_mut()
-                                    .unwrap();
-
-                                let sprite = sprite.borrow();
-                                let fake_vertices = unsafe { std::mem::transmute::<*mut Vertex, &mut [FakeVertex; 4]>(slice.as_mut_ptr()) };
-                                sprite.fill_vertices(fake_vertices, x, y, z);
-                            }
-
-                            // render the tessellation to the surface the regular way and let the vertex shader’s
-                            // magic do the rest!
-                            tess_gate.render(&tess);
-                        }
-                    });
-                });
-            });
-
-        surface.swap_buffers();
-        frame += 1;
-    }
-}
--- a/examples/stdrenderer.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,257 +0,0 @@
-use luminance::blending::{Equation, Factor};
-use luminance::context::GraphicsContext;
-use luminance::pipeline::{BoundTexture, PipelineState};
-use luminance::pixel::NormUnsigned;
-use luminance::render_state::RenderState;
-use luminance::shader::program::{Program, Uniform};
-use luminance::tess::{Mode, TessBuilder, TessSliceIndex};
-use luminance::texture::Dim2;
-use luminance_derive::{Semantics, Vertex, UniformInterface};
-use luminance_glfw::{Action, Key, WindowEvent, GlfwSurface, Surface, WindowDim, WindowOpt};
-use touhou::th06::anm0::Anm0;
-use touhou::th06::anm0_vm::{AnmRunner, Sprite, Vertex as FakeVertex};
-use touhou::th06::std::{Stage, Position, Box2D};
-use touhou::th06::std_vm::StageRunner;
-use touhou::util::prng::Prng;
-use touhou::util::math::perspective;
-use std::cell::RefCell;
-use std::rc::Rc;
-use std::env;
-use std::path::Path;
-
-#[path = "common.rs"]
-mod common;
-use common::{load_file_into_vec, load_anm_image, LoadedTexture};
-
-const VS: &str = r#"
-in ivec3 in_position;
-in vec2 in_texcoord;
-in uvec4 in_color;
-
-uniform mat4 mvp;
-uniform vec3 instance_position;
-
-out vec2 texcoord;
-out vec4 color;
-
-void main()
-{
-    vec3 position = vec3(in_position) + instance_position;
-    gl_Position = mvp * vec4(position, 1.0);
-    texcoord = vec2(in_texcoord);
-
-    // Normalized from the u8 being passed.
-    color = vec4(in_color) / 255.;
-}
-"#;
-
-const FS: &str = r#"
-in vec2 texcoord;
-in vec4 color;
-
-uniform sampler2D color_map;
-uniform float fog_scale;
-uniform float fog_end;
-uniform vec4 fog_color;
-
-out vec4 frag_color;
-
-void main()
-{
-    vec4 temp_color = texture(color_map, texcoord) * color;
-    float depth = gl_FragCoord.z / gl_FragCoord.w;
-    float fog_density = clamp((fog_end - depth) * fog_scale, 0.0, 1.0);
-    frag_color = vec4(mix(fog_color, temp_color, fog_density).rgb, temp_color.a);
-}
-"#;
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Semantics)]
-pub enum Semantics {
-    #[sem(name = "in_position", repr = "[i16; 3]", wrapper = "VertexPosition")]
-    Position,
-
-    #[sem(name = "in_texcoord", repr = "[f32; 2]", wrapper = "VertexTexcoord")]
-    Texcoord,
-
-    #[sem(name = "in_color", repr = "[u8; 4]", wrapper = "VertexColor")]
-    Color,
-}
-
-#[repr(C)]
-#[derive(Clone, Copy, Debug, PartialEq, Vertex)]
-#[vertex(sem = "Semantics")]
-struct Vertex {
-    pos: VertexPosition,
-    uv: VertexTexcoord,
-    rgba: VertexColor,
-}
-
-#[derive(UniformInterface)]
-struct ShaderInterface {
-    // the 'static lifetime acts as “anything” here
-    color_map: Uniform<&'static BoundTexture<'static, Dim2, NormUnsigned>>,
-
-    #[uniform(name = "mvp")]
-    mvp: Uniform<[[f32; 4]; 4]>,
-
-    #[uniform(name = "instance_position")]
-    instance_position: Uniform<[f32; 3]>,
-
-    #[uniform(name = "fog_scale")]
-    fog_scale: Uniform<f32>,
-
-    #[uniform(name = "fog_end")]
-    fog_end: Uniform<f32>,
-
-    #[uniform(name = "fog_color")]
-    fog_color: Uniform<[f32; 4]>,
-}
-
-fn main() {
-    // Parse arguments.
-    let args: Vec<_> = env::args().collect();
-    if args.len() != 3 {
-        eprintln!("Usage: {} <STD file> <ANM file>", args[0]);
-        return;
-    }
-    let std_filename = Path::new(&args[1]);
-    let anm_filename = Path::new(&args[2]);
-
-    // Open the STD file.
-    let buf = load_file_into_vec(std_filename).unwrap();
-    let (_, stage) = Stage::from_slice(&buf).unwrap();
-
-    // Open the ANM file.
-    let buf = load_file_into_vec(anm_filename).unwrap();
-    let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
-    let anm0 = anms.pop().unwrap();
-
-    // TODO: seed this PRNG with a valid seed.
-    let prng = Rc::new(RefCell::new(Prng::new(0)));
-
-    let mut surface = GlfwSurface::new(WindowDim::Windowed(384, 448), "Touhou", WindowOpt::default()).unwrap();
-
-    // Open the image atlas matching this ANM.
-    let tex = load_anm_image(&mut surface, &anm0, anm_filename).expect("image loading");
-
-    assert_eq!(std::mem::size_of::<Vertex>(), std::mem::size_of::<FakeVertex>());
-    let mut vertices: Vec<Vertex> = vec![];
-    let mut indices = vec![];
-
-    {
-        let anms = Rc::new(RefCell::new([anm0]));
-        for model in stage.models.iter() {
-            let begin = vertices.len();
-            for quad in model.quads.iter() {
-                let Position { x, y, z } = quad.pos;
-                let Box2D { width, height } = quad.size_override;
-
-                // Create the AnmRunner from the ANM and the sprite.
-                let sprite = Rc::new(RefCell::new(Sprite::with_size(width, height)));
-                let _anm_runner = AnmRunner::new(anms.clone(), quad.anm_script as u8, sprite.clone(), Rc::downgrade(&prng), 0);
-                let mut new_vertices: [Vertex; 6] = {
-                    let data = std::mem::MaybeUninit::uninit();
-                    unsafe { data.assume_init() }
-                };
-                fill_vertices(sprite.clone(), &mut new_vertices, x, y, z);
-                new_vertices[4] = new_vertices[0];
-                new_vertices[5] = new_vertices[2];
-                vertices.extend(&new_vertices);
-            }
-            let end = vertices.len();
-            indices.push((begin, end));
-        }
-    }
-
-    let mut stage_runner = StageRunner::new(Rc::new(RefCell::new(stage)));
-
-    // set the uniform interface to our type so that we can read textures from the shader
-    let program =
-        Program::<Semantics, (), ShaderInterface>::from_strings(None, VS, None, FS).expect("program creation").ignore_warnings();
-
-    let tess = TessBuilder::new(&mut surface)
-        .add_vertices(vertices)
-        .set_mode(Mode::Triangle)
-        .build()
-        .unwrap();
-
-    let mut back_buffer = surface.back_buffer().unwrap();
-    let mut resize = false;
-
-    'app: loop {
-        for event in surface.poll_events() {
-            match event {
-                WindowEvent::Close | WindowEvent::Key(Key::Escape, _, Action::Release, _) => break 'app,
-
-                WindowEvent::FramebufferSize(..) => {
-                    resize = true;
-                }
-
-                _ => (),
-            }
-        }
-
-        if resize {
-            back_buffer = surface.back_buffer().unwrap();
-            resize = false;
-        }
-
-        {
-            stage_runner.run_frame();
-            //let sprites = stage.get_sprites();
-            //fill_vertices_ptr(sprites, slice.as_mut_ptr());
-        }
-
-        // here, we need to bind the pipeline variable; it will enable us to bind the texture to the GPU
-        // and use it in the shader
-        surface
-            .pipeline_builder()
-            .pipeline(&back_buffer, &PipelineState::default(), |pipeline, mut shd_gate| {
-                // bind our fancy texture to the GPU: it gives us a bound texture we can use with the shader
-                let bound_tex = match &tex {
-                    LoadedTexture::Rgb(tex) => pipeline.bind_texture(tex),
-                    LoadedTexture::Rgba(tex) => pipeline.bind_texture(tex),
-                    LoadedTexture::RgbaArray(tex) => unreachable!(),
-                };
-
-                shd_gate.shade(&program, |iface, mut rdr_gate| {
-                    // update the texture; strictly speaking, this update doesn’t do much: it just tells the GPU
-                    // to use the texture passed as argument (no allocation or copy is performed)
-                    iface.color_map.update(&bound_tex);
-
-                    let proj = perspective(0.5235987755982988, 384. / 448., 101010101./2010101., 101010101./10101.);
-                    let model_view = stage_runner.get_model_view();
-                    let mvp = model_view * proj;
-                    // TODO: check how to pass by reference.
-                    iface.mvp.update(*mvp.borrow_inner());
-
-                    let near = stage_runner.fog_near - 101010101. / 2010101.;
-                    let far = stage_runner.fog_far - 101010101. / 2010101.;
-                    iface.fog_color.update(stage_runner.fog_color);
-                    iface.fog_scale.update(1. / (far - near));
-                    iface.fog_end.update(far);
-
-                    let render_state = RenderState::default()
-                        .set_blending((Equation::Additive, Factor::SrcAlpha, Factor::SrcAlphaComplement));
-
-                    let stage = stage_runner.stage.borrow();
-                    for instance in stage.instances.iter() {
-                        iface.instance_position.update([instance.pos.x, instance.pos.y, instance.pos.z]);
-
-                        rdr_gate.render(&render_state, |mut tess_gate| {
-                            let (begin, end) = indices[instance.id as usize];
-                            tess_gate.render(tess.slice(begin..end));
-                        });
-                    }
-                });
-            });
-
-        surface.swap_buffers();
-    }
-}
-
-fn fill_vertices(sprite: Rc<RefCell<Sprite>>, vertices: &mut [Vertex; 6], x: f32, y: f32, z: f32) {
-    let mut fake_vertices = unsafe { std::mem::transmute::<&mut [Vertex; 6], &mut [FakeVertex; 4]>(vertices) };
-    let sprite = sprite.borrow();
-    sprite.fill_vertices(&mut fake_vertices, x, y, z);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/formats/Cargo.toml	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,16 @@
+[package]
+name = "touhou-formats"
+version = "0.1.0"
+authors = ["Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>"]
+edition = "2018"
+description = "Collection of parsers for Touhou formats"
+homepage = "https://pytouhou.linkmauve.fr"
+license = "GPL-3.0-or-later"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+nom = { version = "6", default-features = false, features = ["alloc"] }
+encoding_rs = "0.8"
+bitflags = "1"
+touhou-utils = "*"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/formats/src/bin/dump_ecl.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,62 @@
+use touhou_formats::th06::ecl::{Ecl, CallMain, CallSub, Rank};
+use std::env;
+use std::path::Path;
+use std::fs::File;
+use std::io::{self, BufReader, Read};
+
+pub fn load_file_into_vec<P: AsRef<Path>>(filename: P) -> io::Result<Vec<u8>> {
+    let file = File::open(filename)?;
+    let mut file = BufReader::new(file);
+    let mut buf = Vec::new();
+    file.read_to_end(&mut buf)?;
+    Ok(buf)
+}
+
+fn format_rank(rank: &Rank) -> String {
+    format!("{}{}{}{}", if rank.contains(Rank::EASY) { 'E' } else { ' ' },
+                        if rank.contains(Rank::NORMAL) { 'N' } else { ' ' },
+                        if rank.contains(Rank::HARD) { 'H' } else { ' ' },
+                        if rank.contains(Rank::LUNATIC) { 'L' } else { ' ' })
+}
+
+fn print_sub_instruction(call: &CallSub) {
+    let CallSub { time, rank_mask, param_mask: _, instr } = call;
+    println!("    {:>5}: {}: {:?}", time, format_rank(rank_mask), instr);
+}
+
+fn print_main_instruction(call: &CallMain) {
+    let CallMain { time, sub, instr } = call;
+    println!("    {:>5}: sub {:>2}: {:?}", time, sub, instr);
+}
+
+fn main() {
+    // Parse arguments.
+    let args: Vec<_> = env::args().collect();
+    if args.len() != 2 {
+        eprintln!("Usage: {} <ECL file>", args[0]);
+        return;
+    }
+    let ecl_filename = Path::new(&args[1]);
+
+    // Open the ECL file.
+    let buf = load_file_into_vec(ecl_filename).unwrap();
+    let (_, ecl) = Ecl::from_slice(&buf).unwrap();
+
+    for (i, main) in ecl.mains.iter().enumerate() {
+        println!("Main {} {{", i);
+        for call in main.instructions.iter() {
+            print_main_instruction(call);
+        }
+        println!("}}");
+        println!();
+    }
+
+    for (i, sub) in ecl.subs.iter().enumerate() {
+        println!("Sub {} {{", i);
+        for call in sub.instructions.iter() {
+            print_sub_instruction(call);
+        }
+        println!("}}");
+        println!();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/formats/src/lib.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,6 @@
+#![feature(concat_idents)]
+#![deny(missing_docs)]
+
+//! Touhou formats.
+
+pub mod th06;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/formats/src/th06/anm0.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,297 @@
+//! ANM0 animation format support.
+
+use nom::{
+    IResult,
+    bytes::complete::{tag, take_while_m_n},
+    number::complete::{le_u8, le_u16, le_u32, le_i32, le_f32},
+    sequence::tuple,
+    multi::{many_m_n, many0},
+};
+use std::collections::HashMap;
+
+/// Coordinates of a sprite into the image.
+#[derive(Debug, Clone)]
+pub struct Sprite {
+    /// Index inside the anm0.
+    pub index: u32,
+
+    /// X coordinate in the sprite sheet.
+    pub x: f32,
+
+    /// Y coordinate in the sprite sheet.
+    pub y: f32,
+
+    /// Width of the sprite.
+    pub width: f32,
+
+    /// Height of the sprite.
+    pub height: f32,
+}
+
+/// A single instruction, part of a `Script`.
+#[derive(Debug, Clone)]
+pub struct Call {
+    /// Time at which this instruction will be called.
+    pub time: u16,
+
+    /// The instruction to call.
+    pub instr: Instruction,
+}
+
+/// Script driving an animation.
+#[derive(Debug, Clone)]
+pub struct Script {
+    /// List of instructions in this script.
+    pub instructions: Vec<Call>,
+
+    /// List of interrupts in this script.
+    pub interrupts: HashMap<i32, u8>
+}
+
+/// Main struct of the ANM0 animation format.
+#[derive(Debug, Clone)]
+pub struct Anm0 {
+    /// Resolution of the image used by this ANM.
+    pub size: (u32, u32),
+
+    /// Format of this ANM.
+    pub format: u32,
+
+    /// File name of the main image.
+    pub png_filename: String,
+
+    /// File name of an alpha channel image.
+    pub alpha_filename: Option<String>,
+
+    /// A list of sprites, coordinates into the attached image.
+    pub sprites: Vec<Sprite>,
+
+    /// A map of scripts.
+    pub scripts: HashMap<u8, Script>,
+}
+
+impl Anm0 {
+    /// Parse a slice of bytes into an `Anm0` struct.
+    pub fn from_slice(data: &[u8]) -> IResult<&[u8], Vec<Anm0>> {
+        many0(parse_anm0)(data)
+    }
+
+    /// TODO
+    pub fn inv_size(&self) -> (f32, f32) {
+        let (x, y) = self.size;
+        (1. / x as f32, 1. / y as f32)
+    }
+}
+
+fn parse_name(i: &[u8]) -> IResult<&[u8], String> {
+    let (_, slice) = take_while_m_n(0, 32, |c| c != 0)(i)?;
+    let string = match String::from_utf8(slice.to_vec()) {
+        Ok(string) => string,
+        // XXX: use a more specific error instead.
+        Err(_) => return Err(nom::Err::Failure(nom::error::Error::new(i, nom::error::ErrorKind::Eof)))
+    };
+    Ok((i, string))
+}
+
+fn parse_sprite(i: &[u8]) -> IResult<&[u8], Sprite> {
+    let (i, (index, x, y, width, height)) = tuple((le_u32, le_f32, le_f32, le_f32, le_f32))(i)?;
+    Ok((i, Sprite {
+        index,
+        x,
+        y,
+        width,
+        height,
+    }))
+}
+
+macro_rules! declare_anm_instructions {
+    ($($opcode:tt => fn $name:ident($($arg:ident: $arg_type:ident),*)),*,) => {
+        /// Available instructions in an `Anm0`.
+        #[allow(missing_docs)]
+        #[derive(Debug, Clone, Copy)]
+        pub enum Instruction {
+            $(
+                $name($($arg_type),*)
+            ),*
+        }
+
+        fn parse_instruction_args(mut i: &[u8], opcode: u8) -> IResult<&[u8], Instruction> {
+            let instr = match opcode {
+                $(
+                    $opcode => {
+                        $(
+                            let (i2, $arg) = concat_idents!(le_, $arg_type)(i)?;
+                            i = i2;
+                        )*
+                        Instruction::$name($($arg),*)
+                    }
+                )*
+                // XXX: use a more specific error instead.
+                _ => return Err(nom::Err::Failure(nom::error::Error::new(i, nom::error::ErrorKind::Eof)))
+            };
+            Ok((i, instr))
+        }
+    };
+}
+
+declare_anm_instructions!{
+    0 => fn Delete(),
+    1 => fn LoadSprite(sprite_number: u32),
+    2 => fn SetScale(sx: f32, sy: f32),
+    3 => fn SetAlpha(alpha: u32),
+    4 => fn SetColor(red: u8, green: u8, blue: u8/*, XXX: x8*/),
+    5 => fn Jump(instruction: u32),
+    7 => fn ToggleMirrored(),
+    9 => fn SetRotations3d(x: f32, y: f32, z: f32),
+    10 => fn SetRotationsSpeed3d(x: f32, y: f32, z: f32),
+    11 => fn SetScaleSpeed(sx: f32, sy: f32),
+    12 => fn Fade(alpha: u32, duration: u32),
+    13 => fn SetBlendmodeAdd(),
+    14 => fn SetBlendmodeAlphablend(),
+    15 => fn KeepStill(),
+    16 => fn LoadRandomSprite(min_index: u32, amplitude: u32),
+    17 => fn Move(x: f32, y: f32, z: f32),
+    18 => fn MoveToLinear(x: f32, y: f32, z: f32, duration: u32),
+    19 => fn MoveToDecel(x: f32, y: f32, z: f32, duration: u32),
+    20 => fn MoveToAccel(x: f32, y: f32, z: f32, duration: u32),
+    21 => fn Wait(),
+    22 => fn InterruptLabel(label: i32),
+    23 => fn SetCornerRelativePlacement(),
+    24 => fn WaitEx(),
+    25 => fn SetAllowOffset(allow: u32), // TODO: better name
+    26 => fn SetAutomaticOrientation(automatic: u32),
+    27 => fn ShiftTextureX(dx: f32),
+    28 => fn ShiftTextureY(dy: f32),
+    29 => fn SetVisible(visible: u32),
+    30 => fn ScaleIn(sx: f32, sy: f32, duration: u32),
+    31 => fn Todo(todo: u32),
+}
+
+fn parse_anm0(input: &[u8]) -> IResult<&[u8], Anm0> {
+    let (i, (num_sprites, num_scripts, _, width, height, format, _unknown1,
+             first_name_offset, _unused, second_name_offset, version, _unknown2,
+             _texture_offset, has_data, _next_offset, unknown3)) =
+        tuple((le_u32, le_u32, tag(b"\0\0\0\0"), le_u32, le_u32, le_u32, le_u32, le_u32,
+               le_u32, le_u32, le_u32, le_u32, le_u32, le_u32, le_u32, le_u32))(input)?;
+
+    assert_eq!(version, 0);
+    assert_eq!(unknown3, 0);
+    assert_eq!(has_data, 0);
+    let num_sprites = num_sprites as usize;
+    let num_scripts = num_scripts as usize;
+
+    let (i, sprite_offsets) = many_m_n(num_sprites, num_sprites, le_u32)(i)?;
+    let (_, script_offsets) = many_m_n(num_scripts, num_scripts, tuple((le_u32, le_u32)))(i)?;
+
+    let png_filename = if first_name_offset > 0 {
+        if input.len() < first_name_offset as usize {
+            return Err(nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Eof)));
+        }
+        let i = &input[first_name_offset as usize..];
+        let (_, name) = parse_name(i)?;
+        name
+    } else {
+        String::new()
+    };
+
+    let alpha_filename = if second_name_offset > 0 {
+        if input.len() < second_name_offset as usize {
+            return Err(nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Eof)));
+        }
+        let i = &input[second_name_offset as usize..];
+        let (_, name) = parse_name(i)?;
+        Some(name)
+    } else {
+        None
+    };
+
+    let mut sprites = vec![];
+    let mut i = &input[..];
+    for offset in sprite_offsets.into_iter().map(|x| x as usize) {
+        if input.len() < offset {
+            return Err(nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Eof)));
+        }
+        i = &input[offset..];
+        let (_, sprite) = parse_sprite(i)?;
+        sprites.push(sprite);
+    }
+
+    let mut scripts = HashMap::new();
+    for (index, offset) in script_offsets.into_iter().map(|(index, offset)| (index as u8, offset as usize)) {
+        if input.len() < offset {
+            return Err(nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Eof)));
+        }
+        i = &input[offset..];
+        let mut instruction_offsets = vec![];
+
+        let mut instructions = vec![];
+        loop {
+            let tell = input.len() - i.len();
+            instruction_offsets.push(tell - offset);
+            // TODO: maybe check against the size of parsed data?
+            let (i2, (time, opcode, _size)) = tuple((le_u16, le_u8, le_u8))(i)?;
+            let (i2, instr) = parse_instruction_args(i2, opcode)?;
+            instructions.push(Call { time, instr });
+            i = i2;
+            if opcode == 0 {
+                break;
+            }
+        }
+        let mut interrupts = HashMap::new();
+        let mut j = 0;
+        for Call { time: _, instr } in &mut instructions {
+            match instr {
+                Instruction::Jump(ref mut offset) => {
+                    let result = instruction_offsets.binary_search(&(*offset as usize));
+                    match result {
+                        Ok(ptr) => *offset = ptr as u32,
+                        Err(ptr) => {
+                            // XXX: use a more specific error instead.
+                            return Err(nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Eof)));
+                            //println!("Instruction offset not found for pointer: {}", ptr);
+                        }
+                    }
+                }
+                Instruction::InterruptLabel(interrupt) => {
+                    interrupts.insert(*interrupt, j + 1);
+                }
+                _ => ()
+            }
+            j += 1;
+        }
+        scripts.insert(index, Script {
+            instructions,
+            interrupts,
+        });
+    }
+
+    let anm0 = Anm0 {
+        size: (width, height),
+        format,
+        png_filename,
+        alpha_filename,
+        sprites,
+        scripts,
+    };
+    Ok((i, anm0))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::{self, Read};
+    use std::fs::File;
+
+    #[test]
+    fn anm0() {
+        let file = File::open("EoSD/CM/player01.anm").unwrap();
+        let mut file = io::BufReader::new(file);
+        let mut buf = vec![];
+        file.read_to_end(&mut buf).unwrap();
+        let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
+        assert_eq!(anms.len(), 1);
+        let anm0 = anms.pop().unwrap();
+        assert_eq!(anm0.size, (256, 256));
+        assert_eq!(anm0.format, 5);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/formats/src/th06/ecl.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,428 @@
+//! ECL enemy script format support.
+
+use nom::{
+    IResult,
+    number::complete::{le_u8, le_u16, le_u32, le_i16, le_i32, le_f32},
+    sequence::tuple,
+    multi::{count, many0},
+    error::ErrorKind,
+    Err,
+};
+use encoding_rs::SHIFT_JIS;
+use bitflags::bitflags;
+
+bitflags! {
+    /// Bit flags describing the current difficulty level.
+    pub struct Rank: u16 {
+        /// Easy mode.
+        const EASY = 0x100;
+
+        /// Normal mode.
+        const NORMAL = 0x200;
+
+        /// Hard mode.
+        const HARD = 0x400;
+
+        /// Lunatic mode.
+        const LUNATIC = 0x800;
+
+        /// Any or all modes.
+        const ALL = 0xff00;
+    }
+}
+
+impl std::str::FromStr for Rank {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Rank, Self::Err> {
+        Ok(match s {
+            "easy" => Rank::EASY,
+            "normal" => Rank::NORMAL,
+            "hard" => Rank::HARD,
+            "lunatic" => Rank::LUNATIC,
+            _ => return Err(format!("unknown rank {}", s))
+        })
+    }
+}
+
+/// A single instruction, part of a `Script`.
+#[derive(Debug, Clone)]
+pub struct CallSub {
+    /// Time at which this instruction will be called.
+    pub time: i32,
+
+    /// The difficulty level(s) this instruction will be called at.
+    pub rank_mask: Rank,
+
+    /// TODO
+    pub param_mask: u16,
+
+    /// The instruction to call.
+    pub instr: SubInstruction,
+}
+
+impl CallSub {
+    /// Create a new instruction call.
+    pub fn new(time: i32, rank_mask: Rank, instr: SubInstruction) -> CallSub {
+        CallSub {
+            time,
+            rank_mask,
+            param_mask: 0,
+            instr,
+        }
+    }
+}
+
+/// Script driving an animation.
+#[derive(Debug, Clone)]
+pub struct Sub {
+    /// List of instructions in this script.
+    pub instructions: Vec<CallSub>,
+}
+
+/// A single instruction, part of a `Script`.
+#[derive(Debug, Clone)]
+pub struct CallMain {
+    /// Time at which this instruction will be called.
+    pub time: u16,
+
+    /// Subroutine to call for this enemy.
+    pub sub: u16,
+
+    /// The instruction to call.
+    pub instr: MainInstruction,
+}
+
+/// Script driving an animation.
+#[derive(Debug, Clone)]
+pub struct Main {
+    /// List of instructions in this script.
+    pub instructions: Vec<CallMain>,
+}
+
+/// Main struct of the ANM0 animation format.
+#[derive(Debug, Clone)]
+pub struct Ecl {
+    /// A list of subs.
+    pub subs: Vec<Sub>,
+
+    /// A list of mains.
+    pub mains: Vec<Main>,
+}
+
+impl Ecl {
+    /// Parse a slice of bytes into an `Ecl` struct.
+    pub fn from_slice(data: &[u8]) -> IResult<&[u8], Ecl> {
+        parse_ecl(data)
+    }
+}
+
+macro_rules! declare_main_instructions {
+    ($($opcode:tt => fn $name:ident($($arg:ident: $arg_type:ident),*)),*,) => {
+        /// Available instructions in an `Ecl`.
+        #[allow(missing_docs)]
+        #[derive(Debug, Clone, Copy)]
+        pub enum MainInstruction {
+            $(
+                $name($($arg_type),*)
+            ),*
+        }
+
+        fn parse_main_instruction_args(input: &[u8], opcode: u16) -> IResult<&[u8], MainInstruction> {
+            let mut i = &input[..];
+            let instr = match opcode {
+                $(
+                    $opcode => {
+                        $(
+                            let (i2, $arg) = concat_idents!(le_, $arg_type)(i)?;
+                            i = i2;
+                        )*
+                        MainInstruction::$name($($arg),*)
+                    }
+                )*
+                _ => unreachable!()
+            };
+            Ok((i, instr))
+        }
+    };
+}
+
+/// Parse a SHIFT_JIS byte string of length 34 into a String.
+#[allow(non_snake_case)]
+pub fn le_String(i: &[u8]) -> IResult<&[u8], String> {
+    let data = i.splitn(2, |c| *c == b'\0').nth(0).unwrap();
+    let (string, _encoding, _replaced) = SHIFT_JIS.decode(data);
+    Ok((&i[34..], string.into_owned()))
+}
+
+macro_rules! declare_sub_instructions {
+    ($($opcode:tt => fn $name:ident($($arg:ident: $arg_type:ident),*)),*,) => {
+        /// Available instructions in an `Ecl`.
+        #[allow(missing_docs)]
+        #[derive(Debug, Clone)]
+        pub enum SubInstruction {
+            $(
+                $name($($arg_type),*)
+            ),*
+        }
+
+        fn parse_sub_instruction_args(input: &[u8], opcode: u16) -> IResult<&[u8], SubInstruction> {
+            let mut i = &input[..];
+            let instr = match opcode {
+                $(
+                    $opcode => {
+                        $(
+                            let (i2, $arg) = concat_idents!(le_, $arg_type)(i)?;
+                            i = i2;
+                        )*
+                        SubInstruction::$name($($arg),*)
+                    }
+                )*
+                _ => unreachable!()
+            };
+            Ok((i, instr))
+        }
+    };
+}
+
+declare_main_instructions!{
+    0 => fn SpawnEnemy(x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: u32),
+    2 => fn SpawnEnemyMirrored(x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: u32),
+    4 => fn SpawnEnemyRandom(x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: u32),
+    6 => fn SpawnEnemyMirroredRandom(x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: u32),
+    8 => fn CallMessage(),
+    9 => fn WaitMessage(),
+    10 => fn ResumeEcl(x: f32, y: f32),
+    12 => fn WaitForBossDeath(),
+}
+
+declare_sub_instructions!{
+    0 => fn Noop(),
+    1 => fn Destroy(unused: u32),
+    2 => fn RelativeJump(frame: i32, ip: i32),
+    3 => fn RelativeJumpEx(frame: i32, ip: i32, variable_id: i32),
+    4 => fn SetInt(var: i32, value: i32),
+    5 => fn SetFloat(var: i32, value: f32),
+    6 => fn SetRandomInt(var: i32, max: i32),
+    7 => fn SetRandomIntMin(var: i32, max: i32, min: i32),
+    8 => fn SetRandomFloat(var: i32, max: f32),
+    9 => fn SetRandomFloatMin(var: i32, amplitude: f32, min: f32),
+    10 => fn StoreX(var: i32),
+    11 => fn StoreY(var: i32),
+    12 => fn StoreZ(var: i32),
+    13 => fn AddInt(var: i32, a: i32, b: i32),
+    14 => fn SubstractInt(var: i32, a: i32, b: i32),
+    15 => fn MultiplyInt(var: i32, a: i32, b: i32),
+    16 => fn DivideInt(var: i32, a: i32, b: i32),
+    17 => fn ModuloInt(var: i32, a: i32, b: i32),
+    18 => fn Increment(var: i32),
+    19 => fn Decrement(var: i32),
+    20 => fn AddFloat(var: i32, a: f32, b: f32),
+    21 => fn SubstractFloat(var: i32, a: f32, b: f32),
+    22 => fn MultiplyFloat(var: i32, a: f32, b: f32),
+    23 => fn DivideFloat(var: i32, a: f32, b: f32),
+    24 => fn ModuloFloat(var: i32, a: f32, b: f32),
+    25 => fn GetDirection(var: i32, x1: f32, y1: f32, x2: f32, y2: f32),
+    26 => fn FloatToUnitCircle(var: i32),
+    27 => fn CompareInts(a: i32, b: i32),
+    28 => fn CompareFloats(a: f32, b: f32),
+    29 => fn RelativeJumpIfLowerThan(frame: i32, ip: i32),
+    30 => fn RelativeJumpIfLowerOrEqual(frame: i32, ip: i32),
+    31 => fn RelativeJumpIfEqual(frame: i32, ip: i32),
+    32 => fn RelativeJumpIfGreaterThan(frame: i32, ip: i32),
+    33 => fn RelativeJumpIfGreaterOrEqual(frame: i32, ip: i32),
+    34 => fn RelativeJumpIfNotEqual(frame: i32, ip: i32),
+    35 => fn Call(sub: i32, param1: i32, param2: f32),
+    36 => fn Return(),
+    37 => fn CallIfSuperior(sub: i32, param1: i32, param2: f32, a: i32, b: i32),
+    38 => fn CallIfSuperiorOrEqual(sub: i32, param1: i32, param2: f32, a: i32, b: i32),
+    39 => fn CallIfEqual(sub: i32, param1: i32, param2: f32, a: i32, b: i32),
+    40 => fn CallIfInferior(sub: i32, param1: i32, param2: f32, a: i32, b: i32),
+    41 => fn CallIfInferiorOrEqual(sub: i32, param1: i32, param2: f32, a: i32, b: i32),
+    42 => fn CallIfNotEqual(sub: i32, param1: i32, param2: f32, a: i32, b: i32),
+    43 => fn SetPosition(x: f32, y: f32, z: f32),
+    45 => fn SetAngleAndSpeed(angle: f32, speed: f32),
+    46 => fn SetRotationSpeed(speed: f32),
+    47 => fn SetSpeed(speed: f32),
+    48 => fn SetAcceleration(acceleration: f32),
+    49 => fn SetRandomAngle(min: f32, max: f32),
+    50 => fn SetRandomAngleEx(min: f32, max: f32),
+    51 => fn TargetPlayer(angle: f32, speed: f32),
+    52 => fn MoveInDecel(duration: i32, angle: f32, speed: f32),
+    56 => fn MoveToLinear(duration: i32, x: f32, y: f32, z: f32),
+    57 => fn MoveToDecel(duration: i32, x: f32, y: f32, z: f32),
+    59 => fn MoveToAccel(duration: i32, x: f32, y: f32, z: f32),
+    61 => fn StopIn(duration: i32),
+    63 => fn StopInAccel(duration: i32),
+    65 => fn SetScreenBox(xmin: f32, ymin: f32, xmax: f32, ymax: f32),
+    66 => fn ClearScreenBox(),
+    67 => fn SetBulletAttributes1(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
+    68 => fn SetBulletAttributes2(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
+    69 => fn SetBulletAttributes3(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
+    70 => fn SetBulletAttributes4(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
+    71 => fn SetBulletAttributes5(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
+    74 => fn SetBulletAttributes6(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
+    75 => fn SetBulletAttributes7(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
+    76 => fn SetBulletInterval(interval: i32),
+    77 => fn SetBulletIntervalEx(interval: i32),
+    78 => fn DelayAttack(),
+    79 => fn NoDelayAttack(),
+    81 => fn SetBulletLaunchOffset(x: f32, y: f32, z: f32),
+    82 => fn SetExtendedBulletAttributes(a: i32, b: i32, c: i32, d: i32, e: f32, f: f32, g: f32, h: f32),
+    83 => fn ChangeBulletsInStarBonus(),
+    // TODO: Found in stage 4 onward.
+    84 => fn SetBulletSound(sound: i32),
+    85 => fn NewLaser(laser_type: i16, sprite_idx_offset: i16, angle: f32, speed: f32, start_offset: f32, end_offset: f32, max_length: f32, width: f32, start_duration: i32, duration: i32, end_duration: i32, grazing_delay: i32, grazing_extra_duration: i32, UNK1: i32),
+    86 => fn NewLaserTowardsPlayer(laser_type: i16, sprite_idx_offset: i16, angle: f32, speed: f32, start_offset: f32, end_offset: f32, max_length: f32, width: f32, start_duration: i32, duration: i32, end_duration: i32, grazing_delay: i32, grazing_extra_duration: i32, UNK1: i32),
+    87 => fn SetUpcomingLaserId(id: u32),
+    88 => fn AlterLaserAngle(id: u32, delta: f32),
+    90 => fn RepositionLaser(id: u32, ox: f32, oy: f32, oz: f32),
+    91 => fn LaserSetCompare(id: u32),
+    92 => fn CancelLaser(id: u32),
+    93 => fn SetSpellcard(face: i16, number: i16, name: String),
+    94 => fn EndSpellcard(),
+    95 => fn SpawnEnemy(sub: i32, x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: i32),
+    96 => fn KillAllEnemies(),
+    97 => fn SetAnim(script: i32),
+    98 => fn SetMultipleAnims(default: i16, end_left: i16, end_right: i16, left: i16, right: i16, _unused: i16),
+    99 => fn SetAuxAnm(number: i32, script: i32),
+    100 => fn SetDeathAnim(sprite_index: i32),
+    101 => fn SetBossMode(value: i32),
+    102 => fn CreateSquares(UNK1: i32, UNK2: f32, UNK3: f32, UNK4: f32, UNK5: f32),
+    103 => fn SetHitbox(width: f32, height: f32, depth: f32),
+    104 => fn SetCollidable(collidable: i32),
+    105 => fn SetDamageable(damageable: i32),
+    106 => fn PlaySound(index: i32),
+    107 => fn SetDeathFlags(death_flags: u32),
+    108 => fn SetDeathCallback(sub: i32),
+    109 => fn MemoryWriteInt(value: i32, index: i32),
+    111 => fn SetLife(life: i32),
+    112 => fn SetElapsedTime(frame: i32),
+    113 => fn SetLowLifeTrigger(trigger: i32),
+    114 => fn SetLowLifeCallback(sub: i32),
+    115 => fn SetTimeout(timeout: i32),
+    116 => fn SetTimeoutCallback(sub: i32),
+    117 => fn SetTouchable(touchable: i32),
+    118 => fn DropParticles(anim: i32, number: u32, r: u8, g: u8, b: u8, a: u8),
+    119 => fn DropBonus(number: i32),
+    120 => fn SetAutomaticOrientation(automatic: i32),
+    121 => fn CallSpecialFunction(function: i32, argument: i32),
+    122 => fn SetSpecialFunctionCallback(function: i32),
+    123 => fn SkipFrames(frames: i32),
+    124 => fn DropSpecificBonus(type_: i32),
+    // TODO: Found in stage 3.
+    125 => fn UNK_ins125(),
+    126 => fn SetRemainingLives(lives: i32),
+    // TODO: Found in stage 4.
+    127 => fn UNK_ins127(UNK1: i32),
+    128 => fn Interrupt(event: i32),
+    129 => fn InterruptAux(number: i32, event: i32),
+    // TODO: Found in stage 4.
+    130 => fn UNK_ins130(UNK1: i32),
+    131 => fn SetDifficultyCoeffs(speed_a: f32, speed_b: f32, nb_a: i32, nb_b: i32, shots_a: i32, shots_b: i32),
+    132 => fn SetInvisible(invisible: i32),
+    133 => fn CopyCallbacks(),
+    // TODO: Found in stage 4.
+    134 => fn UNK_ins134(),
+    135 => fn EnableSpellcardBonus(UNK1: i32),
+}
+
+fn parse_sub_instruction(input: &[u8]) -> IResult<&[u8], CallSub> {
+    let i = &input[..];
+    let (i, (time, opcode)) = tuple((le_i32, le_u16))(i)?;
+    if time == -1 || opcode == 0xffff {
+        return Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof)));
+    }
+
+    let (i, (size, rank_mask, param_mask)) = tuple((le_u16, le_u16, le_u16))(i)?;
+    let rank_mask = Rank::from_bits(rank_mask).unwrap();
+    let (i, instr) = parse_sub_instruction_args(i, opcode)?;
+    assert_eq!(input.len() - i.len(), size as usize);
+    let call = CallSub { time, rank_mask, param_mask, instr };
+    Ok((i, call))
+}
+
+fn parse_sub(i: &[u8]) -> IResult<&[u8], Sub> {
+    let (i, instructions) = many0(parse_sub_instruction)(i)?;
+    let sub = Sub { instructions };
+    Ok((i, sub))
+}
+
+fn parse_main_instruction(input: &[u8]) -> IResult<&[u8], CallMain> {
+    let i = &input[..];
+    let (i, (time, sub)) = tuple((le_u16, le_u16))(i)?;
+    if time == 0xffff && sub == 4 {
+        return Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof)));
+    }
+
+    let (i, (opcode, size)) = tuple((le_u16, le_u16))(i)?;
+    let size = size as usize;
+    let (i, instr) = parse_main_instruction_args(i, opcode)?;
+    assert_eq!(input.len() - i.len(), size as usize);
+    let call = CallMain { time, sub, instr };
+    Ok((i, call))
+}
+
+fn parse_main(i: &[u8]) -> IResult<&[u8], Main> {
+    let (i, instructions) = many0(parse_main_instruction)(i)?;
+    let main = Main { instructions };
+    Ok((i, main))
+}
+
+fn parse_ecl(input: &[u8]) -> IResult<&[u8], Ecl> {
+    let i = input;
+
+    let (i, (sub_count, main_count)) = tuple((le_u16, le_u16))(i)?;
+    let sub_count = sub_count as usize;
+
+    if main_count != 0 {
+        // TODO: use a better error.
+        return Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof)));
+    }
+
+    let (_, (main_offsets, sub_offsets)) = tuple((
+        count(le_u32, 3),
+        count(le_u32, sub_count),
+    ))(i)?;
+
+    // Read all subs.
+    let mut subs = Vec::new();
+    for offset in sub_offsets.into_iter().map(|offset| offset as usize) {
+        let (_, sub) = parse_sub(&input[offset..])?;
+        subs.push(sub);
+    }
+
+    // Read all mains (always a single one atm).
+    let mut mains = Vec::new();
+    for offset in main_offsets.into_iter().map(|offset| offset as usize) {
+        if offset == 0 {
+            break;
+        }
+        let (_, main) = parse_main(&input[offset..])?;
+        mains.push(main);
+    }
+
+    let ecl = Ecl {
+        subs,
+        mains,
+    };
+    Ok((b"", ecl))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::{self, Read};
+    use std::fs::File;
+
+    #[test]
+    fn ecl() {
+        let file = File::open("EoSD/ST/ecldata1.ecl").unwrap();
+        let mut file = io::BufReader::new(file);
+        let mut buf = vec![];
+        file.read_to_end(&mut buf).unwrap();
+        let (_, ecl) = Ecl::from_slice(&buf).unwrap();
+        assert_eq!(ecl.subs.len(), 24);
+        assert_eq!(ecl.mains.len(), 1);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/formats/src/th06/mod.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,6 @@
+//! Touhou 6: EoSD implementation.
+
+pub mod pbg3;
+pub mod anm0;
+pub mod ecl;
+pub mod std;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/formats/src/th06/pbg3.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,199 @@
+//! PBG3 archive files handling.
+//!
+//! This module provides classes for handling the PBG3 file format.
+//! The PBG3 format is the archive format used by Touhou 6: EoSD.
+//!
+//! PBG3 files are merely a bitstream composed of a header, a file
+//! table, and LZSS-compressed files.
+
+use touhou_utils::bitstream::BitStream;
+use touhou_utils::lzss;
+use std::fs::File;
+use std::io;
+use std::collections::hash_map::{self, HashMap};
+use std::path::Path;
+
+/// Helper struct to handle strings and integers in PBG3 bitstreams.
+pub struct PBG3BitStream<R: io::Read + io::Seek> {
+    bitstream: BitStream<R>,
+}
+
+impl<R: io::Read + io::Seek> PBG3BitStream<R> {
+    /// Create a bitstream capable of reading u32 and strings.
+    pub fn new(bitstream: BitStream<R>) -> PBG3BitStream<R> {
+        PBG3BitStream {
+            bitstream,
+        }
+    }
+
+    /// Seek inside the bitstream, ditching any unused data read.
+    pub fn seek(&mut self, seek_from: io::SeekFrom) -> io::Result<u64> {
+        self.bitstream.seek(seek_from)
+    }
+
+    /// Return the current position in the stream.
+    pub fn tell(&mut self) -> io::Result<u64> {
+        self.bitstream.seek(io::SeekFrom::Current(0))
+    }
+
+    /// Read a given amount of bits.
+    pub fn read(&mut self, nb_bits: usize) -> io::Result<usize> {
+        self.bitstream.read(nb_bits)
+    }
+
+    /// Read a given amount of bytes.
+    pub fn read_bytes(&mut self, nb_bytes: usize) -> io::Result<Vec<u8>> {
+        self.bitstream.read_bytes(nb_bytes)
+    }
+
+    /// Read an integer from the bitstream.
+    ///
+    /// Integers have variable sizes. They begin with a two-bit value indicating
+    /// the number of (non-aligned) bytes to read.
+    pub fn read_u32(&mut self) -> io::Result<u32> {
+        let size = self.read(2)?;
+        Ok(self.read((size + 1) * 8)? as u32)
+    }
+
+    /// Read a string from the bitstream.
+    ///
+    /// Strings are stored as NULL-terminated sequences of bytes.
+    /// The only catch is that they are not byte-aligned.
+    pub fn read_string(&mut self, mut max_size: usize) -> io::Result<Vec<u8>> {
+        let mut buf = Vec::new();
+        while max_size > 0 {
+            let byte = self.read(8)? as u8;
+            if byte == 0 {
+                break;
+            }
+            buf.push(byte);
+            max_size -= 1;
+        }
+        Ok(buf)
+    }
+}
+
+type Entry = (u32, u32, u32, u32, u32);
+
+/// Handle PBG3 archive files.
+///
+/// PBG3 is a file archive format used in Touhou 6: EoSD.
+/// This class provides a representation of such files, as well as functions to
+/// read and extract files from a PBG3 archive.
+pub struct PBG3<R: io::Read + io::Seek> {
+    /// List of PBG3Entry objects describing files present in the archive.
+    entries: HashMap<String, Entry>,
+
+    /// PBG3BitStream struct.
+    bitstream: PBG3BitStream<R>,
+}
+
+impl<R: io::Read + io::Seek> PBG3<R> {
+    /// Create a PBG3 archive.
+    fn new(entries: HashMap<String, Entry>, bitstream: PBG3BitStream<R>) -> PBG3<R> {
+        PBG3 {
+            entries,
+            bitstream,
+        }
+    }
+
+    /// Open a PBG3 archive.
+    pub fn from_file(mut file: R) -> io::Result<PBG3<R>> {
+        let mut magic = [0; 4];
+        file.read(&mut magic)?;
+        if &magic != b"PBG3" {
+            return Err(io::Error::new(io::ErrorKind::Other, "Wrong magic!"));
+        }
+
+        let bitstream = BitStream::new(file);
+        let mut bitstream = PBG3BitStream::new(bitstream);
+        let mut entries = HashMap::new();
+
+        let nb_entries = bitstream.read_u32()?;
+        let offset = bitstream.read_u32()?;
+        bitstream.seek(io::SeekFrom::Start(offset as u64))?;
+
+        for _ in 0..nb_entries {
+            let unknown_1 = bitstream.read_u32()?;
+            let unknown_2 = bitstream.read_u32()?;
+            let checksum = bitstream.read_u32()?; // Checksum of *compressed data*
+            let offset = bitstream.read_u32()?;
+            let size = bitstream.read_u32()?;
+            let name = bitstream.read_string(255)?;
+            // XXX: no unwrap!
+            let name = String::from_utf8(name).unwrap();
+            entries.insert(name, (unknown_1, unknown_2, checksum, offset, size));
+        }
+
+        Ok(PBG3::new(entries, bitstream))
+    }
+
+    /// List all file entries in this PBG3 archive.
+    pub fn list_files(&self) -> hash_map::Keys<String, Entry> {
+        self.entries.keys()
+    }
+
+    /// Read a single file from this PBG3 archive.
+    pub fn get_file(&mut self, filename: &str, check: bool) -> io::Result<Vec<u8>> {
+        // XXX: no unwrap!
+        let (_unknown_1, _unknown_2, checksum, offset, size) = self.entries.get(filename).unwrap();
+        self.bitstream.seek(io::SeekFrom::Start(*offset as u64))?;
+        let data = lzss::decompress(&mut self.bitstream.bitstream, *size as usize, 0x2000, 13, 4, 3)?;
+        if check {
+            // Verify the checksum.
+            let compressed_size = self.bitstream.tell()? as u32 - *offset;
+            self.bitstream.seek(io::SeekFrom::Start(*offset as u64))?;
+            let mut value: u32 = 0;
+            for c in self.bitstream.read_bytes(compressed_size as usize)? {
+                value += c as u32;
+                value &= 0xffffffff;
+            }
+            if value != *checksum {
+                return Err(io::Error::new(io::ErrorKind::Other, "Corrupted data!"));
+            }
+        }
+        Ok(data)
+    }
+}
+
+/// Open a PBG3 archive from its path.
+pub fn from_path_buffered<P: AsRef<Path>>(path: P) -> io::Result<PBG3<io::BufReader<File>>> {
+    let file = File::open(path)?;
+    let buf_file = io::BufReader::new(file);
+    PBG3::from_file(buf_file)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::util::SeekableSlice;
+    use std::fs::File;
+
+    #[test]
+    fn bitstream() {
+        let data = SeekableSlice::new(b"Hello world!\0");
+        let bitstream = BitStream::new(data);
+        let mut pbg3 = PBG3BitStream::new(bitstream);
+        assert_eq!(pbg3.read_string(42).unwrap(), b"Hello world!");
+    }
+
+    #[test]
+    fn file_present() {
+        let file = File::open("EoSD/MD.DAT").unwrap();
+        let file = io::BufReader::new(file);
+        let pbg3 = PBG3::from_file(file).unwrap();
+        let files = pbg3.list_files().cloned().collect::<Vec<String>>();
+        assert!(files.contains(&String::from("th06_01.pos")));
+    }
+
+    #[test]
+    fn check_all_files() {
+        let file = File::open("EoSD/MD.DAT").unwrap();
+        let file = io::BufReader::new(file);
+        let mut pbg3 = PBG3::from_file(file).unwrap();
+        let files = pbg3.list_files().cloned().collect::<Vec<String>>();
+        for filename in files {
+            pbg3.get_file(filename, true).unwrap();
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/formats/src/th06/std.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,271 @@
+//! STD background format support.
+
+use nom::{
+    IResult,
+    bytes::complete::tag,
+    number::complete::{le_u8, le_u16, le_u32, le_i32, le_f32},
+    sequence::tuple,
+    combinator::map,
+    multi::{many0, count},
+    error::ErrorKind,
+    Err,
+};
+use encoding_rs::SHIFT_JIS;
+
+/// A float position in the 3D space.
+#[derive(Debug, Clone)]
+pub struct Position {
+    /// X component.
+    pub x: f32,
+
+    /// Y component.
+    pub y: f32,
+
+    /// Z component.
+    pub z: f32,
+}
+
+/// A 3D box around something.
+#[derive(Debug, Clone)]
+struct Box3D {
+    width: f32,
+    height: f32,
+    depth: f32,
+}
+
+/// A 2D box around something.
+#[derive(Debug, Clone)]
+pub struct Box2D {
+    /// Width.
+    pub width: f32,
+
+    /// Height.
+    pub height: f32,
+}
+
+/// A quad in the 3D space.
+#[derive(Debug, Clone)]
+pub struct Quad {
+    /// The anm script to run for this quad.
+    pub anm_script: u16,
+
+    /// The position of this quad in the 3D space.
+    pub pos: Position,
+
+    /// The size of this quad.
+    pub size_override: Box2D,
+}
+
+/// A model formed of multiple quads in space.
+#[derive(Debug, Clone)]
+pub struct Model {
+    /// TODO: find what that is.
+    pub unknown: u16,
+
+    /// The bounding box around this model.
+    pub bounding_box: [f32; 6],
+
+    /// The quads composing this model.
+    pub quads: Vec<Quad>,
+}
+
+/// An instance of a model.
+#[derive(Debug, Clone)]
+pub struct Instance {
+    /// The instance identifier.
+    pub id: u16,
+
+    /// Where to position the instance of this model.
+    pub pos: Position,
+}
+
+/// A single instruction, part of a `Script`.
+#[derive(Debug, Clone)]
+pub struct Call {
+    /// Time at which this instruction will be called.
+    pub time: u32,
+
+    /// The instruction to call.
+    pub instr: Instruction,
+}
+
+/// Parse a SHIFT_JIS byte string of length 128 into a String.
+#[allow(non_snake_case)]
+pub fn le_String(i: &[u8]) -> IResult<&[u8], String> {
+    let data = i.splitn(2, |c| *c == b'\0').nth(0).unwrap();
+    let (string, _encoding, _replaced) = SHIFT_JIS.decode(data);
+    Ok((&i[128..], string.into_owned()))
+}
+
+/// Main struct of the STD stage format.
+#[derive(Debug, Clone)]
+pub struct Stage {
+    /// The name of the stage.
+    pub name: String,
+
+    /// A list of (name, path) of background music.
+    // TODO: there are maximum four of them, and in practice never more than 2.
+    pub musics: Vec<Option<(String, String)>>,
+
+    /// List of models.
+    pub models: Vec<Model>,
+
+    /// List of instances.
+    pub instances: Vec<Instance>,
+
+    /// List of instructions in the script.
+    pub script: Vec<Call>,
+}
+
+impl Stage {
+    /// Parse a slice of bytes into an `Stage` struct.
+    pub fn from_slice(data: &[u8]) -> IResult<&[u8], Stage> {
+        parse_stage(data)
+    }
+}
+
+macro_rules! declare_stage_instructions {
+    ($($opcode:tt => fn $name:ident($($arg:ident: $arg_type:ident),*)),*,) => {
+        /// Available instructions in an `Stage`.
+        #[allow(missing_docs)]
+        #[derive(Debug, Clone, Copy)]
+        pub enum Instruction {
+            $(
+                $name($($arg_type),*)
+            ),*
+        }
+
+        fn parse_instruction_args(input: &[u8], opcode: u16) -> IResult<&[u8], Instruction> {
+            let mut i = &input[..];
+            let instr = match opcode {
+                $(
+                    $opcode => {
+                        $(
+                            let (i2, $arg) = concat_idents!(le_, $arg_type)(i)?;
+                            i = i2;
+                        )*
+                        Instruction::$name($($arg),*)
+                    }
+                )*
+                _ => unreachable!()
+            };
+            Ok((i, instr))
+        }
+    };
+}
+
+declare_stage_instructions!{
+    0 => fn SetViewpos(x: f32, y: f32, z: f32),
+    1 => fn SetFog(r: u8, g: u8, b: u8, a: u8, near: f32, far: f32),
+    2 => fn SetViewpos2(x: f32, y: f32, z: f32),
+    3 => fn StartInterpolatingViewpos2(frame: u32, _unused: i32, _unused: i32),
+    4 => fn StartInterpolatingFog(frame: u32, _unused: i32, _unused: i32),
+    5 => fn Unknown(_unused: i32, _unused: i32, _unused: i32),
+}
+
+fn parse_quad(i: &[u8]) -> IResult<&[u8], Quad> {
+    let (i, (unk1, size)) = tuple((le_u16, le_u16))(i)?;
+    if unk1 == 0xffff {
+        return Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof)));
+    }
+    // TODO: replace this assert with a custom error.
+    assert_eq!(size, 0x1c);
+    let (i, (anm_script, _, x, y, z, width, height)) = tuple((le_u16, tag(b"\0\0"), le_f32, le_f32, le_f32, le_f32, le_f32))(i)?;
+    let quad = Quad {
+        anm_script,
+        pos: Position { x, y, z },
+        size_override: Box2D { width, height },
+    };
+    Ok((i, quad))
+}
+
+fn parse_model(i: &[u8]) -> IResult<&[u8], Model> {
+    let (i, (_id, unknown, x, y, z, width, height, depth, quads)) = tuple((le_u16, le_u16, le_f32, le_f32, le_f32, le_f32, le_f32, le_f32, many0(parse_quad)))(i)?;
+    let bounding_box = [x, y, z, width, height, depth];
+    let model = Model {
+        unknown,
+        bounding_box,
+        quads,
+    };
+    Ok((i, model))
+}
+
+fn parse_instance(i: &[u8]) -> IResult<&[u8], Instance> {
+    let (i, (id, unknown, x, y, z)) = tuple((le_u16, le_u16, le_f32, le_f32, le_f32))(i)?;
+    if id == 0xffff && unknown == 0xffff {
+        return Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof)));
+    }
+    // TODO: replace this assert with a custom error.
+    assert_eq!(unknown, 0x100);
+    let instance = Instance {
+        id,
+        pos: Position { x, y, z },
+    };
+    Ok((i, instance))
+}
+
+fn parse_instruction(i: &[u8]) -> IResult<&[u8], Call> {
+    let (i, (time, opcode, size)) = tuple((le_u32, le_u16, le_u16))(i)?;
+    if time == 0xffffffff && opcode == 0xffff && size == 0xffff {
+        return Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof)));
+    }
+    // TODO: replace this assert with a custom error.
+    assert_eq!(size, 12);
+    let (i, instr) = parse_instruction_args(i, opcode)?;
+    println!("{} {:?}", time, instr);
+    let call = Call { time, instr };
+    Ok((i, call))
+}
+
+fn parse_stage(input: &[u8]) -> IResult<&[u8], Stage> {
+    let i = &input[..];
+
+    let (i, (num_models, _num_faces, object_instances_offset, script_offset, _, name, music_names, music_paths)) = tuple((
+        le_u16, le_u16, le_u32, le_u32, tag(b"\0\0\0\0"),
+        le_String,
+        map(tuple((le_String, le_String, le_String, le_String)), |(a, b, c, d)| [a, b, c, d]),
+        map(tuple((le_String, le_String, le_String, le_String)), |(a, b, c, d)| [a, b, c, d])
+    ))(i)?;
+    let musics = music_names.iter().zip(&music_paths).map(|(name, path)| if name == " " { None } else { Some((name.clone(), path.clone())) }).collect();
+
+    let (_, offsets) = count(le_u32, num_models as usize)(i)?;
+
+    let mut models = vec![];
+    for offset in offsets {
+        let (_, model) = parse_model(&input[offset as usize..])?;
+        models.push(model);
+    }
+
+    let (_, instances) = many0(parse_instance)(&input[object_instances_offset as usize..])?;
+    let (_, script) = many0(parse_instruction)(&input[script_offset as usize..])?;
+
+    let stage = Stage {
+        name,
+        musics,
+        models,
+        instances,
+        script,
+    };
+    Ok((b"", stage))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::{self, Read};
+    use std::fs::File;
+
+    #[test]
+    fn std() {
+        let file = File::open("EoSD/ST/stage1.std").unwrap();
+        let mut file = io::BufReader::new(file);
+        let mut buf = vec![];
+        file.read_to_end(&mut buf).unwrap();
+        let (_, stage) = Stage::from_slice(&buf).unwrap();
+        assert_eq!(stage.name, "夢幻夜行絵巻 ~ Mystic Flier");
+        assert_eq!(stage.musics.len(), 4);
+        assert_eq!(stage.models.len(), 13);
+        assert_eq!(stage.instances.len(), 90);
+        assert_eq!(stage.script.len(), 21);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interpreters/Cargo.toml	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,14 @@
+[package]
+name = "touhou-interpreters"
+version = "0.1.0"
+authors = ["Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>"]
+edition = "2018"
+description = "Interpreters for Touhou games"
+homepage = "https://pytouhou.linkmauve.fr"
+license = "GPL-3.0-or-later"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+touhou-formats = "*"
+touhou-utils = "*"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interpreters/src/lib.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,6 @@
+#![deny(missing_docs)]
+#![feature(concat_idents)]
+
+//! Crate implementing interpreters for various Touhou formats.
+
+pub mod th06;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interpreters/src/th06/anm0.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,506 @@
+//! Animation runner.
+
+use touhou_formats::th06::anm0::{
+    Script,
+    Anm0,
+    Call,
+    Instruction,
+};
+use crate::th06::interpolator::{Interpolator1, Interpolator2, Interpolator3, Formula};
+use touhou_utils::math::Mat4;
+use touhou_utils::prng::Prng;
+use std::cell::RefCell;
+use std::rc::{Rc, Weak};
+
+/// TODO
+#[repr(C)]
+#[derive(Debug)]
+pub struct Vertex {
+    /// XXX
+    pub pos: [i16; 3],
+    /// XXX
+    pub layer: u16,
+    /// XXX
+    pub uv: [f32; 2],
+    /// XXX
+    pub color: [u8; 4],
+}
+
+/// Base visual element.
+#[derive(Debug, Clone, Default)]
+pub struct Sprite {
+    blendfunc: u32,
+    frame: u32,
+
+    width_override: f32,
+    height_override: f32,
+    angle: f32,
+
+    removed: bool,
+    changed: bool,
+    visible: bool,
+    force_rotation: bool,
+    automatic_orientation: bool,
+    allow_dest_offset: bool,
+    mirrored: bool,
+    corner_relative_placement: bool,
+
+    scale_interpolator: Option<Interpolator2<f32>>,
+    fade_interpolator: Option<Interpolator1<f32>>, // XXX: should be u8!
+    offset_interpolator: Option<Interpolator3<f32>>,
+    rotation_interpolator: Option<Interpolator3<f32>>,
+    color_interpolator: Option<Interpolator3<f32>>, // XXX: should be u8!
+
+    anm: Option<Anm0>,
+
+    dest_offset: [f32; 3],
+    texcoords: [f32; 4],
+    texoffsets: [f32; 2],
+    rescale: [f32; 2],
+    scale_speed: [f32; 2],
+    rotations_3d: [f32; 3],
+    rotations_speed_3d: [f32; 3],
+    color: [u8; 4],
+    layer: u16,
+}
+
+impl Sprite {
+    /// Create a new sprite.
+    pub fn new() -> Sprite {
+        Sprite {
+            changed: true,
+            visible: true,
+            rescale: [1., 1.],
+            color: [255, 255, 255, 255],
+            ..Default::default()
+        }
+    }
+
+    /// Create a new sprite overriding its size.
+    pub fn with_size(width_override: f32, height_override: f32) -> Sprite {
+        Sprite {
+            width_override,
+            height_override,
+            changed: true,
+            visible: true,
+            rescale: [1., 1.],
+            color: [255, 255, 255, 255],
+            ..Default::default()
+        }
+    }
+
+    /// TODO
+    pub fn fill_vertices(&self, vertices: &mut [Vertex; 4], x: f32, y: f32, z: f32) {
+        let mut mat = Mat4::new([[-0.5, 0.5, 0.5, -0.5],
+                                 [-0.5, -0.5, 0.5, 0.5],
+                                 [0., 0., 0., 0.],
+                                 [1., 1., 1., 1.]]);
+
+        let [tx, ty, tw, th] = self.texcoords;
+        let [sx, sy] = self.rescale;
+        let width = if self.width_override > 0. { self.width_override } else { tw * sx };
+        let height = if self.height_override > 0. { self.height_override } else { th * sy };
+
+        mat.scale2d(width, height);
+        if self.mirrored {
+            mat.flip();
+        }
+
+        let [rx, ry, mut rz] = self.rotations_3d;
+        if self.automatic_orientation {
+            rz += std::f32::consts::PI / 2. - self.angle;
+        } else if self.force_rotation {
+            rz += self.angle;
+        }
+
+        if rx != 0. {
+            mat.rotate_x(-rx);
+        }
+        if ry != 0. {
+            mat.rotate_y(ry);
+        }
+        if rz != 0. {
+            mat.rotate_z(-rz);
+        }
+
+        if self.allow_dest_offset {
+            mat.translate(self.dest_offset);
+        }
+        if self.corner_relative_placement {
+            mat.translate_2d(width / 2., height / 2.);
+        }
+
+        mat.translate([x, y, z]);
+
+        let mat = mat.borrow_inner();
+        vertices[0].pos[0] = mat[0][0] as i16;
+        vertices[0].pos[1] = mat[1][0] as i16;
+        vertices[0].pos[2] = mat[2][0] as i16;
+        vertices[1].pos[0] = mat[0][1] as i16;
+        vertices[1].pos[1] = mat[1][1] as i16;
+        vertices[1].pos[2] = mat[2][1] as i16;
+        vertices[2].pos[0] = mat[0][2] as i16;
+        vertices[2].pos[1] = mat[1][2] as i16;
+        vertices[2].pos[2] = mat[2][2] as i16;
+        vertices[3].pos[0] = mat[0][3] as i16;
+        vertices[3].pos[1] = mat[1][3] as i16;
+        vertices[3].pos[2] = mat[2][3] as i16;
+
+        // XXX: don’t clone here.
+        let (x_1, y_1) = self.anm.clone().unwrap().inv_size();
+        let [tox, toy] = self.texoffsets;
+        let left = tx * x_1 + tox;
+        let right = (tx + tw) * x_1 + tox;
+        let bottom = ty * y_1 + toy;
+        let top = (ty + th) * y_1 + toy;
+
+        vertices[0].uv[0] = left;
+        vertices[0].uv[1] = bottom;
+        vertices[1].uv[0] = right;
+        vertices[1].uv[1] = bottom;
+        vertices[2].uv[0] = right;
+        vertices[2].uv[1] = top;
+        vertices[3].uv[0] = left;
+        vertices[3].uv[1] = top;
+
+        vertices[0].color = self.color;
+        vertices[1].color = self.color;
+        vertices[2].color = self.color;
+        vertices[3].color = self.color;
+
+        vertices[0].layer = self.layer;
+        vertices[1].layer = self.layer;
+        vertices[2].layer = self.layer;
+        vertices[3].layer = self.layer;
+    }
+
+    /// Update sprite values from the interpolators.
+    pub fn update(&mut self) {
+        self.frame += 1;
+        self.corner_relative_placement = true;
+
+        let [sax, say, saz] = self.rotations_speed_3d;
+        if sax != 0. || say != 0. || saz != 0. {
+            let [ax, ay, az] = self.rotations_3d;
+            self.rotations_3d = [ax + sax, ay + say, az + saz];
+            self.changed = true;
+        } else if let Some(ref interpolator) = self.rotation_interpolator {
+            self.rotations_3d = interpolator.values(self.frame);
+            self.changed = true;
+        }
+
+        let [rsx, rsy] = self.scale_speed;
+        if rsx != 0. || rsy != 0. {
+            let [rx, ry] = self.rescale;
+            self.rescale = [rx + rsx, ry + rsy];
+            self.changed = true;
+        }
+
+        if let Some(ref interpolator) = self.fade_interpolator {
+            self.color[3] = interpolator.values(self.frame)[0] as u8;
+            self.changed = true;
+        }
+
+        if let Some(ref interpolator) = self.scale_interpolator {
+            self.rescale = interpolator.values(self.frame);
+            self.changed = true;
+        }
+
+        if let Some(ref interpolator) = self.offset_interpolator {
+            self.dest_offset = interpolator.values(self.frame);
+            self.changed = true;
+        }
+
+        if let Some(ref interpolator) = self.color_interpolator {
+            let color = interpolator.values(self.frame);
+            // TODO: this can probably be made to look nicer.
+            self.color[0] = color[0] as u8;
+            self.color[1] = color[1] as u8;
+            self.color[2] = color[2] as u8;
+            self.changed = true;
+        }
+    }
+}
+
+struct Anms {
+    inner: Rc<RefCell<[Anm0]>>,
+}
+
+impl Anms {
+    fn new(anms: Rc<RefCell<[Anm0]>>) -> Anms {
+        Anms {
+            inner: anms,
+        }
+    }
+
+    fn load_sprite(&self, sprite: &mut Sprite, id: u8) {
+        let anms = self.inner.borrow();
+        let mut anm = None;
+        let mut texcoords = None;
+        let mut layer = 0;
+        'anm: for anm0 in anms.iter() {
+            for sp in anm0.sprites.iter() {
+                if sp.index == id as u32 {
+                    texcoords = Some(sp);
+                    anm = Some(anm0.clone());
+                    break 'anm;
+                }
+            }
+            layer += 1;
+        }
+        sprite.anm = anm;
+        sprite.layer = layer;
+        if let Some(texcoords) = texcoords {
+            sprite.texcoords = [texcoords.x, texcoords.y, texcoords.width, texcoords.height];
+        }
+    }
+
+    fn get_script(&self, id: u8) -> Script {
+        let anms = self.inner.borrow();
+        for anm0 in anms.iter() {
+            if anm0.scripts.contains_key(&id) {
+                return anm0.scripts[&id].clone();
+            }
+        }
+        unreachable!();
+    }
+}
+
+/// Interpreter for `Anm0` instructions to update a `Sprite`.
+pub struct AnmRunner {
+    anms: Anms,
+    sprite: Rc<RefCell<Sprite>>,
+    prng: Weak<RefCell<Prng>>,
+    running: bool,
+    sprite_index_offset: u32,
+    script: Script,
+    instruction_pointer: usize,
+    frame: u16,
+    waiting: bool,
+    variables: ([i32; 4], [f32; 4], [i32; 4]),
+    timeout: Option<u32>,
+}
+
+impl AnmRunner {
+    /// Create a new `AnmRunner`.
+    pub fn new(anms: Rc<RefCell<[Anm0]>>, script_id: u8, sprite: Rc<RefCell<Sprite>>, prng: Weak<RefCell<Prng>>, sprite_index_offset: u32) -> AnmRunner {
+        let anms = Anms::new(anms);
+        let script = anms.get_script(script_id);
+        let mut runner = AnmRunner {
+            anms,
+            sprite: sprite,
+            prng,
+            running: true,
+            waiting: false,
+
+            script,
+            frame: 0,
+            timeout: None,
+            instruction_pointer: 0,
+            variables: ([0,  0,  0,  0 ],
+                        [0., 0., 0., 0.],
+                        [0,  0,  0,  0 ]),
+
+            sprite_index_offset: sprite_index_offset,
+        };
+        runner.run_frame();
+        runner.sprite_index_offset = 0;
+        runner
+    }
+
+    /// Get a Rc from the inner Sprite.
+    pub fn get_sprite(&self) -> Rc<RefCell<Sprite>> {
+        self.sprite.clone()
+    }
+
+    /// Trigger an interrupt.
+    pub fn interrupt(&mut self, interrupt: i32) -> bool {
+        let mut new_ip = self.script.interrupts.get(&interrupt);
+        if new_ip.is_none() {
+            new_ip = self.script.interrupts.get(&-1);
+        }
+        let new_ip = if let Some(new_ip) = new_ip {
+            *new_ip as usize
+        } else {
+            return false;
+        };
+        self.instruction_pointer = new_ip;
+        let Call { time: frame, instr: _ } = &self.script.instructions[self.instruction_pointer];
+        self.frame = *frame;
+        self.waiting = false;
+        self.sprite.borrow_mut().visible = true;
+        true
+    }
+
+    /// Advance the Anm of a single frame.
+    pub fn run_frame(&mut self) -> bool {
+        if !self.running {
+            return false;
+        }
+
+        while self.running && !self.waiting {
+            let Call { time: frame, instr } = self.script.instructions[self.instruction_pointer];
+            let frame = frame.clone();
+
+            if frame > self.frame {
+                break;
+            } else {
+                self.instruction_pointer += 1;
+            }
+
+            if frame == self.frame {
+                self.run_instruction(instr);
+                self.sprite.borrow_mut().changed = true;
+            }
+        }
+
+        if !self.waiting {
+            self.frame += 1;
+        } else if let Some(timeout) = self.timeout {
+            if timeout == self.sprite.borrow().frame { // TODO: check if it’s happening at the correct frame.
+                self.waiting = false;
+            }
+        }
+
+        self.sprite.borrow_mut().update();
+
+        self.running
+    }
+
+    fn run_instruction(&mut self, instruction: Instruction) {
+        let mut sprite = self.sprite.borrow_mut();
+        match instruction {
+            Instruction::Delete() => {
+                sprite.removed = true;
+                self.running = false;
+            }
+            Instruction::LoadSprite(sprite_index) => {
+                self.anms.load_sprite(&mut sprite, (sprite_index + self.sprite_index_offset) as u8);
+            }
+            Instruction::SetScale(sx, sy) => {
+                sprite.rescale = [sx, sy];
+            }
+            Instruction::SetAlpha(alpha) => {
+                // TODO: check this modulo.
+                sprite.color[3] = (alpha % 256) as u8;
+            }
+            Instruction::SetColor(b, g, r) => {
+                if sprite.fade_interpolator.is_none() {
+                    sprite.color[0] = r;
+                    sprite.color[1] = g;
+                    sprite.color[2] = b;
+                }
+            }
+            Instruction::Jump(pointer) => {
+                // TODO: is that really how it works?
+                self.instruction_pointer = pointer as usize;
+                self.frame = self.script.instructions[pointer as usize].time;
+            }
+            Instruction::ToggleMirrored() => {
+                sprite.mirrored = !sprite.mirrored;
+            }
+            Instruction::SetRotations3d(rx, ry, rz) => {
+                sprite.rotations_3d = [rx, ry, rz];
+            }
+            Instruction::SetRotationsSpeed3d(srx, sry, srz) => {
+                sprite.rotations_speed_3d = [srx, sry, srz];
+            }
+            Instruction::SetScaleSpeed(ssx, ssy) => {
+                sprite.scale_speed = [ssx, ssy];
+            }
+            Instruction::Fade(new_alpha, duration) => {
+                sprite.fade_interpolator = Some(Interpolator1::new([sprite.color[3] as f32], sprite.frame, [new_alpha as f32], sprite.frame + duration, Formula::Linear));
+            }
+            Instruction::SetBlendmodeAlphablend() => {
+                sprite.blendfunc = 1;
+            }
+            Instruction::SetBlendmodeAdd() => {
+                sprite.blendfunc = 0;
+            }
+            Instruction::KeepStill() => {
+                self.running = false;
+            }
+            Instruction::LoadRandomSprite(min_index, mut amplitude) => {
+                if amplitude > 0 {
+                    let prng = self.prng.upgrade().unwrap();
+                    let rand = prng.borrow_mut().get_u16();
+                    amplitude = (rand as u32) % amplitude;
+                }
+                let sprite_index = min_index + amplitude;
+                self.anms.load_sprite(&mut sprite, (sprite_index + self.sprite_index_offset) as u8);
+            }
+            Instruction::Move(x, y, z) => {
+                sprite.dest_offset = [x, y, z];
+            }
+            Instruction::MoveToLinear(x, y, z, duration) => {
+                sprite.offset_interpolator = Some(Interpolator3::new(sprite.dest_offset, sprite.frame, [x, y, z], sprite.frame + duration, Formula::Linear));
+            }
+            Instruction::MoveToDecel(x, y, z, duration) => {
+                sprite.offset_interpolator = Some(Interpolator3::new(sprite.dest_offset, sprite.frame, [x, y, z], sprite.frame + duration, Formula::InvertPower2));
+            }
+            Instruction::MoveToAccel(x, y, z, duration) => {
+                sprite.offset_interpolator = Some(Interpolator3::new(sprite.dest_offset, sprite.frame, [x, y, z], sprite.frame + duration, Formula::Power2));
+            }
+            Instruction::Wait() => {
+                self.waiting = true;
+            }
+            // There is nothing to do here.
+            Instruction::InterruptLabel(_label) => (),
+            Instruction::SetCornerRelativePlacement() => {
+                sprite.corner_relative_placement = true;
+            }
+            Instruction::WaitEx() => {
+                sprite.visible = false;
+                self.waiting = true;
+            }
+            Instruction::SetAllowOffset(value) => {
+                sprite.allow_dest_offset = value == 1
+            }
+            Instruction::SetAutomaticOrientation(value) => {
+                sprite.automatic_orientation = value == 1
+            }
+            Instruction::ShiftTextureX(dx) => {
+                let [tox, toy] = sprite.texoffsets;
+                sprite.texoffsets = [tox + dx, toy];
+            }
+            Instruction::ShiftTextureY(dy) => {
+                let [tox, toy] = sprite.texoffsets;
+                sprite.texoffsets = [tox, toy + dy];
+            }
+            Instruction::SetVisible(visible) => {
+                sprite.visible = (visible & 1) != 0;
+            }
+            Instruction::ScaleIn(sx, sy, duration) => {
+                sprite.scale_interpolator = Some(Interpolator2::new(sprite.rescale, sprite.frame, [sx, sy], sprite.frame + duration, Formula::Linear));
+            }
+            Instruction::Todo(_todo) => {
+                // TODO.
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::{self, Read};
+    use std::fs::File;
+
+    #[test]
+    fn anm_runner() {
+        let file = File::open("EoSD/CM/player01.anm").unwrap();
+        let mut file = io::BufReader::new(file);
+        let mut buf = vec![];
+        file.read_to_end(&mut buf).unwrap();
+        let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
+        let anm0 = anms.pop().unwrap();
+        assert_eq!(anm0.size, (256, 256));
+        assert_eq!(anm0.format, 5);
+        let sprite = Rc::new(RefCell::new(Sprite::new()));
+        let prng = Rc::new(RefCell::new(Prng::new(0)));
+        let mut anm_runner = AnmRunner::new(&anm0, 1, sprite.clone(), Rc::downgrade(&prng), 0);
+        for _ in 0..50 {
+            anm_runner.run_frame();
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interpreters/src/th06/ecl.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,1333 @@
+//! ECL runner.
+
+use touhou_formats::th06::ecl::{Ecl, SubInstruction};
+use crate::th06::enemy::{Enemy, Offset, BulletAttributes, Position};
+use touhou_utils::prng::Prng;
+use std::cell::RefCell;
+use std::rc::Rc;
+
+macro_rules! gen_SetBulletAttributes {
+    ($self:ident, $opcode:tt, $anim:ident, $sprite_index_offset:ident, $bullets_per_shot:ident,
+     $number_of_shots:ident, $speed:ident, $speed2:ident, $launch_angle:ident, $angle:ident,
+     $flags:ident) => {{
+        let sprite_index_offset = $self.get_i32($sprite_index_offset as i32) as i16;
+        let bullets_per_shot = $self.get_i32($bullets_per_shot) as i16;
+        let number_of_shots = $self.get_i32($number_of_shots) as i16;
+        let speed = $self.get_f32($speed);
+        let speed2 = $self.get_f32($speed2);
+        let launch_angle = $self.get_f32($launch_angle);
+        let angle = $self.get_f32($angle);
+
+        let mut enemy = $self.enemy.borrow_mut();
+        enemy.set_bullet_attributes($opcode, $anim, sprite_index_offset, bullets_per_shot,
+                                    number_of_shots, speed, speed2, launch_angle, angle, $flags);
+    }};
+}
+
+#[derive(Clone, Default)]
+struct StackFrame {
+    frame: i32,
+    ip: i32,
+    //ins122_callback: Option<Box<FnMut(Enemy)>>,
+    ints1: [i32; 4],
+    floats: [f32; 4],
+    ints2: [i32; 4],
+    comparison_reg: i32,
+    sub: u16,
+}
+
+/// Interpreter for enemy scripts.
+#[derive(Default)]
+pub struct EclRunner {
+    /// XXX
+    pub enemy: Rc<RefCell<Enemy>>,
+
+    ecl: Option<Ecl>,
+    /// XXX
+    pub running: bool,
+    frame: StackFrame,
+    // TODO: there are only 8 of these.
+    stack: Vec<StackFrame>,
+}
+
+impl EclRunner {
+    /// Create a new ECL runner.
+    pub fn new(ecl: &Ecl, enemy: Rc<RefCell<Enemy>>, sub: u16) -> EclRunner {
+        let mut ecl_runner = EclRunner {
+            enemy,
+            // XXX: no clone.
+            ecl: Some(ecl.clone()),
+            running: true,
+            ..Default::default()
+        };
+        ecl_runner.frame.sub = sub;
+        ecl_runner
+    }
+
+    /// Advance the ECL of a single frame.
+    pub fn run_frame(&mut self) {
+        while self.running {
+            let ecl = self.ecl.clone().unwrap();
+            let sub = &ecl.subs[self.frame.sub as usize];
+            let call = match sub.instructions.get(self.frame.ip as usize) {
+                Some(call) => call,
+                None => {
+                    self.running = false;
+                    break;
+                }
+            };
+
+            if call.time > self.frame.frame {
+                break;
+            }
+            self.frame.ip += 1;
+
+            let rank = self.enemy.borrow().get_rank();
+            if (call.rank_mask & rank).is_empty() {
+                continue;
+            }
+
+            if call.time == self.frame.frame {
+                self.run_instruction(call.instr.clone());
+            }
+        }
+        self.frame.frame += 1;
+    }
+
+    fn get_i32(&self, var: i32) -> i32 {
+        let enemy = self.enemy.borrow();
+        match var {
+            -10001 => self.frame.ints1[0],
+            -10002 => self.frame.ints1[1],
+            -10003 => self.frame.ints1[2],
+            -10004 => self.frame.ints1[3],
+            -10005 => self.frame.floats[0] as i32,
+            -10006 => self.frame.floats[1] as i32,
+            -10007 => self.frame.floats[2] as i32,
+            -10008 => self.frame.floats[3] as i32,
+            -10009 => self.frame.ints2[0],
+            -10010 => self.frame.ints2[1],
+            -10011 => self.frame.ints2[2],
+            -10012 => self.frame.ints2[3],
+            -10013 => enemy.get_rank().bits() as i32,
+            -10014 => enemy.get_difficulty(),
+            -10015 => enemy.pos.x as i32,
+            -10016 => enemy.pos.y as i32,
+            -10017 => enemy.z as i32,
+            -10018 => unimplemented!(),
+            -10019 => unimplemented!(),
+            -10020 => unreachable!(),
+            -10021 => unimplemented!(),
+            -10022 => enemy.frame as i32,
+            -10023 => unreachable!(),
+            -10024 => enemy.life as i32,
+            -10025 => unimplemented!(),
+            _ => var
+        }
+    }
+
+    fn get_f32(&self, var: f32) -> f32 {
+        let enemy = self.enemy.borrow();
+        match var {
+            -10001.0 => self.frame.ints1[0] as f32,
+            -10002.0 => self.frame.ints1[1] as f32,
+            -10003.0 => self.frame.ints1[2] as f32,
+            -10004.0 => self.frame.ints1[3] as f32,
+            -10005.0 => self.frame.floats[0],
+            -10006.0 => self.frame.floats[1],
+            -10007.0 => self.frame.floats[2],
+            -10008.0 => self.frame.floats[3],
+            -10009.0 => self.frame.ints2[0] as f32,
+            -10010.0 => self.frame.ints2[1] as f32,
+            -10011.0 => self.frame.ints2[2] as f32,
+            -10012.0 => self.frame.ints2[3] as f32,
+            -10013.0 => enemy.get_rank().bits() as f32,
+            -10014.0 => enemy.get_difficulty() as f32,
+            -10015.0 => enemy.pos.x,
+            -10016.0 => enemy.pos.y,
+            -10017.0 => enemy.z,
+            -10018.0 => unimplemented!(),
+            -10019.0 => unimplemented!(),
+            -10020.0 => unreachable!(),
+            -10021.0 => unimplemented!(),
+            -10022.0 => enemy.frame as f32,
+            -10023.0 => unreachable!(),
+            -10024.0 => enemy.life as f32,
+            -10025.0 => unimplemented!(),
+            _ => var
+        }
+    }
+
+    fn set_i32(&mut self, var: i32, value: i32) {
+        let mut enemy = self.enemy.borrow_mut();
+        match var {
+            -10001 => self.frame.ints1[0] = value,
+            -10002 => self.frame.ints1[1] = value,
+            -10003 => self.frame.ints1[2] = value,
+            -10004 => self.frame.ints1[3] = value,
+            -10005 => unimplemented!(),
+            -10006 => unimplemented!(),
+            -10007 => unimplemented!(),
+            -10008 => unimplemented!(),
+            -10009 => self.frame.ints2[0] = value,
+            -10010 => self.frame.ints2[1] = value,
+            -10011 => self.frame.ints2[2] = value,
+            -10012 => self.frame.ints2[3] = value,
+            -10013 => unreachable!(),
+            -10014 => unreachable!(),
+            -10015 => unimplemented!(),
+            -10016 => unimplemented!(),
+            -10017 => unimplemented!(),
+            -10018 => unreachable!(),
+            -10019 => unreachable!(),
+            -10020 => unreachable!(),
+            -10021 => unreachable!(),
+            -10022 => enemy.frame = value as u32,
+            -10023 => unreachable!(),
+            -10024 => enemy.life = value as u32,
+            -10025 => unreachable!(),
+            _ => panic!("Unknown variable {}", var)
+        }
+    }
+
+    fn set_f32(&mut self, var: f32, value: f32) {
+        let mut enemy = self.enemy.borrow_mut();
+        match var {
+            -10001.0 => unimplemented!(),
+            -10002.0 => unimplemented!(),
+            -10003.0 => unimplemented!(),
+            -10004.0 => unimplemented!(),
+            -10005.0 => self.frame.floats[0] = value,
+            -10006.0 => self.frame.floats[1] = value,
+            -10007.0 => self.frame.floats[2] = value,
+            -10008.0 => self.frame.floats[3] = value,
+            -10009.0 => unimplemented!(),
+            -10010.0 => unimplemented!(),
+            -10011.0 => unimplemented!(),
+            -10012.0 => unimplemented!(),
+            -10013.0 => unreachable!(),
+            -10014.0 => unreachable!(),
+            -10015.0 => enemy.pos.x = value,
+            -10016.0 => enemy.pos.y = value,
+            -10017.0 => enemy.z = value,
+            -10018.0 => unreachable!(),
+            -10019.0 => unreachable!(),
+            -10020.0 => unreachable!(),
+            -10021.0 => unreachable!(),
+            -10022.0 => unimplemented!(),
+            -10023.0 => unreachable!(),
+            -10024.0 => unimplemented!(),
+            -10025.0 => unreachable!(),
+            _ => panic!("Unknown variable {}", var)
+        }
+    }
+
+    fn get_prng(&mut self) -> Rc<RefCell<Prng>> {
+        let enemy = self.enemy.borrow();
+        enemy.prng.upgrade().unwrap()
+    }
+
+    fn run_instruction(&mut self, instruction: SubInstruction) {
+        println!("Running instruction {:?}", instruction);
+        match instruction {
+            SubInstruction::Noop() => {
+                // really
+            }
+            // 1
+            SubInstruction::Destroy(_unused) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.removed = true;
+            }
+            // 2
+            SubInstruction::RelativeJump(frame, ip) => {
+                self.frame.frame = frame;
+                // ip = ip + flag in th06
+                self.frame.ip = ip;
+                // we jump back to the main of the interpreter
+            }
+            // 3
+            // GHIDRA SAYS THERE IS A COMPARISON_REG BUFFER BUT THERE IS NOT!!!
+            //
+            // MOV        ECX,dword ptr [EBP + 0x8]                     jumptable 00407544 case 31
+            // CMP        dword ptr [0x9d4 + ECX],0x0
+            // JLE        LAB_00407abb
+            // aka ECX = enemy pointer
+            // ECX->9d4 (aka enemy_pointer_copy->comparison_reg) == 0
+            // only the pointer is copied, not the value, thus we are safe
+            SubInstruction::RelativeJumpEx(frame, ip, var_id) => {
+                // TODO: counter_value is a field of "enemy" in th06, to check
+                let counter_value = self.get_i32(var_id) - 1;
+                if counter_value > 0 {
+                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
+                }
+            }
+            // 4
+            SubInstruction::SetInt(var_id, value) => {
+                self.set_i32(var_id, value);
+            }
+            // 5
+            SubInstruction::SetFloat(var_id, value) => {
+                self.set_f32(var_id as f32, value);
+            }
+            // 6
+            SubInstruction::SetRandomInt(var_id, maxval) => {
+                let random = self.get_prng().borrow_mut().get_u32() as i32;
+                self.set_i32(var_id, random % self.get_i32(maxval));
+            }
+            // 7
+            SubInstruction::SetRandomIntMin(var_id, maxval, minval) => {
+                let random = self.get_prng().borrow_mut().get_u32() as i32;
+                self.set_i32(var_id, (random % self.get_i32(maxval)) + self.get_i32(minval));
+            }
+            // 8
+            SubInstruction::SetRandomFloat(var_id, maxval) => {
+                let random = self.get_prng().borrow_mut().get_f64() as f32;
+                self.set_f32(var_id as f32, self.get_f32(maxval) * random)
+            }
+            // 9
+            SubInstruction::SetRandomFloatMin(var_id, maxval, minval) => {
+                let random = self.get_prng().borrow_mut().get_f64() as f32;
+                self.set_f32(var_id as f32, self.get_f32(maxval) * random + self.get_f32(minval))
+            }
+            // 10
+            SubInstruction::StoreX(var_id) => {
+                let x = {
+                    let enemy = self.enemy.borrow();
+                    enemy.pos.x
+                };
+                // TODO: is this really an i32?
+                self.set_i32(var_id, x as i32);
+            }
+            // 11
+            SubInstruction::StoreY(var_id) => {
+                let y = {
+                    let enemy = self.enemy.borrow();
+                    enemy.pos.y
+                };
+                self.set_i32(var_id, y as i32);
+            }
+            // 12
+            SubInstruction::StoreZ(var_id) => {
+                let z = {
+                    let enemy = self.enemy.borrow();
+                    enemy.z
+                };
+                self.set_i32(var_id, z as i32);
+            }
+            // 13(int), 20(float), same impl in th06
+            SubInstruction::AddInt(var_id, a, b) => {
+                self.set_i32(var_id, self.get_i32(a) + self.get_i32(b));
+            }
+            SubInstruction::AddFloat(var_id, a, b) => {
+                self.set_f32(var_id as f32, self.get_f32(a) + self.get_f32(b));
+            }
+            // 14(int), 21(float), same impl in th06
+            SubInstruction::SubstractInt(var_id, a, b) => {
+                self.set_i32(var_id, self.get_i32(a) - self.get_i32(b));
+            }
+            SubInstruction::SubstractFloat(var_id, a, b) => {
+                self.set_f32(var_id as f32, self.get_f32(a) - self.get_f32(b));
+            }
+            // 15(int), 22(unused)
+            SubInstruction::MultiplyInt(var_id, a, b) => {
+                self.set_i32(var_id, self.get_i32(a) * self.get_i32(b));
+            }
+            /*
+            SubInstruction::MultiplyFloat(var_id, a, b) => {
+                self.set_f32(var_id as f32, self.get_f32(a) * self.get_f32(b));
+            }
+            */
+             // 16(int), 23(unused)
+            SubInstruction::DivideInt(var_id, a, b) => {
+                self.set_i32(var_id, self.get_i32(a) / self.get_i32(b));
+            }
+
+            SubInstruction::DivideFloat(var_id, a, b) => {
+                self.set_f32(var_id as f32, self.get_f32(a) / self.get_f32(b));
+            }
+
+            // 17(int) 24(unused)
+            SubInstruction::ModuloInt(var_id, a, b) => {
+                self.set_i32(var_id, self.get_i32(a) % self.get_i32(b));
+            }
+
+            SubInstruction::ModuloFloat(var_id, a, b) => {
+                self.set_f32(var_id as f32, self.get_f32(a) % self.get_f32(b));
+            }
+
+            // 18
+            // setval used by pytouhou, but not in game(???)
+            SubInstruction::Increment(var_id) => {
+                self.set_i32(var_id, self.get_i32(var_id) + 1);
+            }
+
+            // 19
+            SubInstruction::Decrement(var_id) => {
+                self.set_i32(var_id, self.get_i32(var_id) - 1);
+            }
+
+            //25
+            SubInstruction::GetDirection(var_id, x1, y1, x2, y2) => {
+                //__ctrandisp2 in ghidra, let's assume from pytouhou it's atan2
+                self.set_f32(var_id as f32, (self.get_f32(y2) - self.get_f32(y1)).atan2(self.get_f32(x2) - self.get_f32(x1)));
+            }
+
+            // 26
+            SubInstruction::FloatToUnitCircle(var_id) => {
+                // TODO: atan2(var_id, ??) is used by th06, maybe ?? is pi?
+                // we suck at trigonometry so let's use pytouhou for now
+                self.set_f32(var_id as f32, (self.get_f32(var_id as f32) + std::f32::consts::PI) % (2. * std::f32::consts::PI) - std::f32::consts::PI);
+            }
+
+            // 27(int), 28(float)
+            SubInstruction::CompareInts(a, b) => {
+                let a = self.get_i32(a);
+                let b = self.get_i32(b);
+                if a < b {
+                    self.frame.comparison_reg = -1;
+                }
+                else if  a == b {
+                    self.frame.comparison_reg = 0;
+                }
+                else {
+                    self.frame.comparison_reg = 1;
+                }
+            }
+            SubInstruction::CompareFloats(a, b) => {
+                let a = self.get_f32(a);
+                let b = self.get_f32(b);
+                if a < b {
+                    self.frame.comparison_reg = -1;
+                }
+                else if  a == b {
+                    self.frame.comparison_reg = 0;
+                }
+                else {
+                    self.frame.comparison_reg = 1;
+                }
+            }
+            // 29
+            SubInstruction::RelativeJumpIfLowerThan(frame, ip) => {
+                if self.frame.comparison_reg == -1 {
+                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
+                }
+            }
+            // 30
+            SubInstruction::RelativeJumpIfLowerOrEqual(frame, ip) => {
+                if self.frame.comparison_reg != 1 {
+                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
+                }
+            }
+            // 31
+            SubInstruction::RelativeJumpIfEqual(frame, ip) => {
+                if self.frame.comparison_reg == 0 {
+                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
+                }
+            }
+            // 32
+            SubInstruction::RelativeJumpIfGreaterThan(frame, ip) => {
+                if self.frame.comparison_reg == 1 {
+                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
+                }
+            }
+            // 33
+            SubInstruction::RelativeJumpIfGreaterOrEqual(frame, ip) => {
+                if self.frame.comparison_reg != -1 {
+                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
+                }
+            }
+            // 34
+            SubInstruction::RelativeJumpIfNotEqual(frame, ip) => {
+                if self.frame.comparison_reg != 0 {
+                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
+                }
+            }
+            // 35
+            SubInstruction::Call(sub, param1, param2) => {
+                self.stack.push(self.frame.clone());
+                self.frame.sub = sub as u16;
+                self.frame.ints1[0] = param1;
+                self.frame.floats[0] = param2;
+                self.frame.frame = 0;
+                self.frame.ip = 0;
+            }
+
+            // 36
+            SubInstruction::Return() => {
+                self.frame = self.stack.pop().unwrap();
+            }
+            // 37
+            SubInstruction::CallIfSuperior(sub, param1, param2, a, b) => {
+                if self.get_i32(a) < self.get_i32(b) {
+                    self.run_instruction(SubInstruction::Call(sub, param1, param2));
+                }
+            }
+            // 38
+            SubInstruction::CallIfSuperiorOrEqual(sub, param1, param2, a, b) => {
+                if self.get_i32(a) <= self.get_i32(b) {
+                    self.run_instruction(SubInstruction::Call(sub, param1, param2));
+                }
+            }
+            // 39
+            SubInstruction::CallIfEqual(sub, param1, param2, a, b) => {
+                if self.get_i32(a) == self.get_i32(b) {
+                    self.run_instruction(SubInstruction::Call(sub, param1, param2));
+                }
+            }
+            // 40
+            SubInstruction::CallIfInferior(sub, param1, param2, a, b) => {
+                if self.get_i32(b) < self.get_i32(a) {
+                    self.run_instruction(SubInstruction::Call(sub, param1, param2));
+                }
+            }
+
+            // 41
+            SubInstruction::CallIfInferiorOrEqual(sub, param1, param2, a, b) => {
+                if self.get_i32(b) <= self.get_i32(a) {
+                    self.run_instruction(SubInstruction::Call(sub, param1, param2));
+                }
+            }
+            //42
+            SubInstruction::CallIfNotEqual(sub, param1, param2, a, b) => {
+                if self.get_i32(a) != self.get_i32(b) {
+                    self.run_instruction(SubInstruction::Call(sub, param1, param2));
+                }
+            }
+
+            // 43
+            SubInstruction::SetPosition(x, y, z) => {
+                let (x, y, z) = (self.get_f32(x), self.get_f32(y), self.get_f32(z));
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.set_pos(x, y, z);
+            }
+            // 44
+            /*
+            SubInstruction::SetAngularSpeed(x, y, z) => {
+                // same as above, except for angular speed
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.set_angular_speed(self.get_f32(x), self.get_f32(y), self.get_f32(z));
+            }
+            */
+            // 45
+            SubInstruction::SetAngleAndSpeed(angle, speed) => {
+                let angle = self.get_f32(angle);
+                let speed = self.get_f32(speed);
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.update_mode = 0;
+                enemy.angle = angle;
+                enemy.speed = speed;
+            }
+            // 46
+            SubInstruction::SetRotationSpeed(speed) => {
+                let rotation_speed = self.get_f32(speed);
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.update_mode = 0;
+                enemy.rotation_speed = rotation_speed;
+            }
+            // 47
+            SubInstruction::SetSpeed(speed) => {
+                let speed = self.get_f32(speed);
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.update_mode = 0;
+                enemy.speed = speed;
+            }
+            // 48
+            SubInstruction::SetAcceleration(acceleration) => {
+                let acceleration = self.get_f32(acceleration);
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.update_mode = 0;
+                enemy.acceleration = acceleration;
+            }
+            // 49
+            SubInstruction::SetRandomAngle(min_angle, max_angle) => {
+                let angle = self.get_prng().borrow_mut().get_f64() as f32 * (max_angle - min_angle) + min_angle;
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.angle = angle;
+            }
+            // 51
+            SubInstruction::TargetPlayer(delta_angle, speed) => {
+                let speed = self.get_f32(speed);
+                let mut enemy = self.enemy.borrow_mut();
+                let game = enemy.game.upgrade().unwrap();
+                let player = game.borrow().get_player();
+                enemy.update_mode = 0;
+                enemy.speed = speed;
+                enemy.angle = enemy.get_angle_to(player) + delta_angle;
+            }
+
+            // 52 to 64 are different interlacing fields
+
+            // 65
+            // to note: in game a flag is set to enable the screenbox and is set by 66 to disable
+            // it on top of setting our values. But we have a good engine and can detect if that's
+            // changed without setting a flag :)
+            SubInstruction::SetScreenBox(xmin, ymin, xmax, ymax) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.screen_box = Some((xmin, ymin, xmax, ymax));
+            }
+             // 66
+            SubInstruction::ClearScreenBox() => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.screen_box = None;
+            }
+
+            // 67 to 75 are set bullet attributes and it seems a pain to reverse rn
+            SubInstruction::SetBulletAttributes1(anim, sprite_index_offset, bullets_per_shot,
+                                                 number_of_shots, speed, speed2, launch_angle,
+                                                 angle, flags) => {
+                gen_SetBulletAttributes!(self, 67, anim, sprite_index_offset, bullets_per_shot,
+                                         number_of_shots, speed, speed2, launch_angle, angle,
+                                         flags);
+            }
+            SubInstruction::SetBulletAttributes2(anim, sprite_index_offset, bullets_per_shot,
+                                                 number_of_shots, speed, speed2, launch_angle,
+                                                 angle, flags) => {
+                gen_SetBulletAttributes!(self, 68, anim, sprite_index_offset, bullets_per_shot,
+                                         number_of_shots, speed, speed2, launch_angle, angle,
+                                         flags);
+            }
+            SubInstruction::SetBulletAttributes3(anim, sprite_index_offset, bullets_per_shot,
+                                                 number_of_shots, speed, speed2, launch_angle,
+                                                 angle, flags) => {
+                gen_SetBulletAttributes!(self, 69, anim, sprite_index_offset, bullets_per_shot,
+                                         number_of_shots, speed, speed2, launch_angle, angle,
+                                         flags);
+            }
+            SubInstruction::SetBulletAttributes4(anim, sprite_index_offset, bullets_per_shot,
+                                                 number_of_shots, speed, speed2, launch_angle,
+                                                 angle, flags) => {
+                gen_SetBulletAttributes!(self, 70, anim, sprite_index_offset, bullets_per_shot,
+                                         number_of_shots, speed, speed2, launch_angle, angle,
+                                         flags);
+            }
+            SubInstruction::SetBulletAttributes5(anim, sprite_index_offset, bullets_per_shot,
+                                                 number_of_shots, speed, speed2, launch_angle,
+                                                 angle, flags) => {
+                gen_SetBulletAttributes!(self, 71, anim, sprite_index_offset, bullets_per_shot,
+                                         number_of_shots, speed, speed2, launch_angle, angle,
+                                         flags);
+            }
+            SubInstruction::SetBulletAttributes6(anim, sprite_index_offset, bullets_per_shot,
+                                                 number_of_shots, speed, speed2, launch_angle,
+                                                 angle, flags) => {
+                gen_SetBulletAttributes!(self, 74, anim, sprite_index_offset, bullets_per_shot,
+                                         number_of_shots, speed, speed2, launch_angle, angle,
+                                         flags);
+            }
+            SubInstruction::SetBulletAttributes7(anim, sprite_index_offset, bullets_per_shot,
+                                                 number_of_shots, speed, speed2, launch_angle,
+                                                 angle, flags) => {
+                gen_SetBulletAttributes!(self, 75, anim, sprite_index_offset, bullets_per_shot,
+                                         number_of_shots, speed, speed2, launch_angle, angle,
+                                         flags);
+            }
+
+            // 76
+            SubInstruction::SetBulletInterval(interval) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.set_bullet_launch_interval(0, interval);
+            }
+
+            // 77
+            SubInstruction::SetBulletIntervalEx(interval) => {
+                let rand_start = self.get_prng().borrow_mut().get_u32();
+
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.set_bullet_launch_interval(rand_start, interval);
+            }
+
+            // 78-79 are more interpolation flags
+            // 78
+            SubInstruction::DelayAttack() => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.delay_attack = true;
+            }
+            // 79
+            SubInstruction::NoDelayAttack() => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.delay_attack = false;
+            }
+            // 80
+            /*
+            SubInstruction::NoClue() => {
+                let mut enemy = self.enemy.borrow_mut();
+                //bullet_pos = launch offset
+                (enemy->bullet_attributes).bullets_per_shot = enemy.pos.x + enemy->bullet_pos.pos.x;
+                (enemy->bullet_attributes).number_of_shots = enemy.pos.pos.y + enemy.bullet_pos.pos.y;
+                (enemy->bullet_attributes).speed = enemy.z + bullet_pos.z;
+                enemy.fire(bullet_attributes=bullet_attributes)
+            }
+            */
+
+            // 81
+            SubInstruction::SetBulletLaunchOffset(dx, dy, dz) => {
+                let (dx, dy, dz) = (self.get_f32(dx), self.get_f32(dy), self.get_f32(dz));
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.bullet_offset = Offset { dx, dy };
+            }
+
+            // 82
+            SubInstruction::SetExtendedBulletAttributes(a, b, c, d, e, f, g, h) => {
+                let (a, b, c, d) = (self.get_i32(a), self.get_i32(b), self.get_i32(c), self.get_i32(d));
+                let (e, f, g, h) = (self.get_f32(e), self.get_f32(f), self.get_f32(g), self.get_f32(h));
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.bullet_attributes.extended_attributes = (a, b, c, d, e, f, g, h);
+            }
+
+            // 83
+            /*
+            SubInstruction::ChangeBulletsIntoStarBonus() => {
+                let mut game = self.game.borrow_mut();
+                game.change_bullets_into_star_items();
+            }
+            */
+
+            // 84
+            SubInstruction::SetBulletSound(sound) => {
+                let mut enemy = self.enemy.borrow_mut();
+                if sound < 0 {
+                    enemy.bullet_attributes.sound = None;
+                } else {
+                    // This assert isn’t part of the original engine, but it would crash on high
+                    // values anyway.
+                    assert!(sound <= 255);
+                    enemy.bullet_attributes.sound = Some(sound as u8);
+                }
+            }
+
+            // 85-86 ire newlaser functions
+
+            // 87
+            SubInstruction::SetUpcomingLaserId(laser_id) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.current_laser_id = laser_id;
+            }
+
+            // 88
+            /*
+            SubInstruction::AlterLaserAngle(laser_id, delta) => {
+                let mut enemy = self.enemy.borrow_mut();
+                if enemy.laser_by_id.contains_key(&laser_id) {
+                    let mut laser = enemy.laser_by_id.get(&laser_id);
+                    laser.angle += self.get_f32(delta);
+                }
+            }
+            */
+
+            // 89
+            /*
+            SubInstruction::AlterLaserAnglePlayer(laser_id, delta) => {
+                let mut enemy = self.enemy.borrow_mut();
+                if enemy.laser_by_id.contains_key(&laser_id) {
+                    let mut laser = enemy.laser_by_id.get(laser_id);
+                    let player = enemy.select_player();
+                    laser.angle = enemy.get_angle(player) + angle;
+                }
+            }
+            */
+
+            // 90
+            /*
+            SubInstruction::RepositionLaser(laser_id, ox, oy, oz) => {
+                let mut enemy = self.enemy.borrow_mut();
+                if enemy.laser_by_id.contains_key(&laser_id) {
+                    let mut laser = enemy.laser_by_id.get(&laser_id);
+                    laser.set_base_pos(enemy.pos.x + ox, enemy.pos.y + oy, enemy.z + oz)
+                }
+            }
+            */
+            // 91
+            // wat
+            SubInstruction::LaserSetCompare(laser_id) => {
+                let enemy = self.enemy.borrow_mut();
+                // in game it checks if either the laser exists OR if one of its member is set to 0
+                // which, uhhhh, we are not going to reimplement for obvious reasons
+                // the correct implementation would be: if this laser does not exist have a
+                // 1/100000 chance to continue, otherwise crash
+                if enemy.laser_by_id.contains_key(&laser_id) {
+                    // let's assume we gud
+                    self.frame.comparison_reg = 1;
+                }
+                else{
+                    self.frame.comparison_reg = 0;
+                }
+            }
+
+            // 92
+            /*
+            SubInstruction::RepositionLaser(laser_id, ox, oy, oz) => {
+                let mut enemy = self.enemy.borrow_mut();
+                if enemy.laser_by_id.contains_key(&laser_id) {
+                    let mut laser = enemy.laser_by_id.get(laser_id);
+                    laser.cancel();
+                }
+            }
+            */
+            // 93
+            // TODO: actually implement that hell
+            SubInstruction::SetSpellcard(face, number, name) => {
+                unimplemented!("spellcard start");
+
+            }
+            // 94
+            SubInstruction::EndSpellcard() => {
+                unimplemented!("spellcard end");
+
+            }
+
+            // 95
+            SubInstruction::SpawnEnemy(sub, x, y, z, life, bonus, score) => {
+                let x = self.get_f32(x);
+                let y = self.get_f32(y);
+                let _z = self.get_f32(z);
+                let enemy = self.enemy.borrow_mut();
+                let anm0 = enemy.anm0.upgrade().unwrap();
+                let game = enemy.game.upgrade().unwrap();
+                let enemy = Enemy::new(Position::new(x, y), life, bonus, score as u32, false, Rc::downgrade(&anm0), Rc::downgrade(&game));
+                let ecl = self.ecl.clone().unwrap();
+                let mut runner = EclRunner::new(&ecl, enemy, sub as u16);
+                runner.run_frame();
+            }
+
+            // 96
+            /*
+            SubInstruction::KillEnemies() => {
+                let mut game = self.game.borrow_mut();
+                game.kill_enemies();
+            }
+            */
+
+
+
+            // 97
+            SubInstruction::SetAnim(index) => {
+                // seems correct, game internally gets base_addr =(iVar13 + 0x1c934), pointer_addr = iVar14 * 4
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.set_anim(index as u8);
+            }
+            // 98
+            SubInstruction::SetMultipleAnims(default, end_left, end_right, left, right, _unused) => {
+                // _unused was supposed to set movement_dependant_sprites, but internally the game
+                // assigns it 0xff
+                // TODO: THIS DOES NOT CALL set_anim. this only assigns all parameters to their
+                // internal struct. To check if the anims are set somewhere else
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.movement_dependant_sprites = if left == -1 {
+                    None
+                } else {
+                    enemy.set_anim(default as u8);
+                    Some((end_left as u8, end_right as u8, left as u8, right as u8))
+                };
+            }
+            // 99
+            SubInstruction::SetAuxAnm(number, script) => {
+                assert!(number < 8);
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.set_aux_anm(number, script);
+            }
+
+            // 100
+            SubInstruction::SetDeathAnim(index) => {
+                // TODO: takes 3 parameters in game as u8 unlike our single u32.
+                // To reverse!
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.death_anim = index;
+            }
+            // 101
+            SubInstruction::SetBossMode(value) => {
+                let enemy = self.enemy.borrow_mut();
+                if value < 0 {
+                    enemy.set_boss(false);
+                }
+                else {
+                    // the boss pointer is written somewhere in memory and overwrote by a 0 when
+                    // the boss mode is false, might want to look into that
+                    enemy.set_boss(true);
+                }
+            }
+
+            // 102
+            // TODO: title says it all
+            /*
+            SubInstruction::ParticlesVoodooMagic(unk1, unk2, unk3, unk4, unk5) => {
+            }
+            */
+
+            // 103
+            SubInstruction::SetHitbox(width, height, depth) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.set_hitbox(width, height);
+            }
+
+            // 104
+            SubInstruction::SetCollidable(collidable) => {
+                // TODO: me and my siblings(105, 107, 117) are implemented as a single variable in the touhou 6
+                // original engine. While our behaviour seems correct we might want to implement
+                // that as a single variable
+                // TODO[2]: THE BITFLAG MIGHT BE INCORRECT FOR OTHER SIBLING INSTRUCTIONS, the
+                // behavior was DEFINITELY incorrect in pytouhou for SetTouchable at the very least
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.collidable = (collidable&1) != 0;
+            }
+
+            // 105
+            SubInstruction::SetDamageable(damageable) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.damageable = (damageable&1) != 0;
+            }
+
+            // 106
+            SubInstruction::PlaySound(index) => {
+                let enemy = self.enemy.borrow_mut();
+                enemy.play_sound(index);
+            }
+
+            // 107
+            SubInstruction::SetDeathFlags(death_flags) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.death_flags = death_flags;
+            }
+            // 108
+            SubInstruction::SetDeathCallback(sub) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.death_callback = Some(sub);
+            }
+
+            // 109
+            SubInstruction::MemoryWriteInt(value, index) => {
+                unimplemented!("not again that damn foe corrupted my ret\\x41\\x41\\x41\\x41");
+            }
+
+            // 110
+            /*
+            SubInstruction::KillEnemy(enemy) => {
+                let mut game = self.game.borrow_mut();
+                game.kill_enemy(enemy);
+            }
+            */
+
+            // 111
+            /*
+            SubInstruction::SetLife(value) => {
+                let mut enemy = self.enemy.borrow_mut();
+                let mut game = self.game.borrow_mut();
+                enemy.life = value;
+                game.interface.set_boss_life();
+            }
+            */
+            // 112
+            SubInstruction::SetElapsedTime(value) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.frame = value as u32;
+            }
+            // 113
+            /*
+            SubInstruction::SetLowLifeTrigger(value) => {
+                let mut enemy = self.enemy.borrow_mut();
+                let mut game = self.game.borrow_mut();
+                enemy.low_life_trigger = value;
+                game.interface.set_spell_life();
+            }
+            */
+            // 114
+            /*
+             SubInstruction::SetLowLifeCallback(sub) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.low_life_callback.enable(self.switch_to_sub, (sub,));
+            }
+            */
+            // 115
+            /*
+            SubInstruction::SetTimeout(timeout) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.frame = value;
+                enemy.timeout = timeout;
+            }
+            */
+            // 116
+            /*
+             SubInstruction::SetTimeoutCallback(sub) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.timeout_callback.enable(self.switch_to_sub, (sub,));
+            }
+            */
+
+
+            // 117
+            SubInstruction::SetTouchable(touchable) => {
+                let mut enemy = self.enemy.borrow_mut();
+                enemy.touchable = touchable != 0;
+            }
+
+            // 121
+            // Here lies the Di Sword of sadness
+            SubInstruction::CallSpecialFunction(function, arg) => {
+                match function {
+                    0 => {
+                        let mut enemy = self.enemy.borrow_mut();
+                        let game = enemy.game.upgrade().unwrap();
+                        let mut game = game.borrow_mut();
+                        //game.drop_particle(12, enemy.pos, 1, 0xffffffff);
+                        //game.iter_bullets(|mut bullet| {
+                        for bullet in game.bullets.iter() {
+                            //game.new_effect(bullet.sprite, TODO);
+                            let mut bullet = bullet.borrow_mut();
+                            if arg == 0 {
+                                bullet.speed = 0.;
+                                bullet.dpos = [0., 0., 0.];
+                            } else if arg == 1 {
+                                bullet.flags |= 0x10;
+                                bullet.frame = 220;
+                                let rand_angle = game.prng.borrow_mut().get_f64() * 2. * std::f64::consts::PI - std::f64::consts::PI;
+                                bullet.attributes[0] = (rand_angle.cos() * 0.01) as f32;
+                                bullet.attributes[1] = (rand_angle.sin() * 0.01) as f32;
+                            }
+                        }
+                    }
+                    1 => {
+                        let range_x = arg as f64;
+                        let range_y = (arg as f32 * 0.75) as f64;
+                        let rand_x = self.get_prng().borrow_mut().get_f64();
+                        let rand_y = self.get_prng().borrow_mut().get_f64();
+                        let mut enemy = self.enemy.borrow_mut();
+                        let pos = [rand_x * range_x + enemy.pos.x as f64 - range_x / 2.,
+                                   rand_y * range_y + enemy.pos.x as f64 - range_y / 2.];
+                        enemy.bullet_attributes.fire();
+                    }
+                    3 => { // Patchouli’s dual sign spellcard selector
+                        let mut enemy = self.enemy.borrow_mut();
+                        let mut knowledge: [[i32; 3]; 4] =
+                            [[0, 3, 1],
+                             [2, 3, 4],
+                             [1, 4, 0],
+                             [4, 2, 3]];
+
+                        //TODO: implement select_player and replace character by the correct one
+                        //let character = enemy.select_player().character;
+                        let character = 0;
+                        for i in 1..=3 {
+                            self.frame.ints1[i] = knowledge[character][i];
+                        }
+                    }
+                    4 => { // Sakuya random daggers and time stop
+                        /*
+                        if arg < 2 {
+                            drop_particle(&PARTICLES_ARRAY,0xc,enemy->pos,1,0xffffffff);
+                            //TODO: is that the timestop?
+                            LEVEL.field_0x2c = arg;
+                            return;
+                        }
+                        // this changes the orientation of random bullets
+                        let mut max_bullets = 0xe;
+                        if (LEVEL.rank >= 2) {max_bullets = 0x34;}
+                        i = 0;
+                        for bullet in game.bullets {
+                            if bullet->state != 0 && bullet->state != 5 && 30. <= (bullet->sprites[0].additional_infos)->height && bullet->field_0x5ba != 5 && (uVar3 = prng16(&PRNG_STATE), (uVar3 & 3) == 0) {
+                                bullet->field_0x5ba = 5;
+                                new_effect(GAME_OBJECT,(sprite *)bullet, (int)bullet->sprites[0].sometimes_copy_of_UNK1 + (int)bullet->field_0x5ba);
+                                x = bullet->pos[0] - PLAYER.pos[0];
+                                y = bullet->pos[1] - PLAYER.pos[1];
+                                if sqrt(x*x+y*y) > 128. {
+                                    if LEVEL.rank >= 2 {bullet->automatic_angle = prng_double() * 2*pi;}
+                                    else{bullet->automatic_angle = (prng_double() * ((pi/8)*6) + pi/4);}
+                                    else {
+                                        // TODO: check player_get_angle, might be what ST0 is
+                                        player_get_angle_to(&PLAYER,bullet->pos,local_38);
+                                        bullet->automatic_angle = (extraout_ST0_00 + pi/2 + (prng_double() * pi*2));
+                                    }
+                                    bullet->dpos[0] = cos(bullet->automatic_angle) * bullet->speed;
+                                    bullet->dpos[1] = sin(bullet->automatic_angle) * bullet->speed;
+                                    max_bullets -= 1;
+                                    if (max_bullets == 0) break;
+                                }
+                            }
+                            (enemy->ecl_frame).var_ints_1[2] = 0;*/
+                        }
+                    7 => { // Remilia's lazer maze
+                        // so what this does is kinda complex: 2 rounds of 3 subrounds of 8 shots, either
+                        // laser or standard bullets depending on the argument passed.
+                        // it is done in 2 steps: first we precalculate coordinates of the 8 shots for the first subround
+                        // set the shot properties depending on difficulties and current round and then
+                        // edit  the coordinates for the next round
+                        let rnd_pos = self.get_prng().borrow_mut().get_f64() * 2. * std::f64::consts::PI;
+                        let enemy = self.enemy.borrow();
+                        for i in 0..2 {
+                            let mut pos: [f64; 8*3] = [0.; 8*3];
+                            let mut offset = rnd_pos -((std::f64::consts::PI/8.)*7.);
+                            let mut next_offset = -std::f64::consts::PI/4.;
+                            if (i == 0) {
+                                offset = rnd_pos -std::f64::consts::PI;
+                                next_offset = std::f64::consts::PI/4.;
+                            }
+
+                            // we calculate angle, speed and offset for the 8 shots
+                            let mut offset_copy=offset;
+                            for y in 0..8 {
+                                pos[y * 3] = offset_copy.cos() * 32. + enemy.pos.x as f64;
+                                pos[y * 3 + 1] = offset_copy.sin() * 32. + enemy.pos.y as f64;
+                                pos[y * 3 + 2] = enemy.z as f64;
+                                offset_copy += std::f64::consts::PI/4.;
+                            }
+
+                            // 3 rounds of 8 shots
+                            for z in 0..3 {
+
+                                let mut length = 112.;
+                                // last subround
+                                if (z == 2) {length = 480.;}
+
+                                for y in 0..8 {
+                                    /*
+                                    if (arg == 0) {
+                                        let (mut si, mut ged, mut ed) = (8, 20.,ed=430.);
+                                        if (LEVEL.rank < 2) {si=2; ged=28.; ed=length;}
+                                        laser_args.angle = pos[y * 3];
+                                        laser_args.speed = pos[y * 3 + 1];
+                                        laser_args.start_offset = pos[y * 3 + 2];
+                                        laser_args.type = 1;
+                                        laser_args.sprite_index_offset = si;
+                                        laser_args.end_offset = offset;
+                                        laser_args.width = 0.;
+                                        laser_args.duration = 0;
+                                        laser_args.grazing_extra_duration = ged;
+                                        laser_args.end_duration = ed;
+                                        laser_args.UNK1 = z * 0x10 + 0x3c;
+                                        laser_args.grazing_delay = laser_args.end_duration;
+                                        fire_laser(&ETAMA_ARRAY,&laser_args);
+                                    }
+                                    else {
+                                        (enemy->bullet_attributes).pos[0] = pos[y * 3];
+                                        (enemy->bullet_attributes).pos[1] = pos[y*3+1];
+                                        (enemy->bullet_attributes).pos[2] = pos[y*3+2];
+                                        bullet_fire(&enemy->bullet_attributes,&ETAMA_ARRAY);
+                                    }
+                                    */
+                                    pos[y * 3] = offset.cos() * length + pos[y * 3];
+                                    pos[y * 3 + 1] = offset.sin() * length + pos[y * 3 + 1];
+                                    offset = offset + std::f64::consts::PI/4.;
+                                }
+                                offset = (next_offset - 2.*std::f64::consts::PI) + offset;
+                            }
+                        }
+                    }
+                    8 => { // Vampire Fantasy
+                        let n = {
+                            let enemy = self.enemy.borrow();
+                            let game = enemy.game.upgrade().unwrap();
+                            let mut game = game.borrow_mut();
+                            let mut n = 0;
+                            for bullet in game.bullets.iter() {
+                                let mut bullet = bullet.borrow();
+                                // TODO: uncomment that one.
+                                if bullet.state != 0 && bullet.state != 5 /* && (30. <= (bullet.sprites[0].additional_infos).height) */ {
+                                    let prng = enemy.prng.upgrade().unwrap();
+                                    let random = prng.borrow_mut().get_f64();
+                                    let launch_angle = (random * (2. * std::f64::consts::PI) - std::f64::consts::PI) as f32;
+                                    let mut attribs = BulletAttributes {
+                                        // TODO: check if the z value of this pos is really used.
+                                        pos: bullet.pos,
+                                        anim: 3,
+                                        sprite_index_offset: 1,
+                                        launch_angle,
+                                        speed: 0.,
+                                        angle: 0.,
+                                        speed2: 0.,
+                                        bullets_per_shot: 1,
+                                        number_of_shots: 1,
+                                        flags: 8,
+                                        bullet_type: 1,
+                                        extended_attributes: Default::default(),
+                                        sound: None,
+                                    };
+                                    attribs.fire();
+                                    n += 1
+                                }
+                            }
+                            n
+                        };
+                        //TODO: this variable might not always be correct! it uses the argument in
+                        //th06: *(int *)(param_1 + 0x9b0) = local_60;
+                        self.set_i32(-10004, n);
+                    }
+
+                    9 => {
+                        let mut rnd = self.get_prng().borrow_mut().get_f64();
+                        //TODO: the game does that
+                        //drop_particle(&PARTICLES_ARRAY,0xc,enemy->pos,1,0xffffffff);
+                        //self._game.new_effect((enemy.x, enemy.y), 17)
+                        /*
+                        for bullet in game.bullets {
+                            if bullet._bullet_type.state != 0 && bullet._bullet_type.state != 5 && (30. <= (bullet.sprites[0].additional_infos)->height) && bullet.speed == 0. {
+                                bullet.flags = bullet.flags | 0x10;
+                                //TODO: reverse this field and effect
+                                bullet->field_0x5ba = 2;
+                                new_effect(GAME_OBJECT,(sprite *)bullet, (int)bullet->sprites[0].sometimes_copy_of_UNK1 + (int)bullet->field_0x5ba);
+                                bullet.speed=0.01;
+                                bullet.frame=0x78;
+
+                                let mut dx = bullet.x - enemy.x;
+                                let mut distance = dx.hypot(bullet.y - enemy.y);
+
+                                if distance > 0.01 {
+                                    distance = sqrt(distance);
+                                }else{distance = 0.;}
+                                let mut angle = (distance * std::f64::consts::PI) / 256. + (rnd * (2*std::f64::consts::PI) - std::f64::consts::PI);
+                                bullet->attributes[0] = cos(angle) * 0.01000000;
+                                bullet->attributes[1] = sin(angle) * 0.01000000;
+                            }
+                        }
+                        */
+                    }
+                    11 => {
+                        self.get_prng().borrow_mut().get_f64();
+                        //TODO: the game does that
+                        //drop_particle(&PARTICLES_ARRAY,0xc,enemy->pos,1,0xffffffff);
+                        //self._game.new_effect((enemy.x, enemy.y), 17)
+                        /*
+                        for bullet in game.bullets {
+                            if bullet._bullet_type.state != 0 && bullet._bullet_type.state != 5 && (30. <= (bullet.sprites[0].additional_infos)->height) && bullet.speed == 0. {
+                                bullet.flags = bullet.flags | 0x10;
+                                //TODO: reverse this field and effect
+                                bullet->field_0x5ba = 2;
+                                new_effect(GAME_OBJECT,(sprite *)bullet, (int)bullet->sprites[0].sometimes_copy_of_UNK1 + (int)bullet->field_0x5ba);
+                                bullet.speed=0.01;
+                                bullet.frame=0x78;
+                                let mut angle = self.get_prng().borrow_mut().get_f64() * (2*std::f64::consts::PI) - std::f64::consts::PI;
+                                bullet->attributes[0] = cos(angle) * 0.01000000;
+                                bullet->attributes[1] = sin(angle) * 0.01000000;
+
+
+                            }
+                        }
+                        */
+                    }
+                    13 => {
+                        if self.frame.ints1[3] % 6 == 0 {
+                            let mut _angle=self.frame.floats[2];
+                            /*
+                            (type_, anim, sprite_idx_offset, bullets_per_shot, number_of_shots,
+                            speed, speed2, launch_angle, angle, flags) = self._enemy.bullet_attributes
+                            for i in range(arg) {
+                                //TODO: distance is obtained directly by copying bullet attributes
+                                //in memory
+                                launch_pos = (192 + cos(_angle) * _distance,
+                                    224 + sin(_angle) * _distance);
+
+                                bullet_attributes = (type_, anim, sprite_idx_offset,
+                                     bullets_per_shot, number_of_shots,
+                                     speed, speed2,
+                                     _angle + self.frame.floats[1], angle, flags);
+                                enemy.fire(launch_pos=launch_pos,bullet_attributes=bullet_attributes);
+                                _angle += 2*std::f64::consts::PI/arg;
+                            }*/
+                        }
+                        self.frame.ints1[3] += 1;
+                    }
+                    14 => { // Lävatein
+                        let mut enemy = self.enemy.borrow_mut();
+                        self.frame.ints1[3] = 0;
+                        for laser in enemy.laser_by_id.values() {
+                            //for pos in laser.get_bullets_pos(){
+                            //TODO: the game checks for laser end_offset before firing
+                            //  enemy.fire(launch_pos=pos);
+                            //}
+                            self.frame.ints1[3] += 1;
+                        }
+                    }
+                    16 => { // QED: Ripples of 495 years
+                        let mut enemy = self.enemy.borrow_mut();
+                        let game = enemy.game.upgrade().unwrap();
+                        let mut game = game.borrow_mut();
+                        if arg == 0 {
+                            self.frame.floats[3] = 2. - (enemy.life as f32) / 6000.;
+                            self.frame.ints2[1] = ((enemy.life * 240) / 6000 + 40) as i32;
+                        } else {
+                            let fx = (320. - ((enemy.life as f32) * 160.) / 6000.) as f64;
+                            let fy = (128. - ((enemy.life as f32) * 64.) / 6000.) as f64;
+                            let rand_x = game.prng.borrow_mut().get_f64();
+                            let rand_y = game.prng.borrow_mut().get_f64();
+                            self.frame.floats[2] = (rand_x * fx + (192. - fx / 2.)) as f32;
+                            self.frame.floats[3] = (rand_y * fy + (96. - fy / 2.)) as f32;
+                        }
+                    }
+                    _ => unimplemented!("Special function {:?} not found!", function)
+                }
+            }
+
+            // 122
+            // Here lies the Di Sword of despair
+            SubInstruction::SetSpecialFunctionCallback(function) => {
+                //TODO: those functions are executed at each frame and needs to be written to the
+                //callback function but so far i'm simply focusing on the implementation
+                //NB: the original engine doesn't differenciate between function blocks for ins 121
+                //and 122 but we do here, since it wouldn't make sense the other way around.
+                match function {
+                    12 => {
+                        for i in 0..8 {
+                            /*
+                            if ((enemy->lasers[i] != (laser *)0x0) && (enemy->lasers[i]->state != 0)) {
+                                (enemy->bullet_attributes).pos[0] = cos(enemy->lasers[i]->angle) * 64. + enemy.pos.x;
+                                // yes, it reads pos[0] after it has been modified and yes, this most
+                                // likely is a bug
+                                (enemy->bullet_attributes).pos[1] = cos(enemy->lasers[i]->angle) * (enemy->bullet_attributes).pos[0] + enemy.pos.y;
+                                (enemy->bullet_attributes).pos[2] = enemy.z;
+                                bullet_fire(&enemy->bullet_attributes,&ETAMA_ARRAY);
+                            }*/
+                        }
+                    }
+                    _ => unimplemented!("Special function {:?} not found!", function)
+                }
+            }
+            _ => unimplemented!("{:?}", instruction)
+        }
+
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::th06::anm0::Anm0;
+    use crate::th06::ecl::{Sub, CallSub, Rank};
+    use crate::th06::enemy::Game;
+    use std::io::{self, Read};
+    use std::fs::File;
+
+    fn setup() -> (Rc<RefCell<Game>>, Rc<RefCell<Enemy>>) {
+        let file = File::open("EoSD/ST/stg1enm.anm").unwrap();
+        let mut file = io::BufReader::new(file);
+        let mut buf = vec![];
+        file.read_to_end(&mut buf).unwrap();
+        let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
+        let anm0 = anms.pop().unwrap();
+        let anm0 = Rc::new(RefCell::new(anm0));
+        let prng = Rc::new(RefCell::new(Prng::new(0)));
+        let game = Game::new(prng, Rank::EASY);
+        let game = Rc::new(RefCell::new(game));
+        let enemy = Enemy::new(Position::new(0., 0.), 500, 0, 640, Rc::downgrade(&anm0), Rc::downgrade(&game));
+        (game, enemy)
+    }
+
+    #[test]
+    fn call_and_return() {
+        let (game, enemy) = setup();
+        let ecl = Ecl { mains: vec![], subs: vec![
+            Sub { instructions: vec![
+                CallSub::new(0, Rank::EASY, SubInstruction::Call(1, 13, 12.)),
+            ]},
+            Sub { instructions: vec![
+                CallSub::new(0, Rank::EASY, SubInstruction::Noop()),
+                CallSub::new(1, Rank::EASY, SubInstruction::Return()),
+            ]},
+        ]};
+        let mut ecl_runner = EclRunner::new(&ecl, enemy, 0);
+        ecl_runner.run_frame();
+        assert_eq!(ecl_runner.frame.ints1[0], 13);
+        assert_eq!(ecl_runner.frame.floats[0], 12.);
+        assert_eq!(ecl_runner.stack.len(), 1);
+        ecl_runner.run_frame();
+        assert_eq!(ecl_runner.frame.ints1[0], 0);
+        assert_eq!(ecl_runner.frame.floats[0], 0.);
+        assert_eq!(ecl_runner.stack.len(), 0);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interpreters/src/th06/enemy.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,571 @@
+//! Module providing an Enemy struct, to be changed by EclRunner.
+
+use touhou_formats::th06::anm0::Anm0;
+use touhou_formats::th06::ecl::Rank;
+use crate::th06::anm0::{Sprite, AnmRunner};
+use crate::th06::interpolator::{Interpolator1, Interpolator2};
+use touhou_utils::prng::Prng;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::rc::{Rc, Weak};
+
+/// The 2D position of an object in the game.
+#[derive(Debug, Clone, Copy, Default, PartialEq)]
+pub struct Position {
+    pub(crate) x: f32,
+    pub(crate) y: f32,
+}
+
+/// An offset which can be added to a Position.
+#[derive(Debug, Clone, Copy, Default, PartialEq)]
+pub struct Offset {
+    pub(crate) dx: f32,
+    pub(crate) dy: f32,
+}
+
+impl Position {
+    /// Create said position.
+    pub fn new(x: f32, y: f32) -> Position {
+        Position { x, y }
+    }
+}
+
+impl Offset {
+    /// Create said offset.
+    pub fn new(dx: f32, dy: f32) -> Offset {
+        Offset { dx, dy }
+    }
+}
+
+impl std::ops::Add<Offset> for Position {
+    type Output = Position;
+    fn add(self, offset: Offset) -> Position {
+        Position {
+            x: self.x + offset.dx,
+            y: self.y + offset.dy,
+        }
+    }
+}
+
+impl std::ops::Sub<Position> for Position {
+    type Output = Offset;
+    fn sub(self, other: Position) -> Offset {
+        Offset {
+            dx: other.x - self.x,
+            dy: other.y - self.y,
+        }
+    }
+}
+
+type Callback = i32;
+
+#[derive(Debug, Clone)]
+/// XXX
+pub struct Laser {
+    /// XXX
+    pub placeholder: u32
+}
+
+#[derive(Debug, Clone, Default)]
+struct Process;
+
+/// Struct representing the player.
+pub struct Player {
+    pos: Position,
+}
+
+/// Struct representing an enemy bullet.
+pub struct Bullet {
+    /// Current position of the bullet.
+    pub pos: Position,
+
+    /// Current speed of the bullet.
+    pub speed: f32,
+
+    /// Current XXX of the bullet.
+    pub dpos: [f32; 3],
+
+    /// Current XXX of the bullet.
+    pub flags: u32,
+
+    /// Current frame of the bullet.
+    pub frame: i32,
+
+    /// Current attributes of the bullet.
+    pub attributes: [f32; 2],
+
+    /// TODO: what are the values?
+    pub state: i8,
+}
+
+/// God struct of our game.
+pub struct Game {
+    enemies: Vec<Rc<RefCell<Enemy>>>,
+    anmrunners: Vec<Rc<RefCell<AnmRunner>>>,
+    pub(crate) bullets: Vec<Rc<RefCell<Bullet>>>,
+    player: Rc<RefCell<Player>>,
+    pub(crate) prng: Rc<RefCell<Prng>>,
+    rank: Rank,
+    difficulty: i32,
+}
+
+impl Game {
+    /// Create said god struct.
+    pub fn new(prng: Rc<RefCell<Prng>>, rank: Rank) -> Game {
+        Game {
+            enemies: Vec::new(),
+            anmrunners: Vec::new(),
+            bullets: Vec::new(),
+            player: Rc::new(RefCell::new(Player { pos: Position { x: 192., y: 384. } })),
+            prng,
+            rank,
+            difficulty: 0,
+        }
+    }
+
+    /// Run the simulation for a single frame.
+    pub fn run_frame(&mut self) {
+        /*
+        for eclrunner in self.eclrunners {
+            eclrunner.run_frame();
+        }
+        */
+
+        for anmrunner in self.anmrunners.iter() {
+            let mut anmrunner = anmrunner.borrow_mut();
+            anmrunner.run_frame();
+        }
+    }
+
+    /// Returns a list of all sprites currently being displayed on screen.
+    pub fn get_sprites(&self) -> Vec<(f32, f32, f32, Rc<RefCell<Sprite>>)> {
+        let mut sprites = vec![];
+        for enemy in self.enemies.iter() {
+            let enemy = enemy.borrow();
+            let anmrunner = enemy.anmrunner.upgrade().unwrap();
+            let anmrunner = anmrunner.borrow();
+            let sprite = anmrunner.get_sprite();
+            sprites.push((enemy.pos.x, enemy.pos.y, enemy.z, sprite));
+        }
+        sprites
+    }
+
+    // TODO: Fix this function so we can stop making Game::bullets pub.
+    /*
+    /// Apply a function on all bullets.
+    pub fn iter_bullets(&mut self, mut f: impl FnMut(Bullet)) {
+        self.bullets.iter().map(|bullet| {
+            let mut bullet = bullet.borrow_mut();
+            f(*bullet)
+        });
+    }
+    */
+
+    pub(crate) fn get_player(&self) -> Rc<RefCell<Player>> {
+        self.player.clone()
+    }
+}
+
+/// Common to all elements in game.
+struct Element {
+    pos: Position,
+    removed: bool,
+    anmrunner: AnmRunner,
+}
+
+#[derive(PartialEq)]
+pub(crate) struct DifficultyCoeffs {
+    pub(crate) speed_a: f32,
+    pub(crate) speed_b: f32,
+    pub(crate) nb_a: i16,
+    pub(crate) nb_b: i16,
+    pub(crate) shots_a: i16,
+    pub(crate) shots_b: i16,
+}
+
+impl Default for DifficultyCoeffs {
+    fn default() -> DifficultyCoeffs {
+        DifficultyCoeffs {
+            speed_a: -0.5,
+            speed_b: 0.5,
+            nb_a: 0,
+            nb_b: 0,
+            shots_a: 0,
+            shots_b: 0,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Default, PartialEq)]
+pub(crate) struct BulletAttributes {
+    pub(crate) anim: i16,
+    pub(crate) sprite_index_offset: i16,
+    pub(crate) pos: Position, // Doesn’t have a z field.
+    pub(crate) launch_angle: f32,
+    pub(crate) angle: f32,
+    pub(crate) speed: f32,
+    pub(crate) speed2: f32,
+    pub(crate) extended_attributes: (i32, i32, i32, i32, f32, f32, f32, f32),
+    // unknown: x32,
+    pub(crate) bullets_per_shot: i16,
+    pub(crate) number_of_shots: i16,
+    pub(crate) bullet_type: i16,
+    // zero: x32,
+    pub(crate) flags: u32,
+
+    /// Which sound to play when the bullet gets fired.
+    pub sound: Option<u8>,
+}
+
+impl BulletAttributes {
+    /// Fire!
+    pub fn fire(&mut self) {
+        println!("PAN!");
+    }
+}
+
+#[derive(PartialEq)]
+pub(crate) enum Direction {
+    Left,
+    Center,
+    Right,
+}
+
+impl Default for Direction {
+    fn default() -> Direction {
+        Direction::Center
+    }
+}
+
+/// The enemy struct, containing everything pertaining to an enemy.
+#[derive(Default)]
+pub struct Enemy {
+    // Common to all elements in game.
+    pub(crate) pos: Position,
+    pub(crate) removed: bool,
+    pub(crate) anmrunner: Weak<RefCell<AnmRunner>>,
+
+    // Specific to enemy.
+    // Floats.
+    pub(crate) z: f32,
+    pub(crate) angle: f32,
+    pub(crate) speed: f32,
+    pub(crate) rotation_speed: f32,
+    pub(crate) acceleration: f32,
+
+    // Ints.
+    pub(crate) type_: u32,
+    pub(crate) bonus_dropped: u32,
+    pub(crate) die_score: u32,
+    /// XXX
+    pub frame: u32,
+    pub(crate) life: u32,
+    pub(crate) death_flags: u32,
+    pub(crate) current_laser_id: u32,
+    pub(crate) low_life_trigger: Option<u32>,
+    pub(crate) timeout: Option<u32>,
+    pub(crate) remaining_lives: u32,
+    bullet_launch_interval: u32,
+    bullet_launch_timer: u32,
+    pub(crate) death_anim: i32,
+    pub(crate) direction: Direction,
+    pub(crate) update_mode: u32,
+
+    // Bools.
+    pub(crate) visible: bool,
+    pub(crate) was_visible: bool,
+    pub(crate) touchable: bool,
+    pub(crate) collidable: bool,
+    pub(crate) damageable: bool,
+    pub(crate) boss: bool,
+    pub(crate) automatic_orientation: bool,
+    pub(crate) delay_attack: bool,
+    // Actually part of type_ atm.
+    pub(crate) mirror: bool,
+
+    // Tuples.
+    pub(crate) difficulty_coeffs: DifficultyCoeffs,
+    pub(crate) bullet_attributes: BulletAttributes,
+    pub(crate) bullet_offset: Offset,
+    pub(crate) movement_dependant_sprites: Option<(u8, u8, u8, u8)>,
+    pub(crate) screen_box: Option<(f32, f32, f32, f32)>,
+
+    // Callbacks.
+    pub(crate) death_callback: Option<Callback>,
+    pub(crate) boss_callback: Option<Callback>,
+    pub(crate) low_life_callback: Option<Callback>,
+    pub(crate) timeout_callback: Option<Callback>,
+
+    // Laser.
+    pub(crate) laser_by_id: HashMap<u32, Laser>,
+
+    // Options.
+    // TODO: actually a 8 element array.
+    options: Vec<Element>,
+
+    // Interpolators.
+    pub(crate) interpolator: Option<Interpolator2<f32>>,
+    pub(crate) speed_interpolator: Option<Interpolator1<f32>>,
+
+    // Misc stuff, do we need them?
+    pub(crate) anm0: Weak<RefCell<[Anm0; 2]>>,
+    process: Rc<RefCell<Process>>,
+    pub(crate) game: Weak<RefCell<Game>>,
+    pub(crate) prng: Weak<RefCell<Prng>>,
+    pub(crate) hitbox_half_size: [f32; 2],
+}
+
+impl Enemy {
+    /// Create a new enemy.
+    pub fn new(pos: Position, life: i16, bonus_dropped: i16, die_score: u32, mirror: bool, anm0: Weak<RefCell<[Anm0; 2]>>, game: Weak<RefCell<Game>>) -> Rc<RefCell<Enemy>> {
+        let game_rc = game.upgrade().unwrap();
+        let mut enemy = Enemy {
+            pos,
+            anm0,
+            game,
+            visible: true,
+            // XXX: shouldn’t be u32, since that can be -1.
+            bonus_dropped: bonus_dropped as u32,
+            die_score,
+            life: if life < 0 { 1 } else { life as u32 },
+            touchable: true,
+            collidable: true,
+            damageable: true,
+            mirror,
+            ..Default::default()
+        };
+        let mut game = game_rc.borrow_mut();
+        enemy.prng = Rc::downgrade(&game.prng);
+        let enemy = Rc::new(RefCell::new(enemy));
+        game.enemies.push(enemy.clone());
+        enemy
+    }
+
+    /// Sets the animation to the one indexed by index in the current anm0.
+    pub fn set_anim(&mut self, index: u8) {
+        let anm0 = self.anm0.upgrade().unwrap();
+        let game = self.game.upgrade().unwrap();
+        let sprite = Rc::new(RefCell::new(Sprite::new()));
+        let anmrunner = AnmRunner::new(anm0, index, sprite, self.prng.clone(), 0);
+        let anmrunner = Rc::new(RefCell::new(anmrunner));
+        self.anmrunner = Rc::downgrade(&anmrunner);
+        (*game.borrow_mut()).anmrunners.push(anmrunner);
+    }
+
+    /// Sets the current position of the enemy.
+    pub fn set_pos(&mut self, x: f32, y: f32, z: f32) {
+        self.pos.x = x;
+        self.pos.y = y;
+        self.z = z;
+    }
+
+    /// Sets the hitbox around the enemy.
+    pub fn set_hitbox(&mut self, width: f32, height: f32) {
+        self.hitbox_half_size = [width / 2., height / 2.];
+    }
+
+    /// Defines the attributes for the next bullet fired, and fire it if delay_attack isn’t set!
+    pub fn set_bullet_attributes(&mut self, opcode: u16, anim: i16, sprite_index_offset: i16,
+                                 bullets_per_shot: i16, number_of_shots: i16, speed: f32,
+                                 speed2: f32, launch_angle: f32, angle: f32, flags: u32) {
+        // Get the coeffs for the current difficulty.
+        let difficulty = self.get_difficulty() as i16;
+        let coeff_nb = self.difficulty_coeffs.nb_a + (self.difficulty_coeffs.nb_b - self.difficulty_coeffs.nb_a) * difficulty / 32;
+        let coeff_shots = self.difficulty_coeffs.shots_a + (self.difficulty_coeffs.shots_b - self.difficulty_coeffs.shots_a) * difficulty / 32;
+        let coeff_speed = self.difficulty_coeffs.speed_a + (self.difficulty_coeffs.speed_b - self.difficulty_coeffs.speed_a) * difficulty as f32 / 32.;
+
+        let opcode = 67;
+        let mut bullet = &mut self.bullet_attributes;
+
+        bullet.anim = anim;
+        bullet.bullet_type = opcode - 67;
+        bullet.sprite_index_offset = sprite_index_offset;
+
+        bullet.bullets_per_shot = bullets_per_shot + coeff_nb;
+        if bullet.bullets_per_shot < 1 {
+            bullet.bullets_per_shot = 1;
+        }
+
+        bullet.number_of_shots = number_of_shots + coeff_shots;
+        if bullet.number_of_shots < 1 {
+            bullet.number_of_shots = 1;
+        }
+
+        bullet.pos = self.pos + self.bullet_offset;
+
+        bullet.speed = speed + coeff_speed;
+        if bullet.speed < 0.3 {
+            bullet.speed = 0.3;
+        }
+
+        bullet.speed2 = speed2 + coeff_speed / 2.;
+        if bullet.speed2 < 0.3 {
+            bullet.speed2 = 0.3;
+        }
+
+        bullet.launch_angle = launch_angle.atan2(0.);
+        bullet.angle = angle;
+        bullet.flags = flags;
+
+        if !self.delay_attack {
+            bullet.fire();
+        }
+    }
+
+    /// Sets the bullet launch interval.
+    pub(crate) fn set_bullet_launch_interval(&mut self, rand_start: u32, interval: i32) {
+        let coeff_interval = interval / 5;
+        let difficulty_modifier = coeff_interval + (-coeff_interval * 2) * self.get_difficulty() / 32;
+        self.bullet_launch_interval = (interval + difficulty_modifier) as u32;
+        if self.bullet_launch_interval > 0 {
+            self.bullet_launch_timer = rand_start % self.bullet_launch_interval;
+        }
+    }
+
+    /// Stubbed for now.
+    pub(crate) fn play_sound(&self, sound_index: i32) {
+        println!("Playing sound {}!", sound_index);
+    }
+
+    /// Stubbed for now.
+    pub(crate) fn set_boss(&self, enable: bool) {
+        match enable {
+            true => println!("Enemy is now boss!"),
+            false => println!("Enemy is not boss anymore."),
+        }
+    }
+
+    /// Run all interpolators and such, and update internal variables once per
+    /// frame.
+    pub fn update(&mut self) {
+        let Position { mut x, mut y } = self.pos;
+
+        let speed = if self.update_mode == 1 {
+            let mut speed = 0.;
+            if let Some(interpolator) = &self.interpolator {
+                let values = interpolator.values(self.frame);
+                x = values[0];
+                y = values[1];
+            }
+            if let Some(interpolator) = &self.speed_interpolator {
+                let values = interpolator.values(self.frame);
+                speed = values[0];
+            }
+            speed
+        } else {
+            let speed = self.speed;
+            self.speed += self.acceleration;
+            self.angle += self.rotation_speed;
+            speed
+        };
+
+        let dx = self.angle.cos() * speed;
+        let dy = self.angle.sin() * speed;
+        if self.mirror {
+            x -= dx;
+        } else {
+            x += dx;
+        }
+        y += dy;
+
+        if let Some((end_left, end_right, left, right)) = self.movement_dependant_sprites {
+            if x < self.pos.x && self.direction != Direction::Left {
+                self.set_anim(left);
+                self.direction = Direction::Left;
+            } else if x > self.pos.x && self.direction != Direction::Right {
+                self.set_anim(right);
+                self.direction = Direction::Right;
+            } else if x == self.pos.x && self.direction != Direction::Center {
+                let anim = if self.direction == Direction::Left {
+                    end_left
+                } else {
+                    end_right
+                };
+                self.set_anim(anim);
+                self.direction = Direction::Center;
+            }
+        }
+
+        self.pos = Position { x, y };
+
+        if self.bullet_launch_interval != 0 {
+            if self.bullet_launch_timer == 0 {
+                self.bullet_attributes.fire();
+                self.bullet_launch_timer = self.bullet_launch_interval;
+            }
+            self.bullet_launch_timer += 1;
+            self.bullet_launch_timer %= self.bullet_launch_interval;
+        }
+
+        self.frame += 1;
+    }
+
+    pub(crate) fn get_rank(&self) -> Rank {
+        let game = self.game.upgrade().unwrap();
+        let game = game.borrow();
+        game.rank
+    }
+
+    pub(crate) fn get_difficulty(&self) -> i32 {
+        let game = self.game.upgrade().unwrap();
+        let game = game.borrow();
+        game.difficulty
+    }
+
+    // TODO: use a trait for positionable entities.
+    pub(crate) fn get_angle_to(&self, player: Rc<RefCell<Player>>) -> f32 {
+        let player = player.borrow();
+        let offset = self.pos - player.pos;
+        offset.dy.atan2(offset.dx)
+    }
+
+    pub(crate) fn set_aux_anm(&self, number: i32, script: i32) {
+        println!("TODO: Spawn aux anm {} with script {}.", number, script);
+    }
+}
+
+trait Renderable {
+    fn get_sprites(&self) -> Vec<Rc<RefCell<Sprite>>>;
+}
+
+impl Renderable for Enemy {
+    fn get_sprites(&self) -> Vec<Rc<RefCell<Sprite>>> {
+        let anmrunner = self.anmrunner.upgrade().unwrap();
+        let anmrunner = anmrunner.borrow();
+        vec![anmrunner.get_sprite()]
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::{self, Read};
+    use std::fs::File;
+
+    #[test]
+    fn enemy() {
+        let file = File::open("EoSD/ST/stg1enm.anm").unwrap();
+        let mut file = io::BufReader::new(file);
+        let mut buf = vec![];
+        file.read_to_end(&mut buf).unwrap();
+        let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
+        let anm0 = anms.pop().unwrap();
+
+        let file = File::open("EoSD/ST/stg1enm2.anm").unwrap();
+        let mut file = io::BufReader::new(file);
+        let mut buf = vec![];
+        file.read_to_end(&mut buf).unwrap();
+        let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
+        let anm0_bis = anms.pop().unwrap();
+
+        let anm0 = Rc::new(RefCell::new([anm0, anm0_bis]));
+        let prng = Rc::new(RefCell::new(Prng::new(0)));
+        let game = Game::new(prng, Rank::EASY);
+        let game = Rc::new(RefCell::new(game));
+        let enemy = Enemy::new(Position::new(0., 0.), 500, 0, 640, Rc::downgrade(&anm0), Rc::downgrade(&game));
+        let mut enemy = enemy.borrow_mut();
+        assert!(enemy.anmrunner.upgrade().is_none());
+        enemy.set_anim(0);
+        assert!(enemy.anmrunner.upgrade().is_some());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interpreters/src/th06/interpolator.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,96 @@
+//! Animation runner.
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub(crate) enum Formula {
+    Linear,
+    Power2,
+    InvertPower2,
+}
+
+impl Formula {
+    fn apply(&self, x: f32) -> f32 {
+        match self {
+            Formula::Linear => x,
+            Formula::Power2 => x * x,
+            Formula::InvertPower2 => 2. * x - x * x,
+        }
+    }
+}
+
+macro_rules! generate_interpolator {
+    ($name:ident, $n:tt) => {
+        #[derive(Debug, Clone)]
+        pub(crate) struct $name<T> {
+            start_values: [T; $n],
+            end_values: [T; $n],
+            start_frame: u32,
+            end_frame: u32,
+            formula: Formula,
+        }
+
+        impl<T> $name<T>
+        where f32: From<T>,
+              T: From<f32>,
+              T: std::ops::Sub<Output = T>,
+              T: std::ops::Add<Output = T>,
+              T: Copy,
+              T: Default,
+        {
+            pub fn new(start_values: [T; $n], start_frame: u32, end_values: [T; $n], end_frame: u32, formula: Formula) -> $name<T> {
+                $name {
+                    start_values,
+                    end_values,
+                    start_frame,
+                    end_frame,
+                    formula,
+                }
+            }
+
+            pub fn set_start(&mut self, frame: u32, values: [T; $n]) {
+                self.start_values = values;
+                self.start_frame = frame;
+            }
+
+            pub fn set_end(&mut self, frame: u32, values: [T; $n]) {
+                self.end_values = values;
+                self.end_frame = frame;
+            }
+
+            pub fn set_end_values(&mut self, values: [T; $n]) {
+                self.end_values = values;
+            }
+
+            pub fn set_end_frame(&mut self, frame: u32) {
+                self.end_frame = frame;
+            }
+
+            // XXX: Make it return [T; $n] instead, we don’t want to only do f32 here.
+            pub fn values(&self, frame: u32) -> [f32; $n] {
+                if frame + 1 >= self.end_frame {
+                    // XXX: skip the last interpolation step.
+                    // This bug is replicated from the original game.
+                    //self.start_frame = self.end_frame;
+                    //self.end_values
+                    let mut values: [f32; $n] = [Default::default(); $n];
+                    for (i, value) in self.end_values.iter().enumerate() {
+                        values[i] = f32::from(*value);
+                    }
+                    values
+                } else {
+                    let mut coeff = (frame - self.start_frame) as f32 / (self.end_frame - self.start_frame) as f32;
+                    coeff = self.formula.apply(coeff);
+                    let mut values: [f32; $n] = [Default::default(); $n];
+                    for (i, (start, end)) in self.start_values.iter().zip(&self.end_values).enumerate() {
+                        values[i] = f32::from(*start + T::from(coeff * f32::from(*end - *start)));
+                    }
+                    values
+                }
+            }
+        }
+    };
+}
+
+generate_interpolator!(Interpolator1, 1);
+generate_interpolator!(Interpolator2, 2);
+generate_interpolator!(Interpolator3, 3);
+//generate_interpolator!(Interpolator4, 4);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interpreters/src/th06/std.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,102 @@
+//! Interpreter of STD files.
+
+use touhou_formats::th06::std::{Stage, Call, Instruction};
+use crate::th06::interpolator::{Interpolator3, Formula};
+use touhou_utils::math::{Mat4, setup_camera};
+use std::cell::RefCell;
+use std::rc::Rc;
+
+/// Interpreter for Stage.
+pub struct StageRunner {
+    /// XXX: no pub.
+    pub stage: Rc<RefCell<Stage>>,
+    frame: u32,
+
+    position: Interpolator3<f32>,
+    direction: Interpolator3<f32>,
+
+    /// XXX: no pub.
+    pub fog_color: [f32; 4],
+    /// XXX: no pub.
+    pub fog_near: f32,
+    /// XXX: no pub.
+    pub fog_far: f32,
+}
+
+impl StageRunner {
+    /// Create a new StageRunner attached to a Stage.
+    pub fn new(stage: Rc<RefCell<Stage>>) -> StageRunner {
+        StageRunner {
+            stage,
+            frame: 0,
+            position: Interpolator3::new([0., 0., 0.], 0, [0., 0., 0.], 0, Formula::Linear),
+            direction: Interpolator3::new([0., 0., 0.], 0, [0., 0., 0.], 0, Formula::Linear),
+            fog_color: [1.; 4],
+            fog_near: 0.,
+            fog_far: 1000.,
+        }
+    }
+
+    /// Advance the simulation one frame.
+    pub fn run_frame(&mut self) {
+        let stage = self.stage.borrow();
+
+        for Call { time, instr } in stage.script.iter() {
+            let time = *time;
+            if time != self.frame {
+                continue;
+            }
+
+            println!("{} {:?}", time, instr);
+
+            match *instr {
+                Instruction::SetViewpos(x, y, z) => {
+                    self.position.set_start(time, [x, y, z]);
+                    for Call { time, instr } in stage.script.iter().cloned() {
+                        if time <= self.frame {
+                            continue;
+                        }
+                        if let Instruction::SetViewpos(x, y, z) = instr {
+                            self.position.set_end(time, [x, y, z]);
+                            break;
+                        }
+                    }
+                }
+                Instruction::SetFog(b, g, r, a, near, far) => {
+                    self.fog_color = [r as f32 / 255., g as f32 / 255., b as f32 / 255., a as f32 / 255.];
+                    self.fog_near = near;
+                    self.fog_far = far;
+                }
+                Instruction::SetViewpos2(dx, dy, dz) => {
+                    let direction = [dx, dy, dz];
+                    self.direction.set_start(time, if time == 0 { direction } else { self.direction.values(time) });
+                    self.direction.set_end_values(direction);
+                }
+                Instruction::StartInterpolatingViewpos2(frame, _, _) => {
+                    self.direction.set_end_frame(time + frame);
+                }
+                Instruction::StartInterpolatingFog(frame, _, _) => {
+                }
+                Instruction::Unknown(_, _, _) => {
+                }
+            }
+        }
+
+        self.frame += 1;
+    }
+
+    /// Generate the model-view matrix for the current frame.
+    pub fn get_model_view(&self) -> Mat4 {
+        let [x, y, z] = self.position.values(self.frame);
+
+        let [dx, dy, dz] = self.direction.values(self.frame);
+
+        let view = setup_camera(dx, dy, dz);
+
+        let model = Mat4::new([[1., 0., 0., 0.],
+                               [0., 1., 0., 0.],
+                               [0., 0., 1., 0.],
+                               [-x, -y, -z, 1.]]);
+        model * view
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runners/Cargo.toml	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,20 @@
+[package]
+name = "touhou-runners"
+version = "0.1.0"
+authors = ["Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>"]
+edition = "2018"
+description = "Runners for Touhou games"
+homepage = "https://pytouhou.linkmauve.fr"
+license = "GPL-3.0-or-later"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+touhou-formats = "*"
+touhou-interpreters = "*"
+touhou-utils = "*"
+image = { version = "0.23", default-features = false, features = ["png", "jpeg"] }
+luminance = "0.39"
+luminance-glfw = { version = "0.12", default-features = false, features = ["log-errors"] }
+luminance-derive = "0.5"
+ears = "0.8"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runners/src/bin/anmrenderer.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,214 @@
+use luminance::blending::{Equation, Factor};
+use luminance::context::GraphicsContext;
+use luminance::pipeline::{BoundTexture, PipelineState};
+use luminance::pixel::NormUnsigned;
+use luminance::render_state::RenderState;
+use luminance::shader::program::{Program, Uniform};
+use luminance::tess::{Mode, TessBuilder};
+use luminance::texture::Dim2;
+use luminance_derive::{Semantics, Vertex, UniformInterface};
+use luminance_glfw::{Action, Key, WindowEvent, GlfwSurface, Surface, WindowDim, WindowOpt};
+use touhou_formats::th06::anm0::Anm0;
+use touhou_interpreters::th06::anm0::{AnmRunner, Sprite, Vertex as FakeVertex};
+use touhou_utils::math::{perspective, setup_camera};
+use touhou_utils::prng::Prng;
+use std::cell::RefCell;
+use std::rc::Rc;
+use std::env;
+use std::path::Path;
+
+use touhou_runners::common::{load_file_into_vec, load_anm_image, LoadedTexture};
+
+const VS: &str = r#"
+in ivec3 in_position;
+in vec2 in_texcoord;
+in vec4 in_color;
+
+uniform mat4 mvp;
+
+out vec2 texcoord;
+out vec4 color;
+
+void main()
+{
+    gl_Position = mvp * vec4(vec3(in_position), 1.0);
+    texcoord = vec2(in_texcoord);
+
+    // It’s already normalized from the u8 being passed.
+    color = in_color;
+}
+"#;
+
+const FS: &str = r#"
+in vec2 texcoord;
+in vec4 color;
+
+uniform sampler2D color_map;
+
+out vec4 frag_color;
+
+void main()
+{
+    frag_color = texture(color_map, texcoord) * color;
+}
+"#;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Semantics)]
+pub enum Semantics {
+    #[sem(name = "in_position", repr = "[i16; 3]", wrapper = "VertexPosition")]
+    Position,
+
+    #[sem(name = "in_texcoord", repr = "[f32; 2]", wrapper = "VertexTexcoord")]
+    Texcoord,
+
+    #[sem(name = "in_color", repr = "[u8; 4]", wrapper = "VertexColor")]
+    Color,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, PartialEq, Vertex)]
+#[vertex(sem = "Semantics")]
+struct Vertex {
+    pos: VertexPosition,
+    uv: VertexTexcoord,
+    #[vertex(normalized = "true")]
+    rgba: VertexColor,
+}
+
+#[derive(UniformInterface)]
+struct ShaderInterface {
+    // the 'static lifetime acts as “anything” here
+    color_map: Uniform<&'static BoundTexture<'static, Dim2, NormUnsigned>>,
+
+    #[uniform(name = "mvp")]
+    mvp: Uniform<[[f32; 4]; 4]>,
+}
+
+fn fill_vertices_ptr(sprite: Rc<RefCell<Sprite>>, vertices: *mut Vertex) {
+    let mut fake_vertices = unsafe { std::mem::transmute::<*mut Vertex, &mut [FakeVertex; 4]>(vertices) };
+    sprite.borrow().fill_vertices(&mut fake_vertices, 0., 0., 0.);
+}
+
+fn fill_vertices(sprite: Rc<RefCell<Sprite>>, vertices: &mut [Vertex; 4]) {
+    let mut fake_vertices = unsafe { std::mem::transmute::<&mut [Vertex; 4], &mut [FakeVertex; 4]>(vertices) };
+    sprite.borrow().fill_vertices(&mut fake_vertices, 0., 0., 0.);
+}
+
+fn main() {
+    // Parse arguments.
+    let args: Vec<_> = env::args().collect();
+    if args.len() != 3 {
+        eprintln!("Usage: {} <ANM file> <script number>", args[0]);
+        return;
+    }
+    let anm_filename = Path::new(&args[1]);
+    let script: u8 = args[2].parse().expect("number");
+
+    // Open the ANM file.
+    let buf = load_file_into_vec(anm_filename).unwrap();
+    let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
+    let anm0 = anms.pop().unwrap();
+
+    if !anm0.scripts.contains_key(&script) {
+        eprintln!("This anm0 doesn’t contain a script named {}.", script);
+        return;
+    }
+
+    // Create the sprite.
+    let sprite = Rc::new(RefCell::new(Sprite::new()));
+
+    // TODO: seed this PRNG with a valid seed.
+    let prng = Rc::new(RefCell::new(Prng::new(0)));
+
+    let mut surface = GlfwSurface::new(WindowDim::Windowed(384, 448), "Touhou", WindowOpt::default()).unwrap();
+
+    // Open the image atlas matching this ANM.
+    let tex = load_anm_image(&mut surface, &anm0, anm_filename).expect("image loading");
+
+    // Create the AnmRunner from the ANM and the sprite.
+    let anms = Rc::new(RefCell::new([anm0]));
+    let mut anm_runner = AnmRunner::new(anms, script, sprite.clone(), Rc::downgrade(&prng), 0);
+
+    assert_eq!(std::mem::size_of::<Vertex>(), std::mem::size_of::<FakeVertex>());
+    let mut vertices: [Vertex; 4] = {
+        let data = std::mem::MaybeUninit::uninit();
+        unsafe { data.assume_init() }
+    };
+    fill_vertices(sprite.clone(), &mut vertices);
+
+    // set the uniform interface to our type so that we can read textures from the shader
+    let program =
+        Program::<Semantics, (), ShaderInterface>::from_strings(None, VS, None, FS).expect("program creation").ignore_warnings();
+
+    let mut tess = TessBuilder::new(&mut surface)
+        .add_vertices(vertices)
+        .set_mode(Mode::TriangleFan)
+        .build()
+        .unwrap();
+
+    let mut back_buffer = surface.back_buffer().unwrap();
+    let mut resize = false;
+
+    'app: loop {
+        for event in surface.poll_events() {
+            match event {
+                WindowEvent::Close | WindowEvent::Key(Key::Escape, _, Action::Release, _) => break 'app,
+
+                WindowEvent::FramebufferSize(..) => {
+                    resize = true;
+                }
+
+                _ => (),
+            }
+        }
+
+        if resize {
+            back_buffer = surface.back_buffer().unwrap();
+            resize = false;
+        }
+
+        {
+            let mut slice = tess
+                .as_slice_mut()
+                .unwrap();
+
+            anm_runner.run_frame();
+            fill_vertices_ptr(sprite.clone(), slice.as_mut_ptr());
+        }
+
+        // here, we need to bind the pipeline variable; it will enable us to bind the texture to the GPU
+        // and use it in the shader
+        surface
+            .pipeline_builder()
+            .pipeline(&back_buffer, &PipelineState::default(), |pipeline, mut shd_gate| {
+                // bind our fancy texture to the GPU: it gives us a bound texture we can use with the shader
+                let bound_tex = match &tex {
+                    LoadedTexture::Rgb(tex) => pipeline.bind_texture(tex),
+                    LoadedTexture::Rgba(tex) => pipeline.bind_texture(tex),
+                    LoadedTexture::RgbaArray(tex) => unreachable!(),
+                };
+
+                shd_gate.shade(&program, |iface, mut rdr_gate| {
+                    // update the texture; strictly speaking, this update doesn’t do much: it just tells the GPU
+                    // to use the texture passed as argument (no allocation or copy is performed)
+                    iface.color_map.update(&bound_tex);
+                    //let mvp = ortho_2d(0., 384., 448., 0.);
+                    let proj = perspective(0.5235987755982988, 384. / 448., 101010101./2010101., 101010101./10101.);
+                    let view = setup_camera(0., 0., 1.);
+                    let mvp = view * proj;
+                    //println!("{:#?}", mvp);
+                    // TODO: check how to pass by reference.
+                    iface.mvp.update(*mvp.borrow_inner());
+
+                    let render_state = RenderState::default()
+                        .set_blending((Equation::Additive, Factor::SrcAlpha, Factor::SrcAlphaComplement));
+
+                    rdr_gate.render(&render_state, |mut tess_gate| {
+                        tess_gate.render(&tess);
+                    });
+                });
+            });
+
+        surface.swap_buffers();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runners/src/bin/eclrenderer.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,235 @@
+use luminance::blending::{Equation, Factor};
+use luminance::context::GraphicsContext;
+use luminance::pipeline::{BoundTexture, PipelineState};
+use luminance::pixel::NormUnsigned;
+use luminance::render_state::RenderState;
+use luminance::shader::program::{Program, Uniform};
+use luminance::tess::{Mode, TessBuilder};
+use luminance::texture::Dim2;
+use luminance_derive::{Semantics, Vertex, UniformInterface};
+use luminance_glfw::{Action, Key, WindowEvent, GlfwSurface, Surface, WindowDim, WindowOpt};
+use touhou_formats::th06::anm0::Anm0;
+use touhou_formats::th06::ecl::{Ecl, Rank};
+use touhou_interpreters::th06::anm0::{Sprite, Vertex as FakeVertex};
+use touhou_interpreters::th06::ecl::EclRunner;
+use touhou_interpreters::th06::enemy::{Enemy, Game, Position};
+use touhou_utils::math::{perspective, setup_camera};
+use touhou_utils::prng::Prng;
+use std::cell::RefCell;
+use std::rc::Rc;
+use std::env;
+use std::path::Path;
+
+use touhou_runners::common::{load_file_into_vec, load_anm_image, LoadedTexture};
+
+const VS: &str = r#"
+in ivec3 in_position;
+in vec2 in_texcoord;
+in uvec4 in_color;
+
+uniform mat4 mvp;
+
+out vec2 texcoord;
+out vec4 color;
+
+void main()
+{
+    gl_Position = mvp * vec4(vec3(in_position), 1.0);
+    texcoord = vec2(in_texcoord);
+
+    // Normalized from the u8 being passed.
+    color = vec4(in_color) / 255.;
+}
+"#;
+
+const FS: &str = r#"
+in vec2 texcoord;
+in vec4 color;
+
+uniform sampler2D color_map;
+
+out vec4 frag_color;
+
+void main()
+{
+    frag_color = texture(color_map, texcoord) * color;
+}
+"#;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Semantics)]
+pub enum Semantics {
+    #[sem(name = "in_position", repr = "[i16; 3]", wrapper = "VertexPosition")]
+    Position,
+
+    #[sem(name = "in_texcoord", repr = "[f32; 2]", wrapper = "VertexTexcoord")]
+    Texcoord,
+
+    #[sem(name = "in_color", repr = "[u8; 4]", wrapper = "VertexColor")]
+    Color,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, PartialEq, Vertex)]
+#[vertex(sem = "Semantics")]
+struct Vertex {
+    pos: VertexPosition,
+    uv: VertexTexcoord,
+    rgba: VertexColor,
+}
+
+#[derive(UniformInterface)]
+struct ShaderInterface {
+    // the 'static lifetime acts as “anything” here
+    color_map: Uniform<&'static BoundTexture<'static, Dim2, NormUnsigned>>,
+
+    #[uniform(name = "mvp")]
+    mvp: Uniform<[[f32; 4]; 4]>,
+}
+
+fn main() {
+    // Parse arguments.
+    let args: Vec<_> = env::args().collect();
+    if args.len() != 5 {
+        eprintln!("Usage: {} <ECL file> <ANM file> <easy|normal|hard|lunatic> <sub number>", args[0]);
+        return;
+    }
+    let ecl_filename = Path::new(&args[1]);
+    let anm_filename = Path::new(&args[2]);
+    let rank: Rank = args[3].parse().expect("rank");
+    let sub: u16 = args[4].parse().expect("number");
+
+    // Open the ECL file.
+    let buf = load_file_into_vec(ecl_filename).unwrap();
+    let (_, ecl) = Ecl::from_slice(&buf).unwrap();
+
+    // Open the ANM file.
+    let buf = load_file_into_vec(anm_filename).unwrap();
+    let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
+    let anm0 = anms.pop().unwrap();
+    let anm0 = Rc::new(RefCell::new([anm0.clone(), anm0]));
+
+    if ecl.subs.len() < sub as usize {
+        eprintln!("This ecl doesn’t contain a sub named {}.", sub);
+        return;
+    }
+
+    // Get the time since January 1970 as a seed for the PRNG.
+    let time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap();
+    let prng = Rc::new(RefCell::new(Prng::new(time.subsec_micros() as u16)));
+
+    // Create the Game god object.
+    let game = Game::new(prng, rank);
+    let game = Rc::new(RefCell::new(game));
+
+    // And the enemy object.
+    let enemy = Enemy::new(Position::new(0., 0.), 500, 0, 640, false, Rc::downgrade(&anm0), Rc::downgrade(&game));
+    let mut ecl_runner = EclRunner::new(&ecl, enemy.clone(), sub);
+
+    assert_eq!(std::mem::size_of::<Vertex>(), std::mem::size_of::<FakeVertex>());
+    let vertices: [Vertex; 4] = {
+        let data = std::mem::MaybeUninit::uninit();
+        unsafe { data.assume_init() }
+    };
+
+    let mut surface = GlfwSurface::new(WindowDim::Windowed(384, 448), "Touhou", WindowOpt::default()).unwrap();
+
+    // Open the image atlas matching this ANM.
+    let tex = load_anm_image(&mut surface, &anm0.borrow()[0], &anm_filename).expect("image loading");
+
+    // set the uniform interface to our type so that we can read textures from the shader
+    let program =
+        Program::<Semantics, (), ShaderInterface>::from_strings(None, VS, None, FS).expect("program creation").ignore_warnings();
+
+    let mut tess = TessBuilder::new(&mut surface)
+        .add_vertices(vertices)
+        .set_mode(Mode::TriangleFan)
+        .build()
+        .unwrap();
+
+    let mut back_buffer = surface.back_buffer().unwrap();
+    let mut resize = false;
+
+    'app: loop {
+        for event in surface.poll_events() {
+            match event {
+                WindowEvent::Close | WindowEvent::Key(Key::Escape, _, Action::Release, _) => break 'app,
+
+                WindowEvent::FramebufferSize(..) => {
+                    resize = true;
+                }
+
+                _ => (),
+            }
+        }
+
+        if resize {
+            back_buffer = surface.back_buffer().unwrap();
+            resize = false;
+        }
+
+        if ecl_runner.running == false {
+            break;
+        }
+
+        {
+            let mut slice = tess
+                .as_slice_mut()
+                .unwrap();
+
+            ecl_runner.run_frame();
+            {
+                let mut enemy = enemy.borrow_mut();
+                enemy.update();
+            }
+            let mut game = game.borrow_mut();
+            game.run_frame();
+            let sprites = game.get_sprites();
+            fill_vertices_ptr(sprites, slice.as_mut_ptr());
+        }
+
+        // here, we need to bind the pipeline variable; it will enable us to bind the texture to the GPU
+        // and use it in the shader
+        surface
+            .pipeline_builder()
+            .pipeline(&back_buffer, &PipelineState::default(), |pipeline, mut shd_gate| {
+                // bind our fancy texture to the GPU: it gives us a bound texture we can use with the shader
+                let bound_tex = match &tex {
+                    LoadedTexture::Rgb(tex) => pipeline.bind_texture(tex),
+                    LoadedTexture::Rgba(tex) => pipeline.bind_texture(tex),
+                    LoadedTexture::RgbaArray(tex) => unreachable!(),
+                };
+
+                shd_gate.shade(&program, |iface, mut rdr_gate| {
+                    // update the texture; strictly speaking, this update doesn’t do much: it just tells the GPU
+                    // to use the texture passed as argument (no allocation or copy is performed)
+                    iface.color_map.update(&bound_tex);
+                    //let mvp = ortho_2d(0., 384., 448., 0.);
+                    let proj = perspective(0.5235987755982988, 384. / 448., 101010101./2010101., 101010101./10101.);
+                    let view = setup_camera(0., 0., 1.);
+                    let mvp = view * proj;
+                    //println!("{:#?}", mvp);
+                    // TODO: check how to pass by reference.
+                    iface.mvp.update(*mvp.borrow_inner());
+
+                    let render_state = RenderState::default()
+                        .set_blending((Equation::Additive, Factor::SrcAlpha, Factor::SrcAlphaComplement));
+
+                    rdr_gate.render(&render_state, |mut tess_gate| {
+                        // render the tessellation to the surface the regular way and let the vertex shader’s
+                        // magic do the rest!
+                        tess_gate.render(&tess);
+                    });
+                });
+            });
+
+        surface.swap_buffers();
+    }
+}
+
+fn fill_vertices_ptr(sprites: Vec<(f32, f32, f32, Rc<RefCell<Sprite>>)>, vertices: *mut Vertex) {
+    let mut fake_vertices = unsafe { std::mem::transmute::<*mut Vertex, &mut [FakeVertex; 4]>(vertices) };
+    for (x, y, z, sprite) in sprites {
+        let sprite = sprite.borrow();
+        sprite.fill_vertices(&mut fake_vertices, x, y, z);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runners/src/bin/menu.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,220 @@
+use ears::{Music, AudioController};
+use luminance::blending::{Equation, Factor};
+use luminance::context::GraphicsContext;
+use luminance::pipeline::{BoundTexture, PipelineState};
+use luminance::pixel::NormUnsigned;
+use luminance::render_state::RenderState;
+use luminance::shader::program::{Program, Uniform};
+use luminance::tess::{Mode, TessBuilder};
+use luminance::texture::Dim2;
+use luminance_derive::{Semantics, Vertex, UniformInterface};
+use luminance_glfw::{Action, Key, WindowEvent, GlfwSurface, Surface, WindowDim, WindowOpt};
+use touhou_formats::th06::pbg3;
+use touhou_formats::th06::anm0::Anm0;
+use touhou_interpreters::th06::anm0::{AnmRunner, Sprite, Vertex as FakeVertex};
+use touhou_utils::math::{perspective, setup_camera, ortho_2d};
+use touhou_utils::prng::Prng;
+use std::cell::RefCell;
+use std::rc::Rc;
+use std::env;
+use std::path::Path;
+
+use touhou_runners::common::{self, LoadedTexture};
+
+const VS: &str = r#"
+in ivec3 in_position;
+in vec2 in_texcoord;
+in vec4 in_color;
+
+uniform mat4 mvp;
+
+out vec2 texcoord;
+out vec4 color;
+
+void main()
+{
+    gl_Position = mvp * vec4(vec3(in_position), 1.0);
+    texcoord = vec2(in_texcoord);
+
+    // It’s already normalized from the u8 being passed.
+    color = in_color;
+}
+"#;
+
+const FS: &str = r#"
+in vec2 texcoord;
+in vec4 color;
+
+uniform sampler2D color_map;
+
+out vec4 frag_color;
+
+void main()
+{
+    frag_color = texture(color_map, texcoord) * color;
+}
+"#;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Semantics)]
+pub enum Semantics {
+    #[sem(name = "in_position", repr = "[i16; 3]", wrapper = "VertexPosition")]
+    Position,
+
+    #[sem(name = "in_texcoord", repr = "[f32; 2]", wrapper = "VertexTexcoord")]
+    Texcoord,
+
+    #[sem(name = "in_color", repr = "[u8; 4]", wrapper = "VertexColor")]
+    Color,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, PartialEq, Vertex)]
+#[vertex(sem = "Semantics")]
+struct Vertex {
+    pos: VertexPosition,
+    uv: VertexTexcoord,
+    #[vertex(normalized = "true")]
+    rgba: VertexColor,
+}
+
+#[derive(UniformInterface)]
+struct ShaderInterface {
+    // the 'static lifetime acts as “anything” here
+    color_map: Uniform<&'static BoundTexture<'static, Dim2, NormUnsigned>>,
+
+    #[uniform(name = "mvp")]
+    mvp: Uniform<[[f32; 4]; 4]>,
+}
+
+const DEFAULT_VERTICES: [Vertex; 4] = [
+    Vertex::new(VertexPosition::new([0, 0, 0]), VertexTexcoord::new([0., 0.]), VertexColor::new([255, 255, 255, 255])),
+    Vertex::new(VertexPosition::new([640, 0, 0]), VertexTexcoord::new([1., 0.]), VertexColor::new([255, 255, 255, 255])),
+    Vertex::new(VertexPosition::new([640, 480, 0]), VertexTexcoord::new([1., 1.]), VertexColor::new([255, 255, 255, 255])),
+    Vertex::new(VertexPosition::new([0, 480, 0]), VertexTexcoord::new([0., 1.]), VertexColor::new([255, 255, 255, 255])),
+];
+
+fn main() {
+    // Parse arguments.
+    let args: Vec<_> = env::args().collect();
+    if args.len() != 2 {
+        eprintln!("Usage: {} <unarchived directory>", args[0]);
+        return;
+    }
+    let directory = Path::new(&args[1]);
+
+    let in_dat = directory.join("IN.DAT");
+    // Since GLFW can be slow to create its window, let’s decode the splash screen in another
+    // thread in the meantime.
+    let jpeg_thread = std::thread::spawn(|| {
+        let mut in_pbg3 = pbg3::from_path_buffered(in_dat).expect("IN.DAT present");
+        let jpeg = in_pbg3.get_file("th06logo.jpg", true).expect("th06logo.jpg in IN.DAT");
+        let image = common::load_from_data(&jpeg).expect("th06logo.jpg decodable");
+        image
+    });
+
+    let music_filename = directory.join("bgm").join("th06_01.wav");
+    let music_filename = music_filename.to_str().expect("non-UTF-8 music filename");
+    let music = match Music::new(music_filename) {
+        Ok(mut music) => {
+            music.set_looping(true);
+            music.play();
+            music
+        }
+        Err(err) => {
+            eprintln!("Impossible to open or play music file: {}", err);
+            return;
+        }
+    };
+
+    let mut surface = GlfwSurface::new(WindowDim::Windowed(640, 480), "Touhou", WindowOpt::default()).expect("GLFW window");
+
+    let image = jpeg_thread.join().expect("image loading");
+    let background = common::upload_texture_from_rgb_image(&mut surface, image).expect("upload data to texture");
+
+    let mut background = match background {
+        LoadedTexture::Rgb(tex) => tex,
+        LoadedTexture::Rgba(tex) => unreachable!(),
+        LoadedTexture::RgbaArray(tex) => unreachable!(),
+    };
+
+    // set the uniform interface to our type so that we can read textures from the shader
+    let program =
+        Program::<Semantics, (), ShaderInterface>::from_strings(None, VS, None, FS).expect("program creation").ignore_warnings();
+
+    let mut tess = TessBuilder::new(&mut surface)
+        .add_vertices(DEFAULT_VERTICES)
+        .set_mode(Mode::TriangleFan)
+        .build()
+        .unwrap();
+
+    let tl_dat = directory.join("TL.DAT");
+    let mut tl_pbg3 = pbg3::from_path_buffered(tl_dat).expect("TL.DAT present");
+
+    let mut back_buffer = surface.back_buffer().unwrap();
+    let mut resize = false;
+    let mut frame = 0;
+    let mut z_pressed = false;
+    let mut x_pressed = false;
+
+    'app: loop {
+        for event in surface.poll_events() {
+            match event {
+                WindowEvent::Close | WindowEvent::Key(Key::Escape, _, Action::Release, _) => break 'app,
+
+                WindowEvent::Key(Key::Z, _, Action::Press, _) => z_pressed = true,
+                WindowEvent::Key(Key::X, _, Action::Press, _) => x_pressed = true,
+
+                WindowEvent::FramebufferSize(..) => {
+                    resize = true;
+                }
+
+                _ => (),
+            }
+        }
+
+        if resize {
+            back_buffer = surface.back_buffer().unwrap();
+            resize = false;
+        }
+
+        frame += 1;
+        if frame == 60 {
+            let jpeg = tl_pbg3.get_file("title00.jpg", true).expect("title00.jpg in TL.DAT");
+            let image = common::load_from_data(&jpeg).expect("th06logo.jpg decodable");
+            common::reupload_texture_from_rgb_image(&mut background, image).expect("upload data to texture");
+        }
+
+        if frame >= 60 && z_pressed {
+            let jpeg = tl_pbg3.get_file("select00.jpg", true).expect("select00.jpg in TL.DAT");
+            let image = common::load_from_data(&jpeg).expect("select00.jpg decodable");
+            common::reupload_texture_from_rgb_image(&mut background, image).expect("upload data to texture");
+        }
+
+        // here, we need to bind the pipeline variable; it will enable us to bind the texture to the GPU
+        // and use it in the shader
+        surface
+            .pipeline_builder()
+            .pipeline(&back_buffer, &PipelineState::default(), |pipeline, mut shd_gate| {
+                // bind our fancy texture to the GPU: it gives us a bound texture we can use with the shader
+                let tex = pipeline.bind_texture(&background);
+
+                shd_gate.shade(&program, |iface, mut rdr_gate| {
+                    // update the texture; strictly speaking, this update doesn’t do much: it just tells the GPU
+                    // to use the texture passed as argument (no allocation or copy is performed)
+                    iface.color_map.update(&tex);
+                    let mvp = ortho_2d(0., 640., 480., 0.);
+                    // TODO: check how to pass by reference.
+                    iface.mvp.update(*mvp.borrow_inner());
+
+                    let render_state = RenderState::default()
+                        .set_blending((Equation::Additive, Factor::SrcAlpha, Factor::SrcAlphaComplement));
+
+                    rdr_gate.render(&render_state, |mut tess_gate| {
+                        tess_gate.render(&tess);
+                    });
+                });
+            });
+
+        surface.swap_buffers();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runners/src/bin/stagerunner.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,259 @@
+use luminance::blending::{Equation, Factor};
+use luminance::context::GraphicsContext;
+use luminance::pipeline::{BoundTexture, PipelineState};
+use luminance::pixel::NormUnsigned;
+use luminance::render_state::RenderState;
+use luminance::shader::program::{Program, Uniform};
+use luminance::tess::{Mode, TessBuilder};
+use luminance::texture::Dim2Array;
+use luminance_derive::{Semantics, Vertex, UniformInterface};
+use luminance_glfw::{Action, Key, WindowEvent, GlfwSurface, Surface, WindowDim, WindowOpt};
+use touhou_formats::th06::anm0::Anm0;
+use touhou_formats::th06::ecl::{Ecl, Rank, MainInstruction};
+use touhou_interpreters::th06::anm0::Vertex as FakeVertex;
+use touhou_interpreters::th06::ecl::EclRunner;
+use touhou_interpreters::th06::enemy::{Enemy, Game, Position};
+use touhou_utils::math::{perspective, setup_camera};
+use touhou_utils::prng::Prng;
+use std::cell::RefCell;
+use std::rc::Rc;
+use std::env;
+use std::path::Path;
+
+use touhou_runners::common::{load_file_into_vec, load_multiple_anm_images, LoadedTexture};
+
+const VS: &str = r#"
+in ivec3 in_position;
+in uint in_layer;
+in vec2 in_texcoord;
+in uvec4 in_color;
+
+uniform mat4 mvp;
+
+flat out uint layer;
+out vec2 texcoord;
+out vec4 color;
+
+void main()
+{
+    gl_Position = mvp * vec4(vec3(in_position), 1.0);
+    texcoord = vec2(in_texcoord);
+
+    // Normalized from the u8 being passed.
+    color = vec4(in_color) / 255.;
+
+    layer = in_layer;
+}
+"#;
+
+const FS: &str = r#"
+flat in uint layer;
+in vec2 texcoord;
+in vec4 color;
+
+uniform sampler2DArray color_map;
+
+out vec4 frag_color;
+
+void main()
+{
+    frag_color = texture(color_map, vec3(texcoord, layer)) * color;
+}
+"#;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Semantics)]
+pub enum Semantics {
+    #[sem(name = "in_position", repr = "[i16; 3]", wrapper = "VertexPosition")]
+    Position,
+
+    #[sem(name = "in_layer", repr = "u16", wrapper = "VertexLayer")]
+    Layer,
+
+    #[sem(name = "in_texcoord", repr = "[f32; 2]", wrapper = "VertexTexcoord")]
+    Texcoord,
+
+    #[sem(name = "in_color", repr = "[u8; 4]", wrapper = "VertexColor")]
+    Color,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, PartialEq, Vertex)]
+#[vertex(sem = "Semantics")]
+struct Vertex {
+    pos: VertexPosition,
+    layer: VertexLayer,
+    uv: VertexTexcoord,
+    rgba: VertexColor,
+}
+
+#[derive(UniformInterface)]
+struct ShaderInterface {
+    // the 'static lifetime acts as “anything” here
+    color_map: Uniform<&'static BoundTexture<'static, Dim2Array, NormUnsigned>>,
+
+    #[uniform(name = "mvp")]
+    mvp: Uniform<[[f32; 4]; 4]>,
+}
+
+fn main() {
+    // Parse arguments.
+    let args: Vec<_> = env::args().collect();
+    if args.len() != 4 {
+        eprintln!("Usage: {} <unarchived ST.DAT directory> <stage number> <easy|normal|hard|lunatic>", args[0]);
+        return;
+    }
+    let directory = Path::new(&args[1]);
+    let stage_number: u8 = args[2].parse().expect("stage");
+    let rank: Rank = args[3].parse().expect("rank");
+
+    // Open the ECL file.
+    let buf = load_file_into_vec(directory.join(format!("ecldata{}.ecl", stage_number))).unwrap();
+    let (_, ecl) = Ecl::from_slice(&buf).unwrap();
+    assert_eq!(ecl.mains.len(), 1);
+    let main = ecl.mains[0].clone();
+
+    // Open the ANM file.
+    let anm_filename = directory.join(format!("stg{}enm.anm", stage_number));
+    let buf = load_file_into_vec(&anm_filename).unwrap();
+    let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
+    let anm0 = anms.pop().unwrap();
+
+    // Open the second ANM file.
+    let anm2_filename = directory.join(format!("stg{}enm2.anm", stage_number));
+    let buf = load_file_into_vec(&anm2_filename).unwrap();
+    let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
+    let anm0_bis = anms.pop().unwrap();
+
+    let anms = [anm0, anm0_bis];
+
+    // Get the time since January 1970 as a seed for the PRNG.
+    let time = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap();
+    let prng = Rc::new(RefCell::new(Prng::new(time.subsec_micros() as u16)));
+
+    // Create the Game god object.
+    let game = Game::new(prng, rank);
+    let game = Rc::new(RefCell::new(game));
+
+    assert_eq!(std::mem::size_of::<Vertex>(), std::mem::size_of::<FakeVertex>());
+    let vertices: [Vertex; 4] = {
+        let data = std::mem::MaybeUninit::uninit();
+        unsafe { data.assume_init() }
+    };
+
+    let mut surface = GlfwSurface::new(WindowDim::Windowed(384, 448), "Touhou", WindowOpt::default()).unwrap();
+
+    // Open the image atlas matching this ANM.
+    let tex = load_multiple_anm_images(&mut surface, &anms, &anm_filename).expect("image loading");
+    let anms = Rc::new(RefCell::new(anms));
+
+    // set the uniform interface to our type so that we can read textures from the shader
+    let program =
+        Program::<Semantics, (), ShaderInterface>::from_strings(None, VS, None, FS).expect("program creation").ignore_warnings();
+
+    let mut tess = TessBuilder::new(&mut surface)
+        .add_vertices(vertices)
+        .set_mode(Mode::TriangleFan)
+        .build()
+        .unwrap();
+
+    let mut back_buffer = surface.back_buffer().unwrap();
+    let mut resize = false;
+    let mut frame = 0;
+    let mut ecl_runners = vec![];
+
+    'app: loop {
+        for event in surface.poll_events() {
+            match event {
+                WindowEvent::Close | WindowEvent::Key(Key::Escape, _, Action::Release, _) => break 'app,
+
+                WindowEvent::FramebufferSize(..) => {
+                    resize = true;
+                }
+
+                _ => (),
+            }
+        }
+
+        if resize {
+            back_buffer = surface.back_buffer().unwrap();
+            resize = false;
+        }
+
+        for call in main.instructions.iter() {
+            if call.time == frame {
+                let sub = call.sub;
+                let instr = call.instr;
+                let (x, y, _z, life, bonus, score, mirror) = match instr {
+                    MainInstruction::SpawnEnemy(x, y, z, life, bonus, score) => (x, y, z, life, bonus, score, false),
+                    MainInstruction::SpawnEnemyMirrored(x, y, z, life, bonus, score) => (x, y, z, life, bonus, score, true),
+                    MainInstruction::SpawnEnemyRandom(x, y, z, life, bonus, score) => (x, y, z, life, bonus, score, false),
+                    MainInstruction::SpawnEnemyMirroredRandom(x, y, z, life, bonus, score) => (x, y, z, life, bonus, score, true),
+                    _ => continue,
+                };
+                let enemy = Enemy::new(Position::new(x, y), life, bonus, score, mirror, Rc::downgrade(&anms), Rc::downgrade(&game));
+                let runner = EclRunner::new(&ecl, enemy, sub);
+                ecl_runners.push(runner);
+            }
+        }
+
+        for runner in ecl_runners.iter_mut() {
+            runner.run_frame();
+            let mut enemy = runner.enemy.borrow_mut();
+            enemy.update();
+        }
+
+        // here, we need to bind the pipeline variable; it will enable us to bind the texture to the GPU
+        // and use it in the shader
+        surface
+            .pipeline_builder()
+            .pipeline(&back_buffer, &PipelineState::default(), |pipeline, mut shd_gate| {
+                // bind our fancy texture to the GPU: it gives us a bound texture we can use with the shader
+                let bound_tex = match &tex {
+                    LoadedTexture::Rgb(tex) => unreachable!(),
+                    LoadedTexture::Rgba(tex) => unreachable!(),
+                    LoadedTexture::RgbaArray(tex) => pipeline.bind_texture(tex),
+                };
+
+                shd_gate.shade(&program, |iface, mut rdr_gate| {
+                    // update the texture; strictly speaking, this update doesn’t do much: it just tells the GPU
+                    // to use the texture passed as argument (no allocation or copy is performed)
+                    iface.color_map.update(&bound_tex);
+                    //let mvp = ortho_2d(0., 384., 448., 0.);
+                    let proj = perspective(0.5235987755982988, 384. / 448., 101010101./2010101., 101010101./10101.);
+                    let view = setup_camera(0., 0., 1.);
+                    let mvp = view * proj;
+                    //println!("{:#?}", mvp);
+                    // TODO: check how to pass by reference.
+                    iface.mvp.update(*mvp.borrow_inner());
+
+                    let render_state = RenderState::default()
+                        .set_depth_test(None)
+                        .set_blending((Equation::Additive, Factor::SrcAlpha, Factor::SrcAlphaComplement));
+
+                    rdr_gate.render(&render_state, |mut tess_gate| {
+                        let mut game = game.borrow_mut();
+                        game.run_frame();
+
+                        for (x, y, z, sprite) in game.get_sprites() {
+                            {
+                                let mut slice = tess
+                                    .as_slice_mut()
+                                    .unwrap();
+
+                                let sprite = sprite.borrow();
+                                let fake_vertices = unsafe { std::mem::transmute::<*mut Vertex, &mut [FakeVertex; 4]>(slice.as_mut_ptr()) };
+                                sprite.fill_vertices(fake_vertices, x, y, z);
+                            }
+
+                            // render the tessellation to the surface the regular way and let the vertex shader’s
+                            // magic do the rest!
+                            tess_gate.render(&tess);
+                        }
+                    });
+                });
+            });
+
+        surface.swap_buffers();
+        frame += 1;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runners/src/bin/stdrenderer.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,255 @@
+use luminance::blending::{Equation, Factor};
+use luminance::context::GraphicsContext;
+use luminance::pipeline::{BoundTexture, PipelineState};
+use luminance::pixel::NormUnsigned;
+use luminance::render_state::RenderState;
+use luminance::shader::program::{Program, Uniform};
+use luminance::tess::{Mode, TessBuilder, TessSliceIndex};
+use luminance::texture::Dim2;
+use luminance_derive::{Semantics, Vertex, UniformInterface};
+use luminance_glfw::{Action, Key, WindowEvent, GlfwSurface, Surface, WindowDim, WindowOpt};
+use touhou_formats::th06::anm0::Anm0;
+use touhou_formats::th06::std::{Stage, Position, Box2D};
+use touhou_interpreters::th06::anm0::{AnmRunner, Sprite, Vertex as FakeVertex};
+use touhou_interpreters::th06::std::StageRunner;
+use touhou_utils::prng::Prng;
+use touhou_utils::math::perspective;
+use std::cell::RefCell;
+use std::rc::Rc;
+use std::env;
+use std::path::Path;
+
+use touhou_runners::common::{load_file_into_vec, load_anm_image, LoadedTexture};
+
+const VS: &str = r#"
+in ivec3 in_position;
+in vec2 in_texcoord;
+in uvec4 in_color;
+
+uniform mat4 mvp;
+uniform vec3 instance_position;
+
+out vec2 texcoord;
+out vec4 color;
+
+void main()
+{
+    vec3 position = vec3(in_position) + instance_position;
+    gl_Position = mvp * vec4(position, 1.0);
+    texcoord = vec2(in_texcoord);
+
+    // Normalized from the u8 being passed.
+    color = vec4(in_color) / 255.;
+}
+"#;
+
+const FS: &str = r#"
+in vec2 texcoord;
+in vec4 color;
+
+uniform sampler2D color_map;
+uniform float fog_scale;
+uniform float fog_end;
+uniform vec4 fog_color;
+
+out vec4 frag_color;
+
+void main()
+{
+    vec4 temp_color = texture(color_map, texcoord) * color;
+    float depth = gl_FragCoord.z / gl_FragCoord.w;
+    float fog_density = clamp((fog_end - depth) * fog_scale, 0.0, 1.0);
+    frag_color = vec4(mix(fog_color, temp_color, fog_density).rgb, temp_color.a);
+}
+"#;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Semantics)]
+pub enum Semantics {
+    #[sem(name = "in_position", repr = "[i16; 3]", wrapper = "VertexPosition")]
+    Position,
+
+    #[sem(name = "in_texcoord", repr = "[f32; 2]", wrapper = "VertexTexcoord")]
+    Texcoord,
+
+    #[sem(name = "in_color", repr = "[u8; 4]", wrapper = "VertexColor")]
+    Color,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, PartialEq, Vertex)]
+#[vertex(sem = "Semantics")]
+struct Vertex {
+    pos: VertexPosition,
+    uv: VertexTexcoord,
+    rgba: VertexColor,
+}
+
+#[derive(UniformInterface)]
+struct ShaderInterface {
+    // the 'static lifetime acts as “anything” here
+    color_map: Uniform<&'static BoundTexture<'static, Dim2, NormUnsigned>>,
+
+    #[uniform(name = "mvp")]
+    mvp: Uniform<[[f32; 4]; 4]>,
+
+    #[uniform(name = "instance_position")]
+    instance_position: Uniform<[f32; 3]>,
+
+    #[uniform(name = "fog_scale")]
+    fog_scale: Uniform<f32>,
+
+    #[uniform(name = "fog_end")]
+    fog_end: Uniform<f32>,
+
+    #[uniform(name = "fog_color")]
+    fog_color: Uniform<[f32; 4]>,
+}
+
+fn main() {
+    // Parse arguments.
+    let args: Vec<_> = env::args().collect();
+    if args.len() != 3 {
+        eprintln!("Usage: {} <STD file> <ANM file>", args[0]);
+        return;
+    }
+    let std_filename = Path::new(&args[1]);
+    let anm_filename = Path::new(&args[2]);
+
+    // Open the STD file.
+    let buf = load_file_into_vec(std_filename).unwrap();
+    let (_, stage) = Stage::from_slice(&buf).unwrap();
+
+    // Open the ANM file.
+    let buf = load_file_into_vec(anm_filename).unwrap();
+    let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
+    let anm0 = anms.pop().unwrap();
+
+    // TODO: seed this PRNG with a valid seed.
+    let prng = Rc::new(RefCell::new(Prng::new(0)));
+
+    let mut surface = GlfwSurface::new(WindowDim::Windowed(384, 448), "Touhou", WindowOpt::default()).unwrap();
+
+    // Open the image atlas matching this ANM.
+    let tex = load_anm_image(&mut surface, &anm0, anm_filename).expect("image loading");
+
+    assert_eq!(std::mem::size_of::<Vertex>(), std::mem::size_of::<FakeVertex>());
+    let mut vertices: Vec<Vertex> = vec![];
+    let mut indices = vec![];
+
+    {
+        let anms = Rc::new(RefCell::new([anm0]));
+        for model in stage.models.iter() {
+            let begin = vertices.len();
+            for quad in model.quads.iter() {
+                let Position { x, y, z } = quad.pos;
+                let Box2D { width, height } = quad.size_override;
+
+                // Create the AnmRunner from the ANM and the sprite.
+                let sprite = Rc::new(RefCell::new(Sprite::with_size(width, height)));
+                let _anm_runner = AnmRunner::new(anms.clone(), quad.anm_script as u8, sprite.clone(), Rc::downgrade(&prng), 0);
+                let mut new_vertices: [Vertex; 6] = {
+                    let data = std::mem::MaybeUninit::uninit();
+                    unsafe { data.assume_init() }
+                };
+                fill_vertices(sprite.clone(), &mut new_vertices, x, y, z);
+                new_vertices[4] = new_vertices[0];
+                new_vertices[5] = new_vertices[2];
+                vertices.extend(&new_vertices);
+            }
+            let end = vertices.len();
+            indices.push((begin, end));
+        }
+    }
+
+    let mut stage_runner = StageRunner::new(Rc::new(RefCell::new(stage)));
+
+    // set the uniform interface to our type so that we can read textures from the shader
+    let program =
+        Program::<Semantics, (), ShaderInterface>::from_strings(None, VS, None, FS).expect("program creation").ignore_warnings();
+
+    let tess = TessBuilder::new(&mut surface)
+        .add_vertices(vertices)
+        .set_mode(Mode::Triangle)
+        .build()
+        .unwrap();
+
+    let mut back_buffer = surface.back_buffer().unwrap();
+    let mut resize = false;
+
+    'app: loop {
+        for event in surface.poll_events() {
+            match event {
+                WindowEvent::Close | WindowEvent::Key(Key::Escape, _, Action::Release, _) => break 'app,
+
+                WindowEvent::FramebufferSize(..) => {
+                    resize = true;
+                }
+
+                _ => (),
+            }
+        }
+
+        if resize {
+            back_buffer = surface.back_buffer().unwrap();
+            resize = false;
+        }
+
+        {
+            stage_runner.run_frame();
+            //let sprites = stage.get_sprites();
+            //fill_vertices_ptr(sprites, slice.as_mut_ptr());
+        }
+
+        // here, we need to bind the pipeline variable; it will enable us to bind the texture to the GPU
+        // and use it in the shader
+        surface
+            .pipeline_builder()
+            .pipeline(&back_buffer, &PipelineState::default(), |pipeline, mut shd_gate| {
+                // bind our fancy texture to the GPU: it gives us a bound texture we can use with the shader
+                let bound_tex = match &tex {
+                    LoadedTexture::Rgb(tex) => pipeline.bind_texture(tex),
+                    LoadedTexture::Rgba(tex) => pipeline.bind_texture(tex),
+                    LoadedTexture::RgbaArray(tex) => unreachable!(),
+                };
+
+                shd_gate.shade(&program, |iface, mut rdr_gate| {
+                    // update the texture; strictly speaking, this update doesn’t do much: it just tells the GPU
+                    // to use the texture passed as argument (no allocation or copy is performed)
+                    iface.color_map.update(&bound_tex);
+
+                    let proj = perspective(0.5235987755982988, 384. / 448., 101010101./2010101., 101010101./10101.);
+                    let model_view = stage_runner.get_model_view();
+                    let mvp = model_view * proj;
+                    // TODO: check how to pass by reference.
+                    iface.mvp.update(*mvp.borrow_inner());
+
+                    let near = stage_runner.fog_near - 101010101. / 2010101.;
+                    let far = stage_runner.fog_far - 101010101. / 2010101.;
+                    iface.fog_color.update(stage_runner.fog_color);
+                    iface.fog_scale.update(1. / (far - near));
+                    iface.fog_end.update(far);
+
+                    let render_state = RenderState::default()
+                        .set_blending((Equation::Additive, Factor::SrcAlpha, Factor::SrcAlphaComplement));
+
+                    let stage = stage_runner.stage.borrow();
+                    for instance in stage.instances.iter() {
+                        iface.instance_position.update([instance.pos.x, instance.pos.y, instance.pos.z]);
+
+                        rdr_gate.render(&render_state, |mut tess_gate| {
+                            let (begin, end) = indices[instance.id as usize];
+                            tess_gate.render(tess.slice(begin..end));
+                        });
+                    }
+                });
+            });
+
+        surface.swap_buffers();
+    }
+}
+
+fn fill_vertices(sprite: Rc<RefCell<Sprite>>, vertices: &mut [Vertex; 6], x: f32, y: f32, z: f32) {
+    let mut fake_vertices = unsafe { std::mem::transmute::<&mut [Vertex; 6], &mut [FakeVertex; 4]>(vertices) };
+    let sprite = sprite.borrow();
+    sprite.fill_vertices(&mut fake_vertices, x, y, z);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runners/src/common.rs	Tue Jan 05 02:16:32 2021 +0100
@@ -0,0 +1,164 @@
+use image::{GenericImageView, DynamicImage, GrayImage, ImageError};
+use luminance::pixel::{NormRGB8UI, NormRGBA8UI};
+use luminance::texture::{Dim2, Dim2Array, Sampler, Texture, GenMipmaps};
+use luminance_glfw::GlfwSurface;
+use touhou_formats::th06::anm0::Anm0;
+use std::fs::File;
+use std::io::{self, BufReader, Read};
+use std::path::Path;
+
+pub fn load_file_into_vec<P: AsRef<Path>>(filename: P) -> io::Result<Vec<u8>> {
+    let file = File::open(filename)?;
+    let mut file = BufReader::new(file);
+    let mut buf = vec![];
+    file.read_to_end(&mut buf)?;
+    Ok(buf)
+}
+
+pub enum LoadedTexture {
+    Rgba(Texture<Dim2, NormRGBA8UI>),
+    Rgb(Texture<Dim2, NormRGB8UI>),
+    RgbaArray(Texture<Dim2Array, NormRGBA8UI>),
+}
+
+#[derive(Debug)]
+pub enum TextureLoadError {
+    CannotOpenRgb(String, ImageError),
+    CannotOpenAlpha(String, ImageError),
+    AlphaToGrayscale(String),
+}
+
+fn open_rgb_png(path: &Path) -> Result<DynamicImage, TextureLoadError> {
+    // load the texture into memory as a whole bloc (i.e. no streaming)
+    image::open(&path).map_err(|e| TextureLoadError::CannotOpenRgb(path.to_str().unwrap().to_owned(), e))
+}
+
+fn open_alpha_png(path: &Path) -> Result<DynamicImage, TextureLoadError> {
+    // load the texture into memory as a whole bloc (i.e. no streaming)
+    image::open(&path).map_err(|e| TextureLoadError::CannotOpenAlpha(path.to_str().unwrap().to_owned(), e))
+}
+
+fn merge_rgb_alpha(rgb: &DynamicImage, alpha: &GrayImage) -> Vec<(u8, u8, u8, u8)> {
+    rgb
+        .pixels()
+        .zip(alpha.pixels())
+        .map(|((_x, _y, rgb), luma)| (rgb[0], rgb[1], rgb[2], luma[0]))
+        .collect::<Vec<_>>()
+}
+
+pub fn load_from_data(data: &[u8]) -> Result<DynamicImage, ImageError> {
+    image::load_from_memory(data)
+}
+
+pub fn reupload_texture_from_rgb_image(tex: &mut Texture<Dim2, NormRGB8UI>, img: DynamicImage) -> Result<(), TextureLoadError> {
+    let texels = img
+        .pixels()
+        .map(|(_x, _y, rgb)| (rgb[0], rgb[1], rgb[2]))
+        .collect::<Vec<_>>();
+
+    // the first argument disables mipmap generation (we don’t care so far)
+    tex.upload(GenMipmaps::No, &texels).unwrap();
+
+    Ok(())
+}
+
+pub fn upload_texture_from_rgb_image(surface: &mut GlfwSurface, img: DynamicImage) -> Result<LoadedTexture, TextureLoadError> {
+    let (width, height) = img.dimensions();
+
+    // create the luminance texture; the third argument is the number of mipmaps we want (leave it
+    // to 0 for now) and the latest is a the sampler to use when sampling the texels in the
+    // shader (we’ll just use the default one)
+    let mut tex =
+        Texture::new(surface, [width, height], 0, Sampler::default()).expect("luminance texture creation");
+
+    reupload_texture_from_rgb_image(&mut tex, img)?;
+
+    Ok(LoadedTexture::Rgb(tex))
+}
+
+pub fn load_rgb_texture(surface: &mut GlfwSurface, path: &Path) -> Result<LoadedTexture, TextureLoadError> {
+    let img = open_rgb_png(&path)?;
+    upload_texture_from_rgb_image(surface, img)
+}
+
+fn load_rgb_a_pngs(surface: &mut GlfwSurface, rgb: &Path, alpha: &Path) -> Result<LoadedTexture, TextureLoadError> {
+    let img = open_alpha_png(&alpha)?;
+    let alpha = match img.grayscale() {
+        DynamicImage::ImageLuma8(img) => img,
+        _ => {
+            return Err(TextureLoadError::AlphaToGrayscale(alpha.to_str().unwrap().to_owned()))
+        }
+    };
+    let (width, height) = img.dimensions();
+    let img = open_rgb_png(&rgb)?;
+    assert_eq!((width, height), img.dimensions());
+    let texels = merge_rgb_alpha(&img, &alpha);
+
+    // create the luminance texture; the third argument is the number of mipmaps we want (leave it
+    // to 0 for now) and the latest is a the sampler to use when sampling the texels in the
+    // shader (we’ll just use the default one)
+    let tex =
+        Texture::new(surface, [width, height], 0, Sampler::default()).expect("luminance texture creation");
+
+    // the first argument disables mipmap generation (we don’t care so far)
+    tex.upload(GenMipmaps::No, &texels).unwrap();
+
+    Ok(LoadedTexture::Rgba(tex))
+}
+
+pub fn load_anm_image<P: AsRef<Path>>(mut surface: &mut GlfwSurface, anm0: &Anm0, anm_filename: P) -> Result<LoadedTexture, TextureLoadError> {
+    let anm_filename = anm_filename.as_ref();
+    let png_filename = anm_filename.with_file_name(Path::new(&anm0.png_filename).file_name().unwrap());
+    match anm0.alpha_filename {
+        Some(ref filename) => {
+            let alpha_filename = anm_filename.with_file_name(Path::new(filename).file_name().unwrap());
+            load_rgb_a_pngs(&mut surface, &png_filename, &alpha_filename)
+        },
+        None => {
+            load_rgb_texture(&mut surface, &png_filename)
+        }
+    }
+}
+
+fn load_array_texture(surface: &mut GlfwSurface, images: &[(&Path, &Path)]) -> Result<LoadedTexture, TextureLoadError> {
+    let mut decoded = vec![];
+    let dimensions = (256, 256);
+    for (rgb, alpha) in images {
+        let img = open_alpha_png(&alpha)?;
+        assert_eq!(dimensions, img.dimensions());
+        let alpha = match img.grayscale() {
+            DynamicImage::ImageLuma8(img) => img,
+            _ => {
+                return Err(TextureLoadError::AlphaToGrayscale(alpha.to_str().unwrap().to_owned()))
+            }
+        };
+        let img = open_rgb_png(&rgb)?;
+        assert_eq!(dimensions, img.dimensions());
+        let texels = merge_rgb_alpha(&img, &alpha);
+        decoded.push(texels);
+    }
+
+    // create the luminance texture; the third argument is the number of mipmaps we want (leave it
+    // to 0 for now) and the latest is a the sampler to use when sampling the texels in the
+    // shader (we’ll just use the default one)
+    let tex =
+        Texture::new(surface, ([dimensions.0, dimensions.1], images.len() as u32), 0, Sampler::default()).expect("luminance texture creation");
+
+    // the first argument disables mipmap generation (we don’t care so far)
+    tex.upload(GenMipmaps::No, &decoded.into_iter().flatten().collect::<Vec<_>>()).unwrap();
+
+    Ok(LoadedTexture::RgbaArray(tex))
+}
+
+pub fn load_multiple_anm_images<P: AsRef<Path>>(mut surface: &mut GlfwSurface, anms: &[Anm0], anm_filename: P) -> Result<LoadedTexture, TextureLoadError> {
+    let anm_filename = anm_filename.as_ref();
+    let mut paths = vec![];
+    for anm0 in anms.iter() {
+        let rgb_filename = anm_filename.with_file_name(Path::new(&anm0.png_filename).file_name().unwrap());
+        let filename = anm0.alpha_filename.as_ref().expect("Can’t not have alpha here!");
+        let alpha_filename = anm_filename.with_file_name(Path::new(filename).file_name().unwrap());
+        paths.push((rgb_filename, alpha_filename));
+    }
+    let paths: Vec<_> = paths.iter().map(|(rgb, alpha)| (rgb.as_ref(), alpha.as_ref())).collect();
+    load_array_texture(&mut surface, paths.as_slice())
+}
--- a/src/lib.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-#![deny(missing_docs)]
-#![feature(concat_idents)]
-
-//! Crate implementing various Touhou formats.
-
-pub mod util;
-pub mod th06;
--- a/src/th06/anm0.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,297 +0,0 @@
-//! ANM0 animation format support.
-
-use nom::{
-    IResult,
-    bytes::complete::{tag, take_while_m_n},
-    number::complete::{le_u8, le_u16, le_u32, le_i32, le_f32},
-    sequence::tuple,
-    multi::{many_m_n, many0},
-};
-use std::collections::HashMap;
-
-/// Coordinates of a sprite into the image.
-#[derive(Debug, Clone)]
-pub struct Sprite {
-    /// Index inside the anm0.
-    pub index: u32,
-
-    /// X coordinate in the sprite sheet.
-    pub x: f32,
-
-    /// Y coordinate in the sprite sheet.
-    pub y: f32,
-
-    /// Width of the sprite.
-    pub width: f32,
-
-    /// Height of the sprite.
-    pub height: f32,
-}
-
-/// A single instruction, part of a `Script`.
-#[derive(Debug, Clone)]
-pub struct Call {
-    /// Time at which this instruction will be called.
-    pub time: u16,
-
-    /// The instruction to call.
-    pub instr: Instruction,
-}
-
-/// Script driving an animation.
-#[derive(Debug, Clone)]
-pub struct Script {
-    /// List of instructions in this script.
-    pub instructions: Vec<Call>,
-
-    /// List of interrupts in this script.
-    pub interrupts: HashMap<i32, u8>
-}
-
-/// Main struct of the ANM0 animation format.
-#[derive(Debug, Clone)]
-pub struct Anm0 {
-    /// Resolution of the image used by this ANM.
-    pub size: (u32, u32),
-
-    /// Format of this ANM.
-    pub format: u32,
-
-    /// File name of the main image.
-    pub png_filename: String,
-
-    /// File name of an alpha channel image.
-    pub alpha_filename: Option<String>,
-
-    /// A list of sprites, coordinates into the attached image.
-    pub sprites: Vec<Sprite>,
-
-    /// A map of scripts.
-    pub scripts: HashMap<u8, Script>,
-}
-
-impl Anm0 {
-    /// Parse a slice of bytes into an `Anm0` struct.
-    pub fn from_slice(data: &[u8]) -> IResult<&[u8], Vec<Anm0>> {
-        many0(parse_anm0)(data)
-    }
-
-    /// TODO
-    pub fn inv_size(&self) -> (f32, f32) {
-        let (x, y) = self.size;
-        (1. / x as f32, 1. / y as f32)
-    }
-}
-
-fn parse_name(i: &[u8]) -> IResult<&[u8], String> {
-    let (_, slice) = take_while_m_n(0, 32, |c| c != 0)(i)?;
-    let string = match String::from_utf8(slice.to_vec()) {
-        Ok(string) => string,
-        // XXX: use a more specific error instead.
-        Err(_) => return Err(nom::Err::Failure(nom::error::Error::new(i, nom::error::ErrorKind::Eof)))
-    };
-    Ok((i, string))
-}
-
-fn parse_sprite(i: &[u8]) -> IResult<&[u8], Sprite> {
-    let (i, (index, x, y, width, height)) = tuple((le_u32, le_f32, le_f32, le_f32, le_f32))(i)?;
-    Ok((i, Sprite {
-        index,
-        x,
-        y,
-        width,
-        height,
-    }))
-}
-
-macro_rules! declare_anm_instructions {
-    ($($opcode:tt => fn $name:ident($($arg:ident: $arg_type:ident),*)),*,) => {
-        /// Available instructions in an `Anm0`.
-        #[allow(missing_docs)]
-        #[derive(Debug, Clone, Copy)]
-        pub enum Instruction {
-            $(
-                $name($($arg_type),*)
-            ),*
-        }
-
-        fn parse_instruction_args(mut i: &[u8], opcode: u8) -> IResult<&[u8], Instruction> {
-            let instr = match opcode {
-                $(
-                    $opcode => {
-                        $(
-                            let (i2, $arg) = concat_idents!(le_, $arg_type)(i)?;
-                            i = i2;
-                        )*
-                        Instruction::$name($($arg),*)
-                    }
-                )*
-                // XXX: use a more specific error instead.
-                _ => return Err(nom::Err::Failure(nom::error::Error::new(i, nom::error::ErrorKind::Eof)))
-            };
-            Ok((i, instr))
-        }
-    };
-}
-
-declare_anm_instructions!{
-    0 => fn Delete(),
-    1 => fn LoadSprite(sprite_number: u32),
-    2 => fn SetScale(sx: f32, sy: f32),
-    3 => fn SetAlpha(alpha: u32),
-    4 => fn SetColor(red: u8, green: u8, blue: u8/*, XXX: x8*/),
-    5 => fn Jump(instruction: u32),
-    7 => fn ToggleMirrored(),
-    9 => fn SetRotations3d(x: f32, y: f32, z: f32),
-    10 => fn SetRotationsSpeed3d(x: f32, y: f32, z: f32),
-    11 => fn SetScaleSpeed(sx: f32, sy: f32),
-    12 => fn Fade(alpha: u32, duration: u32),
-    13 => fn SetBlendmodeAdd(),
-    14 => fn SetBlendmodeAlphablend(),
-    15 => fn KeepStill(),
-    16 => fn LoadRandomSprite(min_index: u32, amplitude: u32),
-    17 => fn Move(x: f32, y: f32, z: f32),
-    18 => fn MoveToLinear(x: f32, y: f32, z: f32, duration: u32),
-    19 => fn MoveToDecel(x: f32, y: f32, z: f32, duration: u32),
-    20 => fn MoveToAccel(x: f32, y: f32, z: f32, duration: u32),
-    21 => fn Wait(),
-    22 => fn InterruptLabel(label: i32),
-    23 => fn SetCornerRelativePlacement(),
-    24 => fn WaitEx(),
-    25 => fn SetAllowOffset(allow: u32), // TODO: better name
-    26 => fn SetAutomaticOrientation(automatic: u32),
-    27 => fn ShiftTextureX(dx: f32),
-    28 => fn ShiftTextureY(dy: f32),
-    29 => fn SetVisible(visible: u32),
-    30 => fn ScaleIn(sx: f32, sy: f32, duration: u32),
-    31 => fn Todo(todo: u32),
-}
-
-fn parse_anm0(input: &[u8]) -> IResult<&[u8], Anm0> {
-    let (i, (num_sprites, num_scripts, _, width, height, format, _unknown1,
-             first_name_offset, _unused, second_name_offset, version, _unknown2,
-             _texture_offset, has_data, _next_offset, unknown3)) =
-        tuple((le_u32, le_u32, tag(b"\0\0\0\0"), le_u32, le_u32, le_u32, le_u32, le_u32,
-               le_u32, le_u32, le_u32, le_u32, le_u32, le_u32, le_u32, le_u32))(input)?;
-
-    assert_eq!(version, 0);
-    assert_eq!(unknown3, 0);
-    assert_eq!(has_data, 0);
-    let num_sprites = num_sprites as usize;
-    let num_scripts = num_scripts as usize;
-
-    let (i, sprite_offsets) = many_m_n(num_sprites, num_sprites, le_u32)(i)?;
-    let (_, script_offsets) = many_m_n(num_scripts, num_scripts, tuple((le_u32, le_u32)))(i)?;
-
-    let png_filename = if first_name_offset > 0 {
-        if input.len() < first_name_offset as usize {
-            return Err(nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Eof)));
-        }
-        let i = &input[first_name_offset as usize..];
-        let (_, name) = parse_name(i)?;
-        name
-    } else {
-        String::new()
-    };
-
-    let alpha_filename = if second_name_offset > 0 {
-        if input.len() < second_name_offset as usize {
-            return Err(nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Eof)));
-        }
-        let i = &input[second_name_offset as usize..];
-        let (_, name) = parse_name(i)?;
-        Some(name)
-    } else {
-        None
-    };
-
-    let mut sprites = vec![];
-    let mut i = &input[..];
-    for offset in sprite_offsets.into_iter().map(|x| x as usize) {
-        if input.len() < offset {
-            return Err(nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Eof)));
-        }
-        i = &input[offset..];
-        let (_, sprite) = parse_sprite(i)?;
-        sprites.push(sprite);
-    }
-
-    let mut scripts = HashMap::new();
-    for (index, offset) in script_offsets.into_iter().map(|(index, offset)| (index as u8, offset as usize)) {
-        if input.len() < offset {
-            return Err(nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Eof)));
-        }
-        i = &input[offset..];
-        let mut instruction_offsets = vec![];
-
-        let mut instructions = vec![];
-        loop {
-            let tell = input.len() - i.len();
-            instruction_offsets.push(tell - offset);
-            // TODO: maybe check against the size of parsed data?
-            let (i2, (time, opcode, _size)) = tuple((le_u16, le_u8, le_u8))(i)?;
-            let (i2, instr) = parse_instruction_args(i2, opcode)?;
-            instructions.push(Call { time, instr });
-            i = i2;
-            if opcode == 0 {
-                break;
-            }
-        }
-        let mut interrupts = HashMap::new();
-        let mut j = 0;
-        for Call { time: _, instr } in &mut instructions {
-            match instr {
-                Instruction::Jump(ref mut offset) => {
-                    let result = instruction_offsets.binary_search(&(*offset as usize));
-                    match result {
-                        Ok(ptr) => *offset = ptr as u32,
-                        Err(ptr) => {
-                            // XXX: use a more specific error instead.
-                            return Err(nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Eof)));
-                            //println!("Instruction offset not found for pointer: {}", ptr);
-                        }
-                    }
-                }
-                Instruction::InterruptLabel(interrupt) => {
-                    interrupts.insert(*interrupt, j + 1);
-                }
-                _ => ()
-            }
-            j += 1;
-        }
-        scripts.insert(index, Script {
-            instructions,
-            interrupts,
-        });
-    }
-
-    let anm0 = Anm0 {
-        size: (width, height),
-        format,
-        png_filename,
-        alpha_filename,
-        sprites,
-        scripts,
-    };
-    Ok((i, anm0))
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use std::io::{self, Read};
-    use std::fs::File;
-
-    #[test]
-    fn anm0() {
-        let file = File::open("EoSD/CM/player01.anm").unwrap();
-        let mut file = io::BufReader::new(file);
-        let mut buf = vec![];
-        file.read_to_end(&mut buf).unwrap();
-        let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
-        assert_eq!(anms.len(), 1);
-        let anm0 = anms.pop().unwrap();
-        assert_eq!(anm0.size, (256, 256));
-        assert_eq!(anm0.format, 5);
-    }
-}
--- a/src/th06/anm0_vm.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,506 +0,0 @@
-//! Animation runner.
-
-use crate::th06::anm0::{
-    Script,
-    Anm0,
-    Call,
-    Instruction,
-};
-use crate::th06::interpolator::{Interpolator1, Interpolator2, Interpolator3, Formula};
-use crate::util::math::Mat4;
-use crate::util::prng::Prng;
-use std::cell::RefCell;
-use std::rc::{Rc, Weak};
-
-/// TODO
-#[repr(C)]
-#[derive(Debug)]
-pub struct Vertex {
-    /// XXX
-    pub pos: [i16; 3],
-    /// XXX
-    pub layer: u16,
-    /// XXX
-    pub uv: [f32; 2],
-    /// XXX
-    pub color: [u8; 4],
-}
-
-/// Base visual element.
-#[derive(Debug, Clone, Default)]
-pub struct Sprite {
-    blendfunc: u32,
-    frame: u32,
-
-    width_override: f32,
-    height_override: f32,
-    angle: f32,
-
-    removed: bool,
-    changed: bool,
-    visible: bool,
-    force_rotation: bool,
-    automatic_orientation: bool,
-    allow_dest_offset: bool,
-    mirrored: bool,
-    corner_relative_placement: bool,
-
-    scale_interpolator: Option<Interpolator2<f32>>,
-    fade_interpolator: Option<Interpolator1<f32>>, // XXX: should be u8!
-    offset_interpolator: Option<Interpolator3<f32>>,
-    rotation_interpolator: Option<Interpolator3<f32>>,
-    color_interpolator: Option<Interpolator3<f32>>, // XXX: should be u8!
-
-    anm: Option<Anm0>,
-
-    dest_offset: [f32; 3],
-    texcoords: [f32; 4],
-    texoffsets: [f32; 2],
-    rescale: [f32; 2],
-    scale_speed: [f32; 2],
-    rotations_3d: [f32; 3],
-    rotations_speed_3d: [f32; 3],
-    color: [u8; 4],
-    layer: u16,
-}
-
-impl Sprite {
-    /// Create a new sprite.
-    pub fn new() -> Sprite {
-        Sprite {
-            changed: true,
-            visible: true,
-            rescale: [1., 1.],
-            color: [255, 255, 255, 255],
-            ..Default::default()
-        }
-    }
-
-    /// Create a new sprite overriding its size.
-    pub fn with_size(width_override: f32, height_override: f32) -> Sprite {
-        Sprite {
-            width_override,
-            height_override,
-            changed: true,
-            visible: true,
-            rescale: [1., 1.],
-            color: [255, 255, 255, 255],
-            ..Default::default()
-        }
-    }
-
-    /// TODO
-    pub fn fill_vertices(&self, vertices: &mut [Vertex; 4], x: f32, y: f32, z: f32) {
-        let mut mat = Mat4::new([[-0.5, 0.5, 0.5, -0.5],
-                                 [-0.5, -0.5, 0.5, 0.5],
-                                 [0., 0., 0., 0.],
-                                 [1., 1., 1., 1.]]);
-
-        let [tx, ty, tw, th] = self.texcoords;
-        let [sx, sy] = self.rescale;
-        let width = if self.width_override > 0. { self.width_override } else { tw * sx };
-        let height = if self.height_override > 0. { self.height_override } else { th * sy };
-
-        mat.scale2d(width, height);
-        if self.mirrored {
-            mat.flip();
-        }
-
-        let [rx, ry, mut rz] = self.rotations_3d;
-        if self.automatic_orientation {
-            rz += std::f32::consts::PI / 2. - self.angle;
-        } else if self.force_rotation {
-            rz += self.angle;
-        }
-
-        if rx != 0. {
-            mat.rotate_x(-rx);
-        }
-        if ry != 0. {
-            mat.rotate_y(ry);
-        }
-        if rz != 0. {
-            mat.rotate_z(-rz);
-        }
-
-        if self.allow_dest_offset {
-            mat.translate(self.dest_offset);
-        }
-        if self.corner_relative_placement {
-            mat.translate_2d(width / 2., height / 2.);
-        }
-
-        mat.translate([x, y, z]);
-
-        let mat = mat.borrow_inner();
-        vertices[0].pos[0] = mat[0][0] as i16;
-        vertices[0].pos[1] = mat[1][0] as i16;
-        vertices[0].pos[2] = mat[2][0] as i16;
-        vertices[1].pos[0] = mat[0][1] as i16;
-        vertices[1].pos[1] = mat[1][1] as i16;
-        vertices[1].pos[2] = mat[2][1] as i16;
-        vertices[2].pos[0] = mat[0][2] as i16;
-        vertices[2].pos[1] = mat[1][2] as i16;
-        vertices[2].pos[2] = mat[2][2] as i16;
-        vertices[3].pos[0] = mat[0][3] as i16;
-        vertices[3].pos[1] = mat[1][3] as i16;
-        vertices[3].pos[2] = mat[2][3] as i16;
-
-        // XXX: don’t clone here.
-        let (x_1, y_1) = self.anm.clone().unwrap().inv_size();
-        let [tox, toy] = self.texoffsets;
-        let left = tx * x_1 + tox;
-        let right = (tx + tw) * x_1 + tox;
-        let bottom = ty * y_1 + toy;
-        let top = (ty + th) * y_1 + toy;
-
-        vertices[0].uv[0] = left;
-        vertices[0].uv[1] = bottom;
-        vertices[1].uv[0] = right;
-        vertices[1].uv[1] = bottom;
-        vertices[2].uv[0] = right;
-        vertices[2].uv[1] = top;
-        vertices[3].uv[0] = left;
-        vertices[3].uv[1] = top;
-
-        vertices[0].color = self.color;
-        vertices[1].color = self.color;
-        vertices[2].color = self.color;
-        vertices[3].color = self.color;
-
-        vertices[0].layer = self.layer;
-        vertices[1].layer = self.layer;
-        vertices[2].layer = self.layer;
-        vertices[3].layer = self.layer;
-    }
-
-    /// Update sprite values from the interpolators.
-    pub fn update(&mut self) {
-        self.frame += 1;
-        self.corner_relative_placement = true;
-
-        let [sax, say, saz] = self.rotations_speed_3d;
-        if sax != 0. || say != 0. || saz != 0. {
-            let [ax, ay, az] = self.rotations_3d;
-            self.rotations_3d = [ax + sax, ay + say, az + saz];
-            self.changed = true;
-        } else if let Some(ref interpolator) = self.rotation_interpolator {
-            self.rotations_3d = interpolator.values(self.frame);
-            self.changed = true;
-        }
-
-        let [rsx, rsy] = self.scale_speed;
-        if rsx != 0. || rsy != 0. {
-            let [rx, ry] = self.rescale;
-            self.rescale = [rx + rsx, ry + rsy];
-            self.changed = true;
-        }
-
-        if let Some(ref interpolator) = self.fade_interpolator {
-            self.color[3] = interpolator.values(self.frame)[0] as u8;
-            self.changed = true;
-        }
-
-        if let Some(ref interpolator) = self.scale_interpolator {
-            self.rescale = interpolator.values(self.frame);
-            self.changed = true;
-        }
-
-        if let Some(ref interpolator) = self.offset_interpolator {
-            self.dest_offset = interpolator.values(self.frame);
-            self.changed = true;
-        }
-
-        if let Some(ref interpolator) = self.color_interpolator {
-            let color = interpolator.values(self.frame);
-            // TODO: this can probably be made to look nicer.
-            self.color[0] = color[0] as u8;
-            self.color[1] = color[1] as u8;
-            self.color[2] = color[2] as u8;
-            self.changed = true;
-        }
-    }
-}
-
-struct Anms {
-    inner: Rc<RefCell<[Anm0]>>,
-}
-
-impl Anms {
-    fn new(anms: Rc<RefCell<[Anm0]>>) -> Anms {
-        Anms {
-            inner: anms,
-        }
-    }
-
-    fn load_sprite(&self, sprite: &mut Sprite, id: u8) {
-        let anms = self.inner.borrow();
-        let mut anm = None;
-        let mut texcoords = None;
-        let mut layer = 0;
-        'anm: for anm0 in anms.iter() {
-            for sp in anm0.sprites.iter() {
-                if sp.index == id as u32 {
-                    texcoords = Some(sp);
-                    anm = Some(anm0.clone());
-                    break 'anm;
-                }
-            }
-            layer += 1;
-        }
-        sprite.anm = anm;
-        sprite.layer = layer;
-        if let Some(texcoords) = texcoords {
-            sprite.texcoords = [texcoords.x, texcoords.y, texcoords.width, texcoords.height];
-        }
-    }
-
-    fn get_script(&self, id: u8) -> Script {
-        let anms = self.inner.borrow();
-        for anm0 in anms.iter() {
-            if anm0.scripts.contains_key(&id) {
-                return anm0.scripts[&id].clone();
-            }
-        }
-        unreachable!();
-    }
-}
-
-/// Interpreter for `Anm0` instructions to update a `Sprite`.
-pub struct AnmRunner {
-    anms: Anms,
-    sprite: Rc<RefCell<Sprite>>,
-    prng: Weak<RefCell<Prng>>,
-    running: bool,
-    sprite_index_offset: u32,
-    script: Script,
-    instruction_pointer: usize,
-    frame: u16,
-    waiting: bool,
-    variables: ([i32; 4], [f32; 4], [i32; 4]),
-    timeout: Option<u32>,
-}
-
-impl AnmRunner {
-    /// Create a new `AnmRunner`.
-    pub fn new(anms: Rc<RefCell<[Anm0]>>, script_id: u8, sprite: Rc<RefCell<Sprite>>, prng: Weak<RefCell<Prng>>, sprite_index_offset: u32) -> AnmRunner {
-        let anms = Anms::new(anms);
-        let script = anms.get_script(script_id);
-        let mut runner = AnmRunner {
-            anms,
-            sprite: sprite,
-            prng,
-            running: true,
-            waiting: false,
-
-            script,
-            frame: 0,
-            timeout: None,
-            instruction_pointer: 0,
-            variables: ([0,  0,  0,  0 ],
-                        [0., 0., 0., 0.],
-                        [0,  0,  0,  0 ]),
-
-            sprite_index_offset: sprite_index_offset,
-        };
-        runner.run_frame();
-        runner.sprite_index_offset = 0;
-        runner
-    }
-
-    /// Get a Rc from the inner Sprite.
-    pub fn get_sprite(&self) -> Rc<RefCell<Sprite>> {
-        self.sprite.clone()
-    }
-
-    /// Trigger an interrupt.
-    pub fn interrupt(&mut self, interrupt: i32) -> bool {
-        let mut new_ip = self.script.interrupts.get(&interrupt);
-        if new_ip.is_none() {
-            new_ip = self.script.interrupts.get(&-1);
-        }
-        let new_ip = if let Some(new_ip) = new_ip {
-            *new_ip as usize
-        } else {
-            return false;
-        };
-        self.instruction_pointer = new_ip;
-        let Call { time: frame, instr: _ } = &self.script.instructions[self.instruction_pointer];
-        self.frame = *frame;
-        self.waiting = false;
-        self.sprite.borrow_mut().visible = true;
-        true
-    }
-
-    /// Advance the Anm of a single frame.
-    pub fn run_frame(&mut self) -> bool {
-        if !self.running {
-            return false;
-        }
-
-        while self.running && !self.waiting {
-            let Call { time: frame, instr } = self.script.instructions[self.instruction_pointer];
-            let frame = frame.clone();
-
-            if frame > self.frame {
-                break;
-            } else {
-                self.instruction_pointer += 1;
-            }
-
-            if frame == self.frame {
-                self.run_instruction(instr);
-                self.sprite.borrow_mut().changed = true;
-            }
-        }
-
-        if !self.waiting {
-            self.frame += 1;
-        } else if let Some(timeout) = self.timeout {
-            if timeout == self.sprite.borrow().frame { // TODO: check if it’s happening at the correct frame.
-                self.waiting = false;
-            }
-        }
-
-        self.sprite.borrow_mut().update();
-
-        self.running
-    }
-
-    fn run_instruction(&mut self, instruction: Instruction) {
-        let mut sprite = self.sprite.borrow_mut();
-        match instruction {
-            Instruction::Delete() => {
-                sprite.removed = true;
-                self.running = false;
-            }
-            Instruction::LoadSprite(sprite_index) => {
-                self.anms.load_sprite(&mut sprite, (sprite_index + self.sprite_index_offset) as u8);
-            }
-            Instruction::SetScale(sx, sy) => {
-                sprite.rescale = [sx, sy];
-            }
-            Instruction::SetAlpha(alpha) => {
-                // TODO: check this modulo.
-                sprite.color[3] = (alpha % 256) as u8;
-            }
-            Instruction::SetColor(b, g, r) => {
-                if sprite.fade_interpolator.is_none() {
-                    sprite.color[0] = r;
-                    sprite.color[1] = g;
-                    sprite.color[2] = b;
-                }
-            }
-            Instruction::Jump(pointer) => {
-                // TODO: is that really how it works?
-                self.instruction_pointer = pointer as usize;
-                self.frame = self.script.instructions[pointer as usize].time;
-            }
-            Instruction::ToggleMirrored() => {
-                sprite.mirrored = !sprite.mirrored;
-            }
-            Instruction::SetRotations3d(rx, ry, rz) => {
-                sprite.rotations_3d = [rx, ry, rz];
-            }
-            Instruction::SetRotationsSpeed3d(srx, sry, srz) => {
-                sprite.rotations_speed_3d = [srx, sry, srz];
-            }
-            Instruction::SetScaleSpeed(ssx, ssy) => {
-                sprite.scale_speed = [ssx, ssy];
-            }
-            Instruction::Fade(new_alpha, duration) => {
-                sprite.fade_interpolator = Some(Interpolator1::new([sprite.color[3] as f32], sprite.frame, [new_alpha as f32], sprite.frame + duration, Formula::Linear));
-            }
-            Instruction::SetBlendmodeAlphablend() => {
-                sprite.blendfunc = 1;
-            }
-            Instruction::SetBlendmodeAdd() => {
-                sprite.blendfunc = 0;
-            }
-            Instruction::KeepStill() => {
-                self.running = false;
-            }
-            Instruction::LoadRandomSprite(min_index, mut amplitude) => {
-                if amplitude > 0 {
-                    let prng = self.prng.upgrade().unwrap();
-                    let rand = prng.borrow_mut().get_u16();
-                    amplitude = (rand as u32) % amplitude;
-                }
-                let sprite_index = min_index + amplitude;
-                self.anms.load_sprite(&mut sprite, (sprite_index + self.sprite_index_offset) as u8);
-            }
-            Instruction::Move(x, y, z) => {
-                sprite.dest_offset = [x, y, z];
-            }
-            Instruction::MoveToLinear(x, y, z, duration) => {
-                sprite.offset_interpolator = Some(Interpolator3::new(sprite.dest_offset, sprite.frame, [x, y, z], sprite.frame + duration, Formula::Linear));
-            }
-            Instruction::MoveToDecel(x, y, z, duration) => {
-                sprite.offset_interpolator = Some(Interpolator3::new(sprite.dest_offset, sprite.frame, [x, y, z], sprite.frame + duration, Formula::InvertPower2));
-            }
-            Instruction::MoveToAccel(x, y, z, duration) => {
-                sprite.offset_interpolator = Some(Interpolator3::new(sprite.dest_offset, sprite.frame, [x, y, z], sprite.frame + duration, Formula::Power2));
-            }
-            Instruction::Wait() => {
-                self.waiting = true;
-            }
-            // There is nothing to do here.
-            Instruction::InterruptLabel(_label) => (),
-            Instruction::SetCornerRelativePlacement() => {
-                sprite.corner_relative_placement = true;
-            }
-            Instruction::WaitEx() => {
-                sprite.visible = false;
-                self.waiting = true;
-            }
-            Instruction::SetAllowOffset(value) => {
-                sprite.allow_dest_offset = value == 1
-            }
-            Instruction::SetAutomaticOrientation(value) => {
-                sprite.automatic_orientation = value == 1
-            }
-            Instruction::ShiftTextureX(dx) => {
-                let [tox, toy] = sprite.texoffsets;
-                sprite.texoffsets = [tox + dx, toy];
-            }
-            Instruction::ShiftTextureY(dy) => {
-                let [tox, toy] = sprite.texoffsets;
-                sprite.texoffsets = [tox, toy + dy];
-            }
-            Instruction::SetVisible(visible) => {
-                sprite.visible = (visible & 1) != 0;
-            }
-            Instruction::ScaleIn(sx, sy, duration) => {
-                sprite.scale_interpolator = Some(Interpolator2::new(sprite.rescale, sprite.frame, [sx, sy], sprite.frame + duration, Formula::Linear));
-            }
-            Instruction::Todo(_todo) => {
-                // TODO.
-            }
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use std::io::{self, Read};
-    use std::fs::File;
-
-    #[test]
-    fn anm_runner() {
-        let file = File::open("EoSD/CM/player01.anm").unwrap();
-        let mut file = io::BufReader::new(file);
-        let mut buf = vec![];
-        file.read_to_end(&mut buf).unwrap();
-        let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
-        let anm0 = anms.pop().unwrap();
-        assert_eq!(anm0.size, (256, 256));
-        assert_eq!(anm0.format, 5);
-        let sprite = Rc::new(RefCell::new(Sprite::new()));
-        let prng = Rc::new(RefCell::new(Prng::new(0)));
-        let mut anm_runner = AnmRunner::new(&anm0, 1, sprite.clone(), Rc::downgrade(&prng), 0);
-        for _ in 0..50 {
-            anm_runner.run_frame();
-        }
-    }
-}
--- a/src/th06/ecl.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,428 +0,0 @@
-//! ECL enemy script format support.
-
-use nom::{
-    IResult,
-    number::complete::{le_u8, le_u16, le_u32, le_i16, le_i32, le_f32},
-    sequence::tuple,
-    multi::{count, many0},
-    error::ErrorKind,
-    Err,
-};
-use encoding_rs::SHIFT_JIS;
-use bitflags::bitflags;
-
-bitflags! {
-    /// Bit flags describing the current difficulty level.
-    pub struct Rank: u16 {
-        /// Easy mode.
-        const EASY = 0x100;
-
-        /// Normal mode.
-        const NORMAL = 0x200;
-
-        /// Hard mode.
-        const HARD = 0x400;
-
-        /// Lunatic mode.
-        const LUNATIC = 0x800;
-
-        /// Any or all modes.
-        const ALL = 0xff00;
-    }
-}
-
-impl std::str::FromStr for Rank {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<Rank, Self::Err> {
-        Ok(match s {
-            "easy" => Rank::EASY,
-            "normal" => Rank::NORMAL,
-            "hard" => Rank::HARD,
-            "lunatic" => Rank::LUNATIC,
-            _ => return Err(format!("unknown rank {}", s))
-        })
-    }
-}
-
-/// A single instruction, part of a `Script`.
-#[derive(Debug, Clone)]
-pub struct CallSub {
-    /// Time at which this instruction will be called.
-    pub time: i32,
-
-    /// The difficulty level(s) this instruction will be called at.
-    pub rank_mask: Rank,
-
-    /// TODO
-    pub param_mask: u16,
-
-    /// The instruction to call.
-    pub instr: SubInstruction,
-}
-
-impl CallSub {
-    /// Create a new instruction call.
-    pub fn new(time: i32, rank_mask: Rank, instr: SubInstruction) -> CallSub {
-        CallSub {
-            time,
-            rank_mask,
-            param_mask: 0,
-            instr,
-        }
-    }
-}
-
-/// Script driving an animation.
-#[derive(Debug, Clone)]
-pub struct Sub {
-    /// List of instructions in this script.
-    pub instructions: Vec<CallSub>,
-}
-
-/// A single instruction, part of a `Script`.
-#[derive(Debug, Clone)]
-pub struct CallMain {
-    /// Time at which this instruction will be called.
-    pub time: u16,
-
-    /// Subroutine to call for this enemy.
-    pub sub: u16,
-
-    /// The instruction to call.
-    pub instr: MainInstruction,
-}
-
-/// Script driving an animation.
-#[derive(Debug, Clone)]
-pub struct Main {
-    /// List of instructions in this script.
-    pub instructions: Vec<CallMain>,
-}
-
-/// Main struct of the ANM0 animation format.
-#[derive(Debug, Clone)]
-pub struct Ecl {
-    /// A list of subs.
-    pub subs: Vec<Sub>,
-
-    /// A list of mains.
-    pub mains: Vec<Main>,
-}
-
-impl Ecl {
-    /// Parse a slice of bytes into an `Ecl` struct.
-    pub fn from_slice(data: &[u8]) -> IResult<&[u8], Ecl> {
-        parse_ecl(data)
-    }
-}
-
-macro_rules! declare_main_instructions {
-    ($($opcode:tt => fn $name:ident($($arg:ident: $arg_type:ident),*)),*,) => {
-        /// Available instructions in an `Ecl`.
-        #[allow(missing_docs)]
-        #[derive(Debug, Clone, Copy)]
-        pub enum MainInstruction {
-            $(
-                $name($($arg_type),*)
-            ),*
-        }
-
-        fn parse_main_instruction_args(input: &[u8], opcode: u16) -> IResult<&[u8], MainInstruction> {
-            let mut i = &input[..];
-            let instr = match opcode {
-                $(
-                    $opcode => {
-                        $(
-                            let (i2, $arg) = concat_idents!(le_, $arg_type)(i)?;
-                            i = i2;
-                        )*
-                        MainInstruction::$name($($arg),*)
-                    }
-                )*
-                _ => unreachable!()
-            };
-            Ok((i, instr))
-        }
-    };
-}
-
-/// Parse a SHIFT_JIS byte string of length 34 into a String.
-#[allow(non_snake_case)]
-pub fn le_String(i: &[u8]) -> IResult<&[u8], String> {
-    let data = i.splitn(2, |c| *c == b'\0').nth(0).unwrap();
-    let (string, _encoding, _replaced) = SHIFT_JIS.decode(data);
-    Ok((&i[34..], string.into_owned()))
-}
-
-macro_rules! declare_sub_instructions {
-    ($($opcode:tt => fn $name:ident($($arg:ident: $arg_type:ident),*)),*,) => {
-        /// Available instructions in an `Ecl`.
-        #[allow(missing_docs)]
-        #[derive(Debug, Clone)]
-        pub enum SubInstruction {
-            $(
-                $name($($arg_type),*)
-            ),*
-        }
-
-        fn parse_sub_instruction_args(input: &[u8], opcode: u16) -> IResult<&[u8], SubInstruction> {
-            let mut i = &input[..];
-            let instr = match opcode {
-                $(
-                    $opcode => {
-                        $(
-                            let (i2, $arg) = concat_idents!(le_, $arg_type)(i)?;
-                            i = i2;
-                        )*
-                        SubInstruction::$name($($arg),*)
-                    }
-                )*
-                _ => unreachable!()
-            };
-            Ok((i, instr))
-        }
-    };
-}
-
-declare_main_instructions!{
-    0 => fn SpawnEnemy(x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: u32),
-    2 => fn SpawnEnemyMirrored(x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: u32),
-    4 => fn SpawnEnemyRandom(x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: u32),
-    6 => fn SpawnEnemyMirroredRandom(x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: u32),
-    8 => fn CallMessage(),
-    9 => fn WaitMessage(),
-    10 => fn ResumeEcl(x: f32, y: f32),
-    12 => fn WaitForBossDeath(),
-}
-
-declare_sub_instructions!{
-    0 => fn Noop(),
-    1 => fn Destroy(unused: u32),
-    2 => fn RelativeJump(frame: i32, ip: i32),
-    3 => fn RelativeJumpEx(frame: i32, ip: i32, variable_id: i32),
-    4 => fn SetInt(var: i32, value: i32),
-    5 => fn SetFloat(var: i32, value: f32),
-    6 => fn SetRandomInt(var: i32, max: i32),
-    7 => fn SetRandomIntMin(var: i32, max: i32, min: i32),
-    8 => fn SetRandomFloat(var: i32, max: f32),
-    9 => fn SetRandomFloatMin(var: i32, amplitude: f32, min: f32),
-    10 => fn StoreX(var: i32),
-    11 => fn StoreY(var: i32),
-    12 => fn StoreZ(var: i32),
-    13 => fn AddInt(var: i32, a: i32, b: i32),
-    14 => fn SubstractInt(var: i32, a: i32, b: i32),
-    15 => fn MultiplyInt(var: i32, a: i32, b: i32),
-    16 => fn DivideInt(var: i32, a: i32, b: i32),
-    17 => fn ModuloInt(var: i32, a: i32, b: i32),
-    18 => fn Increment(var: i32),
-    19 => fn Decrement(var: i32),
-    20 => fn AddFloat(var: i32, a: f32, b: f32),
-    21 => fn SubstractFloat(var: i32, a: f32, b: f32),
-    22 => fn MultiplyFloat(var: i32, a: f32, b: f32),
-    23 => fn DivideFloat(var: i32, a: f32, b: f32),
-    24 => fn ModuloFloat(var: i32, a: f32, b: f32),
-    25 => fn GetDirection(var: i32, x1: f32, y1: f32, x2: f32, y2: f32),
-    26 => fn FloatToUnitCircle(var: i32),
-    27 => fn CompareInts(a: i32, b: i32),
-    28 => fn CompareFloats(a: f32, b: f32),
-    29 => fn RelativeJumpIfLowerThan(frame: i32, ip: i32),
-    30 => fn RelativeJumpIfLowerOrEqual(frame: i32, ip: i32),
-    31 => fn RelativeJumpIfEqual(frame: i32, ip: i32),
-    32 => fn RelativeJumpIfGreaterThan(frame: i32, ip: i32),
-    33 => fn RelativeJumpIfGreaterOrEqual(frame: i32, ip: i32),
-    34 => fn RelativeJumpIfNotEqual(frame: i32, ip: i32),
-    35 => fn Call(sub: i32, param1: i32, param2: f32),
-    36 => fn Return(),
-    37 => fn CallIfSuperior(sub: i32, param1: i32, param2: f32, a: i32, b: i32),
-    38 => fn CallIfSuperiorOrEqual(sub: i32, param1: i32, param2: f32, a: i32, b: i32),
-    39 => fn CallIfEqual(sub: i32, param1: i32, param2: f32, a: i32, b: i32),
-    40 => fn CallIfInferior(sub: i32, param1: i32, param2: f32, a: i32, b: i32),
-    41 => fn CallIfInferiorOrEqual(sub: i32, param1: i32, param2: f32, a: i32, b: i32),
-    42 => fn CallIfNotEqual(sub: i32, param1: i32, param2: f32, a: i32, b: i32),
-    43 => fn SetPosition(x: f32, y: f32, z: f32),
-    45 => fn SetAngleAndSpeed(angle: f32, speed: f32),
-    46 => fn SetRotationSpeed(speed: f32),
-    47 => fn SetSpeed(speed: f32),
-    48 => fn SetAcceleration(acceleration: f32),
-    49 => fn SetRandomAngle(min: f32, max: f32),
-    50 => fn SetRandomAngleEx(min: f32, max: f32),
-    51 => fn TargetPlayer(angle: f32, speed: f32),
-    52 => fn MoveInDecel(duration: i32, angle: f32, speed: f32),
-    56 => fn MoveToLinear(duration: i32, x: f32, y: f32, z: f32),
-    57 => fn MoveToDecel(duration: i32, x: f32, y: f32, z: f32),
-    59 => fn MoveToAccel(duration: i32, x: f32, y: f32, z: f32),
-    61 => fn StopIn(duration: i32),
-    63 => fn StopInAccel(duration: i32),
-    65 => fn SetScreenBox(xmin: f32, ymin: f32, xmax: f32, ymax: f32),
-    66 => fn ClearScreenBox(),
-    67 => fn SetBulletAttributes1(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
-    68 => fn SetBulletAttributes2(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
-    69 => fn SetBulletAttributes3(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
-    70 => fn SetBulletAttributes4(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
-    71 => fn SetBulletAttributes5(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
-    74 => fn SetBulletAttributes6(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
-    75 => fn SetBulletAttributes7(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32),
-    76 => fn SetBulletInterval(interval: i32),
-    77 => fn SetBulletIntervalEx(interval: i32),
-    78 => fn DelayAttack(),
-    79 => fn NoDelayAttack(),
-    81 => fn SetBulletLaunchOffset(x: f32, y: f32, z: f32),
-    82 => fn SetExtendedBulletAttributes(a: i32, b: i32, c: i32, d: i32, e: f32, f: f32, g: f32, h: f32),
-    83 => fn ChangeBulletsInStarBonus(),
-    // TODO: Found in stage 4 onward.
-    84 => fn SetBulletSound(sound: i32),
-    85 => fn NewLaser(laser_type: i16, sprite_idx_offset: i16, angle: f32, speed: f32, start_offset: f32, end_offset: f32, max_length: f32, width: f32, start_duration: i32, duration: i32, end_duration: i32, grazing_delay: i32, grazing_extra_duration: i32, UNK1: i32),
-    86 => fn NewLaserTowardsPlayer(laser_type: i16, sprite_idx_offset: i16, angle: f32, speed: f32, start_offset: f32, end_offset: f32, max_length: f32, width: f32, start_duration: i32, duration: i32, end_duration: i32, grazing_delay: i32, grazing_extra_duration: i32, UNK1: i32),
-    87 => fn SetUpcomingLaserId(id: u32),
-    88 => fn AlterLaserAngle(id: u32, delta: f32),
-    90 => fn RepositionLaser(id: u32, ox: f32, oy: f32, oz: f32),
-    91 => fn LaserSetCompare(id: u32),
-    92 => fn CancelLaser(id: u32),
-    93 => fn SetSpellcard(face: i16, number: i16, name: String),
-    94 => fn EndSpellcard(),
-    95 => fn SpawnEnemy(sub: i32, x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: i32),
-    96 => fn KillAllEnemies(),
-    97 => fn SetAnim(script: i32),
-    98 => fn SetMultipleAnims(default: i16, end_left: i16, end_right: i16, left: i16, right: i16, _unused: i16),
-    99 => fn SetAuxAnm(number: i32, script: i32),
-    100 => fn SetDeathAnim(sprite_index: i32),
-    101 => fn SetBossMode(value: i32),
-    102 => fn CreateSquares(UNK1: i32, UNK2: f32, UNK3: f32, UNK4: f32, UNK5: f32),
-    103 => fn SetHitbox(width: f32, height: f32, depth: f32),
-    104 => fn SetCollidable(collidable: i32),
-    105 => fn SetDamageable(damageable: i32),
-    106 => fn PlaySound(index: i32),
-    107 => fn SetDeathFlags(death_flags: u32),
-    108 => fn SetDeathCallback(sub: i32),
-    109 => fn MemoryWriteInt(value: i32, index: i32),
-    111 => fn SetLife(life: i32),
-    112 => fn SetElapsedTime(frame: i32),
-    113 => fn SetLowLifeTrigger(trigger: i32),
-    114 => fn SetLowLifeCallback(sub: i32),
-    115 => fn SetTimeout(timeout: i32),
-    116 => fn SetTimeoutCallback(sub: i32),
-    117 => fn SetTouchable(touchable: i32),
-    118 => fn DropParticles(anim: i32, number: u32, r: u8, g: u8, b: u8, a: u8),
-    119 => fn DropBonus(number: i32),
-    120 => fn SetAutomaticOrientation(automatic: i32),
-    121 => fn CallSpecialFunction(function: i32, argument: i32),
-    122 => fn SetSpecialFunctionCallback(function: i32),
-    123 => fn SkipFrames(frames: i32),
-    124 => fn DropSpecificBonus(type_: i32),
-    // TODO: Found in stage 3.
-    125 => fn UNK_ins125(),
-    126 => fn SetRemainingLives(lives: i32),
-    // TODO: Found in stage 4.
-    127 => fn UNK_ins127(UNK1: i32),
-    128 => fn Interrupt(event: i32),
-    129 => fn InterruptAux(number: i32, event: i32),
-    // TODO: Found in stage 4.
-    130 => fn UNK_ins130(UNK1: i32),
-    131 => fn SetDifficultyCoeffs(speed_a: f32, speed_b: f32, nb_a: i32, nb_b: i32, shots_a: i32, shots_b: i32),
-    132 => fn SetInvisible(invisible: i32),
-    133 => fn CopyCallbacks(),
-    // TODO: Found in stage 4.
-    134 => fn UNK_ins134(),
-    135 => fn EnableSpellcardBonus(UNK1: i32),
-}
-
-fn parse_sub_instruction(input: &[u8]) -> IResult<&[u8], CallSub> {
-    let i = &input[..];
-    let (i, (time, opcode)) = tuple((le_i32, le_u16))(i)?;
-    if time == -1 || opcode == 0xffff {
-        return Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof)));
-    }
-
-    let (i, (size, rank_mask, param_mask)) = tuple((le_u16, le_u16, le_u16))(i)?;
-    let rank_mask = Rank::from_bits(rank_mask).unwrap();
-    let (i, instr) = parse_sub_instruction_args(i, opcode)?;
-    assert_eq!(input.len() - i.len(), size as usize);
-    let call = CallSub { time, rank_mask, param_mask, instr };
-    Ok((i, call))
-}
-
-fn parse_sub(i: &[u8]) -> IResult<&[u8], Sub> {
-    let (i, instructions) = many0(parse_sub_instruction)(i)?;
-    let sub = Sub { instructions };
-    Ok((i, sub))
-}
-
-fn parse_main_instruction(input: &[u8]) -> IResult<&[u8], CallMain> {
-    let i = &input[..];
-    let (i, (time, sub)) = tuple((le_u16, le_u16))(i)?;
-    if time == 0xffff && sub == 4 {
-        return Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof)));
-    }
-
-    let (i, (opcode, size)) = tuple((le_u16, le_u16))(i)?;
-    let size = size as usize;
-    let (i, instr) = parse_main_instruction_args(i, opcode)?;
-    assert_eq!(input.len() - i.len(), size as usize);
-    let call = CallMain { time, sub, instr };
-    Ok((i, call))
-}
-
-fn parse_main(i: &[u8]) -> IResult<&[u8], Main> {
-    let (i, instructions) = many0(parse_main_instruction)(i)?;
-    let main = Main { instructions };
-    Ok((i, main))
-}
-
-fn parse_ecl(input: &[u8]) -> IResult<&[u8], Ecl> {
-    let i = input;
-
-    let (i, (sub_count, main_count)) = tuple((le_u16, le_u16))(i)?;
-    let sub_count = sub_count as usize;
-
-    if main_count != 0 {
-        // TODO: use a better error.
-        return Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof)));
-    }
-
-    let (_, (main_offsets, sub_offsets)) = tuple((
-        count(le_u32, 3),
-        count(le_u32, sub_count),
-    ))(i)?;
-
-    // Read all subs.
-    let mut subs = Vec::new();
-    for offset in sub_offsets.into_iter().map(|offset| offset as usize) {
-        let (_, sub) = parse_sub(&input[offset..])?;
-        subs.push(sub);
-    }
-
-    // Read all mains (always a single one atm).
-    let mut mains = Vec::new();
-    for offset in main_offsets.into_iter().map(|offset| offset as usize) {
-        if offset == 0 {
-            break;
-        }
-        let (_, main) = parse_main(&input[offset..])?;
-        mains.push(main);
-    }
-
-    let ecl = Ecl {
-        subs,
-        mains,
-    };
-    Ok((b"", ecl))
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use std::io::{self, Read};
-    use std::fs::File;
-
-    #[test]
-    fn ecl() {
-        let file = File::open("EoSD/ST/ecldata1.ecl").unwrap();
-        let mut file = io::BufReader::new(file);
-        let mut buf = vec![];
-        file.read_to_end(&mut buf).unwrap();
-        let (_, ecl) = Ecl::from_slice(&buf).unwrap();
-        assert_eq!(ecl.subs.len(), 24);
-        assert_eq!(ecl.mains.len(), 1);
-    }
-}
--- a/src/th06/ecl_vm.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1333 +0,0 @@
-//! ECL runner.
-
-use crate::th06::ecl::{Ecl, SubInstruction};
-use crate::th06::enemy::{Enemy, Offset, BulletAttributes, Position};
-use crate::util::prng::Prng;
-use std::cell::RefCell;
-use std::rc::Rc;
-
-macro_rules! gen_SetBulletAttributes {
-    ($self:ident, $opcode:tt, $anim:ident, $sprite_index_offset:ident, $bullets_per_shot:ident,
-     $number_of_shots:ident, $speed:ident, $speed2:ident, $launch_angle:ident, $angle:ident,
-     $flags:ident) => {{
-        let sprite_index_offset = $self.get_i32($sprite_index_offset as i32) as i16;
-        let bullets_per_shot = $self.get_i32($bullets_per_shot) as i16;
-        let number_of_shots = $self.get_i32($number_of_shots) as i16;
-        let speed = $self.get_f32($speed);
-        let speed2 = $self.get_f32($speed2);
-        let launch_angle = $self.get_f32($launch_angle);
-        let angle = $self.get_f32($angle);
-
-        let mut enemy = $self.enemy.borrow_mut();
-        enemy.set_bullet_attributes($opcode, $anim, sprite_index_offset, bullets_per_shot,
-                                    number_of_shots, speed, speed2, launch_angle, angle, $flags);
-    }};
-}
-
-#[derive(Clone, Default)]
-struct StackFrame {
-    frame: i32,
-    ip: i32,
-    //ins122_callback: Option<Box<FnMut(Enemy)>>,
-    ints1: [i32; 4],
-    floats: [f32; 4],
-    ints2: [i32; 4],
-    comparison_reg: i32,
-    sub: u16,
-}
-
-/// Interpreter for enemy scripts.
-#[derive(Default)]
-pub struct EclRunner {
-    /// XXX
-    pub enemy: Rc<RefCell<Enemy>>,
-
-    ecl: Option<Ecl>,
-    /// XXX
-    pub running: bool,
-    frame: StackFrame,
-    // TODO: there are only 8 of these.
-    stack: Vec<StackFrame>,
-}
-
-impl EclRunner {
-    /// Create a new ECL runner.
-    pub fn new(ecl: &Ecl, enemy: Rc<RefCell<Enemy>>, sub: u16) -> EclRunner {
-        let mut ecl_runner = EclRunner {
-            enemy,
-            // XXX: no clone.
-            ecl: Some(ecl.clone()),
-            running: true,
-            ..Default::default()
-        };
-        ecl_runner.frame.sub = sub;
-        ecl_runner
-    }
-
-    /// Advance the ECL of a single frame.
-    pub fn run_frame(&mut self) {
-        while self.running {
-            let ecl = self.ecl.clone().unwrap();
-            let sub = &ecl.subs[self.frame.sub as usize];
-            let call = match sub.instructions.get(self.frame.ip as usize) {
-                Some(call) => call,
-                None => {
-                    self.running = false;
-                    break;
-                }
-            };
-
-            if call.time > self.frame.frame {
-                break;
-            }
-            self.frame.ip += 1;
-
-            let rank = self.enemy.borrow().get_rank();
-            if (call.rank_mask & rank).is_empty() {
-                continue;
-            }
-
-            if call.time == self.frame.frame {
-                self.run_instruction(call.instr.clone());
-            }
-        }
-        self.frame.frame += 1;
-    }
-
-    fn get_i32(&self, var: i32) -> i32 {
-        let enemy = self.enemy.borrow();
-        match var {
-            -10001 => self.frame.ints1[0],
-            -10002 => self.frame.ints1[1],
-            -10003 => self.frame.ints1[2],
-            -10004 => self.frame.ints1[3],
-            -10005 => self.frame.floats[0] as i32,
-            -10006 => self.frame.floats[1] as i32,
-            -10007 => self.frame.floats[2] as i32,
-            -10008 => self.frame.floats[3] as i32,
-            -10009 => self.frame.ints2[0],
-            -10010 => self.frame.ints2[1],
-            -10011 => self.frame.ints2[2],
-            -10012 => self.frame.ints2[3],
-            -10013 => enemy.get_rank().bits() as i32,
-            -10014 => enemy.get_difficulty(),
-            -10015 => enemy.pos.x as i32,
-            -10016 => enemy.pos.y as i32,
-            -10017 => enemy.z as i32,
-            -10018 => unimplemented!(),
-            -10019 => unimplemented!(),
-            -10020 => unreachable!(),
-            -10021 => unimplemented!(),
-            -10022 => enemy.frame as i32,
-            -10023 => unreachable!(),
-            -10024 => enemy.life as i32,
-            -10025 => unimplemented!(),
-            _ => var
-        }
-    }
-
-    fn get_f32(&self, var: f32) -> f32 {
-        let enemy = self.enemy.borrow();
-        match var {
-            -10001.0 => self.frame.ints1[0] as f32,
-            -10002.0 => self.frame.ints1[1] as f32,
-            -10003.0 => self.frame.ints1[2] as f32,
-            -10004.0 => self.frame.ints1[3] as f32,
-            -10005.0 => self.frame.floats[0],
-            -10006.0 => self.frame.floats[1],
-            -10007.0 => self.frame.floats[2],
-            -10008.0 => self.frame.floats[3],
-            -10009.0 => self.frame.ints2[0] as f32,
-            -10010.0 => self.frame.ints2[1] as f32,
-            -10011.0 => self.frame.ints2[2] as f32,
-            -10012.0 => self.frame.ints2[3] as f32,
-            -10013.0 => enemy.get_rank().bits() as f32,
-            -10014.0 => enemy.get_difficulty() as f32,
-            -10015.0 => enemy.pos.x,
-            -10016.0 => enemy.pos.y,
-            -10017.0 => enemy.z,
-            -10018.0 => unimplemented!(),
-            -10019.0 => unimplemented!(),
-            -10020.0 => unreachable!(),
-            -10021.0 => unimplemented!(),
-            -10022.0 => enemy.frame as f32,
-            -10023.0 => unreachable!(),
-            -10024.0 => enemy.life as f32,
-            -10025.0 => unimplemented!(),
-            _ => var
-        }
-    }
-
-    fn set_i32(&mut self, var: i32, value: i32) {
-        let mut enemy = self.enemy.borrow_mut();
-        match var {
-            -10001 => self.frame.ints1[0] = value,
-            -10002 => self.frame.ints1[1] = value,
-            -10003 => self.frame.ints1[2] = value,
-            -10004 => self.frame.ints1[3] = value,
-            -10005 => unimplemented!(),
-            -10006 => unimplemented!(),
-            -10007 => unimplemented!(),
-            -10008 => unimplemented!(),
-            -10009 => self.frame.ints2[0] = value,
-            -10010 => self.frame.ints2[1] = value,
-            -10011 => self.frame.ints2[2] = value,
-            -10012 => self.frame.ints2[3] = value,
-            -10013 => unreachable!(),
-            -10014 => unreachable!(),
-            -10015 => unimplemented!(),
-            -10016 => unimplemented!(),
-            -10017 => unimplemented!(),
-            -10018 => unreachable!(),
-            -10019 => unreachable!(),
-            -10020 => unreachable!(),
-            -10021 => unreachable!(),
-            -10022 => enemy.frame = value as u32,
-            -10023 => unreachable!(),
-            -10024 => enemy.life = value as u32,
-            -10025 => unreachable!(),
-            _ => panic!("Unknown variable {}", var)
-        }
-    }
-
-    fn set_f32(&mut self, var: f32, value: f32) {
-        let mut enemy = self.enemy.borrow_mut();
-        match var {
-            -10001.0 => unimplemented!(),
-            -10002.0 => unimplemented!(),
-            -10003.0 => unimplemented!(),
-            -10004.0 => unimplemented!(),
-            -10005.0 => self.frame.floats[0] = value,
-            -10006.0 => self.frame.floats[1] = value,
-            -10007.0 => self.frame.floats[2] = value,
-            -10008.0 => self.frame.floats[3] = value,
-            -10009.0 => unimplemented!(),
-            -10010.0 => unimplemented!(),
-            -10011.0 => unimplemented!(),
-            -10012.0 => unimplemented!(),
-            -10013.0 => unreachable!(),
-            -10014.0 => unreachable!(),
-            -10015.0 => enemy.pos.x = value,
-            -10016.0 => enemy.pos.y = value,
-            -10017.0 => enemy.z = value,
-            -10018.0 => unreachable!(),
-            -10019.0 => unreachable!(),
-            -10020.0 => unreachable!(),
-            -10021.0 => unreachable!(),
-            -10022.0 => unimplemented!(),
-            -10023.0 => unreachable!(),
-            -10024.0 => unimplemented!(),
-            -10025.0 => unreachable!(),
-            _ => panic!("Unknown variable {}", var)
-        }
-    }
-
-    fn get_prng(&mut self) -> Rc<RefCell<Prng>> {
-        let enemy = self.enemy.borrow();
-        enemy.prng.upgrade().unwrap()
-    }
-
-    fn run_instruction(&mut self, instruction: SubInstruction) {
-        println!("Running instruction {:?}", instruction);
-        match instruction {
-            SubInstruction::Noop() => {
-                // really
-            }
-            // 1
-            SubInstruction::Destroy(_unused) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.removed = true;
-            }
-            // 2
-            SubInstruction::RelativeJump(frame, ip) => {
-                self.frame.frame = frame;
-                // ip = ip + flag in th06
-                self.frame.ip = ip;
-                // we jump back to the main of the interpreter
-            }
-            // 3
-            // GHIDRA SAYS THERE IS A COMPARISON_REG BUFFER BUT THERE IS NOT!!!
-            //
-            // MOV        ECX,dword ptr [EBP + 0x8]                     jumptable 00407544 case 31
-            // CMP        dword ptr [0x9d4 + ECX],0x0
-            // JLE        LAB_00407abb
-            // aka ECX = enemy pointer
-            // ECX->9d4 (aka enemy_pointer_copy->comparison_reg) == 0
-            // only the pointer is copied, not the value, thus we are safe
-            SubInstruction::RelativeJumpEx(frame, ip, var_id) => {
-                // TODO: counter_value is a field of "enemy" in th06, to check
-                let counter_value = self.get_i32(var_id) - 1;
-                if counter_value > 0 {
-                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
-                }
-            }
-            // 4
-            SubInstruction::SetInt(var_id, value) => {
-                self.set_i32(var_id, value);
-            }
-            // 5
-            SubInstruction::SetFloat(var_id, value) => {
-                self.set_f32(var_id as f32, value);
-            }
-            // 6
-            SubInstruction::SetRandomInt(var_id, maxval) => {
-                let random = self.get_prng().borrow_mut().get_u32() as i32;
-                self.set_i32(var_id, random % self.get_i32(maxval));
-            }
-            // 7
-            SubInstruction::SetRandomIntMin(var_id, maxval, minval) => {
-                let random = self.get_prng().borrow_mut().get_u32() as i32;
-                self.set_i32(var_id, (random % self.get_i32(maxval)) + self.get_i32(minval));
-            }
-            // 8
-            SubInstruction::SetRandomFloat(var_id, maxval) => {
-                let random = self.get_prng().borrow_mut().get_f64() as f32;
-                self.set_f32(var_id as f32, self.get_f32(maxval) * random)
-            }
-            // 9
-            SubInstruction::SetRandomFloatMin(var_id, maxval, minval) => {
-                let random = self.get_prng().borrow_mut().get_f64() as f32;
-                self.set_f32(var_id as f32, self.get_f32(maxval) * random + self.get_f32(minval))
-            }
-            // 10
-            SubInstruction::StoreX(var_id) => {
-                let x = {
-                    let enemy = self.enemy.borrow();
-                    enemy.pos.x
-                };
-                // TODO: is this really an i32?
-                self.set_i32(var_id, x as i32);
-            }
-            // 11
-            SubInstruction::StoreY(var_id) => {
-                let y = {
-                    let enemy = self.enemy.borrow();
-                    enemy.pos.y
-                };
-                self.set_i32(var_id, y as i32);
-            }
-            // 12
-            SubInstruction::StoreZ(var_id) => {
-                let z = {
-                    let enemy = self.enemy.borrow();
-                    enemy.z
-                };
-                self.set_i32(var_id, z as i32);
-            }
-            // 13(int), 20(float), same impl in th06
-            SubInstruction::AddInt(var_id, a, b) => {
-                self.set_i32(var_id, self.get_i32(a) + self.get_i32(b));
-            }
-            SubInstruction::AddFloat(var_id, a, b) => {
-                self.set_f32(var_id as f32, self.get_f32(a) + self.get_f32(b));
-            }
-            // 14(int), 21(float), same impl in th06
-            SubInstruction::SubstractInt(var_id, a, b) => {
-                self.set_i32(var_id, self.get_i32(a) - self.get_i32(b));
-            }
-            SubInstruction::SubstractFloat(var_id, a, b) => {
-                self.set_f32(var_id as f32, self.get_f32(a) - self.get_f32(b));
-            }
-            // 15(int), 22(unused)
-            SubInstruction::MultiplyInt(var_id, a, b) => {
-                self.set_i32(var_id, self.get_i32(a) * self.get_i32(b));
-            }
-            /*
-            SubInstruction::MultiplyFloat(var_id, a, b) => {
-                self.set_f32(var_id as f32, self.get_f32(a) * self.get_f32(b));
-            }
-            */
-             // 16(int), 23(unused)
-            SubInstruction::DivideInt(var_id, a, b) => {
-                self.set_i32(var_id, self.get_i32(a) / self.get_i32(b));
-            }
-
-            SubInstruction::DivideFloat(var_id, a, b) => {
-                self.set_f32(var_id as f32, self.get_f32(a) / self.get_f32(b));
-            }
-
-            // 17(int) 24(unused)
-            SubInstruction::ModuloInt(var_id, a, b) => {
-                self.set_i32(var_id, self.get_i32(a) % self.get_i32(b));
-            }
-
-            SubInstruction::ModuloFloat(var_id, a, b) => {
-                self.set_f32(var_id as f32, self.get_f32(a) % self.get_f32(b));
-            }
-
-            // 18
-            // setval used by pytouhou, but not in game(???)
-            SubInstruction::Increment(var_id) => {
-                self.set_i32(var_id, self.get_i32(var_id) + 1);
-            }
-
-            // 19
-            SubInstruction::Decrement(var_id) => {
-                self.set_i32(var_id, self.get_i32(var_id) - 1);
-            }
-
-            //25
-            SubInstruction::GetDirection(var_id, x1, y1, x2, y2) => {
-                //__ctrandisp2 in ghidra, let's assume from pytouhou it's atan2
-                self.set_f32(var_id as f32, (self.get_f32(y2) - self.get_f32(y1)).atan2(self.get_f32(x2) - self.get_f32(x1)));
-            }
-
-            // 26
-            SubInstruction::FloatToUnitCircle(var_id) => {
-                // TODO: atan2(var_id, ??) is used by th06, maybe ?? is pi?
-                // we suck at trigonometry so let's use pytouhou for now
-                self.set_f32(var_id as f32, (self.get_f32(var_id as f32) + std::f32::consts::PI) % (2. * std::f32::consts::PI) - std::f32::consts::PI);
-            }
-
-            // 27(int), 28(float)
-            SubInstruction::CompareInts(a, b) => {
-                let a = self.get_i32(a);
-                let b = self.get_i32(b);
-                if a < b {
-                    self.frame.comparison_reg = -1;
-                }
-                else if  a == b {
-                    self.frame.comparison_reg = 0;
-                }
-                else {
-                    self.frame.comparison_reg = 1;
-                }
-            }
-            SubInstruction::CompareFloats(a, b) => {
-                let a = self.get_f32(a);
-                let b = self.get_f32(b);
-                if a < b {
-                    self.frame.comparison_reg = -1;
-                }
-                else if  a == b {
-                    self.frame.comparison_reg = 0;
-                }
-                else {
-                    self.frame.comparison_reg = 1;
-                }
-            }
-            // 29
-            SubInstruction::RelativeJumpIfLowerThan(frame, ip) => {
-                if self.frame.comparison_reg == -1 {
-                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
-                }
-            }
-            // 30
-            SubInstruction::RelativeJumpIfLowerOrEqual(frame, ip) => {
-                if self.frame.comparison_reg != 1 {
-                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
-                }
-            }
-            // 31
-            SubInstruction::RelativeJumpIfEqual(frame, ip) => {
-                if self.frame.comparison_reg == 0 {
-                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
-                }
-            }
-            // 32
-            SubInstruction::RelativeJumpIfGreaterThan(frame, ip) => {
-                if self.frame.comparison_reg == 1 {
-                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
-                }
-            }
-            // 33
-            SubInstruction::RelativeJumpIfGreaterOrEqual(frame, ip) => {
-                if self.frame.comparison_reg != -1 {
-                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
-                }
-            }
-            // 34
-            SubInstruction::RelativeJumpIfNotEqual(frame, ip) => {
-                if self.frame.comparison_reg != 0 {
-                    self.run_instruction(SubInstruction::RelativeJump(frame, ip));
-                }
-            }
-            // 35
-            SubInstruction::Call(sub, param1, param2) => {
-                self.stack.push(self.frame.clone());
-                self.frame.sub = sub as u16;
-                self.frame.ints1[0] = param1;
-                self.frame.floats[0] = param2;
-                self.frame.frame = 0;
-                self.frame.ip = 0;
-            }
-
-            // 36
-            SubInstruction::Return() => {
-                self.frame = self.stack.pop().unwrap();
-            }
-            // 37
-            SubInstruction::CallIfSuperior(sub, param1, param2, a, b) => {
-                if self.get_i32(a) < self.get_i32(b) {
-                    self.run_instruction(SubInstruction::Call(sub, param1, param2));
-                }
-            }
-            // 38
-            SubInstruction::CallIfSuperiorOrEqual(sub, param1, param2, a, b) => {
-                if self.get_i32(a) <= self.get_i32(b) {
-                    self.run_instruction(SubInstruction::Call(sub, param1, param2));
-                }
-            }
-            // 39
-            SubInstruction::CallIfEqual(sub, param1, param2, a, b) => {
-                if self.get_i32(a) == self.get_i32(b) {
-                    self.run_instruction(SubInstruction::Call(sub, param1, param2));
-                }
-            }
-            // 40
-            SubInstruction::CallIfInferior(sub, param1, param2, a, b) => {
-                if self.get_i32(b) < self.get_i32(a) {
-                    self.run_instruction(SubInstruction::Call(sub, param1, param2));
-                }
-            }
-
-            // 41
-            SubInstruction::CallIfInferiorOrEqual(sub, param1, param2, a, b) => {
-                if self.get_i32(b) <= self.get_i32(a) {
-                    self.run_instruction(SubInstruction::Call(sub, param1, param2));
-                }
-            }
-            //42
-            SubInstruction::CallIfNotEqual(sub, param1, param2, a, b) => {
-                if self.get_i32(a) != self.get_i32(b) {
-                    self.run_instruction(SubInstruction::Call(sub, param1, param2));
-                }
-            }
-
-            // 43
-            SubInstruction::SetPosition(x, y, z) => {
-                let (x, y, z) = (self.get_f32(x), self.get_f32(y), self.get_f32(z));
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.set_pos(x, y, z);
-            }
-            // 44
-            /*
-            SubInstruction::SetAngularSpeed(x, y, z) => {
-                // same as above, except for angular speed
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.set_angular_speed(self.get_f32(x), self.get_f32(y), self.get_f32(z));
-            }
-            */
-            // 45
-            SubInstruction::SetAngleAndSpeed(angle, speed) => {
-                let angle = self.get_f32(angle);
-                let speed = self.get_f32(speed);
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.update_mode = 0;
-                enemy.angle = angle;
-                enemy.speed = speed;
-            }
-            // 46
-            SubInstruction::SetRotationSpeed(speed) => {
-                let rotation_speed = self.get_f32(speed);
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.update_mode = 0;
-                enemy.rotation_speed = rotation_speed;
-            }
-            // 47
-            SubInstruction::SetSpeed(speed) => {
-                let speed = self.get_f32(speed);
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.update_mode = 0;
-                enemy.speed = speed;
-            }
-            // 48
-            SubInstruction::SetAcceleration(acceleration) => {
-                let acceleration = self.get_f32(acceleration);
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.update_mode = 0;
-                enemy.acceleration = acceleration;
-            }
-            // 49
-            SubInstruction::SetRandomAngle(min_angle, max_angle) => {
-                let angle = self.get_prng().borrow_mut().get_f64() as f32 * (max_angle - min_angle) + min_angle;
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.angle = angle;
-            }
-            // 51
-            SubInstruction::TargetPlayer(delta_angle, speed) => {
-                let speed = self.get_f32(speed);
-                let mut enemy = self.enemy.borrow_mut();
-                let game = enemy.game.upgrade().unwrap();
-                let player = game.borrow().get_player();
-                enemy.update_mode = 0;
-                enemy.speed = speed;
-                enemy.angle = enemy.get_angle_to(player) + delta_angle;
-            }
-
-            // 52 to 64 are different interlacing fields
-
-            // 65
-            // to note: in game a flag is set to enable the screenbox and is set by 66 to disable
-            // it on top of setting our values. But we have a good engine and can detect if that's
-            // changed without setting a flag :)
-            SubInstruction::SetScreenBox(xmin, ymin, xmax, ymax) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.screen_box = Some((xmin, ymin, xmax, ymax));
-            }
-             // 66
-            SubInstruction::ClearScreenBox() => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.screen_box = None;
-            }
-
-            // 67 to 75 are set bullet attributes and it seems a pain to reverse rn
-            SubInstruction::SetBulletAttributes1(anim, sprite_index_offset, bullets_per_shot,
-                                                 number_of_shots, speed, speed2, launch_angle,
-                                                 angle, flags) => {
-                gen_SetBulletAttributes!(self, 67, anim, sprite_index_offset, bullets_per_shot,
-                                         number_of_shots, speed, speed2, launch_angle, angle,
-                                         flags);
-            }
-            SubInstruction::SetBulletAttributes2(anim, sprite_index_offset, bullets_per_shot,
-                                                 number_of_shots, speed, speed2, launch_angle,
-                                                 angle, flags) => {
-                gen_SetBulletAttributes!(self, 68, anim, sprite_index_offset, bullets_per_shot,
-                                         number_of_shots, speed, speed2, launch_angle, angle,
-                                         flags);
-            }
-            SubInstruction::SetBulletAttributes3(anim, sprite_index_offset, bullets_per_shot,
-                                                 number_of_shots, speed, speed2, launch_angle,
-                                                 angle, flags) => {
-                gen_SetBulletAttributes!(self, 69, anim, sprite_index_offset, bullets_per_shot,
-                                         number_of_shots, speed, speed2, launch_angle, angle,
-                                         flags);
-            }
-            SubInstruction::SetBulletAttributes4(anim, sprite_index_offset, bullets_per_shot,
-                                                 number_of_shots, speed, speed2, launch_angle,
-                                                 angle, flags) => {
-                gen_SetBulletAttributes!(self, 70, anim, sprite_index_offset, bullets_per_shot,
-                                         number_of_shots, speed, speed2, launch_angle, angle,
-                                         flags);
-            }
-            SubInstruction::SetBulletAttributes5(anim, sprite_index_offset, bullets_per_shot,
-                                                 number_of_shots, speed, speed2, launch_angle,
-                                                 angle, flags) => {
-                gen_SetBulletAttributes!(self, 71, anim, sprite_index_offset, bullets_per_shot,
-                                         number_of_shots, speed, speed2, launch_angle, angle,
-                                         flags);
-            }
-            SubInstruction::SetBulletAttributes6(anim, sprite_index_offset, bullets_per_shot,
-                                                 number_of_shots, speed, speed2, launch_angle,
-                                                 angle, flags) => {
-                gen_SetBulletAttributes!(self, 74, anim, sprite_index_offset, bullets_per_shot,
-                                         number_of_shots, speed, speed2, launch_angle, angle,
-                                         flags);
-            }
-            SubInstruction::SetBulletAttributes7(anim, sprite_index_offset, bullets_per_shot,
-                                                 number_of_shots, speed, speed2, launch_angle,
-                                                 angle, flags) => {
-                gen_SetBulletAttributes!(self, 75, anim, sprite_index_offset, bullets_per_shot,
-                                         number_of_shots, speed, speed2, launch_angle, angle,
-                                         flags);
-            }
-
-            // 76
-            SubInstruction::SetBulletInterval(interval) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.set_bullet_launch_interval(0, interval);
-            }
-
-            // 77
-            SubInstruction::SetBulletIntervalEx(interval) => {
-                let rand_start = self.get_prng().borrow_mut().get_u32();
-
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.set_bullet_launch_interval(rand_start, interval);
-            }
-
-            // 78-79 are more interpolation flags
-            // 78
-            SubInstruction::DelayAttack() => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.delay_attack = true;
-            }
-            // 79
-            SubInstruction::NoDelayAttack() => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.delay_attack = false;
-            }
-            // 80
-            /*
-            SubInstruction::NoClue() => {
-                let mut enemy = self.enemy.borrow_mut();
-                //bullet_pos = launch offset
-                (enemy->bullet_attributes).bullets_per_shot = enemy.pos.x + enemy->bullet_pos.pos.x;
-                (enemy->bullet_attributes).number_of_shots = enemy.pos.pos.y + enemy.bullet_pos.pos.y;
-                (enemy->bullet_attributes).speed = enemy.z + bullet_pos.z;
-                enemy.fire(bullet_attributes=bullet_attributes)
-            }
-            */
-
-            // 81
-            SubInstruction::SetBulletLaunchOffset(dx, dy, dz) => {
-                let (dx, dy, dz) = (self.get_f32(dx), self.get_f32(dy), self.get_f32(dz));
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.bullet_offset = Offset { dx, dy };
-            }
-
-            // 82
-            SubInstruction::SetExtendedBulletAttributes(a, b, c, d, e, f, g, h) => {
-                let (a, b, c, d) = (self.get_i32(a), self.get_i32(b), self.get_i32(c), self.get_i32(d));
-                let (e, f, g, h) = (self.get_f32(e), self.get_f32(f), self.get_f32(g), self.get_f32(h));
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.bullet_attributes.extended_attributes = (a, b, c, d, e, f, g, h);
-            }
-
-            // 83
-            /*
-            SubInstruction::ChangeBulletsIntoStarBonus() => {
-                let mut game = self.game.borrow_mut();
-                game.change_bullets_into_star_items();
-            }
-            */
-
-            // 84
-            SubInstruction::SetBulletSound(sound) => {
-                let mut enemy = self.enemy.borrow_mut();
-                if sound < 0 {
-                    enemy.bullet_attributes.sound = None;
-                } else {
-                    // This assert isn’t part of the original engine, but it would crash on high
-                    // values anyway.
-                    assert!(sound <= 255);
-                    enemy.bullet_attributes.sound = Some(sound as u8);
-                }
-            }
-
-            // 85-86 ire newlaser functions
-
-            // 87
-            SubInstruction::SetUpcomingLaserId(laser_id) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.current_laser_id = laser_id;
-            }
-
-            // 88
-            /*
-            SubInstruction::AlterLaserAngle(laser_id, delta) => {
-                let mut enemy = self.enemy.borrow_mut();
-                if enemy.laser_by_id.contains_key(&laser_id) {
-                    let mut laser = enemy.laser_by_id.get(&laser_id);
-                    laser.angle += self.get_f32(delta);
-                }
-            }
-            */
-
-            // 89
-            /*
-            SubInstruction::AlterLaserAnglePlayer(laser_id, delta) => {
-                let mut enemy = self.enemy.borrow_mut();
-                if enemy.laser_by_id.contains_key(&laser_id) {
-                    let mut laser = enemy.laser_by_id.get(laser_id);
-                    let player = enemy.select_player();
-                    laser.angle = enemy.get_angle(player) + angle;
-                }
-            }
-            */
-
-            // 90
-            /*
-            SubInstruction::RepositionLaser(laser_id, ox, oy, oz) => {
-                let mut enemy = self.enemy.borrow_mut();
-                if enemy.laser_by_id.contains_key(&laser_id) {
-                    let mut laser = enemy.laser_by_id.get(&laser_id);
-                    laser.set_base_pos(enemy.pos.x + ox, enemy.pos.y + oy, enemy.z + oz)
-                }
-            }
-            */
-            // 91
-            // wat
-            SubInstruction::LaserSetCompare(laser_id) => {
-                let enemy = self.enemy.borrow_mut();
-                // in game it checks if either the laser exists OR if one of its member is set to 0
-                // which, uhhhh, we are not going to reimplement for obvious reasons
-                // the correct implementation would be: if this laser does not exist have a
-                // 1/100000 chance to continue, otherwise crash
-                if enemy.laser_by_id.contains_key(&laser_id) {
-                    // let's assume we gud
-                    self.frame.comparison_reg = 1;
-                }
-                else{
-                    self.frame.comparison_reg = 0;
-                }
-            }
-
-            // 92
-            /*
-            SubInstruction::RepositionLaser(laser_id, ox, oy, oz) => {
-                let mut enemy = self.enemy.borrow_mut();
-                if enemy.laser_by_id.contains_key(&laser_id) {
-                    let mut laser = enemy.laser_by_id.get(laser_id);
-                    laser.cancel();
-                }
-            }
-            */
-            // 93
-            // TODO: actually implement that hell
-            SubInstruction::SetSpellcard(face, number, name) => {
-                unimplemented!("spellcard start");
-
-            }
-            // 94
-            SubInstruction::EndSpellcard() => {
-                unimplemented!("spellcard end");
-
-            }
-
-            // 95
-            SubInstruction::SpawnEnemy(sub, x, y, z, life, bonus, score) => {
-                let x = self.get_f32(x);
-                let y = self.get_f32(y);
-                let _z = self.get_f32(z);
-                let enemy = self.enemy.borrow_mut();
-                let anm0 = enemy.anm0.upgrade().unwrap();
-                let game = enemy.game.upgrade().unwrap();
-                let enemy = Enemy::new(Position::new(x, y), life, bonus, score as u32, false, Rc::downgrade(&anm0), Rc::downgrade(&game));
-                let ecl = self.ecl.clone().unwrap();
-                let mut runner = EclRunner::new(&ecl, enemy, sub as u16);
-                runner.run_frame();
-            }
-
-            // 96
-            /*
-            SubInstruction::KillEnemies() => {
-                let mut game = self.game.borrow_mut();
-                game.kill_enemies();
-            }
-            */
-
-
-
-            // 97
-            SubInstruction::SetAnim(index) => {
-                // seems correct, game internally gets base_addr =(iVar13 + 0x1c934), pointer_addr = iVar14 * 4
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.set_anim(index as u8);
-            }
-            // 98
-            SubInstruction::SetMultipleAnims(default, end_left, end_right, left, right, _unused) => {
-                // _unused was supposed to set movement_dependant_sprites, but internally the game
-                // assigns it 0xff
-                // TODO: THIS DOES NOT CALL set_anim. this only assigns all parameters to their
-                // internal struct. To check if the anims are set somewhere else
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.movement_dependant_sprites = if left == -1 {
-                    None
-                } else {
-                    enemy.set_anim(default as u8);
-                    Some((end_left as u8, end_right as u8, left as u8, right as u8))
-                };
-            }
-            // 99
-            SubInstruction::SetAuxAnm(number, script) => {
-                assert!(number < 8);
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.set_aux_anm(number, script);
-            }
-
-            // 100
-            SubInstruction::SetDeathAnim(index) => {
-                // TODO: takes 3 parameters in game as u8 unlike our single u32.
-                // To reverse!
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.death_anim = index;
-            }
-            // 101
-            SubInstruction::SetBossMode(value) => {
-                let enemy = self.enemy.borrow_mut();
-                if value < 0 {
-                    enemy.set_boss(false);
-                }
-                else {
-                    // the boss pointer is written somewhere in memory and overwrote by a 0 when
-                    // the boss mode is false, might want to look into that
-                    enemy.set_boss(true);
-                }
-            }
-
-            // 102
-            // TODO: title says it all
-            /*
-            SubInstruction::ParticlesVoodooMagic(unk1, unk2, unk3, unk4, unk5) => {
-            }
-            */
-
-            // 103
-            SubInstruction::SetHitbox(width, height, depth) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.set_hitbox(width, height);
-            }
-
-            // 104
-            SubInstruction::SetCollidable(collidable) => {
-                // TODO: me and my siblings(105, 107, 117) are implemented as a single variable in the touhou 6
-                // original engine. While our behaviour seems correct we might want to implement
-                // that as a single variable
-                // TODO[2]: THE BITFLAG MIGHT BE INCORRECT FOR OTHER SIBLING INSTRUCTIONS, the
-                // behavior was DEFINITELY incorrect in pytouhou for SetTouchable at the very least
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.collidable = (collidable&1) != 0;
-            }
-
-            // 105
-            SubInstruction::SetDamageable(damageable) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.damageable = (damageable&1) != 0;
-            }
-
-            // 106
-            SubInstruction::PlaySound(index) => {
-                let enemy = self.enemy.borrow_mut();
-                enemy.play_sound(index);
-            }
-
-            // 107
-            SubInstruction::SetDeathFlags(death_flags) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.death_flags = death_flags;
-            }
-            // 108
-            SubInstruction::SetDeathCallback(sub) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.death_callback = Some(sub);
-            }
-
-            // 109
-            SubInstruction::MemoryWriteInt(value, index) => {
-                unimplemented!("not again that damn foe corrupted my ret\\x41\\x41\\x41\\x41");
-            }
-
-            // 110
-            /*
-            SubInstruction::KillEnemy(enemy) => {
-                let mut game = self.game.borrow_mut();
-                game.kill_enemy(enemy);
-            }
-            */
-
-            // 111
-            /*
-            SubInstruction::SetLife(value) => {
-                let mut enemy = self.enemy.borrow_mut();
-                let mut game = self.game.borrow_mut();
-                enemy.life = value;
-                game.interface.set_boss_life();
-            }
-            */
-            // 112
-            SubInstruction::SetElapsedTime(value) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.frame = value as u32;
-            }
-            // 113
-            /*
-            SubInstruction::SetLowLifeTrigger(value) => {
-                let mut enemy = self.enemy.borrow_mut();
-                let mut game = self.game.borrow_mut();
-                enemy.low_life_trigger = value;
-                game.interface.set_spell_life();
-            }
-            */
-            // 114
-            /*
-             SubInstruction::SetLowLifeCallback(sub) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.low_life_callback.enable(self.switch_to_sub, (sub,));
-            }
-            */
-            // 115
-            /*
-            SubInstruction::SetTimeout(timeout) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.frame = value;
-                enemy.timeout = timeout;
-            }
-            */
-            // 116
-            /*
-             SubInstruction::SetTimeoutCallback(sub) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.timeout_callback.enable(self.switch_to_sub, (sub,));
-            }
-            */
-
-
-            // 117
-            SubInstruction::SetTouchable(touchable) => {
-                let mut enemy = self.enemy.borrow_mut();
-                enemy.touchable = touchable != 0;
-            }
-
-            // 121
-            // Here lies the Di Sword of sadness
-            SubInstruction::CallSpecialFunction(function, arg) => {
-                match function {
-                    0 => {
-                        let mut enemy = self.enemy.borrow_mut();
-                        let game = enemy.game.upgrade().unwrap();
-                        let mut game = game.borrow_mut();
-                        //game.drop_particle(12, enemy.pos, 1, 0xffffffff);
-                        //game.iter_bullets(|mut bullet| {
-                        for bullet in game.bullets.iter() {
-                            //game.new_effect(bullet.sprite, TODO);
-                            let mut bullet = bullet.borrow_mut();
-                            if arg == 0 {
-                                bullet.speed = 0.;
-                                bullet.dpos = [0., 0., 0.];
-                            } else if arg == 1 {
-                                bullet.flags |= 0x10;
-                                bullet.frame = 220;
-                                let rand_angle = game.prng.borrow_mut().get_f64() * 2. * std::f64::consts::PI - std::f64::consts::PI;
-                                bullet.attributes[0] = (rand_angle.cos() * 0.01) as f32;
-                                bullet.attributes[1] = (rand_angle.sin() * 0.01) as f32;
-                            }
-                        }
-                    }
-                    1 => {
-                        let range_x = arg as f64;
-                        let range_y = (arg as f32 * 0.75) as f64;
-                        let rand_x = self.get_prng().borrow_mut().get_f64();
-                        let rand_y = self.get_prng().borrow_mut().get_f64();
-                        let mut enemy = self.enemy.borrow_mut();
-                        let pos = [rand_x * range_x + enemy.pos.x as f64 - range_x / 2.,
-                                   rand_y * range_y + enemy.pos.x as f64 - range_y / 2.];
-                        enemy.bullet_attributes.fire();
-                    }
-                    3 => { // Patchouli’s dual sign spellcard selector
-                        let mut enemy = self.enemy.borrow_mut();
-                        let mut knowledge: [[i32; 3]; 4] =
-                            [[0, 3, 1],
-                             [2, 3, 4],
-                             [1, 4, 0],
-                             [4, 2, 3]];
-
-                        //TODO: implement select_player and replace character by the correct one
-                        //let character = enemy.select_player().character;
-                        let character = 0;
-                        for i in 1..=3 {
-                            self.frame.ints1[i] = knowledge[character][i];
-                        }
-                    }
-                    4 => { // Sakuya random daggers and time stop
-                        /*
-                        if arg < 2 {
-                            drop_particle(&PARTICLES_ARRAY,0xc,enemy->pos,1,0xffffffff);
-                            //TODO: is that the timestop?
-                            LEVEL.field_0x2c = arg;
-                            return;
-                        }
-                        // this changes the orientation of random bullets
-                        let mut max_bullets = 0xe;
-                        if (LEVEL.rank >= 2) {max_bullets = 0x34;}
-                        i = 0;
-                        for bullet in game.bullets {
-                            if bullet->state != 0 && bullet->state != 5 && 30. <= (bullet->sprites[0].additional_infos)->height && bullet->field_0x5ba != 5 && (uVar3 = prng16(&PRNG_STATE), (uVar3 & 3) == 0) {
-                                bullet->field_0x5ba = 5;
-                                new_effect(GAME_OBJECT,(sprite *)bullet, (int)bullet->sprites[0].sometimes_copy_of_UNK1 + (int)bullet->field_0x5ba);
-                                x = bullet->pos[0] - PLAYER.pos[0];
-                                y = bullet->pos[1] - PLAYER.pos[1];
-                                if sqrt(x*x+y*y) > 128. {
-                                    if LEVEL.rank >= 2 {bullet->automatic_angle = prng_double() * 2*pi;}
-                                    else{bullet->automatic_angle = (prng_double() * ((pi/8)*6) + pi/4);}
-                                    else {
-                                        // TODO: check player_get_angle, might be what ST0 is
-                                        player_get_angle_to(&PLAYER,bullet->pos,local_38);
-                                        bullet->automatic_angle = (extraout_ST0_00 + pi/2 + (prng_double() * pi*2));
-                                    }
-                                    bullet->dpos[0] = cos(bullet->automatic_angle) * bullet->speed;
-                                    bullet->dpos[1] = sin(bullet->automatic_angle) * bullet->speed;
-                                    max_bullets -= 1;
-                                    if (max_bullets == 0) break;
-                                }
-                            }
-                            (enemy->ecl_frame).var_ints_1[2] = 0;*/
-                        }
-                    7 => { // Remilia's lazer maze
-                        // so what this does is kinda complex: 2 rounds of 3 subrounds of 8 shots, either
-                        // laser or standard bullets depending on the argument passed.
-                        // it is done in 2 steps: first we precalculate coordinates of the 8 shots for the first subround
-                        // set the shot properties depending on difficulties and current round and then
-                        // edit  the coordinates for the next round
-                        let rnd_pos = self.get_prng().borrow_mut().get_f64() * 2. * std::f64::consts::PI;
-                        let enemy = self.enemy.borrow();
-                        for i in 0..2 {
-                            let mut pos: [f64; 8*3] = [0.; 8*3];
-                            let mut offset = rnd_pos -((std::f64::consts::PI/8.)*7.);
-                            let mut next_offset = -std::f64::consts::PI/4.;
-                            if (i == 0) {
-                                offset = rnd_pos -std::f64::consts::PI;
-                                next_offset = std::f64::consts::PI/4.;
-                            }
-
-                            // we calculate angle, speed and offset for the 8 shots
-                            let mut offset_copy=offset;
-                            for y in 0..8 {
-                                pos[y * 3] = offset_copy.cos() * 32. + enemy.pos.x as f64;
-                                pos[y * 3 + 1] = offset_copy.sin() * 32. + enemy.pos.y as f64;
-                                pos[y * 3 + 2] = enemy.z as f64;
-                                offset_copy += std::f64::consts::PI/4.;
-                            }
-
-                            // 3 rounds of 8 shots
-                            for z in 0..3 {
-
-                                let mut length = 112.;
-                                // last subround
-                                if (z == 2) {length = 480.;}
-
-                                for y in 0..8 {
-                                    /*
-                                    if (arg == 0) {
-                                        let (mut si, mut ged, mut ed) = (8, 20.,ed=430.);
-                                        if (LEVEL.rank < 2) {si=2; ged=28.; ed=length;}
-                                        laser_args.angle = pos[y * 3];
-                                        laser_args.speed = pos[y * 3 + 1];
-                                        laser_args.start_offset = pos[y * 3 + 2];
-                                        laser_args.type = 1;
-                                        laser_args.sprite_index_offset = si;
-                                        laser_args.end_offset = offset;
-                                        laser_args.width = 0.;
-                                        laser_args.duration = 0;
-                                        laser_args.grazing_extra_duration = ged;
-                                        laser_args.end_duration = ed;
-                                        laser_args.UNK1 = z * 0x10 + 0x3c;
-                                        laser_args.grazing_delay = laser_args.end_duration;
-                                        fire_laser(&ETAMA_ARRAY,&laser_args);
-                                    }
-                                    else {
-                                        (enemy->bullet_attributes).pos[0] = pos[y * 3];
-                                        (enemy->bullet_attributes).pos[1] = pos[y*3+1];
-                                        (enemy->bullet_attributes).pos[2] = pos[y*3+2];
-                                        bullet_fire(&enemy->bullet_attributes,&ETAMA_ARRAY);
-                                    }
-                                    */
-                                    pos[y * 3] = offset.cos() * length + pos[y * 3];
-                                    pos[y * 3 + 1] = offset.sin() * length + pos[y * 3 + 1];
-                                    offset = offset + std::f64::consts::PI/4.;
-                                }
-                                offset = (next_offset - 2.*std::f64::consts::PI) + offset;
-                            }
-                        }
-                    }
-                    8 => { // Vampire Fantasy
-                        let n = {
-                            let enemy = self.enemy.borrow();
-                            let game = enemy.game.upgrade().unwrap();
-                            let mut game = game.borrow_mut();
-                            let mut n = 0;
-                            for bullet in game.bullets.iter() {
-                                let mut bullet = bullet.borrow();
-                                // TODO: uncomment that one.
-                                if bullet.state != 0 && bullet.state != 5 /* && (30. <= (bullet.sprites[0].additional_infos).height) */ {
-                                    let prng = enemy.prng.upgrade().unwrap();
-                                    let random = prng.borrow_mut().get_f64();
-                                    let launch_angle = (random * (2. * std::f64::consts::PI) - std::f64::consts::PI) as f32;
-                                    let mut attribs = BulletAttributes {
-                                        // TODO: check if the z value of this pos is really used.
-                                        pos: bullet.pos,
-                                        anim: 3,
-                                        sprite_index_offset: 1,
-                                        launch_angle,
-                                        speed: 0.,
-                                        angle: 0.,
-                                        speed2: 0.,
-                                        bullets_per_shot: 1,
-                                        number_of_shots: 1,
-                                        flags: 8,
-                                        bullet_type: 1,
-                                        extended_attributes: Default::default(),
-                                        sound: None,
-                                    };
-                                    attribs.fire();
-                                    n += 1
-                                }
-                            }
-                            n
-                        };
-                        //TODO: this variable might not always be correct! it uses the argument in
-                        //th06: *(int *)(param_1 + 0x9b0) = local_60;
-                        self.set_i32(-10004, n);
-                    }
-
-                    9 => {
-                        let mut rnd = self.get_prng().borrow_mut().get_f64();
-                        //TODO: the game does that
-                        //drop_particle(&PARTICLES_ARRAY,0xc,enemy->pos,1,0xffffffff);
-                        //self._game.new_effect((enemy.x, enemy.y), 17)
-                        /*
-                        for bullet in game.bullets {
-                            if bullet._bullet_type.state != 0 && bullet._bullet_type.state != 5 && (30. <= (bullet.sprites[0].additional_infos)->height) && bullet.speed == 0. {
-                                bullet.flags = bullet.flags | 0x10;
-                                //TODO: reverse this field and effect
-                                bullet->field_0x5ba = 2;
-                                new_effect(GAME_OBJECT,(sprite *)bullet, (int)bullet->sprites[0].sometimes_copy_of_UNK1 + (int)bullet->field_0x5ba);
-                                bullet.speed=0.01;
-                                bullet.frame=0x78;
-
-                                let mut dx = bullet.x - enemy.x;
-                                let mut distance = dx.hypot(bullet.y - enemy.y);
-
-                                if distance > 0.01 {
-                                    distance = sqrt(distance);
-                                }else{distance = 0.;}
-                                let mut angle = (distance * std::f64::consts::PI) / 256. + (rnd * (2*std::f64::consts::PI) - std::f64::consts::PI);
-                                bullet->attributes[0] = cos(angle) * 0.01000000;
-                                bullet->attributes[1] = sin(angle) * 0.01000000;
-                            }
-                        }
-                        */
-                    }
-                    11 => {
-                        self.get_prng().borrow_mut().get_f64();
-                        //TODO: the game does that
-                        //drop_particle(&PARTICLES_ARRAY,0xc,enemy->pos,1,0xffffffff);
-                        //self._game.new_effect((enemy.x, enemy.y), 17)
-                        /*
-                        for bullet in game.bullets {
-                            if bullet._bullet_type.state != 0 && bullet._bullet_type.state != 5 && (30. <= (bullet.sprites[0].additional_infos)->height) && bullet.speed == 0. {
-                                bullet.flags = bullet.flags | 0x10;
-                                //TODO: reverse this field and effect
-                                bullet->field_0x5ba = 2;
-                                new_effect(GAME_OBJECT,(sprite *)bullet, (int)bullet->sprites[0].sometimes_copy_of_UNK1 + (int)bullet->field_0x5ba);
-                                bullet.speed=0.01;
-                                bullet.frame=0x78;
-                                let mut angle = self.get_prng().borrow_mut().get_f64() * (2*std::f64::consts::PI) - std::f64::consts::PI;
-                                bullet->attributes[0] = cos(angle) * 0.01000000;
-                                bullet->attributes[1] = sin(angle) * 0.01000000;
-
-
-                            }
-                        }
-                        */
-                    }
-                    13 => {
-                        if self.frame.ints1[3] % 6 == 0 {
-                            let mut _angle=self.frame.floats[2];
-                            /*
-                            (type_, anim, sprite_idx_offset, bullets_per_shot, number_of_shots,
-                            speed, speed2, launch_angle, angle, flags) = self._enemy.bullet_attributes
-                            for i in range(arg) {
-                                //TODO: distance is obtained directly by copying bullet attributes
-                                //in memory
-                                launch_pos = (192 + cos(_angle) * _distance,
-                                    224 + sin(_angle) * _distance);
-
-                                bullet_attributes = (type_, anim, sprite_idx_offset,
-                                     bullets_per_shot, number_of_shots,
-                                     speed, speed2,
-                                     _angle + self.frame.floats[1], angle, flags);
-                                enemy.fire(launch_pos=launch_pos,bullet_attributes=bullet_attributes);
-                                _angle += 2*std::f64::consts::PI/arg;
-                            }*/
-                        }
-                        self.frame.ints1[3] += 1;
-                    }
-                    14 => { // Lävatein
-                        let mut enemy = self.enemy.borrow_mut();
-                        self.frame.ints1[3] = 0;
-                        for laser in enemy.laser_by_id.values() {
-                            //for pos in laser.get_bullets_pos(){
-                            //TODO: the game checks for laser end_offset before firing
-                            //  enemy.fire(launch_pos=pos);
-                            //}
-                            self.frame.ints1[3] += 1;
-                        }
-                    }
-                    16 => { // QED: Ripples of 495 years
-                        let mut enemy = self.enemy.borrow_mut();
-                        let game = enemy.game.upgrade().unwrap();
-                        let mut game = game.borrow_mut();
-                        if arg == 0 {
-                            self.frame.floats[3] = 2. - (enemy.life as f32) / 6000.;
-                            self.frame.ints2[1] = ((enemy.life * 240) / 6000 + 40) as i32;
-                        } else {
-                            let fx = (320. - ((enemy.life as f32) * 160.) / 6000.) as f64;
-                            let fy = (128. - ((enemy.life as f32) * 64.) / 6000.) as f64;
-                            let rand_x = game.prng.borrow_mut().get_f64();
-                            let rand_y = game.prng.borrow_mut().get_f64();
-                            self.frame.floats[2] = (rand_x * fx + (192. - fx / 2.)) as f32;
-                            self.frame.floats[3] = (rand_y * fy + (96. - fy / 2.)) as f32;
-                        }
-                    }
-                    _ => unimplemented!("Special function {:?} not found!", function)
-                }
-            }
-
-            // 122
-            // Here lies the Di Sword of despair
-            SubInstruction::SetSpecialFunctionCallback(function) => {
-                //TODO: those functions are executed at each frame and needs to be written to the
-                //callback function but so far i'm simply focusing on the implementation
-                //NB: the original engine doesn't differenciate between function blocks for ins 121
-                //and 122 but we do here, since it wouldn't make sense the other way around.
-                match function {
-                    12 => {
-                        for i in 0..8 {
-                            /*
-                            if ((enemy->lasers[i] != (laser *)0x0) && (enemy->lasers[i]->state != 0)) {
-                                (enemy->bullet_attributes).pos[0] = cos(enemy->lasers[i]->angle) * 64. + enemy.pos.x;
-                                // yes, it reads pos[0] after it has been modified and yes, this most
-                                // likely is a bug
-                                (enemy->bullet_attributes).pos[1] = cos(enemy->lasers[i]->angle) * (enemy->bullet_attributes).pos[0] + enemy.pos.y;
-                                (enemy->bullet_attributes).pos[2] = enemy.z;
-                                bullet_fire(&enemy->bullet_attributes,&ETAMA_ARRAY);
-                            }*/
-                        }
-                    }
-                    _ => unimplemented!("Special function {:?} not found!", function)
-                }
-            }
-            _ => unimplemented!("{:?}", instruction)
-        }
-
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::th06::anm0::Anm0;
-    use crate::th06::ecl::{Sub, CallSub, Rank};
-    use crate::th06::enemy::Game;
-    use std::io::{self, Read};
-    use std::fs::File;
-
-    fn setup() -> (Rc<RefCell<Game>>, Rc<RefCell<Enemy>>) {
-        let file = File::open("EoSD/ST/stg1enm.anm").unwrap();
-        let mut file = io::BufReader::new(file);
-        let mut buf = vec![];
-        file.read_to_end(&mut buf).unwrap();
-        let (_, mut anms) = Anm0::from_slice(&buf).unwrap();
-        let anm0 = anms.pop().unwrap();
-        let anm0 = Rc::new(RefCell::new(anm0));
-        let prng = Rc::new(RefCell::new(Prng::new(0)));
-        let game = Game::new(prng, Rank::EASY);
-        let game = Rc::new(RefCell::new(game));
-        let enemy = Enemy::new(Position::new(0., 0.), 500, 0, 640, Rc::downgrade(&anm0), Rc::downgrade(&game));
-        (game, enemy)
-    }
-
-    #[test]
-    fn call_and_return() {
-        let (game, enemy) = setup();
-        let ecl = Ecl { mains: vec![], subs: vec![
-            Sub { instructions: vec![
-                CallSub::new(0, Rank::EASY, SubInstruction::Call(1, 13, 12.)),
-            ]},
-            Sub { instructions: vec![
-                CallSub::new(0, Rank::EASY, SubInstruction::Noop()),
-                CallSub::new(1, Rank::EASY, SubInstruction::Return()),
-            ]},
-        ]};
-        let mut ecl_runner = EclRunner::new(&ecl, enemy, 0);
-        ecl_runner.run_frame();
-        assert_eq!(ecl_runner.frame.ints1[0], 13);
-        assert_eq!(ecl_runner.frame.floats[0], 12.);
-        assert_eq!(ecl_runner.stack.len(), 1);
-        ecl_runner.run_frame();
-        assert_eq!(ecl_runner.frame.ints1[0], 0);
-        assert_eq!(ecl_runner.frame.floats[0], 0.);
-        assert_eq!(ecl_runner.stack.len(), 0);
-    }
-}
--- a/src/th06/enemy.rs	Tue Jan 05 01:14:52 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,571 +0,0 @@
-//! Module providing an Enemy struct, to be changed by EclRunner.
-
-use crate::th06::anm0::Anm0;
-use crate::th06::anm0_vm::{Sprite, AnmRunner};
-use crate::th06::ecl::Rank;
-use crate::th06::interpolator::{Interpolator1, Interpolator2};
-use crate::util::prng::Prng;
-use std::cell::RefCell;
-use std::collections::HashMap;
-use std::rc::{Rc, Weak};
-
-/// The 2D position of an object in the game.
-#[derive(Debug, Clone, Copy, Default, PartialEq)]
-pub struct Position {
-    pub(crate) x: f32,
-    pub(crate) y: f32,
-}
-
-/// An offset which can be added to a Position.
-#[derive(Debug, Clone, Copy, Default, PartialEq)]
-pub struct Offset {
-    pub(crate) dx: f32,
-    pub(crate) dy: f32,
-}
-
-impl Position {
-    /// Create said position.
-    pub fn new(x: f32, y: f32) -> Position {
-        Position { x, y }
-    }
-}
-
-impl Offset {
-    /// Create said offset.
-    pub fn new(dx: f32, dy: f32) -> Offset {
-        Offset { dx, dy }
-    }
-}
-
-impl std::ops::Add<Offset> for Position {
-    type Output = Position;
-    fn add(self, offset: Offset) -> Position {
-        Position {
-            x: self.x + offset.dx,
-            y: self.y + offset.dy,
-        }
-    }
-}
-
-impl std::ops::Sub<Position> for Position {
-    type Output = Offset;
-    fn sub(self, other: Position) -> Offset {
-        Offset {
-            dx: other.x - self.x,
-            dy: other.y - self.y,
-        }
-    }
-}
-
-type Callback = i32;
-
-#[derive(Debug, Clone)]
-/// XXX
-pub struct Laser {
-    /// XXX
-    pub placeholder: u32
-}
-
-#[derive(Debug, Clone, Default)]
-struct Process;
-
-/// Struct representing the player.
-pub struct Player {
-    pos: Position,
-}
-
-/// Struct representing an enemy bullet.
-pub struct Bullet {
-    /// Current position of the bullet.
-    pub pos: Position,
-
-    /// Current speed of the bullet.
-    pub speed: f32,
-
-    /// Current XXX of the bullet.
-    pub dpos: [f32; 3],
-
-    /// Current XXX of the bullet.
-    pub flags: u32,
-
-    /// Current frame of the bullet.
-    pub frame: i32,
-
-    /// Current attributes of the bullet.
-    pub attributes: [f32; 2],
-
-    /// TODO: what are the values?
-    pub state: i8,
-}
-
-/// God struct of our game.
-pub struct Game {
-    enemies: Vec<Rc<RefCell<Enemy>>>,
-    anmrunners: Vec<Rc<RefCell<AnmRunner>>>,
-    pub(crate) bullets: Vec<Rc<RefCell<Bullet>>>,
-    player: Rc<RefCell<Player>>,
-    pub(crate) prng: Rc<RefCell<Prng>>,
-    rank: Rank,
-    difficulty: i32,
-}
-
-impl Game {
-    /// Create said god struct.
-    pub fn new(prng: Rc<RefCell<Prng>>, rank: Rank) -> Game {
-        Game {
-            enemies: Vec::new(),
-            anmrunners: Vec::new(),
-            bullets: Vec::new(),
-            player: Rc::new(RefCell::new(Player { pos: Position { x: 192., y: 384. } })),
-            prng,
-            rank,
-            difficulty: 0,
-        }
-    }
-
-    /// Run the simulation for a single frame.
-    pub fn run_frame(&mut self) {
-        /*
-        for eclrunner in self.eclrunners {
-            eclrunner.run_frame();
-        }
-        */
-
-        for anmrunner in self.anmrunners.iter() {
-            let mut anmrunner = anmrunner.borrow_mut();
-            anmrunner.run_frame();
-        }
-    }
-
-    /// Returns a list of all sprites currently being displayed on screen.
-    pub fn get_sprites(&self) -> Vec<(f32, f32, f32, Rc<RefCell<Sprite>>)> {
-        let mut sprites = vec![];
-        for enemy in self.enemies.iter() {
-            let enemy = enemy.borrow();
-            let anmrunner = enemy.anmrunner.upgrade().unwrap();
-            let anmrunner = anmrunner.borrow();
-            let sprite = anmrunner.get_sprite();
-            sprites.push((enemy.pos.x, enemy.pos.y, enemy.z, sprite));
-        }
-        sprites
-    }
-
-    // TODO: Fix this function so we can stop making Game::bullets pub.
-    /*
-    /// Apply a function on all bullets.
-    pub fn iter_bullets(&mut self, mut f: impl FnMut(Bullet)) {
-        self.bullets.iter().map(|bullet| {
-            let mut bullet = bullet.borrow_mut();
-            f(*bullet)
-        });
-    }
-    */
-
-    pub(crate) fn get_player(&self) -> Rc<RefCell<Player>> {
-        self.player.clone()
-    }
-}
-
-/// Common to all elements in game.
-struct Element {
-    pos: Position,
-    removed: bool,
-    anmrunner: AnmRunner,
-}
-
-#[derive(PartialEq)]
-pub(crate) struct DifficultyCoeffs {
-    pub(crate) speed_a: f32,
-    pub(crate) speed_b: f32,
-    pub(crate) nb_a: i16,
-    pub(crate) nb_b: i16,
-    pub(crate) shots_a: i16,
-    pub(crate) shots_b: i16,
-}
-
-impl Default for DifficultyCoeffs {
-    fn default() -> DifficultyCoeffs {
-        DifficultyCoeffs {
-            speed_a: -0.5,
-            speed_b: 0.5,
-            nb_a: 0,
-            nb_b: 0,
-            shots_a: 0,
-            shots_b: 0,
-        }
-    }
-}
-
-#[derive(Debug, Clone, Default, PartialEq)]
-pub(crate) struct BulletAttributes {
-    pub(crate) anim: i16,
-    pub(crate) sprite_index_offset: i16,
-    pub(crate) pos: Position, // Doesn’t have a z field.
-    pub(crate) launch_angle: f32,
-    pub(crate) angle: f32,
-    pub(crate) speed: f32,
-    pub(crate) speed2: f32,
-    pub(crate) extended_attributes: (i32, i32, i32, i32, f32, f32, f32, f32),
-    // unknown: x32,
-    pub(crate) bullets_per_shot: i16,
-    pub(crate) number_of_shots: i16,
-    pub(crate) bullet_type: i16,
-    // zero: x32,
-    pub(crate) flags: u32,
-
-    /// Which sound to play when the bullet gets fired.
-    pub sound: Option<u8>,
-}
-
-impl BulletAttributes {
-    /// Fire!
-    pub fn fire(&mut self) {
-        println!("PAN!");
-    }
-}
-
-#[derive(PartialEq)]
-pub(crate) enum Direction {
-    Left,
-    Center,
-    Right,
-}
-
-impl Default for Direction {
-    fn default() -> Direction {
-        Direction::Center
-    }
-}
-
-/// The enemy struct, containing everything pertaining to an enemy.
-#[derive(Default)]
-pub struct Enemy {
-    // Common to all elements in game.
-    pub(crate) pos: Position,
-    pub(crate) removed: bool,
-    pub(crate) anmrunner: Weak<RefCell<AnmRunner>>,
-
-    // Specific to enemy.
-    // Floats.
-    pub(crate) z: f32,
-    pub(crate) angle: f32,
-    pub(crate) speed: f32,
-    pub(crate) rotation_speed: f32,
-    pub(crate) acceleration: f32,
-
-    // Ints.
-    pub(crate) type_: u32,
-    pub(crate) bonus_dropped: u32,
-    pub(crate) die_score: u32,
-    /// XXX
-    pub frame: u32,
-    pub(crate) life: u32,
-    pub(crate) death_flags: u32,
-    pub(crate) current_laser_id: u32,
-    pub(crate) low_life_trigger: Option<u32>,
-    pub(crate) timeout: Option<u32>,
-    pub(crate) remaining_lives: u32,
-    bullet_launch_interval: u32,
-    bullet_launch_timer: u32,
-    pub(crate) death_anim: i32,
-    pub(crate) direction: Direction,
-    pub(crate) update_mode: u32,
-
-    // Bools.
-    pub(crate) visible: bool,
-    pub(crate) was_visible: bool,
-    pub(crate) touchable: bool,
-    pub(crate) collidable: bool,
-    pub(crate) damageable: bool,
-    pub(crate) boss: bool,
-    pub(crate) automatic_orientation: bool,
-    pub(crate) delay_attack: bool,
-    // Actually part of type_ atm.
-    pub(crate) mirror: bool,
-
-    // Tuples.
-    pub(crate) difficulty_coeffs: DifficultyCoeffs,
-    pub(crate) bullet_attributes: BulletAttributes,
-    pub(crate) bullet_offset: Offset,
-    pub(crate) movement_dependant_sprites: Option<(u8, u8, u8, u8)>,
-    pub(crate) screen_box: Option<(f32, f32, f32, f32)>,
-
-    // Callbacks.
-    pub(crate) death_callback: Option<Callback>,
-    pub(crate) boss_callback: Option<Callback>,
-    pub(crate) low_life_callback: Option<Callback>,
-    pub(crate) timeout_callback: Option<Callback>,
-
-    // Laser.
-    pub(crate) laser_by_id: HashMap<u32, Laser>,
-
-    // Options.
-    // TODO: actually a 8 element array.
-    options: Vec<Element>,
-
-    // Interpolators.
-    pub(crate) interpolator: Option<Interpolator2<f32>>,
-    pub(crate) speed_interpolator: Option<Interpolator1<f32>>,
-
-    // Misc stuff, do we need them?
-    pub(crate) anm0: Weak<RefCell<[Anm0; 2]>>,
-    process: Rc<RefCell<Process>>,
-    pub(crate) game: Weak<RefCell<Game>>,
-    pub(crate) prng: Weak<RefCell<Prng>>,
-    pub(crate) hitbox_half_size: [f32; 2],
-}
-
-impl Enemy {
-    /// Create a new enemy.
-    pub fn new(pos: Position, life: i16, bonus_dropped: i16, die_score: u32, mirror: bool, anm0: Weak<RefCell<[Anm0; 2]>>, game: Weak<RefCell<Game>>) -> Rc<RefCell<Enemy>> {
-        let game_rc = game.upgrade().unwrap();
-        let mut enemy = Enemy {