Mercurial > touhou
changeset 757:21b186be2590
Split the Rust version into multiple crates.
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 { - 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()); - } -}
--- a/src/th06/interpolator.rs Tue Jan 05 01:14:52 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -//! 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);
--- a/src/th06/mod.rs Tue Jan 05 01:14:52 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -//! Touhou 6: EoSD implementation. - -pub mod pbg3; -pub mod anm0; -pub mod anm0_vm; -pub mod ecl; -pub mod ecl_vm; -pub mod std; -pub mod std_vm; -pub mod enemy; -pub mod interpolator;
--- a/src/th06/pbg3.rs Tue Jan 05 01:14:52 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,199 +0,0 @@ -//! 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 crate::util::bitstream::BitStream; -use crate::util::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(); - } - } -}
--- a/src/th06/std.rs Tue Jan 05 01:14:52 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,271 +0,0 @@ -//! 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); - } -}
--- a/src/th06/std_vm.rs Tue Jan 05 01:14:52 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,102 +0,0 @@ -//! Interpreter of STD files. - -use crate::th06::std::{Stage, Call, Instruction}; -use crate::th06::interpolator::{Interpolator3, Formula}; -use crate::util::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 - } -}
--- a/src/util/bitstream.rs Tue Jan 05 01:14:52 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,143 +0,0 @@ -//! Bitstream module. - -use std::io; - -/// Wrapper around any `Read` trait, to allow bit operations. -pub struct BitStream<R: io::Read + io::Seek> { - io: R, - remaining_bits: usize, - byte: u8, -} - -impl<R: io::Read + io::Seek> BitStream<R> { - /// Create a new bitstream. - pub fn new(io: R) -> BitStream<R> { - BitStream { - io, - remaining_bits: 0, - byte: 0, - } - } - - /// Seek inside the bitstream, ditching any unused data read. - pub fn seek(&mut self, seek_from: io::SeekFrom) -> io::Result<u64> { - self.remaining_bits = 0; - self.byte = 0; - self.io.seek(seek_from) - } - - fn fill_byte(&mut self) -> io::Result<()> { - assert!(self.remaining_bits == 0); - - let mut buf = [0u8; 1]; - self.io.read_exact(&mut buf)?; - self.byte = buf[0]; - self.remaining_bits = 8; - Ok(()) - } - - /// Read only one bit from the stream. - pub fn read_bit(&mut self) -> io::Result<bool> { - if self.remaining_bits == 0 { - self.fill_byte()?; - } - self.remaining_bits -= 1; - Ok((self.byte >> self.remaining_bits) & 0x01 != 0) - } - - /// Read `nb_bits` bits from the stream. - pub fn read(&mut self, nb_bits: usize) -> io::Result<usize> { - let mut nb_bits2 = nb_bits; - let mut value: usize = 0; - while nb_bits2 > 0 { - if self.remaining_bits == 0 { - self.fill_byte()?; - } - let read = if nb_bits2 > self.remaining_bits { self.remaining_bits } else { nb_bits2 }; - nb_bits2 -= read; - self.remaining_bits -= read; - value |= (self.byte as usize >> self.remaining_bits) << nb_bits2; - } - Ok(value & ((1 << nb_bits) - 1)) - } - - /// Read a given amount of bytes. - pub fn read_bytes(&mut self, nb_bytes: usize) -> io::Result<Vec<u8>> { - let mut buf = vec![0u8; nb_bytes]; - self.io.read_exact(&mut buf)?; - Ok(buf) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::util::SeekableSlice; - - #[test] - fn bit_by_bit() { - let data = SeekableSlice::new(&[1, 2, 3]); - let mut bitstream = BitStream::new(data); - - // 1 - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), true); - - // 2 - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), true); - assert_eq!(bitstream.read_bit().unwrap(), false); - - // 3 - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read_bit().unwrap(), true); - assert_eq!(bitstream.read_bit().unwrap(), true); - - // Can’t read after the end. - bitstream.read_bit().unwrap_err(); - } - - #[test] - fn byte_by_byte() { - let data = SeekableSlice::new(&[1, 2, 3]); - let mut bitstream = BitStream::new(data); - - assert_eq!(bitstream.read(8).unwrap(), 1); - assert_eq!(bitstream.read(8).unwrap(), 2); - assert_eq!(bitstream.read(8).unwrap(), 3); - - // Can’t read after the end. - bitstream.read(1).unwrap_err(); - } - - #[test] - fn unaligned_bytes() { - let data = SeekableSlice::new(&[0, 129, 1, 128]); - let mut bitstream = BitStream::new(data); - - assert_eq!(bitstream.read_bit().unwrap(), false); - assert_eq!(bitstream.read(8).unwrap(), 1); - assert_eq!(bitstream.read(8).unwrap(), 2); - assert_eq!(bitstream.read(8).unwrap(), 3); - assert_eq!(bitstream.read(7).unwrap(), 0); - - // Can’t read after the end. - bitstream.read(1).unwrap_err(); - } -}
--- a/src/util/lzss.rs Tue Jan 05 01:14:52 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ -//! LZSS implementation. - -use std::io; -use crate::util::bitstream::BitStream; - -/// Decompresses a LZSS-compressed file. -pub fn decompress<R: io::Read + io::Seek>(bitstream: &mut BitStream<R>, size: usize, dictionary_size: usize, offset_size: usize, length_size: usize, minimum_match_length: usize) -> io::Result<Vec<u8>> { - let mut data = vec![0; size]; - let mut dictionary = vec![0; dictionary_size]; - let mut dictionary_head = 1; - let mut ptr = 0; - - while ptr < size { - if bitstream.read_bit()? { - // The `flag` bit is set, indicating the upcoming chunk of data is a literal. - // Add it to the uncompressed file, and store it in the dictionary. - let byte = bitstream.read(8)? as u8; - dictionary[dictionary_head] = byte; - dictionary_head = (dictionary_head + 1) % dictionary_size; - data[ptr] = byte; - ptr += 1; - } else { - // The `flag` bit is not set, the upcoming chunk is a (offset, length) tuple. - let offset = bitstream.read(offset_size)?; - let length = bitstream.read(length_size)? + minimum_match_length; - if ptr + length > size { - return Err(io::Error::new(io::ErrorKind::Other, "Oh no!")); - } - if offset == 0 && length == 0 { - break; - } - for i in offset..offset + length { - data[ptr] = dictionary[i % dictionary_size]; - dictionary[dictionary_head] = dictionary[i % dictionary_size]; - dictionary_head = (dictionary_head + 1) % dictionary_size; - ptr += 1; - } - } - } - - Ok(data) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::util::SeekableSlice; - - #[test] - #[ignore] - fn bit_by_bit() { - // TODO: find actual lzss data. - let data = SeekableSlice::new(&[0, 0, 0]); - let mut bitstream = BitStream::new(data); - decompress(&mut bitstream, 3, 0x2000, 13, 4, 3).unwrap(); - } -}
--- a/src/util/math.rs Tue Jan 05 01:14:52 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,202 +0,0 @@ -//! Various helpers to deal with vectors and matrices. - -/// A 4×4 f32 matrix type. -pub struct Mat4 { - inner: [[f32; 4]; 4] -} - -impl Mat4 { - /// Create a new matrix from a set of 16 f32. - pub fn new(inner: [[f32; 4]; 4]) -> Mat4 { - Mat4 { - inner - } - } - - fn zero() -> Mat4 { - Mat4 { - inner: [[0.; 4]; 4] - } - } - - fn identity() -> Mat4 { - Mat4 { - inner: [[1., 0., 0., 0.], - [0., 1., 0., 0.], - [0., 0., 1., 0.], - [0., 0., 0., 1.]] - } - } - - /// Immutably borrow the array of f32 inside this matrix. - pub fn borrow_inner(&self) -> &[[f32; 4]; 4] { - &self.inner - } - - /// Scale the matrix in 2D. - pub fn scale2d(&mut self, x: f32, y: f32) { - for i in 0..4 { - self.inner[0][i] *= x; - self.inner[1][i] *= y; - } - } - - /// Flip the matrix. - pub fn flip(&mut self) { - for i in 0..4 { - self.inner[0][i] = -self.inner[0][i]; - } - } - - /// Rotate the matrix around its x angle (in radians). - pub fn rotate_x(&mut self, angle: f32) { - let mut lines: [f32; 8] = [0.; 8]; - let cos_a = angle.cos(); - let sin_a = angle.sin(); - for i in 0..4 { - lines[ i] = self.inner[0][i]; - lines[4 + i] = self.inner[1][i]; - } - for i in 0..4 { - self.inner[1][i] = cos_a * lines[i] - sin_a * lines[4+i]; - self.inner[2][i] = sin_a * lines[i] + cos_a * lines[4+i]; - } - } - - /// Rotate the matrix around its y angle (in radians). - pub fn rotate_y(&mut self, angle: f32) { - let mut lines: [f32; 8] = [0.; 8]; - let cos_a = angle.cos(); - let sin_a = angle.sin(); - for i in 0..4 { - lines[ i] = self.inner[0][i]; - lines[4 + i] = self.inner[2][i]; - } - for i in 0..4 { - self.inner[0][i] = cos_a * lines[i] + sin_a * lines[4+i]; - self.inner[2][i] = -sin_a * lines[i] + cos_a * lines[4+i]; - } - } - - /// Rotate the matrix around its z angle (in radians). - pub fn rotate_z(&mut self, angle: f32) { - let mut lines: [f32; 8] = [0.; 8]; - let cos_a = angle.cos(); - let sin_a = angle.sin(); - for i in 0..4 { - lines[ i] = self.inner[0][i]; - lines[4 + i] = self.inner[1][i]; - } - for i in 0..4 { - self.inner[0][i] = cos_a * lines[i] - sin_a * lines[4+i]; - self.inner[1][i] = sin_a * lines[i] + cos_a * lines[4+i]; - } - } - - /// Translate the matrix by a 3D offset. - pub fn translate(&mut self, offset: [f32; 3]) { - let mut item: [f32; 3] = [0.; 3]; - for i in 0..3 { - item[i] = self.inner[3][i] * offset[i]; - } - for i in 0..3 { - for j in 0..4 { - self.inner[i][j] += item[i]; - } - } - } - - /// Translate the matrix by a 2D offset. - pub fn translate_2d(&mut self, x: f32, y: f32) { - let offset = [x, y, 0.]; - self.translate(offset); - } -} - -impl std::ops::Mul<Mat4> for Mat4 { - type Output = Mat4; - fn mul(self, rhs: Mat4) -> Mat4 { - let mut tmp = Mat4::zero(); - for i in 0..4 { - for j in 0..4 { - for k in 0..4 { - tmp.inner[i][j] += self.inner[i][k] * rhs.inner[k][j]; - } - } - } - tmp - } -} - -/// Create an orthographic projection matrix. -pub fn ortho_2d(left: f32, right: f32, bottom: f32, top: f32) -> Mat4 { - let mut mat = Mat4::identity(); - mat.inner[0][0] = 2. / (right - left); - mat.inner[1][1] = 2. / (top - bottom); - mat.inner[2][2] = -1.; - mat.inner[3][0] = -(right + left) / (right - left); - mat.inner[3][1] = -(top + bottom) / (top - bottom); - mat -} - -/// Setup a camera view matrix. -pub fn setup_camera(dx: f32, dy: f32, dz: f32) -> Mat4 { - // Some explanations on the magic constants: - // 192. = 384. / 2. = width / 2. - // 224. = 448. / 2. = height / 2. - // 835.979370 = 224./math.tan(math.radians(15)) = (height/2.)/math.tan(math.radians(fov/2)) - // This is so that objects on the (O, x, y) plane use pixel coordinates - look_at([192., 224., -835.979370 * dz], [192. + dx, 224. - dy, 0.], [0., -1., 0.]) -} - -/// Creates a perspective projection matrix. -pub fn perspective(fov_y: f32, aspect: f32, z_near: f32, z_far: f32) -> Mat4 { - let top = (fov_y / 2.).tan() * z_near; - let bottom = -top; - let left = -top * aspect; - let right = top * aspect; - - let mut mat = Mat4::identity(); - mat.inner[0][0] = (2. * z_near) / (right - left); - mat.inner[1][1] = (2. * z_near) / (top - bottom); - mat.inner[2][2] = -(z_far + z_near) / (z_far - z_near); - mat.inner[2][3] = -1.; - mat.inner[3][2] = -(2. * z_far * z_near) / (z_far - z_near); - mat.inner[3][3] = 0.; - mat -} - -type Vec3 = [f32; 3]; - -fn look_at(eye: Vec3, center: Vec3, up: Vec3) -> Mat4 { - let f = normalize(sub(center, eye)); - let u = normalize(up); - let s = normalize(cross(f, u)); - let u = cross(s, f); - - Mat4::new([[s[0], u[0], -f[0], 0.], - [s[1], u[1], -f[1], 0.], - [s[2], u[2], -f[2], 0.], - [-dot(s, eye), -dot(u, eye), dot(f, eye), 1.]]) -} - -fn sub(a: Vec3, b: Vec3) -> Vec3 { - [a[0] - b[0], - a[1] - b[1], - a[2] - b[2]] -} - -fn normalize(vec: Vec3) -> Vec3 { - let normal = 1. / (vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]).sqrt(); - [vec[0] * normal, vec[1] * normal, vec[2] * normal] -} - -fn cross(a: Vec3, b: Vec3) -> Vec3 { - [a[1] * b[2] - b[1] * a[2], - a[2] * b[0] - b[2] * a[0], - a[0] * b[1] - b[0] * a[1]] -} - -fn dot(a: Vec3, b: Vec3) -> f32 { - a[0] * b[0] + a[1] * b[1] + a[2] * b[2] -}
--- a/src/util/mod.rs Tue Jan 05 01:14:52 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,52 +0,0 @@ -//! Module containing a bunch of helper modules. - -pub mod bitstream; -pub mod lzss; -pub mod math; -pub mod prng; - -#[cfg(test)] -use std::io; - -#[cfg(test)] -pub struct SeekableSlice<'a> { - slice: &'a [u8], - cursor: usize, -} - -#[cfg(test)] -impl SeekableSlice<'_> { - pub fn new(slice: &[u8]) -> SeekableSlice { - SeekableSlice { - slice, - cursor: 0, - } - } -} - -#[cfg(test)] -impl io::Read for SeekableSlice<'_> { - fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { - let length = (&self.slice[self.cursor..]).read(buf)?; - self.cursor += length; - Ok(length) - } -} - -#[cfg(test)] -impl io::Seek for SeekableSlice<'_> { - fn seek(&mut self, seek_from: io::SeekFrom) -> io::Result<u64> { - match seek_from { - io::SeekFrom::Start(offset) => { - self.cursor = offset as usize; - } - io::SeekFrom::End(offset) => { - self.cursor = (self.slice.len() as i64 + offset) as usize; - } - io::SeekFrom::Current(offset) => { - self.cursor = (self.cursor as i64 + offset) as usize; - } - } - Ok(self.cursor as u64) - } -}
--- a/src/util/prng.rs Tue Jan 05 01:14:52 2021 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,39 +0,0 @@ -//! Random number generator extracted from EoSD. - -/// Pseudo-random number generator from EoSD. -#[derive(Debug)] -pub struct Prng { - seed: u16, -} - -impl Prng { - /// Create a new pseudo-random number generator from this seed. - pub fn new(seed: u16) -> Prng { - Prng { - seed, - } - } - - /// Generates a pseudo-random u16. - /// - /// RE’d from 102h.exe@0x41e780 - pub fn get_u16(&mut self) -> u16 { - let x = (self.seed ^ 0x9630).wrapping_sub(0x6553); - self.seed = ((x & 0xc000) >> 14) | (x << 2); - self.seed - } - - /// Combines two u16 into a single u32. - /// - /// RE’d from 102h.exe@0x41e7f0 - pub fn get_u32(&mut self) -> u32 { - ((self.get_u16() as u32) << 16) | self.get_u16() as u32 - } - - /// Transforms an u32 into a f64. - /// - /// RE’d from 102h.exe@0x41e820 - pub fn get_f64(&mut self) -> f64 { - self.get_u32() as f64 / (0x100000000u64 as f64) - } -}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/utils/Cargo.toml Tue Jan 05 02:16:32 2021 +0100 @@ -0,0 +1,12 @@ +[package] +name = "touhou-utils" +version = "0.1.0" +authors = ["Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>"] +edition = "2018" +description = "Utilities 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]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/utils/src/bitstream.rs Tue Jan 05 02:16:32 2021 +0100 @@ -0,0 +1,143 @@ +//! Bitstream module. + +use std::io; + +/// Wrapper around any `Read` trait, to allow bit operations. +pub struct BitStream<R: io::Read + io::Seek> { + io: R, + remaining_bits: usize, + byte: u8, +} + +impl<R: io::Read + io::Seek> BitStream<R> { + /// Create a new bitstream. + pub fn new(io: R) -> BitStream<R> { + BitStream { + io, + remaining_bits: 0, + byte: 0, + } + } + + /// Seek inside the bitstream, ditching any unused data read. + pub fn seek(&mut self, seek_from: io::SeekFrom) -> io::Result<u64> { + self.remaining_bits = 0; + self.byte = 0; + self.io.seek(seek_from) + } + + fn fill_byte(&mut self) -> io::Result<()> { + assert!(self.remaining_bits == 0); + + let mut buf = [0u8; 1]; + self.io.read_exact(&mut buf)?; + self.byte = buf[0]; + self.remaining_bits = 8; + Ok(()) + } + + /// Read only one bit from the stream. + pub fn read_bit(&mut self) -> io::Result<bool> { + if self.remaining_bits == 0 { + self.fill_byte()?; + } + self.remaining_bits -= 1; + Ok((self.byte >> self.remaining_bits) & 0x01 != 0) + } + + /// Read `nb_bits` bits from the stream. + pub fn read(&mut self, nb_bits: usize) -> io::Result<usize> { + let mut nb_bits2 = nb_bits; + let mut value: usize = 0; + while nb_bits2 > 0 { + if self.remaining_bits == 0 { + self.fill_byte()?; + } + let read = if nb_bits2 > self.remaining_bits { self.remaining_bits } else { nb_bits2 }; + nb_bits2 -= read; + self.remaining_bits -= read; + value |= (self.byte as usize >> self.remaining_bits) << nb_bits2; + } + Ok(value & ((1 << nb_bits) - 1)) + } + + /// Read a given amount of bytes. + pub fn read_bytes(&mut self, nb_bytes: usize) -> io::Result<Vec<u8>> { + let mut buf = vec![0u8; nb_bytes]; + self.io.read_exact(&mut buf)?; + Ok(buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::SeekableSlice; + + #[test] + fn bit_by_bit() { + let data = SeekableSlice::new(&[1, 2, 3]); + let mut bitstream = BitStream::new(data); + + // 1 + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), true); + + // 2 + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), true); + assert_eq!(bitstream.read_bit().unwrap(), false); + + // 3 + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read_bit().unwrap(), true); + assert_eq!(bitstream.read_bit().unwrap(), true); + + // Can’t read after the end. + bitstream.read_bit().unwrap_err(); + } + + #[test] + fn byte_by_byte() { + let data = SeekableSlice::new(&[1, 2, 3]); + let mut bitstream = BitStream::new(data); + + assert_eq!(bitstream.read(8).unwrap(), 1); + assert_eq!(bitstream.read(8).unwrap(), 2); + assert_eq!(bitstream.read(8).unwrap(), 3); + + // Can’t read after the end. + bitstream.read(1).unwrap_err(); + } + + #[test] + fn unaligned_bytes() { + let data = SeekableSlice::new(&[0, 129, 1, 128]); + let mut bitstream = BitStream::new(data); + + assert_eq!(bitstream.read_bit().unwrap(), false); + assert_eq!(bitstream.read(8).unwrap(), 1); + assert_eq!(bitstream.read(8).unwrap(), 2); + assert_eq!(bitstream.read(8).unwrap(), 3); + assert_eq!(bitstream.read(7).unwrap(), 0); + + // Can’t read after the end. + bitstream.read(1).unwrap_err(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/utils/src/lib.rs Tue Jan 05 02:16:32 2021 +0100 @@ -0,0 +1,52 @@ +//! Module containing a bunch of helper modules. + +pub mod bitstream; +pub mod lzss; +pub mod math; +pub mod prng; + +#[cfg(test)] +use std::io; + +#[cfg(test)] +pub struct SeekableSlice<'a> { + slice: &'a [u8], + cursor: usize, +} + +#[cfg(test)] +impl SeekableSlice<'_> { + pub fn new(slice: &[u8]) -> SeekableSlice { + SeekableSlice { + slice, + cursor: 0, + } + } +} + +#[cfg(test)] +impl io::Read for SeekableSlice<'_> { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + let length = (&self.slice[self.cursor..]).read(buf)?; + self.cursor += length; + Ok(length) + } +} + +#[cfg(test)] +impl io::Seek for SeekableSlice<'_> { + fn seek(&mut self, seek_from: io::SeekFrom) -> io::Result<u64> { + match seek_from { + io::SeekFrom::Start(offset) => { + self.cursor = offset as usize; + } + io::SeekFrom::End(offset) => { + self.cursor = (self.slice.len() as i64 + offset) as usize; + } + io::SeekFrom::Current(offset) => { + self.cursor = (self.cursor as i64 + offset) as usize; + } + } + Ok(self.cursor as u64) + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/utils/src/lzss.rs Tue Jan 05 02:16:32 2021 +0100 @@ -0,0 +1,57 @@ +//! LZSS implementation. + +use std::io; +use crate::bitstream::BitStream; + +/// Decompresses a LZSS-compressed file. +pub fn decompress<R: io::Read + io::Seek>(bitstream: &mut BitStream<R>, size: usize, dictionary_size: usize, offset_size: usize, length_size: usize, minimum_match_length: usize) -> io::Result<Vec<u8>> { + let mut data = vec![0; size]; + let mut dictionary = vec![0; dictionary_size]; + let mut dictionary_head = 1; + let mut ptr = 0; + + while ptr < size { + if bitstream.read_bit()? { + // The `flag` bit is set, indicating the upcoming chunk of data is a literal. + // Add it to the uncompressed file, and store it in the dictionary. + let byte = bitstream.read(8)? as u8; + dictionary[dictionary_head] = byte; + dictionary_head = (dictionary_head + 1) % dictionary_size; + data[ptr] = byte; + ptr += 1; + } else { + // The `flag` bit is not set, the upcoming chunk is a (offset, length) tuple. + let offset = bitstream.read(offset_size)?; + let length = bitstream.read(length_size)? + minimum_match_length; + if ptr + length > size { + return Err(io::Error::new(io::ErrorKind::Other, "Oh no!")); + } + if offset == 0 && length == 0 { + break; + } + for i in offset..offset + length { + data[ptr] = dictionary[i % dictionary_size]; + dictionary[dictionary_head] = dictionary[i % dictionary_size]; + dictionary_head = (dictionary_head + 1) % dictionary_size; + ptr += 1; + } + } + } + + Ok(data) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::SeekableSlice; + + #[test] + #[ignore] + fn bit_by_bit() { + // TODO: find actual lzss data. + let data = SeekableSlice::new(&[0, 0, 0]); + let mut bitstream = BitStream::new(data); + decompress(&mut bitstream, 3, 0x2000, 13, 4, 3).unwrap(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/utils/src/math.rs Tue Jan 05 02:16:32 2021 +0100 @@ -0,0 +1,202 @@ +//! Various helpers to deal with vectors and matrices. + +/// A 4×4 f32 matrix type. +pub struct Mat4 { + inner: [[f32; 4]; 4] +} + +impl Mat4 { + /// Create a new matrix from a set of 16 f32. + pub fn new(inner: [[f32; 4]; 4]) -> Mat4 { + Mat4 { + inner + } + } + + fn zero() -> Mat4 { + Mat4 { + inner: [[0.; 4]; 4] + } + } + + fn identity() -> Mat4 { + Mat4 { + inner: [[1., 0., 0., 0.], + [0., 1., 0., 0.], + [0., 0., 1., 0.], + [0., 0., 0., 1.]] + } + } + + /// Immutably borrow the array of f32 inside this matrix. + pub fn borrow_inner(&self) -> &[[f32; 4]; 4] { + &self.inner + } + + /// Scale the matrix in 2D. + pub fn scale2d(&mut self, x: f32, y: f32) { + for i in 0..4 { + self.inner[0][i] *= x; + self.inner[1][i] *= y; + } + } + + /// Flip the matrix. + pub fn flip(&mut self) { + for i in 0..4 { + self.inner[0][i] = -self.inner[0][i]; + } + } + + /// Rotate the matrix around its x angle (in radians). + pub fn rotate_x(&mut self, angle: f32) { + let mut lines: [f32; 8] = [0.; 8]; + let cos_a = angle.cos(); + let sin_a = angle.sin(); + for i in 0..4 { + lines[ i] = self.inner[0][i]; + lines[4 + i] = self.inner[1][i]; + } + for i in 0..4 { + self.inner[1][i] = cos_a * lines[i] - sin_a * lines[4+i]; + self.inner[2][i] = sin_a * lines[i] + cos_a * lines[4+i]; + } + } + + /// Rotate the matrix around its y angle (in radians). + pub fn rotate_y(&mut self, angle: f32) { + let mut lines: [f32; 8] = [0.; 8]; + let cos_a = angle.cos(); + let sin_a = angle.sin(); + for i in 0..4 { + lines[ i] = self.inner[0][i]; + lines[4 + i] = self.inner[2][i]; + } + for i in 0..4 { + self.inner[0][i] = cos_a * lines[i] + sin_a * lines[4+i]; + self.inner[2][i] = -sin_a * lines[i] + cos_a * lines[4+i]; + } + } + + /// Rotate the matrix around its z angle (in radians). + pub fn rotate_z(&mut self, angle: f32) { + let mut lines: [f32; 8] = [0.; 8]; + let cos_a = angle.cos(); + let sin_a = angle.sin(); + for i in 0..4 { + lines[ i] = self.inner[0][i]; + lines[4 + i] = self.inner[1][i]; + } + for i in 0..4 { + self.inner[0][i] = cos_a * lines[i] - sin_a * lines[4+i]; + self.inner[1][i] = sin_a * lines[i] + cos_a * lines[4+i]; + } + } + + /// Translate the matrix by a 3D offset. + pub fn translate(&mut self, offset: [f32; 3]) { + let mut item: [f32; 3] = [0.; 3]; + for i in 0..3 { + item[i] = self.inner[3][i] * offset[i]; + } + for i in 0..3 { + for j in 0..4 { + self.inner[i][j] += item[i]; + } + } + } + + /// Translate the matrix by a 2D offset. + pub fn translate_2d(&mut self, x: f32, y: f32) { + let offset = [x, y, 0.]; + self.translate(offset); + } +} + +impl std::ops::Mul<Mat4> for Mat4 { + type Output = Mat4; + fn mul(self, rhs: Mat4) -> Mat4 { + let mut tmp = Mat4::zero(); + for i in 0..4 { + for j in 0..4 { + for k in 0..4 { + tmp.inner[i][j] += self.inner[i][k] * rhs.inner[k][j]; + } + } + } + tmp + } +} + +/// Create an orthographic projection matrix. +pub fn ortho_2d(left: f32, right: f32, bottom: f32, top: f32) -> Mat4 { + let mut mat = Mat4::identity(); + mat.inner[0][0] = 2. / (right - left); + mat.inner[1][1] = 2. / (top - bottom); + mat.inner[2][2] = -1.; + mat.inner[3][0] = -(right + left) / (right - left); + mat.inner[3][1] = -(top + bottom) / (top - bottom); + mat +} + +/// Setup a camera view matrix. +pub fn setup_camera(dx: f32, dy: f32, dz: f32) -> Mat4 { + // Some explanations on the magic constants: + // 192. = 384. / 2. = width / 2. + // 224. = 448. / 2. = height / 2. + // 835.979370 = 224./math.tan(math.radians(15)) = (height/2.)/math.tan(math.radians(fov/2)) + // This is so that objects on the (O, x, y) plane use pixel coordinates + look_at([192., 224., -835.979370 * dz], [192. + dx, 224. - dy, 0.], [0., -1., 0.]) +} + +/// Creates a perspective projection matrix. +pub fn perspective(fov_y: f32, aspect: f32, z_near: f32, z_far: f32) -> Mat4 { + let top = (fov_y / 2.).tan() * z_near; + let bottom = -top; + let left = -top * aspect; + let right = top * aspect; + + let mut mat = Mat4::identity(); + mat.inner[0][0] = (2. * z_near) / (right - left); + mat.inner[1][1] = (2. * z_near) / (top - bottom); + mat.inner[2][2] = -(z_far + z_near) / (z_far - z_near); + mat.inner[2][3] = -1.; + mat.inner[3][2] = -(2. * z_far * z_near) / (z_far - z_near); + mat.inner[3][3] = 0.; + mat +} + +type Vec3 = [f32; 3]; + +fn look_at(eye: Vec3, center: Vec3, up: Vec3) -> Mat4 { + let f = normalize(sub(center, eye)); + let u = normalize(up); + let s = normalize(cross(f, u)); + let u = cross(s, f); + + Mat4::new([[s[0], u[0], -f[0], 0.], + [s[1], u[1], -f[1], 0.], + [s[2], u[2], -f[2], 0.], + [-dot(s, eye), -dot(u, eye), dot(f, eye), 1.]]) +} + +fn sub(a: Vec3, b: Vec3) -> Vec3 { + [a[0] - b[0], + a[1] - b[1], + a[2] - b[2]] +} + +fn normalize(vec: Vec3) -> Vec3 { + let normal = 1. / (vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]).sqrt(); + [vec[0] * normal, vec[1] * normal, vec[2] * normal] +} + +fn cross(a: Vec3, b: Vec3) -> Vec3 { + [a[1] * b[2] - b[1] * a[2], + a[2] * b[0] - b[2] * a[0], + a[0] * b[1] - b[0] * a[1]] +} + +fn dot(a: Vec3, b: Vec3) -> f32 { + a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/utils/src/prng.rs Tue Jan 05 02:16:32 2021 +0100 @@ -0,0 +1,39 @@ +//! Random number generator extracted from EoSD. + +/// Pseudo-random number generator from EoSD. +#[derive(Debug)] +pub struct Prng { + seed: u16, +} + +impl Prng { + /// Create a new pseudo-random number generator from this seed. + pub fn new(seed: u16) -> Prng { + Prng { + seed, + } + } + + /// Generates a pseudo-random u16. + /// + /// RE’d from 102h.exe@0x41e780 + pub fn get_u16(&mut self) -> u16 { + let x = (self.seed ^ 0x9630).wrapping_sub(0x6553); + self.seed = ((x & 0xc000) >> 14) | (x << 2); + self.seed + } + + /// Combines two u16 into a single u32. + /// + /// RE’d from 102h.exe@0x41e7f0 + pub fn get_u32(&mut self) -> u32 { + ((self.get_u16() as u32) << 16) | self.get_u16() as u32 + } + + /// Transforms an u32 into a f64. + /// + /// RE’d from 102h.exe@0x41e820 + pub fn get_f64(&mut self) -> f64 { + self.get_u32() as f64 / (0x100000000u64 as f64) + } +}
