use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use oden_js::{ module::loader::{ModuleLoader, ModuleSource}, Context, ContextRef, RejectedPromiseTracker, 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::{ClearCommand, GraphicsCommand, PrintCommand}; 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)) } } struct RejectedPromiseHandler { error_lines: Sender, } impl RejectedPromiseTracker for RejectedPromiseHandler { fn on_rejected_promise( &self, ctx: &ContextRef, _promise: &oden_js::ValueRef, reason: &oden_js::ValueRef, is_handled: bool, ) { if !is_handled { let reason_str = reason.to_string(ctx).expect( "Unhandled rejected promise: reason unknown: unable to convert reason to string", ); let reason_str = format!("Unhandled rejected promise:\n{reason_str}"); for line in reason_str.lines().map(|l| l.to_owned()) { let _ = self.error_lines.send(line); } let stack = reason .get_property(ctx, "stack") .and_then(|stack| stack.to_string(ctx)) .unwrap_or_else(|_| String::new()); for line in stack.lines().map(|l| l.to_owned()) { let _ = self.error_lines.send(line); } } } } 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, error_lines: Vec, promise_error_reciever: Receiver, } impl ScriptContext { pub fn new(suspend_state: Option>, reload_trigger: Sender<()>) -> Result { let (promise_error_send, promise_error_recv) = channel(); let mut runtime = Runtime::new(); runtime.set_module_loader(Loader::new(reload_trigger)); runtime.set_rejected_promise_tracker(RejectedPromiseHandler { error_lines: promise_error_send, }); 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, error_lines: Vec::new(), promise_error_reciever: promise_error_recv, }) } /// 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() { match self.suspend.call(&self.context, &[&self.state]) { Ok(suspend_state) => Some( suspend_state .serialize(&self.context) .expect("Unable to serialize state"), ), Err(e) => { eprintln!("WARNING: Error during suspend, state will not be saved: {e}"); None } } } 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, dimensions: (f32, f32)) { let _span = span!("script update"); self.gfx.set_dimensions(dimensions); if self.error_lines.len() > 0 { return; // Don't bother, nothing. } // 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.handle_result(self.context.process_all_jobs()); // Check to make sure we don't have any rejected promises. while let Ok(line) = self.promise_error_reciever.try_recv() { self.error_lines.push(line); } } // Now run the update function. if self.error_lines.len() == 0 { let _span = span!("javascript update"); let old_state = &self.state; self.state = match self.update.call(&self.context, &[old_state]) { Ok(v) => v, Err(e) => { self.handle_error(e); self.context.null() } } } } pub fn render(&mut self) -> Vec { let _span = span!("script render"); if self.error_lines.len() > 0 { // TODO: Use font 0 for a fallback. // TODO: Scale!! Remember you're using a font at size 8 or something. let mut commands = vec![ GraphicsCommand::Clear(ClearCommand { color: [0.0, 0.0, 1.0, 1.0], }), GraphicsCommand::Scale([4.0, 4.0]), GraphicsCommand::Print(PrintCommand { text: "FATAL SCRIPT ERROR".to_owned(), pos: [8.0, 8.0], }), ]; let mut y = 20.0; for line in &self.error_lines { commands.push(GraphicsCommand::Print(PrintCommand { text: line.clone(), pos: [6.0, y], })); y += 8.0; } commands } else { self.handle_result(self.draw.call(&self.context, &[&self.state])); self.gfx.end_frame(); let mut commands = Vec::new(); loop { match self.gfx_receive.recv().unwrap() { GraphicsCommand::EndFrame => break, other => commands.push(other), } } commands } } fn handle_result(&mut self, result: Result) { if let Err(e) = result { self.handle_error(e); } } fn handle_error(&mut self, error: oden_js::Error) { self.error_lines = error.to_string().lines().map(|l| l.to_owned()).collect(); } }