From 9da4648754afbc77f8a8ac7eddd96e6f7f175089 Mon Sep 17 00:00:00 2001 From: Alexander Guryanov Date: Thu, 18 Nov 2021 17:55:42 +0700 Subject: [PATCH 1/3] wasm32-unknown-unknown --- .github/workflows/check.yml | 6 +- .gitignore | 6 + Cargo.lock | 17 +++ Cargo.toml | 15 +- README.md | 39 +++++ bin/boilerplate.rs | 77 +++++++--- bin/road/game.rs | 9 +- bin/road/main.rs | 61 +++++++- bin/web.rs | 277 ++++++++++++++++++++++++++++++++++++ gen.py | 22 +++ src/config/car.rs | 6 + src/config/settings.rs | 8 +- src/level/config.rs | 4 +- src/level/mod.rs | 23 ++- src/render/mod.rs | 16 ++- web/build.sh | 4 + web/index.html | 234 ++++++++++++++++++++++++++++++ web/road.in.css | 3 + web/tailwind.config.js | 9 ++ 19 files changed, 792 insertions(+), 44 deletions(-) create mode 100644 bin/web.rs create mode 100644 gen.py create mode 100755 web/build.sh create mode 100644 web/index.html create mode 100644 web/road.in.css create mode 100644 web/tailwind.config.js diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e8c9f154..5682b2b2 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,9 +1,9 @@ name: Check on: push: - branches: [master] + branches: [master, vange-rs-web] pull_request: - branches: [master] + branches: [master, vange-rs-web] jobs: build: @@ -14,7 +14,9 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 + - run: rustup target add wasm32-unknown-unknown - run: cargo check + - run: cargo check --bin road --features nodata --target wasm32-unknown-unknown - if: matrix.os == 'ubuntu-latest' run: cargo check --features glsl - run: cargo test diff --git a/.gitignore b/.gitignore index e4c8b24c..72f169ed 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ .fuse_hidden* .DS_Store last-shader.wgsl + +# web version +web/assets/res +web/assets/road.css +web/road* +res_linux \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index ede28d70..cb60cb53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,6 +232,16 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + [[package]] name = "copyless" version = "0.1.5" @@ -665,8 +675,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1656,9 +1668,11 @@ dependencies = [ "bytemuck", "byteorder", "cgmath", + "console_error_panic_hook", "env_logger", "futures", "getopts", + "getrandom", "glsl-to-spirv", "log", "m3d", @@ -1675,6 +1689,9 @@ dependencies = [ "serde_scan", "splay", "tiff", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", "wgpu", "winit", ] diff --git a/Cargo.toml b/Cargo.toml index 79d86a77..bbd5287f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ default = [] glsl = ["glsl-to-spirv", "wgpu/spirv"] profile = ["profiling/profile-with-tracy"] +nodata = [] [[bin]] name = "road" @@ -60,13 +61,13 @@ rust-ini = "0.17" serde = "1.0" serde_derive = "1.0" serde_scan = "0.4" -wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "c8d572a", features = [] } +wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "c8d572a", features = ["webgl"] } # binaries env_logger = "0.8" getopts = "0.2" obj = "0.10" png = "0.16" -winit = "0.26" +winit = { version = "0.26", features = [] } [dev-dependencies] naga = { git = "https://github.com/gfx-rs/naga", rev = "c69f676", features = ["wgsl-in"] } @@ -82,5 +83,11 @@ default-features = false #wgpu-core = { path = "../wgpu/wgpu-core" } #wgpu-types = { path = "../wgpu/wgpu-types" } -[patch."https://github.com/gfx-rs/naga"] -#naga = { path = "../naga" } +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { version = "0.3.55", features = [] } +wasm-bindgen = "0.2.78" +wasm-bindgen-futures = "0.4" +console_error_panic_hook = { version = "0.1.7" } + +[target.'cfg(target_arch = "wasm32")'.dependencies.getrandom] +features = ["js"] diff --git a/README.md b/README.md index 595b8fbe..503da48a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,45 @@ Controls: game +### Web version + + Stage | State | Note | + -------------------- | ------------------ | ----------- | + Support FS | :white_check_mark: | [rust-memfs](https://github.com/caiiiycuk/rust-memfs/blob/memfs/README.md) | + WebGL Initialization | :white_check_mark: || + Loading worlds | :white_check_mark: || + Loading heights | :white_check_mark: || + Loading data | :white_check_mark: || + Loading flood | :white_check_mark: || + Render | :white_check_mark: | WebGPU not tested | + +**Requrements** + +* To build game **you must** put resources of original game in `res_linux/data` folder. +* You must install [wasm-bindgen](https://rustwasm.github.io/wasm-bindgen/reference/cli.html) +* If you intended to run the build, then you must use [rust-memfs](https://github.com/caiiiycuk/rust-memfs/blob/memfs/README.md). [explanation (RU)](https://habr.com/ru/post/594611/) + +**Debug build** + +```sh +cargo build --target wasm32-unknown-unknown --bin road --verbose && \ +wasm-bindgen --out-dir web/ --target web --keep-debug target/wasm32-unknown-unknown/debug/road.wasm +``` + +**Release build** +```sh +cargo build --target wasm32-unknown-unknown --bin road --verbose --release && \ +wasm-bindgen --out-dir web/ --target web target/wasm32-unknown-unknown/release/road.wasm +``` + +**Build web page** +```sh +cd web +./build.sh +``` + +After building you can run game just by serving `web` folder. + ### Mechous viewer/debugger `car` binary allows to see the mechos with items selected by the configuration. It also shows the debug collision info. ```bash diff --git a/bin/boilerplate.rs b/bin/boilerplate.rs index fa42cd87..d2ccf4e9 100644 --- a/bin/boilerplate.rs +++ b/bin/boilerplate.rs @@ -4,7 +4,7 @@ use vangers::{ render::{ScreenTargets, DEPTH_FORMAT}, }; -use futures::executor::{LocalPool, LocalSpawner}; +use futures::executor::{block_on, LocalPool, LocalSpawner}; use log::info; use winit::{ event, @@ -12,6 +12,9 @@ use winit::{ window::{Window, WindowBuilder}, }; +#[cfg(target_arch = "wasm32")] +use crate::web; + pub trait Application { fn on_key(&mut self, input: event::KeyboardInput) -> bool; fn on_mouse_wheel(&mut self, _delta: event::MouseScrollDelta) {} @@ -53,12 +56,15 @@ pub struct HarnessOptions { } impl Harness { + #[cfg(not(target_arch = "wasm32"))] pub fn init(options: HarnessOptions) -> (Self, config::Settings) { - env_logger::init(); - let mut task_pool = LocalPool::new(); + block_on(Harness::init_async(options)) + } + pub async fn init_async(options: HarnessOptions) -> (Self, config::Settings) { info!("Loading the settings"); let settings = config::Settings::load("config/settings.ron"); + let extent = wgpu::Extent3d { width: settings.window.size[0], height: settings.window.size[1], @@ -76,19 +82,25 @@ impl Harness { .unwrap(); let surface = unsafe { instance.create_surface(&window) }; - info!("Initializing the device"); - let adapter = task_pool - .run_until(instance.request_adapter(&wgpu::RequestAdapterOptions { + info!("Initializing the device:adapter"); + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, compatible_surface: Some(&surface), force_fallback_adapter: false, - })) - .expect("Unable to initialize GPU via the selected backend."); + }) + .await + .expect("Unable to initialize GPU via the selected backend (adapter)."); let downlevel_caps = adapter.get_downlevel_properties(); let adapter_limits = adapter.limits(); + #[cfg(target_arch = "wasm32")] let mut limits = wgpu::Limits::downlevel_webgl2_defaults(); + + #[cfg(not(target_arch = "wasm32"))] + let mut limits = wgpu::Limits::downlevel_defaults(); + if options.uses_level { let desired_height = 16 << 10; limits.max_texture_dimension_2d = @@ -102,8 +114,10 @@ impl Harness { desired_height }; } - let (device, queue) = task_pool - .run_until(adapter.request_device( + + info!("Initializing the device:request"); + let (device, queue) = adapter + .request_device( &wgpu::DeviceDescriptor { label: None, features: wgpu::Features::empty(), @@ -114,8 +128,9 @@ impl Harness { } else { Some(std::path::Path::new(&settings.render.wgpu_trace_path)) }, - )) - .unwrap(); + ) + .await + .expect("Unable to initialize GPU via the selected backend (request)."); let config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, @@ -141,7 +156,7 @@ impl Harness { .create_view(&wgpu::TextureViewDescriptor::default()); let harness = Harness { - task_pool, + task_pool: LocalPool::new(), event_loop, window, device, @@ -158,9 +173,15 @@ impl Harness { } pub fn main_loop(self, mut app: A) { + #[cfg(not(target_arch = "wasm32"))] use std::time; + #[cfg(target_arch = "wasm32")] + let mut last_time = web::now(); + + #[cfg(not(target_arch = "wasm32"))] let mut last_time = time::Instant::now(); + let mut needs_reload = false; let Harness { mut task_pool, @@ -181,6 +202,9 @@ impl Harness { *control_flow = ControlFlow::Poll; task_pool.run_until_stalled(); + #[cfg(target_arch = "wasm32")] + web::bind_once(&mut app); + match event { event::Event::WindowEvent { event: event::WindowEvent::Resized(size), @@ -241,13 +265,28 @@ impl Harness { }, event::Event::MainEventsCleared => { let spawner = task_pool.spawner(); - let duration = time::Instant::now() - last_time; - last_time += duration; - let delta = duration.as_secs() as f32 + duration.subsec_nanos() as f32 * 1.0e-9; - let update_command_buffers = app.update(&device, delta, &spawner); - if !update_command_buffers.is_empty() { - queue.submit(update_command_buffers); + let delta: f32; + + #[cfg(target_arch = "wasm32")] + { + let duration = web::now() - last_time; + last_time += duration; + delta = (duration / 1000.0) as f32; + } + + #[cfg(not(target_arch = "wasm32"))] + { + let duration = time::Instant::now() - last_time; + last_time += duration; + delta = duration.as_secs() as f32 + duration.subsec_nanos() as f32 * 1.0e-9; + } + + if delta > 0.0 { + let update_command_buffers = app.update(&device, delta, &spawner); + if !update_command_buffers.is_empty() { + queue.submit(update_command_buffers); + } } match surface.get_current_texture() { diff --git a/bin/road/game.rs b/bin/road/game.rs index 6409777a..41e67ff3 100644 --- a/bin/road/game.rs +++ b/bin/road/game.rs @@ -811,6 +811,7 @@ impl Application for Game { } { + #[cfg(not(target_arch = "wasm32"))] use rayon::prelude::*; let clipper = Clipper::new(&self.cam); @@ -818,7 +819,13 @@ impl Application for Game { let common = &self.db.common; let level = &self.level; - self.agents.par_iter_mut().for_each(|a| { + #[cfg(not(target_arch = "wasm32"))] + let agent_iter = self.agents.par_iter_mut(); + + #[cfg(target_arch = "wasm32")] + let agent_iter = self.agents.iter_mut(); + + agent_iter.for_each(|a| { let mut dt = physics_dt; a.cpu_apply_control(input_factor, common); diff --git a/bin/road/main.rs b/bin/road/main.rs index eb44185a..3569f792 100644 --- a/bin/road/main.rs +++ b/bin/road/main.rs @@ -1,19 +1,20 @@ #![allow(irrefutable_let_patterns)] - use log::info; #[path = "../boilerplate.rs"] mod boilerplate; + +#[cfg(target_arch = "wasm32")] +#[path = "../web.rs"] +mod web; + mod game; mod physics; +#[cfg(not(target_arch = "wasm32"))] fn main() { use std::env; - - let (harness, settings) = boilerplate::Harness::init(boilerplate::HarnessOptions { - title: "road", - uses_level: true, - }); + env_logger::init(); info!("Parsing command line"); let args: Vec<_> = env::args().collect(); @@ -30,6 +31,11 @@ fn main() { return; } + let (harness, settings) = boilerplate::Harness::init(boilerplate::HarnessOptions { + title: "road", + uses_level: true, + }); + let game = game::Game::new( &settings, harness.color_format, @@ -41,3 +47,46 @@ fn main() { harness.main_loop(game); } + +#[cfg(target_arch = "wasm32")] +fn main() { + #[path = "../web.rs"] + mod web; + + use env_logger::Builder; + use log::LevelFilter; + + console_error_panic_hook::set_once(); + + Builder::new() + .format(|_buf, record| { + let message = format!("{}: {}", record.level(), record.args()); + web::log(&message); + Ok(()) + }) + .filter(None, LevelFilter::Error) + .init(); + + web::create_fs(); + + async fn run() { + let (harness, settings) = boilerplate::Harness::init_async(boilerplate::HarnessOptions { + title: "road", + uses_level: true, + }) + .await; + + let game = game::Game::new( + &settings, + harness.color_format, + harness.extent, + &harness.device, + &harness.queue, + &harness.downlevel_caps, + ); + + harness.main_loop(game); + } + + wasm_bindgen_futures::spawn_local(run()); +} diff --git a/bin/web.rs b/bin/web.rs new file mode 100644 index 00000000..a2844de6 --- /dev/null +++ b/bin/web.rs @@ -0,0 +1,277 @@ +use log::info; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; + +extern crate console_error_panic_hook; + +use crate::boilerplate::Application; + +static mut BINDED: bool = false; + +pub fn bind_once(mut _application: &mut dyn Application) { + use std::collections::HashMap; + use winit::event::{ElementState, KeyboardInput, ModifiersState, VirtualKeyCode as Key}; + + unsafe { + if BINDED { + return; + } + + BINDED = true; + } + + let application = unsafe { + std::mem::transmute::<&mut dyn Application, &'static mut dyn Application>(_application) + }; + + let window = web_sys::window().expect("should have a window in this context"); + + { + let mappings: HashMap = [ + (String::from("w"), Key::W), + (String::from("ц"), Key::W), + (String::from("a"), Key::A), + (String::from("a"), Key::A), + (String::from("s"), Key::S), + (String::from("ы"), Key::S), + (String::from("d"), Key::D), + (String::from("в"), Key::D), + (String::from("p"), Key::P), + (String::from("з"), Key::P), + (String::from("r"), Key::R), + (String::from("к"), Key::R), + (String::from("e"), Key::E), + (String::from("у"), Key::E), + (String::from("q"), Key::Q), + (String::from("й"), Key::Q), + (String::from("shift"), Key::LShift), + (String::from("alt"), Key::LAlt), + ] + .iter() + .cloned() + .collect(); + + let closure = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| { + let pressed = event.type_() == "keydown"; + let key = event.key().to_ascii_lowercase(); + + match mappings.get(&key) { + Some(key) => application.on_key(KeyboardInput { + state: if pressed { + ElementState::Pressed + } else { + ElementState::Released + }, + virtual_keycode: Some(*key), + modifiers: ModifiersState::default(), + scancode: 0, + }), + None => false, + }; + }) as Box); + + window + .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref()) + .expect("should be able to bind keydown listener"); + window + .add_event_listener_with_callback("keyup", closure.as_ref().unchecked_ref()) + .expect("should be able to bind keyup listener"); + closure.forget(); + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console, js_name = log)] + pub fn log(a: &str); + + #[wasm_bindgen(js_namespace = Date, js_name = now)] + pub fn now() -> f64; + + #[wasm_bindgen(js_namespace = window, js_name = getFile)] + pub fn get_js_file(file: &str) -> String; +} + +fn create_file(file: &str, buf: &[u8]) { + use std::{fs::File, io::Write}; + + info!("Creating file {}", file); + File::create(file) + .expect(&format!("Unable to create {}", file)) + .write(buf) + .expect(&format!("Unable to write in {}", file)); +} + +#[macro_use] +mod rsrc { + macro_rules! get_bundled_file { + () => { + "../res_linux/" + }; + } + + macro_rules! create_file { + ($path:expr, $src_dir:expr) => {{ + create_file($path, include_bytes!(concat!($src_dir, $path))); + }}; + } +} + +pub fn create_fs() { + info!("Creating fs"); + + let js_files = [ + // vange-rs files + "config/settings.ron", + "res/shader/quat.inc.glsl", + "res/shader/debug.wgsl", + "res/shader/object.wgsl", + "res/shader/encode.inc.glsl", + "res/shader/quat.inc.wgsl", + "res/shader/globals.inc.wgsl", + "res/shader/downsample.glsl", + "res/shader/body.inc.wgsl", + "res/shader/shape.inc.glsl", + "res/shader/surface.inc.wgsl", + "res/shader/body.inc.glsl", + "res/shader/globals.inc.glsl", + "res/shader/debug_shape.glsl", + "res/shader/shadow.inc.wgsl", + "res/shader/surface.inc.glsl", + "res/shader/color.inc.wgsl", + "res/shader/terrain/locals.inc.wgsl", + "res/shader/terrain/ray.wgsl", + "res/shader/terrain/paint.wgsl", + "res/shader/terrain/mip.wgsl", + "res/shader/terrain/scatter.wgsl", + "res/shader/terrain/slice.wgsl", + "res/shader/physics/collision.inc.glsl", + "res/shader/physics/collision_clear.glsl", + "res/shader/physics/body_step.glsl", + "res/shader/physics/collision_add.glsl", + "res/shader/physics/body_push.glsl", + "res/shader/physics/pulse.inc.glsl", + "res/shader/physics/body_gather.glsl", + ]; + + for next in js_files { + create_file(next, get_js_file(next).as_bytes()); + } + + #[cfg(not(feature = "nodata"))] { + // vangers resouces + create_file!("data/escaves.prm", get_bundled_file!()); + create_file!("data/common.prm", get_bundled_file!()); + create_file!("data/bunches.prm", get_bundled_file!()); + create_file!("data/game.lst", get_bundled_file!()); + create_file!("data/car.prm", get_bundled_file!()); + create_file!("data/wrlds.dat", get_bundled_file!()); + + // the chain + create_file!("data/thechain/threall/world.ini", get_bundled_file!()); + create_file!("data/thechain/threall/output.vmc", get_bundled_file!()); + create_file!("data/thechain/threall/terrain.prm", get_bundled_file!()); + create_file!("data/thechain/threall/output.vpr", get_bundled_file!()); + create_file!("data/thechain/threall/harmony.pal", get_bundled_file!()); + + create_file!("data/thechain/boozeena/world.ini", get_bundled_file!()); + create_file!("data/thechain/boozeena/output.vmc", get_bundled_file!()); + create_file!("data/thechain/boozeena/terrain.prm", get_bundled_file!()); + create_file!("data/thechain/boozeena/output.vpr", get_bundled_file!()); + create_file!("data/thechain/boozeena/harmony.pal", get_bundled_file!()); + + create_file!("data/thechain/weexow/world.ini", get_bundled_file!()); + create_file!("data/thechain/weexow/output.vmc", get_bundled_file!()); + create_file!("data/thechain/weexow/terrain.prm", get_bundled_file!()); + create_file!("data/thechain/weexow/output.vpr", get_bundled_file!()); + create_file!("data/thechain/weexow/harmony.pal", get_bundled_file!()); + + create_file!("data/thechain/xplo/world.ini", get_bundled_file!()); + create_file!("data/thechain/xplo/output.vmc", get_bundled_file!()); + create_file!("data/thechain/xplo/terrain.prm", get_bundled_file!()); + create_file!("data/thechain/xplo/output.vpr", get_bundled_file!()); + create_file!("data/thechain/xplo/harmony.pal", get_bundled_file!()); + + create_file!("data/thechain/hmok/world.ini", get_bundled_file!()); + create_file!("data/thechain/hmok/output.vmc", get_bundled_file!()); + create_file!("data/thechain/hmok/terrain.prm", get_bundled_file!()); + create_file!("data/thechain/hmok/output.vpr", get_bundled_file!()); + create_file!("data/thechain/hmok/harmony.pal", get_bundled_file!()); + + create_file!("data/thechain/ark-a-znoy/world.ini", get_bundled_file!()); + create_file!("data/thechain/ark-a-znoy/output.vmc", get_bundled_file!()); + create_file!("data/thechain/ark-a-znoy/terrain.prm", get_bundled_file!()); + create_file!("data/thechain/ark-a-znoy/output.vpr", get_bundled_file!()); + create_file!("data/thechain/ark-a-znoy/harmony.pal", get_bundled_file!()); + + create_file!("data/thechain/khox/world.ini", get_bundled_file!()); + create_file!("data/thechain/khox/output.vmc", get_bundled_file!()); + create_file!("data/thechain/khox/terrain.prm", get_bundled_file!()); + create_file!("data/thechain/khox/output.vpr", get_bundled_file!()); + create_file!("data/thechain/khox/harmony.pal", get_bundled_file!()); + + // palettes + create_file!("data/resource/pal/necross.pal", get_bundled_file!()); + create_file!("data/resource/pal/necross1.pal", get_bundled_file!()); + create_file!("data/resource/pal/fostral2.pal", get_bundled_file!()); + create_file!("data/resource/pal/xplo.pal", get_bundled_file!()); + create_file!("data/resource/pal/necross2.pal", get_bundled_file!()); + create_file!("data/resource/pal/fostral1.pal", get_bundled_file!()); + create_file!("data/resource/pal/glorx1.pal", get_bundled_file!()); + create_file!("data/resource/pal/glorx2.pal", get_bundled_file!()); + create_file!("data/resource/pal/fostral.pal", get_bundled_file!()); + create_file!("data/resource/pal/objects.pal", get_bundled_file!()); + create_file!("data/resource/pal/glorx.pal", get_bundled_file!()); + + // models + create_file!("data/resource/m3d/mechous/m13.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m10.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m5.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m9.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/u5.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m1.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/u3.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m11.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/u2.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/u1.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/r2.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/r3.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/r4.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m3.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m14.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m9.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m7.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m10.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/default.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/r1.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m13.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m12.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/r4.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m11.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m2.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m6.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m12.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/u2.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m1.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m4.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m8.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/u1.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m4.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m14.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/r5.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/r3.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m3.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/r5.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/u4.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/u4.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m6.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/r2.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/r1.prm", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m5.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m7.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m8.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/u5.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/m2.m3d", get_bundled_file!()); + create_file!("data/resource/m3d/mechous/u3.prm", get_bundled_file!()); + } +} diff --git a/gen.py b/gen.py new file mode 100644 index 00000000..4fe0ceaf --- /dev/null +++ b/gen.py @@ -0,0 +1,22 @@ +import os +import sys + +print("\t// vangers-rs resources") +for root, subdirs, files in os.walk("res"): + for file in files: + path = os.path.join(root, file) + print("\tcreate_file(\"" + path + "\", include_bytes!(\"../" + path + "\"));"); + +print("\n\t// vangers resouces") +for root, subdirs, files in os.walk("res_linux"): + for file in files: + path = os.path.join(root, file) + if ("/video/" in path or ".git/" in path or + "/sound/" in path or "/actint/" in path or + "/iscreen/" in path or "/music/" in path or + "/savegame/" in path or "/shader/" in path or + ".wgsl" in path or "data.vot" in path or + ".bnl" in path or ".lst" in path): + continue + print("\tcreate_file(\"" + path[len("res_linux/"):] + "\", include_bytes!(\"../" + path + "\"));"); + diff --git a/src/config/car.rs b/src/config/car.rs index 3670ab78..97215d45 100644 --- a/src/config/car.rs +++ b/src/config/car.rs @@ -177,7 +177,13 @@ pub fn load_registry( let (name, data) = fi.next_entry(); let mi = ®.model_infos[name]; let mut prm_path = settings.data_path.join(&mi.path).with_extension("prm"); + + #[cfg(target_arch = "wasm32")] + let is_default = false; + + #[cfg(not(target_arch = "wasm32"))] let is_default = !prm_path.exists(); + if is_default { warn!("Vehicle {} doesn't have parameters, using defaults", name); prm_path.set_file_name("default"); diff --git a/src/config/settings.rs b/src/config/settings.rs index 47730ad5..77c0c2cc 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -81,7 +81,7 @@ pub enum Backend { impl Backend { pub fn to_wgpu(&self) -> wgpu::Backends { match *self { - Backend::Auto => wgpu::Backends::PRIMARY, + Backend::Auto => wgpu::Backends::all(), Backend::Metal => wgpu::Backends::METAL, Backend::Vulkan => wgpu::Backends::VULKAN, Backend::DX12 => wgpu::Backends::DX12, @@ -194,6 +194,12 @@ impl Settings { .unwrap_or_else(|_| panic!("Unable to open game file: {}", path)) } + #[cfg(target_arch = "wasm32")] + pub fn check_path(&self, _path: &str) -> bool { + true + } + + #[cfg(not(target_arch = "wasm32"))] pub fn check_path(&self, path: &str) -> bool { self.data_path.join(path).exists() } diff --git a/src/level/config.rs b/src/level/config.rs index 4e3d0c3d..ced23054 100644 --- a/src/level/config.rs +++ b/src/level/config.rs @@ -34,8 +34,8 @@ pub struct LevelConfig { impl LevelConfig { pub fn load(ini_path: &Path) -> Self { - let ini = Ini::load_from_file(ini_path).unwrap_or_else(|_| { - panic!("Unable to read the level's INI description: {:?}", ini_path) + let ini = Ini::load_from_file(ini_path).unwrap_or_else(|error| { + panic!("Unable to read the level's INI description: {:?} {:?}", ini_path, error) }); let global = &ini["Global Parameters"]; let storage = &ini["Storage"]; diff --git a/src/level/mod.rs b/src/level/mod.rs index 2ea5e72e..e942b61b 100644 --- a/src/level/mod.rs +++ b/src/level/mod.rs @@ -222,8 +222,12 @@ pub fn load_flood(config: &LevelConfig) -> Vec { let flood_offset = (2 * 4 + (1 + 4 + 4) * 4 + 2 * net_size + 2 * geo_pow * 4 + 2 * flood_size * geo_pow * 4) as u64; - let expected_file_size = flood_offset + (flood_size * 4) as u64; - assert_eq!(vpr_file.metadata().unwrap().len(), expected_file_size,); + + #[cfg(not(target_arch = "wasm32"))] { + let expected_file_size = flood_offset + (flood_size * 4) as u64; + assert_eq!(vpr_file.metadata().unwrap().len(), expected_file_size,); + } + let mut vpr = BufReader::new(vpr_file); vpr.seek(SeekFrom::Start(flood_offset)).unwrap(); (0..flood_size) @@ -324,6 +328,7 @@ impl LevelData { } pub fn load_vmc(path: &Path, size: (i32, i32)) -> LevelData { + #[cfg(not(target_arch = "wasm32"))] use rayon::prelude::*; use splay::Splay; @@ -352,14 +357,20 @@ pub fn load_vmc(path: &Path, size: (i32, i32)) -> LevelData { (splay, st_table, sz_table) }; - level + let mut level_iter = level .height .chunks_mut(size.0 as _) .zip(level.meta.chunks_mut(size.0 as _)) .zip(st_table.iter().zip(&sz_table)) - .collect::>() - .par_chunks_mut(64) - .for_each(|source_group| { + .collect::>(); + + #[cfg(not(target_arch = "wasm32"))] + let level_iter = level_iter.par_chunks_mut(64); + + #[cfg(target_arch = "wasm32")] + let level_iter = level_iter.chunks_mut(64); + + level_iter.for_each(|source_group| { //Note: a separate file per group is required let mut vmc = File::open(path).unwrap(); let data_size: i16 = source_group diff --git a/src/render/mod.rs b/src/render/mod.rs index 36bd546d..33a56db4 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -105,7 +105,7 @@ impl ShapeVertexDesc { pub fn make_shader_code(name: &str) -> Result { let base_path = PathBuf::from("res").join("shader"); let path = base_path.join(name).with_extension("wgsl"); - if !path.is_file() { + if !is_file(&path) { panic!("Shader not found: {:?}", path); } @@ -172,7 +172,7 @@ impl Shaders { let base_path = PathBuf::from("res").join("shader"); let path = base_path.join(name).with_extension("glsl"); - if !path.is_file() { + if !is_file(&path) { panic!("Shader not found: {:?}", path); } @@ -263,7 +263,7 @@ impl Shaders { let base_path = PathBuf::from("res").join("shader"); let path = base_path.join(name).with_extension("glsl"); - if !path.is_file() { + if !is_file(&path) { panic!("Shader not found: {:?}", path); } @@ -737,3 +737,13 @@ impl Render { self.terrain_data.out_color.clone() }*/ } + +#[cfg(target_arch = "wasm32")] +fn is_file(_path: &PathBuf) -> bool { + true +} + +#[cfg(not(target_arch = "wasm32"))] +fn is_file(path: &PathBuf) -> bool { + path.is_file() +} diff --git a/web/build.sh b/web/build.sh new file mode 100755 index 00000000..f6a93113 --- /dev/null +++ b/web/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +rm -rf assets/res && cp -r ../res assets/res +NODE_ENV=production npx tailwindcss -i road.in.css -c ./tailwind.config.js -o ./assets/road.css \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 00000000..53b49d56 --- /dev/null +++ b/web/index.html @@ -0,0 +1,234 @@ + + + + + Vange-rs: WebAssembly + Rust + + + + + +
+

