286 lines
8.7 KiB
Rust
286 lines
8.7 KiB
Rust
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<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))
|
|
}
|
|
}
|
|
|
|
struct RejectedPromiseHandler {
|
|
error_lines: Sender<String>,
|
|
}
|
|
|
|
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<graphics::GraphicsCommand>,
|
|
|
|
time: time::TimeAPI,
|
|
input: input::InputAPI,
|
|
|
|
error_lines: Vec<String>,
|
|
promise_error_reciever: Receiver<String>,
|
|
}
|
|
|
|
impl ScriptContext {
|
|
pub fn new(suspend_state: Option<Vec<u8>>, reload_trigger: Sender<()>) -> Result<Self> {
|
|
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<Vec<u8>> {
|
|
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<graphics::GraphicsCommand> {
|
|
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<T>(&mut self, result: Result<T>) {
|
|
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();
|
|
}
|
|
}
|