diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/buffer.rs | 46 | ||||
| -rw-r--r-- | src/file.rs | 154 | ||||
| -rw-r--r-- | src/input.rs | 27 | ||||
| -rw-r--r-- | src/lib.rs | 4 | ||||
| -rw-r--r-- | src/main.rs | 133 | ||||
| -rw-r--r-- | src/scene.rs | 43 | ||||
| -rw-r--r-- | src/script.rs | 168 |
7 files changed, 521 insertions, 54 deletions
diff --git a/src/buffer.rs b/src/buffer.rs index fa0feaa..9e91ae7 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -2,8 +2,8 @@ use std::io::Write; use crossterm::{ cursor::{Hide, MoveTo, Show}, - event::{self, Event}, - execute, + event::{self, KeyEvent}, + execute, queue, style::{ContentStyle, PrintStyledContent, StyledContent}, terminal::{ BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen, @@ -20,15 +20,31 @@ pub struct Cell { pub z: u8, } -#[derive(Debug)] -pub struct Buffer<'a, W: Write> { - stdout: &'a mut W, +pub struct Buffer { + stdout: Box<dyn Write>, pub cells: Vec<Cell>, pub window_size: WindowSize, } -impl<'a, W: Write> Buffer<'a, W> { - pub fn new(stdout: &'a mut W) -> std::io::Result<Self> { +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Event { + Key(KeyEvent), + Render, +} + +impl TryFrom<crossterm::event::Event> for Event { + type Error = (); + + fn try_from(value: crossterm::event::Event) -> Result<Self, Self::Error> { + match value { + crossterm::event::Event::Key(event) => Ok(Event::Key(event)), + _ => Err(()), + } + } +} + +impl Buffer { + pub fn new(stdout: Box<dyn Write>) -> std::io::Result<Self> { Ok(Self { stdout, cells: Vec::new(), @@ -44,15 +60,16 @@ impl<'a, W: Write> Buffer<'a, W> { fn render(&mut self) -> std::io::Result<()> { self.cells.sort_unstable_by_key(|cell| cell.z); - execute!(self.stdout, BeginSynchronizedUpdate, Clear(ClearType::All))?; + queue!(self.stdout, BeginSynchronizedUpdate, Clear(ClearType::All))?; for cell in &self.cells { - execute!( + queue!( self.stdout, MoveTo(cell.x, cell.y), PrintStyledContent(StyledContent::new(cell.style, cell.character)) )?; } - execute!(self.stdout, EndSynchronizedUpdate)?; + queue!(self.stdout, EndSynchronizedUpdate)?; + self.stdout.flush()?; Ok(()) } @@ -65,9 +82,8 @@ impl<'a, W: Write> Buffer<'a, W> { pub fn render_loop( &mut self, mut event_handler: impl FnMut(&mut Self, Event), - mut update: impl FnMut(&mut Self), ) -> std::io::Result<()> { - Self::initialize(self.stdout)?; + Self::initialize(&mut self.stdout)?; let (sender, receiver) = std::sync::mpsc::channel(); std::thread::scope(|scope| -> std::io::Result<()> { scope.spawn(|| { @@ -80,9 +96,11 @@ impl<'a, W: Write> Buffer<'a, W> { }); loop { while let Ok(event) = receiver.try_recv() { - event_handler(self, event); + if let Ok(event) = event.try_into() { + event_handler(self, event); + } } - update(self); + event_handler(self, Event::Render); self.render()?; } })?; diff --git a/src/file.rs b/src/file.rs new file mode 100644 index 0000000..8e96fa9 --- /dev/null +++ b/src/file.rs @@ -0,0 +1,154 @@ +use std::{ffi::OsStr, path::Path, sync::Arc}; + +use rustc_hash::FxHashMap; +use serde::{Deserialize, Serialize}; +use smol::{Task, block_on, unblock}; + +use crate::scene::{Scene, scene_id_by_name}; + +type FileTask = Task<std::io::Result<FileData>>; + +pub struct FileManager { + pub wasm_compiler: Arc<wasmi::Engine>, + active_tasks: FxHashMap<Box<Path>, FileTask>, + cache: FxHashMap<Box<Path>, FileData>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GameManifest { + pub name: Box<str>, + pub first_scene: usize, + pub scenes: Box<[Scene]>, +} + +#[derive(Debug, Serialize, Deserialize)] +struct TomlGameManifest { + name: Box<str>, + first_scene: Box<str>, + scene: Box<[Scene]>, +} + +pub enum FileData { + GameManifest(GameManifest), + WasmModule(wasmi::Module), +} + +fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameManifest> { + let first_scene = scene_id_by_name(&manifest.scene, &manifest.first_scene).ok_or( + std::io::Error::other("Invalid first scene: scene not found"), + )?; + Ok(GameManifest { + name: manifest.name, + first_scene, + scenes: manifest.scene, + }) +} + +fn compile_data(path: &Path, data: &[u8], compiler: &wasmi::Engine) -> std::io::Result<FileData> { + if path == "manifest.postcard" { + return Ok(FileData::GameManifest( + postcard::from_bytes(data).map_err(std::io::Error::other)?, + )); + } else if path == "manifest.toml" { + return Ok(FileData::GameManifest(normalize_game_manifest( + toml::from_slice(data).map_err(std::io::Error::other)?, + )?)); + } + let extension = path + .extension() + .and_then(OsStr::to_str) + .map(|ext| ext.to_lowercase()); + if extension.as_deref() == Some("wasm") { + return Ok(FileData::WasmModule( + wasmi::Module::new(compiler, data).map_err(std::io::Error::other)?, + )); + } + Err(std::io::Error::other("Unrecognized file extension")) +} + +impl FileData { + pub fn unwrap_manifest(&self) -> &GameManifest { + if let FileData::GameManifest(manifest) = self { + manifest + } else { + panic!("Not a game manifest"); + } + } + + pub fn unwrap_wasm_module(&self) -> &wasmi::Module { + if let FileData::WasmModule(module) = self { + module + } else { + panic!("Not a WASM module") + } + } +} + +impl FileManager { + pub fn new() -> Self { + Self { + wasm_compiler: Arc::new(wasmi::Engine::default()), + active_tasks: FxHashMap::default(), + cache: FxHashMap::default(), + } + } + + pub fn load_file(&mut self, path: Box<Path>) -> std::io::Result<&FileData> { + let data = compile_data( + path.as_ref(), + &std::fs::read(path.as_ref()).map(Vec::into_boxed_slice)?, + &self.wasm_compiler.clone(), + )?; + self.cache.entry(path.clone()).insert_entry(data); + Ok(self.cache.get(&path).unwrap()) + } + + pub fn get_loaded_file(&mut self, path: Box<Path>) -> Option<std::io::Result<&FileData>> { + if let Some(task) = self.active_tasks.get(path.as_ref()) + && task.is_finished() + { + let data = match block_on(self.active_tasks.remove(&path).unwrap()) { + Ok(data) => data, + Err(e) => return Some(Err(e)), + }; + self.cache.entry(path.clone()).insert_entry(data); + Some(Ok(self.cache.get(&path).unwrap())) + } else if let Some(data) = self.cache.get(&path) { + Some(Ok(data)) + } else { + None + } + } + + pub fn start_loading_file(&mut self, path: impl AsRef<Path>) { + let boxed_path = Box::from(path.as_ref()); + let compiler = self.wasm_compiler.clone(); + let task = unblock(move || { + compile_data( + &boxed_path, + &std::fs::read(&boxed_path).map(Vec::into_boxed_slice)?, + &compiler, + ) + }); + let boxed_path = Box::from(path.as_ref()); + self.active_tasks.insert(boxed_path, task); + } + + pub fn is_file_loaded(&self, path: impl AsRef<Path>) -> bool { + self.active_tasks + .get(path.as_ref()) + .is_some_and(|task| task.is_finished()) + || self.cache.contains_key(path.as_ref()) + } + + pub fn unload_file(&mut self, path: impl AsRef<Path>) { + self.active_tasks.remove(path.as_ref()); + self.cache.remove(path.as_ref()); + } +} + +impl Default for FileManager { + fn default() -> Self { + Self::new() + } +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..c86daf4 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,27 @@ +use crossterm::event::KeyCode; +use rustc_hash::FxHashSet; + +#[derive(Debug, Default, Clone)] +pub struct InputManager { + pressed_keys: FxHashSet<KeyCode>, +} + +impl InputManager { + pub fn new() -> Self { + Self { + pressed_keys: FxHashSet::default(), + } + } + + pub fn press_key(&mut self, key_code: KeyCode) { + self.pressed_keys.insert(key_code); + } + + pub fn reset_next_frame(&mut self) { + self.pressed_keys.clear(); + } + + pub fn is_key_pressed_this_frame(&self, key_code: KeyCode) -> bool { + self.pressed_keys.contains(&key_code) + } +} @@ -1 +1,5 @@ pub mod buffer; +pub mod file; +pub mod input; +pub mod scene; +pub mod script; diff --git a/src/main.rs b/src/main.rs index e8c9f23..0cbb7c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,48 +1,101 @@ -use std::time::Instant; +use std::time::SystemTime; -use crossterm::{ - event::{Event, KeyCode}, - style::ContentStyle, +use crossterm::{event::KeyCode, style::ContentStyle}; +use mimalloc::MiMalloc; +use tortuise::{ + buffer::{Buffer, Cell, Event}, + file::FileManager, + input::InputManager, + scene::switch_scenes, + script::{ScriptContext, ScriptDeclaration, ScriptManager}, }; -use tortuise::buffer::{Buffer, Cell}; + +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +fn get_timestamp() -> u128 { + SystemTime::UNIX_EPOCH.elapsed().unwrap().as_nanos() +} + +fn init_first_frame<'a>(_: &mut ScriptContext<'a>, state: &mut Box<[u8]>) { + *state = Box::from(get_timestamp().to_ne_bytes()); +} + +fn render_frame_rate<'a>(ctx: &mut ScriptContext<'a>, state: &mut Box<[u8]>) { + ctx.buffer.cells.clear(); + let end = get_timestamp(); + let last_frame_start = u128::from_ne_bytes(*state.as_array().unwrap()); + let frame_time = end - last_frame_start + 1; + let digits = [ + frame_time % 1_000_000 / 100_000, + frame_time % 100_000 / 10_000, + frame_time % 10_000 / 1_000, + frame_time % 1_000 / 100, + frame_time % 100 / 10, + frame_time % 10, + ]; + for (x, digit) in digits.into_iter().enumerate() { + ctx.buffer.cells.push(Cell { + character: char::from_digit(digit as u32, 10).unwrap(), + x: x as u16, + y: 0, + z: 0, + style: ContentStyle::new(), + }); + } + *state = Box::from(end.to_ne_bytes()); +} + +fn exit_on_escape(ctx: &mut ScriptContext, _: &mut Box<[u8]>) { + if ctx.input.is_key_pressed_this_frame(KeyCode::Esc) { + ctx.exit(); + } +} fn main() { - let mut stdout = std::io::stdout().lock(); - let mut buffer = Buffer::new(&mut stdout).unwrap(); - let mut last_frame_start = Instant::now(); + let stdout = Box::new(std::io::stdout().lock()); + let mut buffer = Buffer::new(stdout).unwrap(); + let mut input = InputManager::new(); + let mut context = ScriptContext { + buffer: &mut buffer, + input: &input, + }; + let scripts = [ + ScriptDeclaration::Static { + start: Some(init_first_frame), + update: Some(render_frame_rate), + end: None, + }, + ScriptDeclaration::Static { + start: None, + update: Some(exit_on_escape), + end: None, + }, + ]; + let mut scripts = ScriptManager::new(&scripts); + + let mut files = FileManager::default(); + let manifest = files + .load_file(Box::from("manifest.toml".as_ref())) + .unwrap() + .unwrap_manifest(); + switch_scenes(&manifest.scenes, &mut scripts, &mut context, 0); + buffer - .render_loop( - |renderer, event| { - if let Event::Key(event) = event - && event.code == KeyCode::Esc - { - let _ = renderer.finish(); - std::process::exit(0); - } - }, - |renderer| { - renderer.cells.clear(); - let end = Instant::now(); - let frame_time = (end - last_frame_start).as_micros(); - let digits = [ - frame_time % 1_000_000 / 100_000, - frame_time % 100_000 / 10_000, - frame_time % 10_000 / 1_000, - frame_time % 1_000 / 100, - frame_time % 100 / 10, - frame_time % 10, - ]; - for (x, digit) in digits.into_iter().enumerate() { - renderer.cells.push(Cell { - character: char::from_digit(digit as u32, 10).unwrap(), - x: x as u16, - y: 0, - z: 0, - style: ContentStyle::new(), - }); + .render_loop(|buffer, event| match event { + Event::Key(event) => { + if event.is_press() { + input.press_key(event.code); } - last_frame_start = Instant::now(); - }, - ) + } + Event::Render => { + let mut context = ScriptContext { + buffer, + input: &input, + }; + scripts.run_scripts(&mut context); + input.reset_next_frame(); + } + }) .unwrap(); } diff --git a/src/scene.rs b/src/scene.rs new file mode 100644 index 0000000..143eb0b --- /dev/null +++ b/src/scene.rs @@ -0,0 +1,43 @@ +use rustc_hash::FxHashSet; +use serde::{Deserialize, Serialize}; + +use crate::script::{ScriptContext, ScriptManager}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Scene { + pub name: Box<str>, + pub scripts: ResourceRequirements, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ResourceRequirements { + pub required: Box<[usize]>, + pub preload: Box<[usize]>, +} + +pub fn scene_id_by_name(scenes: &[Scene], name: &str) -> Option<usize> { + for (i, scene) in scenes.iter().enumerate() { + if &*scene.name == name { + return Some(i); + } + } + + None +} + +pub fn switch_scenes( + scenes: &[Scene], + scripts: &mut ScriptManager, + context: &mut ScriptContext<'_>, + new: usize, +) { + let active_scripts = FxHashSet::from_iter(scripts.active_scripts.iter().cloned()); + let scripts_to_load = &scenes[new].scripts.required; + for script in scripts_to_load { + if !active_scripts.contains(script) { + scripts.activate_script(*script, context); + } else { + scripts.reset_script(*script, context); + } + } +} diff --git a/src/script.rs b/src/script.rs new file mode 100644 index 0000000..dd65106 --- /dev/null +++ b/src/script.rs @@ -0,0 +1,168 @@ +use std::path::Path; + +use rustc_hash::FxHashMap; +use wasmer::{Imports, Instance, Module, Store}; + +use crate::{buffer::Buffer, file::FileManager, input::InputManager}; + +pub struct ScriptManager<'a> { + scripts: &'a [ScriptDeclaration], + pub active_scripts: Vec<usize>, + script_states: FxHashMap<usize, Box<[u8]>>, +} + +pub struct ScriptContext<'a> { + pub buffer: &'a mut Buffer, + pub input: &'a InputManager, +} + +type StaticScriptFunction = Option<fn(&mut ScriptContext, &mut Box<[u8]>)>; + +pub enum ScriptDeclaration { + Static { + start: StaticScriptFunction, + update: StaticScriptFunction, + end: StaticScriptFunction, + }, + Wasm { + path: Box<Path>, + }, +} + +pub struct WasmScript { + pub module: Module, + pub instance: Instance, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Profile { + Debug, + Release, +} + +impl<'a> ScriptContext<'a> { + pub fn exit(&mut self) -> ! { + let _ = self.buffer.finish(); + std::process::exit(0); + } +} + +fn get_script_state(states: &mut FxHashMap<usize, Box<[u8]>>, script_id: usize) -> &mut Box<[u8]> { + states.entry(script_id).or_insert(Box::from([])) +} + +impl<'a> ScriptManager<'a> { + pub fn new(scripts: &'a [ScriptDeclaration]) -> Self { + Self { + scripts, + active_scripts: Vec::new(), + script_states: FxHashMap::default(), + } + } + + pub fn activate_script( + &mut self, + script_id: usize, + context: &mut ScriptContext<'_>, + files: &mut FileManager, + ) { + let Some(script_declaration) = self.scripts.get(script_id) else { + unreachable!("The active script does not exist"); + }; + match script_declaration { + ScriptDeclaration::Static { start, .. } => { + start.map(|s| { + s( + context, + get_script_state(&mut self.script_states, script_id), + ) + }); + } + ScriptDeclaration::Wasm { path } => { + let module = files.load_file(path.clone()).unwrap().unwrap_wasm_module(); + let mut store = wasmi::Store::new(&files.wasm_compiler, context); + let instance = self + .linker + .instantiate_and_start(&mut store, &module) + .unwrap(); + } + } + self.active_scripts.push(script_id); + } + + pub fn deactivate_script(&mut self, script_id: usize, context: &mut ScriptContext<'_>) { + self.active_scripts.retain(|element| { + if *element != script_id { + let Some(script) = self.scripts.get(script_id) else { + unreachable!("The active script does not exist"); + }; + match script { + ScriptDeclaration::Static { end, .. } => { + end.map(|s| { + s( + context, + get_script_state(&mut self.script_states, script_id), + ); + }); + } + ScriptDeclaration::Wasm { .. } => todo!("wasm scripts"), + }; + + true + } else { + false + } + }); + } + + pub fn run_scripts(&mut self, context: &mut ScriptContext<'_>) { + for script_id in &self.active_scripts { + let Some(script) = self.scripts.get(*script_id) else { + unreachable!("The active script does not exist"); + }; + match *script { + ScriptDeclaration::Static { update, .. } => { + if let Some(s) = update { + s( + context, + get_script_state(&mut self.script_states, *script_id), + ) + } + } + ScriptDeclaration::Wasm { .. } => todo!("wasm scripts"), + } + } + } + + pub fn reset_script(&mut self, script_id: usize, context: &mut ScriptContext<'_>) { + let script = &self.scripts[script_id]; + match script { + ScriptDeclaration::Static { start, end, .. } => { + if let Some(s) = start { + s( + context, + get_script_state(&mut self.script_states, script_id), + ) + } + if let Some(s) = end { + s( + context, + get_script_state(&mut self.script_states, script_id), + ) + } + } + ScriptDeclaration::Wasm { .. } => todo!("wasm scripts"), + } + } +} + +impl WasmScript { + fn call_function(&self, name: &str, profile: Profile, store: &mut Store, imports: &Imports) { + let instance = match profile { + Profile::Debug => &Instance::new(store, &self.module, imports).unwrap(), + Profile::Release => &self.instance, + }; + let function = instance.exports.get_function(name).unwrap(); + function.call(store, &[]).unwrap(); + } +} |