Vange-rs: WebAssembly + Rust

+

Controls (click into canvas to obtains keyboard focus)

+

Move: WASD

+

Jump: Left Alt

+

Turbo: Left Shift

+

Reset: R

+

Car view: P

+ + +

Select world and press GO! Then wait patiently...

+ + +
+
World:
+ + +
+ + +
+
+ +
+ + + + + + + \ No newline at end of file diff --git a/web/road.in.css b/web/road.in.css new file mode 100644 index 00000000..bd6213e1 --- /dev/null +++ b/web/road.in.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/web/tailwind.config.js b/web/tailwind.config.js new file mode 100644 index 00000000..a33df5a7 --- /dev/null +++ b/web/tailwind.config.js @@ -0,0 +1,9 @@ +module.exports = { + purge: ["./index.html"], + darkMode: false, + theme: { + extend: {}, + }, + variants: {}, + plugins: [], +} \ No newline at end of file From 63e42555d109454bfc114fa4bcac28872c8def15 Mon Sep 17 00:00:00 2001 From: Vitaliy Busko Date: Fri, 10 Dec 2021 19:42:26 +0700 Subject: [PATCH 2/3] fix: double import bin/web module --- bin/boilerplate.rs | 8 ++++++-- bin/road/main.rs | 8 +------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/bin/boilerplate.rs b/bin/boilerplate.rs index d2ccf4e9..aa6102e7 100644 --- a/bin/boilerplate.rs +++ b/bin/boilerplate.rs @@ -4,7 +4,8 @@ use vangers::{ render::{ScreenTargets, DEPTH_FORMAT}, }; -use futures::executor::{block_on, LocalPool, LocalSpawner}; + +use futures::executor::{LocalPool, LocalSpawner}; use log::info; use winit::{ event, @@ -12,7 +13,10 @@ use winit::{ window::{Window, WindowBuilder}, }; -#[cfg(target_arch = "wasm32")] +#[cfg(not(target_arch="wasm32"))] +use futures::executor::block_on; + +#[cfg(target_arch="wasm32")] use crate::web; pub trait Application { diff --git a/bin/road/main.rs b/bin/road/main.rs index 3569f792..d5640714 100644 --- a/bin/road/main.rs +++ b/bin/road/main.rs @@ -1,6 +1,3 @@ -#![allow(irrefutable_let_patterns)] -use log::info; - #[path = "../boilerplate.rs"] mod boilerplate; @@ -16,7 +13,7 @@ fn main() { use std::env; env_logger::init(); - info!("Parsing command line"); + log::info!("Parsing command line"); let args: Vec<_> = env::args().collect(); let mut options = getopts::Options::new(); options @@ -50,9 +47,6 @@ fn main() { #[cfg(target_arch = "wasm32")] fn main() { - #[path = "../web.rs"] - mod web; - use env_logger::Builder; use log::LevelFilter; From 4ac491eff7a826065e97352941456e20b8c561dd Mon Sep 17 00:00:00 2001 From: Vitaliy Busko Date: Fri, 17 Dec 2021 19:45:47 +0700 Subject: [PATCH 3/3] fix(web): use iterior-mutability instead of unsafe --- bin/boilerplate.rs | 24 ++++++++++++++---------- bin/web.rs | 11 +++++------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/bin/boilerplate.rs b/bin/boilerplate.rs index aa6102e7..75fedb16 100644 --- a/bin/boilerplate.rs +++ b/bin/boilerplate.rs @@ -1,4 +1,6 @@ #![allow(clippy::single_match)] +use std::{cell::RefCell, rc::Rc}; + use vangers::{ config, render::{ScreenTargets, DEPTH_FORMAT}, @@ -176,10 +178,12 @@ impl Harness { (harness, settings) } - pub fn main_loop(self, mut app: A) { + pub fn main_loop(self, app: A) { #[cfg(not(target_arch = "wasm32"))] use std::time; + let app = Rc::new(RefCell::new(app)); + #[cfg(target_arch = "wasm32")] let mut last_time = web::now(); @@ -207,7 +211,7 @@ impl Harness { task_pool.run_until_stalled(); #[cfg(target_arch = "wasm32")] - web::bind_once(&mut app); + web::bind_once(Rc::clone(&app)); match event { event::Event::WindowEvent { @@ -239,7 +243,7 @@ impl Harness { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, }) .create_view(&wgpu::TextureViewDescriptor::default()); - app.resize(&device, extent); + app.borrow_mut().resize(&device, extent); } event::Event::WindowEvent { event, .. } => match event { event::WindowEvent::Focused(false) => { @@ -247,23 +251,23 @@ impl Harness { } event::WindowEvent::Focused(true) if needs_reload => { info!("Reloading shaders"); - app.reload(&device); + app.borrow_mut().reload(&device); needs_reload = false; } event::WindowEvent::CloseRequested => { *control_flow = ControlFlow::Exit; } event::WindowEvent::KeyboardInput { input, .. } => { - if !app.on_key(input) { + if !app.borrow_mut().on_key(input) { *control_flow = ControlFlow::Exit; } } - event::WindowEvent::MouseWheel { delta, .. } => app.on_mouse_wheel(delta), + event::WindowEvent::MouseWheel { delta, .. } => app.borrow_mut().on_mouse_wheel(delta), event::WindowEvent::CursorMoved { position, .. } => { - app.on_cursor_move(position.into()) + app.borrow_mut().on_cursor_move(position.into()) } event::WindowEvent::MouseInput { state, button, .. } => { - app.on_mouse_button(state, button) + app.borrow_mut().on_mouse_button(state, button) } _ => {} }, @@ -287,7 +291,7 @@ impl Harness { } if delta > 0.0 { - let update_command_buffers = app.update(&device, delta, &spawner); + let update_command_buffers = app.borrow_mut().update(&device, delta, &spawner); if !update_command_buffers.is_empty() { queue.submit(update_command_buffers); } @@ -303,7 +307,7 @@ impl Harness { color: &view, depth: &depth_target, }; - let render_command_buffer = app.draw(&device, targets, &spawner); + let render_command_buffer = app.borrow_mut().draw(&device, targets, &spawner); queue.submit(Some(render_command_buffer)); frame.present(); } diff --git a/bin/web.rs b/bin/web.rs index a2844de6..c20d5eee 100644 --- a/bin/web.rs +++ b/bin/web.rs @@ -1,3 +1,6 @@ +use std::cell::RefCell; +use std::rc::Rc; + use log::info; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; @@ -8,7 +11,7 @@ use crate::boilerplate::Application; static mut BINDED: bool = false; -pub fn bind_once(mut _application: &mut dyn Application) { +pub fn bind_once(application: Rc>) { use std::collections::HashMap; use winit::event::{ElementState, KeyboardInput, ModifiersState, VirtualKeyCode as Key}; @@ -20,10 +23,6 @@ pub fn bind_once(mut _application: &mut dyn Application) { BINDED = true; } - let application = unsafe { - std::mem::transmute::<&mut dyn Application, &'static mut dyn Application>(_application) - }; - let window = web_sys::window().expect("should have a window in this context"); { @@ -56,7 +55,7 @@ pub fn bind_once(mut _application: &mut dyn Application) { let key = event.key().to_ascii_lowercase(); match mappings.get(&key) { - Some(key) => application.on_key(KeyboardInput { + Some(key) => application.borrow_mut().on_key(KeyboardInput { state: if pressed { ElementState::Pressed } else {