use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use oden_js::{ module::loader::{ModuleLoader, ModuleSource}, Atom, Context, ContextRef, RejectedPromiseTracker, Result, Runtime, Value, }; use sourcemap::SourceMap; use std::collections::HashMap; use std::ffi::OsStr; use std::sync::mpsc::{channel, Receiver, Sender}; 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: RecommendedWatcher, source_map: HashMap, } impl Loader { pub fn new(reload_trigger: Sender<()>) -> Loader { Loader { watcher: notify::recommended_watcher(move |_| { let _ = reload_trigger.send(()); }) .expect("Unable to create watcher"), source_map: HashMap::new(), } } } impl ModuleLoader for Loader { fn load<'a>(&mut self, context: &'a 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") { let (c, sm) = transpile_to_javascript(name, contents)?; let a = context.new_atom(name)?; self.source_map.insert(a, sm); c } else { contents }; let _ = self.watcher.watch(&path, RecursiveMode::NonRecursive); Ok(ModuleSource::JavaScript(contents)) } fn support_source_map(&self) -> bool { true } fn map_source( &self, context: &ContextRef, file: &oden_js::AtomRef, line: i32, ) -> Result<(Atom, i32)> { let file = file.dup(&context); // Yuck. if let Ok(line) = line.try_into() { if let Some(sm) = self.source_map.get(&file) { if let Some(token) = sm.lookup_token(line, 0) { if let Ok(src_line) = token.get_src_line().try_into() { return Ok((file, src_line)); } } } } eprintln!("Not mapped"); Ok((file, line)) } } 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(); } }