When the script changes from under us it might be bugged for some reason; just let that be for now, ignore the load, and hopefully the engineer will fix it, eventually.
195 lines
5.6 KiB
Rust
195 lines
5.6 KiB
Rust
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<Mutex<RecommendedWatcher>>,
|
|
}
|
|
|
|
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<ModuleSource> {
|
|
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<graphics::GraphicsCommand>,
|
|
|
|
time: time::TimeAPI,
|
|
input: input::InputAPI,
|
|
}
|
|
|
|
impl ScriptContext {
|
|
pub fn new(suspend_state: Option<Vec<u8>>, reload_trigger: Sender<()>) -> Result<Self> {
|
|
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<Vec<u8>> {
|
|
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<graphics::GraphicsCommand> {
|
|
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
|
|
}
|
|
}
|