use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use oden_js::{ module::loader::{ModuleLoader, ModuleSource}, Context, ContextRef, Result, Runtime, Value, }; use std::ffi::OsStr; use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::{Arc, Mutex}; use std::time::Instant; use tracy_client::span; use winit::event::*; pub mod graphics; mod input; mod io; mod time; use graphics::GraphicsCommand; mod typescript; use typescript::transpile_to_javascript; struct Loader { watcher: Arc>, } impl Loader { pub fn new(reload_trigger: Sender<()>) -> Loader { let watcher = Arc::new(Mutex::new( notify::recommended_watcher(move |_| { let _ = reload_trigger.send(()); }) .expect("Unable to create watcher"), )); Loader { watcher } } } impl ModuleLoader for Loader { fn load(&self, _context: &ContextRef, name: &str) -> Result { eprintln!("Loading {name}..."); let path = io::resolve_path(name, &["ts"])?; let contents = std::fs::read_to_string(&path)?; let contents = if path.extension().and_then(OsStr::to_str) == Some("ts") { transpile_to_javascript(name, contents)? } else { contents }; let mut watcher = self.watcher.lock().unwrap(); let _ = watcher.watch(&path, RecursiveMode::NonRecursive); Ok(ModuleSource::JavaScript(contents)) } } pub struct ScriptContext { context: Context, update: Value, draw: Value, suspend: Value, state: Value, gfx: graphics::GraphicsAPI, gfx_receive: Receiver, time: time::TimeAPI, input: input::InputAPI, } impl ScriptContext { pub fn new(suspend_state: Option>, reload_trigger: Sender<()>) -> Result { let mut runtime = Runtime::new(); runtime.set_module_loader(Loader::new(reload_trigger)); let mut context = Context::new(runtime); context.add_intrinsic_bigfloat(); context.add_intrinsic_bigdecimal(); context.add_intrinsic_operators(); let (gfx_send, gfx_receive) = channel(); let gfx = graphics::GraphicsAPI::define(&context, gfx_send.clone())?; let _io = io::IoAPI::define(&context)?; let time = time::TimeAPI::define(&context)?; let input = input::InputAPI::define(&context)?; let module = context.import_module("./main.ts", "")?; let init = module.get_export(&context, "init")?; let suspend = module.get_export(&context, "suspend")?; let resume = module.get_export(&context, "resume")?; let update = module.get_export(&context, "update")?; let draw = module.get_export(&context, "draw")?; let mut state = init.call(&context, &[])?; if let Some(buffer) = suspend_state { if !resume.is_undefined() { let serialized_state = context.deserialize(&buffer)?; state = resume.call(&context, &[&serialized_state, &state])?; } } Ok(ScriptContext { context, suspend, update, draw, state, gfx, gfx_receive, time, input, }) } /// Allow the script to save its state before we destroy it and re-create /// it. pub fn suspend(&self) -> Option> { let _span = span!("script suspend"); if !self.suspend.is_undefined() { let suspend_state = self .suspend .call(&self.context, &[&self.state]) .expect("Exception in suspend"); Some( suspend_state .serialize(&self.context) .expect("Unable to serialize state"), ) } else { None } } // TODO: The script could really be on a background thread you know. // We would want a bi-directional gate for frames to not let the // game thread go to fast probably? And to discard whole frames &c. pub fn input(&mut self, event: &WindowEvent) -> bool { match event { WindowEvent::KeyboardInput { input, .. } => self.input.handle_keyboard_input(input), _ => false, } } pub fn update(&mut self) { let _span = span!("script update"); // Do we update the frame time before of after async completion? // Hmmmmm. self.time.set_frame_time(Instant::now()); // Tell the runtime to process all pending "jobs". This includes // promise completions. { let _span = span!("process jobs"); self.context .process_all_jobs() .expect("Error processing async jobs"); } // Now run the update function. { let _span = span!("javascript update"); let old_state = &self.state; self.state = self .update .call(&self.context, &[old_state]) .expect("Exception in update"); } } pub fn render(&mut self) -> Vec { let _span = span!("script render"); self.draw .call(&self.context, &[&self.state]) .expect("Exception in draw"); self.gfx.end_frame(); let mut commands = Vec::new(); loop { match self.gfx_receive.recv().unwrap() { GraphicsCommand::EndFrame => break, other => commands.push(other), } } commands } }