diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/buffer.rs | 63 | ||||
| -rw-r--r-- | src/file.rs | 255 | ||||
| -rw-r--r-- | src/input.rs | 220 | ||||
| -rw-r--r-- | src/lib.rs | 156 | ||||
| -rw-r--r-- | src/main.rs | 161 | ||||
| -rw-r--r-- | src/scene.rs | 58 | ||||
| -rw-r--r-- | src/script.rs | 230 | ||||
| -rw-r--r-- | src/script/lua.rs | 67 | ||||
| -rw-r--r-- | src/script/wasm.rs | 380 | ||||
| -rw-r--r-- | src/storage.rs | 106 |
10 files changed, 1051 insertions, 645 deletions
diff --git a/src/buffer.rs b/src/buffer.rs index 6d983db..dcb7b8b 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,8 +1,11 @@ -use std::io::Write; +use std::{io::Write, time::Duration}; use crossterm::{ cursor::{Hide, MoveTo, Show}, - event::{self, KeyEvent}, + event::{ + self, DisableFocusChange, DisableMouseCapture, EnableFocusChange, EnableMouseCapture, + KeyEvent, MouseEvent, + }, execute, queue, style::{ContentStyle, PrintStyledContent, StyledContent}, terminal::{ @@ -29,6 +32,8 @@ pub struct Buffer { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Event { Key(KeyEvent), + Mouse(MouseEvent), + FocusChanged(bool), Render, } @@ -38,6 +43,9 @@ impl TryFrom<crossterm::event::Event> for Event { fn try_from(value: crossterm::event::Event) -> Result<Self, Self::Error> { match value { crossterm::event::Event::Key(event) => Ok(Event::Key(event)), + crossterm::event::Event::Mouse(event) => Ok(Event::Mouse(event)), + crossterm::event::Event::FocusGained => Ok(Event::FocusChanged(true)), + crossterm::event::Event::FocusLost => Ok(Event::FocusChanged(false)), _ => Err(()), } } @@ -54,7 +62,13 @@ impl Buffer { fn initialize(writer: &mut impl Write) -> std::io::Result<()> { enable_raw_mode()?; - execute!(writer, Hide, EnterAlternateScreen)?; + queue!( + writer, + Hide, + EnterAlternateScreen, + EnableMouseCapture, + EnableFocusChange + )?; Ok(()) } @@ -82,7 +96,13 @@ impl Buffer { } pub fn finish(&mut self) -> std::io::Result<()> { - execute!(self.stdout, Show, LeaveAlternateScreen)?; + execute!( + self.stdout, + Show, + DisableMouseCapture, + DisableFocusChange, + LeaveAlternateScreen + )?; disable_raw_mode()?; Ok(()) } @@ -92,29 +112,18 @@ impl Buffer { mut event_handler: impl FnMut(&mut Self, Event), ) -> std::io::Result<()> { Self::initialize(&mut self.stdout)?; - let (sender, receiver) = std::sync::mpsc::channel(); - std::thread::scope(|scope| -> std::io::Result<()> { - scope.spawn(|| { - loop { - let Ok(event) = event::read() else { - continue; - }; - let _ = sender.send(event); - } - }); - loop { - puffin::profile_scope!("frame"); - while let Ok(event) = receiver.try_recv() { - if let Ok(event) = event.try_into() { - event_handler(self, event); - } - } - event_handler(self, Event::Render); - self.render()?; - puffin::GlobalProfiler::lock().new_frame(); + loop { + puffin::profile_scope!("frame"); + while matches!(event::poll(Duration::ZERO), Ok(true)) { + let Ok(event) = event::read() else { continue }; + let Ok(event) = event.try_into() else { + continue; + }; + event_handler(self, event); } - })?; - self.finish()?; - Ok(()) + event_handler(self, Event::Render); + self.render()?; + puffin::GlobalProfiler::lock().new_frame(); + } } } diff --git a/src/file.rs b/src/file.rs index 4aedd48..b8f6610 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,178 +1,96 @@ -use std::{ffi::OsStr, path::Path}; +use std::{ + ffi::OsStr, + path::Path, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + mpsc::{Receiver, Sender}, + }, + time::{Duration, Instant, SystemTime}, +}; use rustc_hash::FxHashMap; -use serde::{Deserialize, Serialize}; -use smol::{Task, unblock}; +use smol::{Task, block_on, unblock}; -use crate::{ - scene::{ResourceRequirements, Scene, scene_id_by_name}, - script::{ScriptDeclaration, WasmRunner, lua::LuaModule, wasm::WasmModule}, -}; +use crate::script::{WasmRunner, wasm::WasmModule}; -type FileTask = Task<std::io::Result<FileData>>; +type FileTask = Task<std::io::Result<WasmModule>>; pub struct FileManager { + watcher: Notifier, + updated_files: Receiver<Box<Path>>, active_tasks: FxHashMap<Box<Path>, FileTask>, - cache: FxHashMap<Box<Path>, FileData>, + cache: FxHashMap<Box<Path>, Arc<std::io::Result<WasmModule>>>, } -#[derive(Debug, Clone)] -pub struct GameManifest { - pub name: Box<str>, - pub first_scene: usize, - pub scenes: Box<[Scene]>, - pub scripts: Box<[ScriptDeclaration]>, +struct Notifier { + paths: Arc<spin::Mutex<Vec<Box<Path>>>>, + cancel_token: Arc<AtomicBool>, } -#[derive(Debug, Serialize, Deserialize)] -pub struct TomlGameManifest { - name: Box<str>, - first_scene: Box<str>, - #[serde(default)] - scene: Box<[TomlScene]>, - #[serde(default)] - script: Box<[TomlScript]>, -} - -#[derive(Debug, Serialize, Deserialize)] -struct TomlScene { - name: Box<str>, - #[serde(default)] - preload_scenes: Box<[Box<str>]>, - #[serde(default)] - scripts: Box<[Box<str>]>, - #[serde(default)] - preload_scripts: Box<[Box<str>]>, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "kebab-case")] -enum TomlScript { - Wasm { name: Box<str>, path: Box<Path> }, - Lua { name: Box<str>, path: Box<Path> }, -} - -pub enum FileData { - GameManifest(GameManifest), - WasmModule(Box<WasmModule>), - LuaModule(Box<LuaModule>), -} - -pub fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameManifest> { - let mut scripts = Vec::with_capacity(manifest.script.len()); - let script_map: FxHashMap<Box<str>, usize> = manifest - .script - .into_iter() - .map(|script| match script { - TomlScript::Wasm { name, path } => { - scripts.push(ScriptDeclaration::Wasm { - name: name.clone(), - path, - }); - (name, scripts.len() - 1) - } - TomlScript::Lua { name, path } => { - scripts.push(ScriptDeclaration::Lua { - name: name.clone(), - path, - }); - (name, scripts.len() - 1) - } - }) - .collect(); - let scenes: Box<[Scene]> = manifest - .scene - .into_iter() - .map(|scene| Scene { - name: scene.name, - preload_scenes: Box::from([]), - scripts: ResourceRequirements { - required: scene - .scripts - .iter() - .map(|s| *script_map.get(s).unwrap()) - .collect(), - preload: scene - .preload_scripts - .iter() - .map(|s| *script_map.get(s).unwrap()) - .collect(), - }, - }) - .collect(); - let first_scene = scene_id_by_name(&scenes, &manifest.first_scene).ok_or( - std::io::Error::other("Invalid first scene: scene not found"), - )?; - Ok(GameManifest { - name: manifest.name, - first_scene, - scenes, - scripts: scripts.into_boxed_slice(), - }) -} - -fn compile_data(path: &Path, data: &[u8], compiler: &WasmRunner) -> std::io::Result<FileData> { - if path == "tortuise.toml" { - return Ok(FileData::GameManifest(normalize_game_manifest( - toml::from_slice(data).map_err(std::io::Error::other)?, - )?)); - } +fn compile_data(path: &Path, data: &[u8], compiler: &WasmRunner) -> std::io::Result<WasmModule> { let extension = path .extension() .and_then(OsStr::to_str) .map(|ext| ext.to_lowercase()); if extension.as_deref() == Some("wasm") { - return Ok(FileData::WasmModule(Box::new( - compiler.compile(data).map_err(std::io::Error::other)?, - ))); - } - if extension.as_deref() == Some("lua") { - return Ok(FileData::LuaModule(Box::new( - LuaModule::new(data).map_err(std::io::Error::other)?, - ))); + return compiler.compile(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"); - } - } +impl Notifier { + pub fn new(channel: Sender<Box<Path>>, interval: Duration) -> Self { + let paths = Arc::new(spin::Mutex::new(Vec::new())); + let paths_2 = paths.clone(); + let cancel_token = Arc::new(AtomicBool::new(false)); + let cancel_token_2 = cancel_token.clone(); + std::thread::spawn(move || { + loop { + let last_check = SystemTime::now(); - pub fn unwrap_wasm_module(&self) -> &WasmModule { - if let FileData::WasmModule(module) = self { - module - } else { - panic!("Not a WASM module") - } - } + std::thread::sleep(interval); + if cancel_token_2.load(Ordering::Acquire) { + break; + } + let paths = paths_2.lock().clone(); + for path in paths { + if let Ok(metadata) = std::fs::metadata(&path) + && let Ok(mtime) = metadata.modified() + && mtime > last_check + && let Err(e) = channel.send(path) + { + log::error!("{e}"); + } + } + } + }); - pub fn unwrap_lua_module(&self) -> &LuaModule { - if let FileData::LuaModule(module) = self { - module - } else { - panic!("Not a Lua module") + Self { + paths, + cancel_token, } } - pub fn unwrap_wasm_module_mut(&mut self) -> &mut WasmModule { - if let FileData::WasmModule(module) = self { - module - } else { - panic!("Not a WASM module") + pub fn watch(&mut self, path: Box<Path>) { + let mut paths = self.paths.lock(); + if paths.contains(&path) { + return; } + + paths.push(path); } } impl FileManager { pub fn new() -> Self { + let (sender, updated_files) = std::sync::mpsc::channel(); + let watcher = Notifier::new(sender, Duration::from_secs(10)); Self { active_tasks: FxHashMap::default(), cache: FxHashMap::default(), + watcher, + updated_files, } } @@ -180,28 +98,30 @@ impl FileManager { &mut self, path: Box<Path>, wasm_compiler: &WasmRunner, - ) -> std::io::Result<&FileData> { - if !self.cache.contains_key(&path) { - let data = compile_data( - path.as_ref(), - &std::fs::read(path.as_ref()).map(Vec::into_boxed_slice)?, - wasm_compiler, - )?; - self.cache.entry(path.clone()).insert_entry(data); + ) -> Arc<std::io::Result<WasmModule>> { + if let Some(task) = self.active_tasks.get(&path) + && task.is_finished() + { + log::info!("loaded {}", path.to_string_lossy()); + let task = self.active_tasks.remove(&path).unwrap(); + let data = block_on(task); + self.cache.insert(path.clone(), Arc::new(data)); + } else if !self.cache.contains_key(&path) { + let read_time = Instant::now(); + let data = std::fs::read(path.as_ref()).map(Vec::into_boxed_slice); + log::info!("Load time: {} ms", read_time.elapsed().as_millis()); + let compile_time = Instant::now(); + let data = data.and_then(|data| compile_data(path.as_ref(), &data, wasm_compiler)); + log::info!("Compile time: {} ms", compile_time.elapsed().as_millis()); + self.cache.entry(path.clone()).insert_entry(Arc::new(data)); } - Ok(self.cache.get(&path).unwrap()) - } - pub fn get_cached_file(&self, path: &Path) -> Option<&FileData> { - self.cache.get(path) + self.watcher.watch(path.clone()); + self.cache.get(&path).unwrap().clone() } - pub fn get_cached_file_mut(&mut self, path: &Path) -> Option<&mut FileData> { - self.cache.get_mut(path) - } - - pub fn start_loading_file(&mut self, path: impl AsRef<Path>, wasm_compiler: &WasmRunner) { - let boxed_path = Box::from(path.as_ref()); + pub fn start_loading_file(&mut self, path: Box<Path>, wasm_compiler: &WasmRunner) { + let boxed_path = path.clone(); let compiler = wasm_compiler.clone(); let task = unblock(move || { compile_data( @@ -210,8 +130,15 @@ impl FileManager { &compiler, ) }); - let boxed_path = Box::from(path.as_ref()); - self.active_tasks.insert(boxed_path, task); + self.watcher.watch(path.clone()); + self.active_tasks.insert(path, task); + } + + pub fn reload_files(&mut self, wasm_compiler: &WasmRunner) { + while let Ok(path) = self.updated_files.try_recv() { + log::info!("reloading {}", path.to_string_lossy()); + self.start_loading_file(path, wasm_compiler); + } } pub fn loaded_files(&self) -> Box<[Box<Path>]> { @@ -241,3 +168,9 @@ impl Default for FileManager { Self::new() } } + +impl Drop for Notifier { + fn drop(&mut self) { + self.cancel_token.store(true, Ordering::Release); + } +} diff --git a/src/input.rs b/src/input.rs index 90ddbb2..3eea33c 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,15 +1,65 @@ +use std::{collections::VecDeque, fmt::Debug, time::Duration}; + use crossterm::event::KeyCode; -use rustc_hash::FxHashSet; +use gilrs::{ + Axis, Button, GamepadId, Gilrs, PowerInfo, + ff::{BaseEffect, Replay, Ticks}, +}; +use rustc_hash::{FxHashMap, FxHashSet}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Direction { + Up = 1, + Down, + Left, + Right, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Position { + pub x: u16, + pub y: u16, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +struct ControllerButton { + gamepad_id: usize, + button: Button, +} -#[derive(Debug, Default, Clone)] +#[derive(Debug)] pub struct InputManager { pressed_keys: FxHashSet<KeyCode>, + mouse_clicked: bool, + mouse_location: Option<Position>, + scroll_direction: Option<Direction>, + is_focused: bool, + is_focused_this_frame: bool, + is_unfocused_this_frame: bool, + gil_cx: Option<Gilrs>, + gamepad_map: FxHashMap<usize, GamepadId>, + gamepad_map_rev: FxHashMap<GamepadId, usize>, + connected_gamepads: VecDeque<usize>, + pressed_buttons: FxHashSet<ControllerButton>, + released_buttons: FxHashSet<ControllerButton>, } impl InputManager { pub fn new() -> Self { Self { pressed_keys: FxHashSet::default(), + mouse_clicked: false, + mouse_location: None, + scroll_direction: None, + is_focused: true, + is_focused_this_frame: false, + is_unfocused_this_frame: false, + gil_cx: Gilrs::new().ok(), + gamepad_map: FxHashMap::default(), + gamepad_map_rev: FxHashMap::default(), + connected_gamepads: VecDeque::new(), + pressed_buttons: FxHashSet::default(), + released_buttons: FxHashSet::default(), } } @@ -17,12 +67,178 @@ impl InputManager { self.pressed_keys.insert(key_code); } + pub fn click_mouse(&mut self) { + self.mouse_clicked = true; + } + + pub fn move_mouse(&mut self, location: Position) { + self.mouse_location = Some(location); + } + + pub fn scroll(&mut self, direction: Direction) { + self.scroll_direction = Some(direction); + } + pub fn reset_next_frame(&mut self) { puffin::profile_function!(); self.pressed_keys.clear(); + self.mouse_clicked = false; + self.scroll_direction = None; + self.is_focused_this_frame = false; + self.is_unfocused_this_frame = false; + self.pressed_buttons.clear(); + self.released_buttons.clear(); + self.connected_gamepads.pop_front(); } pub fn is_key_pressed_this_frame(&self, key_code: KeyCode) -> bool { self.pressed_keys.contains(&key_code) } + + pub fn is_mouse_clicked_this_frame(&self) -> bool { + self.mouse_clicked + } + + pub fn mouse_location(&self) -> Option<Position> { + self.mouse_location + } + + pub fn scroll_direction_this_frame(&self) -> Option<Direction> { + self.scroll_direction + } + + pub fn read_gil_events(&mut self) { + let Some(gil) = self.gil_cx.as_mut() else { + return; + }; + while let Some(event) = gil.next_event() { + let gamepad_id = self.gamepad_map_rev.get(&event.id).copied(); + match event.event { + gilrs::EventType::Connected => { + let gamepad_id = self.gamepad_map.len() + 1; + self.gamepad_map.insert(gamepad_id, event.id); + self.gamepad_map_rev.insert(event.id, gamepad_id); + self.connected_gamepads.push_back(gamepad_id); + } + gilrs::EventType::ButtonPressed(button, _) => { + let Some(gamepad_id) = gamepad_id else { + continue; + }; + self.pressed_buttons + .insert(ControllerButton { gamepad_id, button }); + } + gilrs::EventType::ButtonReleased(button, _) => { + let Some(gamepad_id) = gamepad_id else { + continue; + }; + self.released_buttons + .insert(ControllerButton { gamepad_id, button }); + } + _ => {} + } + } + } + + pub fn is_gamepad_connected_this_frame(&self) -> Option<usize> { + self.connected_gamepads.front().copied() + } + + pub fn is_button_pressed_this_frame(&self, gamepad_id: usize, button: Button) -> bool { + self.pressed_buttons + .contains(&ControllerButton { gamepad_id, button }) + } + + pub fn is_button_released_this_frame(&self, gamepad_id: usize, button: Button) -> bool { + self.released_buttons + .contains(&ControllerButton { gamepad_id, button }) + } + + pub fn is_button_down(&self, gamepad_id: usize, button: Button) -> bool { + self.gamepad_map.get(&gamepad_id).is_some_and(|id| { + self.gil_cx + .as_ref() + .is_some_and(|gil| gil.gamepad(*id).is_pressed(button)) + }) + } + + pub fn axis_value(&self, gamepad_id: usize, axis: Axis) -> Option<f32> { + self.gamepad_map + .get(&gamepad_id) + .and_then(|id| self.gil_cx.as_ref().map(|gil| gil.gamepad(*id).value(axis))) + } + + pub fn is_gamepad_connected(&self, gamepad_id: usize) -> bool { + self.gamepad_map.get(&gamepad_id).is_some_and(|id| { + self.gil_cx + .as_ref() + .is_some_and(|gil| gil.gamepad(*id).is_connected()) + }) + } + + pub fn gamepad_power_info(&self, gamepad_id: usize) -> Option<PowerInfo> { + self.gamepad_map.get(&gamepad_id).and_then(|id| { + self.gil_cx + .as_ref() + .map(|gil| gil.gamepad(*id).power_info()) + }) + } + + pub fn vibrate(&mut self, gamepad_id: usize, intensity: f32, duration: Duration) { + let Some(gamepad_id) = self.gamepad_map.get(&gamepad_id) else { + return; + }; + let Some(gil) = self.gil_cx.as_mut() else { + return; + }; + let gamepad = gil.gamepad(*gamepad_id); + let Ok(effect) = gilrs::ff::EffectBuilder::new() + .add_gamepad(&gamepad) + .add_effect(BaseEffect { + kind: gilrs::ff::BaseEffectType::Strong { + magnitude: (intensity * u16::MAX as f32).round() as u16, + }, + scheduling: Replay { + after: Ticks::from_ms(0), + play_for: Ticks::from_ms(duration.as_millis() as u32), + with_delay: Ticks::from_ms(0), + }, + envelope: todo!(), + }) + .repeat(gilrs::ff::Repeat::For(Ticks::from_ms( + duration.as_millis() as u32 + ))) + .finish(gil) + else { + return; + }; + _ = effect.play(); + } + + pub fn focus(&mut self) { + self.is_focused = true; + self.is_focused_this_frame = true; + } + + pub fn unfocus(&mut self) { + self.is_focused = false; + self.is_unfocused_this_frame = true; + } + + pub fn is_focused(&self) -> bool { + self.is_focused + } + + pub fn was_focused_this_frame(&self) -> bool { + self.is_focused_this_frame + } + + pub fn was_unfocused_this_frame(&self) -> bool { + self.is_unfocused_this_frame + } +} + +impl Default for InputManager { + fn default() -> Self { + Self::new() + } } @@ -1,7 +1,163 @@ pub mod buffer; pub mod file; pub mod input; +pub mod manifest; pub mod scene; pub mod script; pub mod storage; pub mod time; + +use crate::{ + buffer::{Buffer, Event}, + file::FileManager, + input::{Direction, InputManager, Position}, + manifest::GameManifest, + scene::switch_scenes, + script::{ScriptContext, ScriptManager, WasmRunner}, + storage::StorageManager, + time::TimeContext, +}; +use crossterm::event::MouseEventKind; +use etcetera::AppStrategy; +use prologger::{ColorMode, ProLogger, RotationConfig}; +use std::{ + fs::File, + io::{BufRead, BufWriter}, + path::PathBuf, + time::{Duration, Instant}, +}; + +pub fn apply_patches( + manifest: &mut GameManifest, + files: &mut FileManager, + wasm_compiler: &WasmRunner, +) -> std::io::Result<()> { + let path = etcetera::choose_app_strategy(manifest.app_strategy_args()) + .unwrap() + .in_config_dir("patches"); + let patchfile = File::open(path)?; + + let mut patches = Vec::new(); + for line in std::io::BufReader::new(patchfile).lines() { + let Ok(line) = line else { + continue; + }; + patches.push(line); + } + + for path in patches { + let path = PathBuf::from(path).into_boxed_path(); + let patch = files.load_file(path.clone(), wasm_compiler); + let patch = patch.as_ref().as_ref().unwrap(); + let patch = wasm_compiler.patch(patch)?; + manifest.apply_wasm_patch(path, patch); + } + + Ok(()) +} + +pub fn init_logger(manifest: &GameManifest) { + let path = etcetera::choose_app_strategy(manifest.app_strategy_args()) + .unwrap() + .in_state_dir("log") + .unwrap(); + let path = path.to_string_lossy(); + ProLogger::builder() + .with_rotating_file(&path, RotationConfig::new(100_000, 2)) + .with_color(ColorMode::Never) + .init() + .unwrap(); +} + +pub fn run_game(mut files: FileManager, wasm_runner: WasmRunner, manifest: GameManifest) { + let start = Instant::now(); + + let stdout = Box::new(BufWriter::new(std::io::stdout().lock())); + let mut framerate_limit: Option<Duration> = None; + let mut buffer = Buffer::new(stdout).unwrap(); + let mut input = InputManager::new(); + let mut time = TimeContext::new(); + let mut storage = StorageManager::load(manifest.app_strategy_args()).unwrap(); + let mut scripts = ScriptManager::new(&manifest.scripts); + let mut context = ScriptContext { + framerate_limit: &mut framerate_limit, + manifest: &manifest, + buffer: &mut buffer, + storage: &mut storage, + input: &input, + time: &time, + operation_queue: Vec::new(), + }; + + let scene_start = Instant::now(); + switch_scenes( + &mut scripts, + &mut context, + &mut files, + &wasm_runner, + manifest.first_scene, + ); + log::info!( + "Switch scenes time: {} ms", + scene_start.elapsed().as_millis() + ); + + let server_addr = format!("127.0.0.1:{}", puffin_http::DEFAULT_PORT); + let _puffin_server = puffin_http::Server::new(&server_addr).unwrap(); + puffin::set_scopes_on(true); + + log::info!("Total startup time: {} ms", start.elapsed().as_millis()); + buffer + .render_loop(|buffer, event| match event { + Event::Key(event) => { + if event.is_press() { + input.press_key(event.code); + } + } + Event::Mouse(event) => { + match event.kind { + MouseEventKind::Down(_) => input.click_mouse(), + MouseEventKind::ScrollDown => input.scroll(Direction::Down), + MouseEventKind::ScrollUp => input.scroll(Direction::Up), + MouseEventKind::ScrollLeft => input.scroll(Direction::Left), + MouseEventKind::ScrollRight => input.scroll(Direction::Right), + _ => (), + } + + input.move_mouse(Position { + x: event.column, + y: event.row, + }); + } + Event::FocusChanged(is_focused) => { + if is_focused { + input.focus(); + } else { + input.unfocus(); + } + } + Event::Render => { + input.read_gil_events(); + buffer.cells.clear(); + let mut context = ScriptContext { + framerate_limit: &mut framerate_limit, + manifest: &manifest, + buffer, + storage: &mut storage, + input: &input, + time: &time, + operation_queue: Vec::new(), + }; + scripts.run_scripts(&mut context, &wasm_runner, &mut files); + files.reload_files(&wasm_runner); + input.reset_next_frame(); + if let Some(framerate_limit) = framerate_limit + && let Ok(elapsed) = time.system_now().elapsed() + { + std::thread::sleep(framerate_limit - elapsed); + } + time.next_frame(); + } + }) + .unwrap(); +} diff --git a/src/main.rs b/src/main.rs index 75068fd..6f5a695 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,155 +1,26 @@ -use std::io::BufWriter; +use std::path::{Path, PathBuf}; use bpaf::Bpaf; -use mimalloc::MiMalloc; -use prologger::{ColorMode, ProLogger, RotationConfig}; -use tortuise::{ - buffer::{Buffer, Event}, - file::{FileManager, TomlGameManifest, normalize_game_manifest}, - input::InputManager, - scene::switch_scenes, - script::{ScriptContext, ScriptManager, WasmRunner}, - storage::StorageManager, - time::TimeContext, -}; - -// #[global_allocator] -// static GLOBAL: MiMalloc = MiMalloc; +use tortuise::{apply_patches, file::FileManager, init_logger, script::WasmRunner}; #[derive(Debug, Clone, Bpaf)] #[bpaf(options)] -enum Options { - #[bpaf(command)] - Run, - #[bpaf(command)] - Compile, +struct Options { + module: PathBuf, } fn main() { - let options = options().run(); - - match options { - Options::Compile => { - let manifest = std::fs::read("tortuise.toml").unwrap(); - let manifest = toml::from_slice::<TomlGameManifest>(&manifest).unwrap(); - let manifest = normalize_game_manifest(manifest).unwrap(); - - let manifest_decl = format!( - r#"let manifest = GameManifest {{ - name: "{}".to_string(), - first_scene: {}, - scenes: Box::from([{}]), - scripts: Box::from([]), - }}"#, - manifest.name, - manifest.first_scene, - manifest - .scenes - .iter() - .map(|scene| format!( - r#"Scene {{ - name: \"{}\".to_string(), - preload_scenes: Box::from([{}]), - scripts: ResourceRequirements {{ - required: Box::from([{}]), - preload: Box::from([{}]), - }} - }}"#, - scene.name, - scene - .preload_scenes - .iter() - .map(|i| i.to_string()) - .collect::<Vec<_>>() - .join(","), - scene - .scripts - .required - .iter() - .map(|i| i.to_string()) - .collect::<Vec<_>>() - .join(","), - scene - .scripts - .preload - .iter() - .map(|i| i.to_string()) - .collect::<Vec<_>>() - .join(","), - )) - .collect::<Vec<_>>() - .join(",") - ); - } - Options::Run => { - ProLogger::builder() - .with_rotating_file(".log", RotationConfig::new(100_000, 2)) - .with_color(ColorMode::Never) - .init() - .unwrap(); + let mut files = FileManager::default(); + let wasm_runner = WasmRunner::new(); - let mut files = FileManager::default(); - let wasm_runner = WasmRunner::new(); - let manifest = files - .load_file(Box::from("tortuise.toml".as_ref()), &wasm_runner) - .unwrap() - .unwrap_manifest() - .clone(); - - let stdout = Box::new(BufWriter::new(std::io::stdout().lock())); - let mut buffer = Buffer::new(stdout).unwrap(); - let mut input = InputManager::new(); - let mut time = TimeContext::new(); - let mut storage = StorageManager::load(etcetera::AppStrategyArgs { - top_level_domain: "com".into(), - author: "botahamec".into(), - app_name: manifest.name.to_string(), - }) - .unwrap(); - let mut scripts = ScriptManager::new(&manifest.scripts); - let mut context = ScriptContext { - manifest: &manifest, - buffer: &mut buffer, - storage: &mut storage, - input: &input, - time: &time, - operation_queue: Vec::new(), - }; - - switch_scenes( - &mut scripts, - &mut context, - &mut files, - &wasm_runner, - manifest.first_scene, - ); - - let server_addr = format!("127.0.0.1:{}", puffin_http::DEFAULT_PORT); - let _puffin_server = puffin_http::Server::new(&server_addr).unwrap(); - puffin::set_scopes_on(true); - buffer - .render_loop(|buffer, event| match event { - Event::Key(event) => { - if event.is_press() { - input.press_key(event.code); - } - } - Event::Render => { - buffer.cells.clear(); - let mut context = ScriptContext { - manifest: &manifest, - buffer, - storage: &mut storage, - input: &input, - time: &time, - operation_queue: Vec::new(), - }; - scripts.run_scripts(&mut context, &wasm_runner, &mut files); - input.reset_next_frame(); - time.next_frame(); - } - }) - .unwrap(); - } - } + let options = options().run(); + let path: Box<Path> = options.module.into(); + let manifest_script = files.load_file(path.clone(), &wasm_runner); + let manifest_script = manifest_script.as_ref().as_ref().unwrap(); + let mut manifest = wasm_runner.manifest(path.clone(), manifest_script).unwrap(); + init_logger(&manifest); + if let Err(e) = apply_patches(&mut manifest, &mut files, &wasm_runner) { + log::error!("{}", e); + }; + tortuise::run_game(files, wasm_runner, manifest); } diff --git a/src/scene.rs b/src/scene.rs index 807628d..aa4a443 100644 --- a/src/scene.rs +++ b/src/scene.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use rustc_hash::FxHashSet; use serde::{Deserialize, Serialize}; @@ -11,14 +9,7 @@ use crate::{ #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Scene { pub name: Box<str>, - pub preload_scenes: Box<[usize]>, - pub scripts: ResourceRequirements, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct ResourceRequirements { - pub required: Box<[usize]>, - pub preload: Box<[usize]>, + pub scripts: Vec<usize>, } pub fn scene_id_by_name(scenes: &[Scene], name: &str) -> Option<usize> { @@ -31,34 +22,6 @@ pub fn scene_id_by_name(scenes: &[Scene], name: &str) -> Option<usize> { None } -pub fn preload_scene( - scenes: &[Scene], - scripts: &mut ScriptManager, - files: &mut FileManager, - wasm_runner: &WasmRunner, - id: usize, -) { - let scene = &scenes[id]; - for script in &scene.scripts.required { - scripts.preload_script(*script, files, wasm_runner); - } -} - -pub fn is_scene_loaded( - scenes: &[Scene], - scripts: &ScriptManager, - loaded_files: &[Box<Path>], - id: usize, -) -> bool { - let scene = &scenes[id]; - scene - .scripts - .required - .iter() - .cloned() - .all(|script_id| scripts.is_script_loaded(script_id, loaded_files)) -} - pub fn switch_scenes( scripts: &mut ScriptManager, context: &mut ScriptContext<'_>, @@ -77,25 +40,8 @@ pub fn switch_scenes( let mut needed_scripts = FxHashSet::default(); let scene = &context.manifest.scenes[new]; - for script_id in &scene.scripts.required { + for script_id in &scene.scripts { needed_scripts.insert(*script_id); scripts.activate_script(*script_id, context, wasm_runner, files); } - for script_id in &scene.scripts.preload { - needed_scripts.insert(*script_id); - scripts.preload_script(*script_id, files, wasm_runner); - } - for scene_to_preload in &scene.preload_scenes { - let scene_to_preload = &context.manifest.scenes[*scene_to_preload]; - for script_id in &scene_to_preload.scripts.required { - needed_scripts.insert(*script_id); - scripts.preload_script(*script_id, files, wasm_runner); - } - } - - let active_scripts: FxHashSet<usize> = active_scripts.iter().cloned().collect(); - let unneeded_scripts = active_scripts.difference(&needed_scripts); - for script_id in unneeded_scripts { - scripts.unload_script(*script_id, files); - } } diff --git a/src/script.rs b/src/script.rs index a2cb76d..306ea49 100644 --- a/src/script.rs +++ b/src/script.rs @@ -1,12 +1,14 @@ -use std::{path::Path, sync::Arc}; +use std::{path::Path, sync::Arc, time::Duration}; use rustc_hash::FxHashMap; +use serde::{Deserialize, Serialize}; use crate::{ buffer::{Buffer, Cell}, - file::{FileManager, GameManifest}, + file::FileManager, input::InputManager, - scene::{preload_scene, switch_scenes}, + manifest::{DeserializedGameManifest, GameManifest, GameManifestPatch}, + scene::switch_scenes, script::wasm::WasmModule, storage::StorageManager, time::TimeContext, @@ -30,6 +32,7 @@ pub struct WasmRunner { } pub struct ScriptContext<'a> { + pub framerate_limit: &'a mut Option<Duration>, pub manifest: &'a GameManifest, pub buffer: &'a mut Buffer, pub input: &'a InputManager, @@ -53,10 +56,8 @@ pub struct FullScriptContext<'a> { pub enum Operation { AddCell(Cell), - PreloadScene(usize), + SetFrameLimit(Option<Duration>), SwitchScene(usize), - PreloadScript(usize), - UnloadScript(usize), ActivateScript(usize), DeactivateScript(usize), UpdateState { @@ -70,23 +71,24 @@ pub enum Operation { UpdateStorage { new_save: Box<[u8]>, }, + UpdateConfig { + new_save: Box<[u8]>, + }, Exit, } -type StaticScriptFunction = Option<fn(&mut ScriptContext<'_>, &mut Box<[u8]>)>; +pub type StaticScriptFunction = Option<Arc<dyn Fn(FullScriptContext<'_>) + Send + Sync>>; -#[derive(Debug, Clone)] +#[derive(Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] pub enum ScriptDeclaration { + #[serde(skip)] Static { name: Box<str>, start: StaticScriptFunction, update: StaticScriptFunction, end: StaticScriptFunction, }, - Lua { - name: Box<str>, - path: Box<Path>, - }, Wasm { name: Box<str>, path: Box<Path>, @@ -100,10 +102,6 @@ impl<'a> ScriptContext<'a> { } } -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 Default for WasmRunner { fn default() -> Self { Self::new() @@ -133,11 +131,54 @@ impl WasmRunner { Ok(WasmModule { instance }) } + pub fn manifest( + &self, + path: Box<Path>, + module: &WasmModule, + ) -> Result<GameManifest, tinywasm::Error> { + let mut store = self.store.lock(); + let offset = module + .instance + .func::<(), u32>(&store, "manifest_addr")? + .call(&mut store, ())?; + let length = module + .instance + .func::<(), u32>(&store, "manifest_len")? + .call(&mut store, ())?; + let manifest = + module + .instance + .memory("memory")? + .read_vec(&store, offset as usize, length as usize)?; + let manifest = + messagepack_serde::from_slice::<DeserializedGameManifest>(&manifest).unwrap(); + Ok(GameManifest::from_wasm(path, manifest)) + } + + pub fn patch(&self, module: &WasmModule) -> Result<GameManifestPatch, tinywasm::Error> { + let mut store = self.store.lock(); + let offset = module + .instance + .func::<(), u32>(&store, "manifest_addr")? + .call(&mut store, ())?; + let length = module + .instance + .func::<(), u32>(&store, "manifest_len")? + .call(&mut store, ())?; + let manifest = + module + .instance + .memory("memory")? + .read_vec(&store, offset as usize, length as usize)?; + let manifest = messagepack_serde::from_slice::<GameManifestPatch>(&manifest).unwrap(); + Ok(manifest) + } + #[expect(clippy::too_many_arguments)] pub fn run_function( &self, script_id: usize, - module: &mut WasmModule, + module: &WasmModule, scripts: &ScriptManager, files: Box<[Box<Path>]>, wasm_runner: &WasmRunner, @@ -171,6 +212,15 @@ impl WasmRunner { } } +impl ScriptDeclaration { + pub fn name(&self) -> &str { + match self { + Self::Static { name, .. } => name, + Self::Wasm { name, .. } => name, + } + } +} + impl<'a> ScriptManager<'a> { pub fn new(scripts: &'a [ScriptDeclaration]) -> Self { Self { @@ -189,11 +239,6 @@ impl<'a> ScriptManager<'a> { return Some(i); } } - ScriptDeclaration::Lua { name, .. } => { - if &**name == needle { - return Some(i); - } - } ScriptDeclaration::Wasm { name, .. } => { if &**name == needle { return Some(i); @@ -205,38 +250,6 @@ impl<'a> ScriptManager<'a> { None } - pub fn preload_script( - &self, - script_id: usize, - files: &mut FileManager, - wasm_runner: &WasmRunner, - ) { - let script = &self.scripts[script_id]; - match script { - ScriptDeclaration::Static { .. } => (), - ScriptDeclaration::Lua { path, .. } => files.start_loading_file(path, wasm_runner), - ScriptDeclaration::Wasm { path, .. } => files.start_loading_file(path, wasm_runner), - } - } - - pub fn is_script_loaded(&self, script_id: usize, loaded_files: &[Box<Path>]) -> bool { - let script = &self.scripts[script_id]; - match script { - ScriptDeclaration::Static { .. } => true, - ScriptDeclaration::Lua { path, .. } => loaded_files.contains(path), - ScriptDeclaration::Wasm { path, .. } => loaded_files.contains(path), - } - } - - pub fn unload_script(&self, script_id: usize, files: &mut FileManager) { - let script = &self.scripts[script_id]; - match script { - ScriptDeclaration::Static { .. } => (), - ScriptDeclaration::Lua { path, .. } => files.unload_file(path), - ScriptDeclaration::Wasm { path, .. } => files.unload_file(path), - } - } - pub fn handle_queue( &mut self, context: &mut ScriptContext<'_>, @@ -246,12 +259,11 @@ impl<'a> ScriptManager<'a> { for operation in &context.operation_queue { match operation { Operation::AddCell(cell) => context.buffer.cells.push(*cell), - Operation::PreloadScene(id) => { - preload_scene(&context.manifest.scenes, self, files, wasm_runner, *id) - } + Operation::SetFrameLimit(duration) => *context.framerate_limit = *duration, Operation::SwitchScene(id) => switch_scenes( self, &mut ScriptContext { + framerate_limit: context.framerate_limit, manifest: context.manifest, buffer: context.buffer, input: context.input, @@ -263,8 +275,6 @@ impl<'a> ScriptManager<'a> { wasm_runner, *id, ), - Operation::PreloadScript(id) => self.preload_script(*id, files, wasm_runner), - Operation::UnloadScript(id) => self.unload_script(*id, files), Operation::ActivateScript(id) => self.activate_script( *id, &mut ScriptContext { @@ -274,6 +284,7 @@ impl<'a> ScriptManager<'a> { time: context.time, storage: context.storage, operation_queue: Vec::new(), + framerate_limit: context.framerate_limit, }, wasm_runner, files, @@ -287,6 +298,7 @@ impl<'a> ScriptManager<'a> { time: context.time, storage: context.storage, operation_queue: Vec::new(), + framerate_limit: context.framerate_limit, }, wasm_runner, files, @@ -301,7 +313,10 @@ impl<'a> ScriptManager<'a> { self.entities.insert(name.clone(), new_state.clone()); } Operation::UpdateStorage { new_save } => { - context.storage.background_save(new_save.clone()); + context.storage.background_save_data(new_save.clone()); + } + Operation::UpdateConfig { new_save } => { + context.storage.background_save_config(new_save.clone()); } Operation::Exit => context.exit(), } @@ -321,20 +336,9 @@ impl<'a> ScriptManager<'a> { }; match script_declaration { ScriptDeclaration::Static { start, .. } => { - start.map(|s| { - s( - context, - get_script_state(&mut self.script_states, script_id), - ) - }); - } - ScriptDeclaration::Lua { path, .. } => { - files.load_file(path.clone(), wasm_runner).unwrap(); - let loaded_files = files.loaded_files(); - let module = files.get_cached_file_mut(path).unwrap().unwrap_lua_module(); - if let Err(e) = module.run_function( - "start", - FullScriptContext { + start.as_ref().inspect(|s| { + let loaded_files = files.loaded_files(); + s(FullScriptContext { script_id, manifest: context.manifest, buffer: context.buffer, @@ -345,18 +349,13 @@ impl<'a> ScriptManager<'a> { storage: context.storage, wasm_runner, operation_queue: &mut context.operation_queue, - }, - ) { - log::error!("{e}"); - } + }) + }); } ScriptDeclaration::Wasm { path, .. } => { - files.load_file(path.clone(), wasm_runner).unwrap(); + let module = files.load_file(path.clone(), wasm_runner); + let module = module.as_ref().as_ref().unwrap(); let loaded_files = files.loaded_files(); - let module = files - .get_cached_file_mut(path) - .unwrap() - .unwrap_wasm_module_mut(); if let Err(e) = wasm_runner.run_function( script_id, module, @@ -364,7 +363,7 @@ impl<'a> ScriptManager<'a> { loaded_files, wasm_runner, context, - "start", + "start_script", ) { log::error!("{e}"); } @@ -393,20 +392,9 @@ impl<'a> ScriptManager<'a> { self.script_states.remove(&script_id); match script { ScriptDeclaration::Static { end, .. } => { - end.map(|s| { - s( - context, - get_script_state(&mut self.script_states, script_id), - ); - }); - } - ScriptDeclaration::Lua { path, .. } => { - files.load_file(path.clone(), wasm_runner).unwrap(); - let loaded_files = files.loaded_files(); - let module = files.get_cached_file_mut(path).unwrap().unwrap_lua_module(); - if let Err(e) = module.run_function( - "end", - FullScriptContext { + end.as_ref().inspect(|s| { + let loaded_files = files.loaded_files(); + s(FullScriptContext { script_id, manifest: context.manifest, buffer: context.buffer, @@ -417,18 +405,13 @@ impl<'a> ScriptManager<'a> { storage: context.storage, wasm_runner, operation_queue: &mut context.operation_queue, - }, - ) { - log::error!("{e}"); - } + }); + }); } ScriptDeclaration::Wasm { path, .. } => { - files.load_file(path.clone(), wasm_runner).unwrap(); + let module = files.load_file(path.clone(), wasm_runner); + let module = module.as_ref().as_ref().unwrap(); let loaded_files = files.loaded_files(); - let module = files - .get_cached_file_mut(path) - .unwrap() - .unwrap_wasm_module_mut(); if let Err(e) = wasm_runner.run_function( script_id, module, @@ -436,7 +419,7 @@ impl<'a> ScriptManager<'a> { loaded_files, wasm_runner, context, - "end", + "end_script", ) { log::error!("{e}"); } @@ -459,21 +442,11 @@ impl<'a> ScriptManager<'a> { unreachable!("The active script does not exist"); }; match script { - ScriptDeclaration::Static { update, .. } => { + ScriptDeclaration::Static { name, update, .. } => { if let Some(s) = update { - s( - context, - get_script_state(&mut self.script_states, *script_id), - ) - } - } - ScriptDeclaration::Lua { path, .. } => { - files.load_file(path.clone(), wasm_runner).unwrap(); - let loaded_files = files.loaded_files(); - let module = files.get_cached_file_mut(path).unwrap().unwrap_lua_module(); - if let Err(e) = module.run_function( - "update", - FullScriptContext { + puffin::profile_scope!("run script", name); + let loaded_files = files.loaded_files(); + s(FullScriptContext { script_id: *script_id, manifest: context.manifest, buffer: context.buffer, @@ -484,18 +457,13 @@ impl<'a> ScriptManager<'a> { storage: context.storage, wasm_runner, operation_queue: &mut context.operation_queue, - }, - ) { - log::error!("{e}"); + }); } } ScriptDeclaration::Wasm { path, .. } => { - files.load_file(path.clone(), wasm_runner).unwrap(); + let module = files.load_file(path.clone(), wasm_runner); + let module = module.as_ref().as_ref().unwrap(); let loaded_files = files.loaded_files(); - let module = files - .get_cached_file_mut(path) - .unwrap() - .unwrap_wasm_module_mut(); if let Err(e) = wasm_runner.run_function( *script_id, module, @@ -503,7 +471,7 @@ impl<'a> ScriptManager<'a> { loaded_files, wasm_runner, context, - "update", + "update_script", ) { log::error!("Error while running scripts: {e}"); } diff --git a/src/script/lua.rs b/src/script/lua.rs index a5c7f6a..92d455d 100644 --- a/src/script/lua.rs +++ b/src/script/lua.rs @@ -50,12 +50,7 @@ impl LuaModule { warn, error, exit, - is_scene_loaded, - preload_scene, switch_scene, - is_script_loaded, - preload_script, - unload_script, activate_script, deactivate_script, state, @@ -246,29 +241,6 @@ fn exit(lua: &Lua, _: ()) -> mlua::Result<()> { Ok(()) } -fn is_scene_loaded(lua: &Lua, scene: String) -> mlua::Result<()> { - puffin::profile_function!(); - let context = unsafe { context(lua) }; - let scene = scene_id_by_name(&context.manifest.scenes, &scene) - .ok_or(mlua::Error::runtime("No scene with the given name found"))?; - crate::scene::is_scene_loaded( - &context.manifest.scenes, - context.scripts, - &context.files, - scene, - ); - Ok(()) -} - -fn preload_scene(lua: &Lua, scene: String) -> mlua::Result<()> { - puffin::profile_function!(); - let context = unsafe { context(lua) }; - let scene = scene_id_by_name(&context.manifest.scenes, &scene) - .ok_or(mlua::Error::runtime("No scene with the given name found"))?; - context.operation_queue.push(Operation::PreloadScene(scene)); - Ok(()) -} - fn switch_scene(lua: &Lua, scene: String) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; @@ -278,43 +250,6 @@ fn switch_scene(lua: &Lua, scene: String) -> mlua::Result<()> { Ok(()) } -fn is_script_loaded(lua: &Lua, script: String) -> mlua::Result<()> { - puffin::profile_function!(); - let context = unsafe { context(lua) }; - let script = context - .scripts - .script_id_by_name(&script) - .ok_or(mlua::Error::runtime("No script with the given name found"))?; - context.scripts.is_script_loaded(script, &context.files); - Ok(()) -} - -fn preload_script(lua: &Lua, script: String) -> mlua::Result<()> { - puffin::profile_function!(); - let context = unsafe { context(lua) }; - let script = context - .scripts - .script_id_by_name(&script) - .ok_or(mlua::Error::runtime("No script with the given name found"))?; - context - .operation_queue - .push(Operation::PreloadScript(script)); - Ok(()) -} - -fn unload_script(lua: &Lua, script: String) -> mlua::Result<()> { - puffin::profile_function!(); - let context = unsafe { context(lua) }; - let script = context - .scripts - .script_id_by_name(&script) - .ok_or(mlua::Error::runtime("No script with the given name found"))?; - context - .operation_queue - .push(Operation::UnloadScript(script)); - Ok(()) -} - fn activate_script(lua: &Lua, script: String) -> mlua::Result<()> { puffin::profile_function!(); let context = unsafe { context(lua) }; @@ -436,7 +371,7 @@ fn storage(lua: &Lua, default: MyLuaValue) -> mlua::Result<MyLuaValue> { let context = unsafe { context(lua) }; Ok(context .storage - .get() + .data() .map(|state| postcard::from_bytes(state).unwrap()) .unwrap_or(default)) } diff --git a/src/script/wasm.rs b/src/script/wasm.rs index 9b9cd91..aaf25cf 100644 --- a/src/script/wasm.rs +++ b/src/script/wasm.rs @@ -1,4 +1,4 @@ -use std::time::UNIX_EPOCH; +use std::time::{Duration, UNIX_EPOCH}; use crossterm::{ event::KeyCode, @@ -35,6 +35,60 @@ fn context_ref<'b>(address: u64) -> &'b FullScriptContext<'b> { } } +fn number_to_button(number: u32) -> gilrs::Button { + match number { + 0 => gilrs::Button::Unknown, + 1 => gilrs::Button::South, + 2 => gilrs::Button::East, + 3 => gilrs::Button::C, + 4 => gilrs::Button::North, + 5 => gilrs::Button::West, + 6 => gilrs::Button::Z, + 7 => gilrs::Button::LeftTrigger, + 8 => gilrs::Button::RightTrigger, + 9 => gilrs::Button::LeftTrigger2, + 10 => gilrs::Button::RightTrigger2, + 11 => gilrs::Button::Select, + 12 => gilrs::Button::Start, + 13 => gilrs::Button::Mode, + 14 => gilrs::Button::LeftThumb, + 15 => gilrs::Button::RightThumb, + 16 => gilrs::Button::DPadUp, + 17 => gilrs::Button::DPadDown, + 18 => gilrs::Button::DPadLeft, + 19 => gilrs::Button::DPadRight, + _ => gilrs::Button::Unknown, + } +} + +fn number_to_axis(number: u32) -> gilrs::Axis { + match number { + 0 => gilrs::Axis::Unknown, + 1 => gilrs::Axis::LeftStickX, + 2 => gilrs::Axis::LeftStickY, + 3 => gilrs::Axis::LeftZ, + 4 => gilrs::Axis::RightStickX, + 5 => gilrs::Axis::RightStickY, + 6 => gilrs::Axis::RightZ, + 7 => gilrs::Axis::DPadX, + 8 => gilrs::Axis::DPadY, + _ => gilrs::Axis::Unknown, + } +} + +fn set_framerate_limit( + _caller: FuncContext<'_>, + (context, limit): (u64, u32), +) -> tinywasm::Result<()> { + puffin::profile_function!(); + let context = context_mut(context); + let duration = (limit != 0).then(|| Duration::from_nanos(1_000_000_000 / limit as u64)); + context + .operation_queue + .push(Operation::SetFrameLimit(duration)); + Ok(()) +} + fn add_cell( _caller: FuncContext<'_>, ( @@ -219,6 +273,174 @@ fn is_key_pressed_this_frame( ) } +fn is_mouse_clicked_this_frame( + _caller: FuncContext<'_>, + (context,): (u64,), +) -> tinywasm::Result<u32> { + puffin::profile_function!(); + if context_ref(context).input.is_mouse_clicked_this_frame() { + Ok(1) + } else { + Ok(0) + } +} + +fn mouse_location(_caller: FuncContext<'_>, (context,): (u64,)) -> tinywasm::Result<u32> { + puffin::profile_function!(); + let location = context_ref(context).input.mouse_location(); + + if let Some(location) = location { + Ok(((location.y as u32) << 16) | location.x as u32) + } else { + Ok(u32::MAX) + } +} + +fn scroll_direction_this_frame( + _caller: FuncContext<'_>, + (context,): (u64,), +) -> tinywasm::Result<u32> { + puffin::profile_function!(); + let direction = context_ref(context).input.scroll_direction_this_frame(); + Ok(direction.map(|d| d as u32).unwrap_or_default()) +} + +fn is_focused(_caller: FuncContext<'_>, (context,): (u64,)) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok(if context_ref(context).input.is_focused() { + 1 + } else { + 0 + }) +} + +fn is_gamepad_connected_this_frame( + _caller: FuncContext<'_>, + context: u64, +) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok(context_ref(context) + .input + .is_gamepad_connected_this_frame() + .unwrap_or_default() as u32) +} + +fn is_button_pressed_this_frame( + _caller: FuncContext<'_>, + (context, gamepad_id, button): (u64, u32, u32), +) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok( + if context_ref(context) + .input + .is_button_pressed_this_frame(gamepad_id as usize, number_to_button(button)) + { + 1 + } else { + 0 + }, + ) +} + +fn is_button_released_this_frame( + _caller: FuncContext<'_>, + (context, gamepad_id, button): (u64, u32, u32), +) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok( + if context_ref(context) + .input + .is_button_released_this_frame(gamepad_id as usize, number_to_button(button)) + { + 1 + } else { + 0 + }, + ) +} + +fn is_button_down( + _caller: FuncContext<'_>, + (context, gamepad_id, button): (u64, u32, u32), +) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok( + if context_ref(context) + .input + .is_button_down(gamepad_id as usize, number_to_button(button)) + { + 1 + } else { + 0 + }, + ) +} + +fn is_gamepad_connected( + _caller: FuncContext<'_>, + (context, gamepad_id): (u64, u32), +) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok( + if context_ref(context) + .input + .is_gamepad_connected(gamepad_id as usize) + { + 1 + } else { + 0 + }, + ) +} + +fn axis_value( + _caller: FuncContext<'_>, + (context, gamepad_id, axis): (u64, u32, u32), +) -> tinywasm::Result<f32> { + puffin::profile_function!(); + Ok(context_ref(context) + .input + .axis_value(gamepad_id as usize, number_to_axis(axis)) + .unwrap_or_default()) +} + +fn gamepad_power_info( + _caller: FuncContext<'_>, + (context, gamepad_id): (u64, u32), +) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok( + match context_ref(context) + .input + .gamepad_power_info(gamepad_id as usize) + { + None | Some(gilrs::PowerInfo::Unknown) => 0, + Some(gilrs::PowerInfo::Wired) => 1 << 16, + Some(gilrs::PowerInfo::Discharging(power)) => 2 << 16 | power as u32, + Some(gilrs::PowerInfo::Charging(power)) => 3 << 16 | power as u32, + Some(gilrs::PowerInfo::Charged) => 4 << 16, + }, + ) +} + +fn was_focused_this_frame(_caller: FuncContext<'_>, (context,): (u64,)) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok(if context_ref(context).input.was_focused_this_frame() { + 1 + } else { + 0 + }) +} + +fn was_unfocused_this_frame(_caller: FuncContext<'_>, (context,): (u64,)) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok(if context_ref(context).input.was_unfocused_this_frame() { + 1 + } else { + 0 + }) +} + fn log( caller: FuncContext<'_>, (severity, offset, length): (u32, u32, u32), @@ -261,30 +483,31 @@ fn scene_id_by_name( ) } -fn preload_scene(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> { +fn switch_scene(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> { puffin::profile_function!(); context_mut(context) .operation_queue - .push(Operation::PreloadScene(id as usize)); + .push(Operation::SwitchScene(id as usize)); Ok(()) } -fn is_scene_loaded(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<u32> { +fn script_name_size(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> { puffin::profile_function!(); let context = context_ref(context); - Ok(crate::scene::is_scene_loaded( - &context.manifest.scenes, - context.scripts, - &context.files, - id as usize, - ) as u32) + Ok(context.scripts.scripts[context.script_id].name().len() as u32) } -fn switch_scene(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> { +fn load_script_name( + mut caller: FuncContext<'_>, + (context, offset): (u64, u32), +) -> tinywasm::Result<()> { puffin::profile_function!(); - context_mut(context) - .operation_queue - .push(Operation::SwitchScene(id as usize)); + let memory = caller.memory("memory").unwrap(); + let context = context_ref(context); + let name = context.scripts.scripts[context.script_id].name(); + memory + .write(&mut caller, offset as usize, name.as_bytes()) + .unwrap(); Ok(()) } @@ -302,30 +525,6 @@ fn script_id_by_name( .unwrap_or(u32::MAX)) } -fn preload_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> { - puffin::profile_function!(); - context_mut(context) - .operation_queue - .push(Operation::PreloadScript(id as usize)); - Ok(()) -} - -fn is_script_loaded(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<u32> { - puffin::profile_function!(); - let context = context_ref(context); - Ok(context - .scripts - .is_script_loaded(id as usize, &context.files) as u32) -} - -fn unload_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> { - puffin::profile_function!(); - context_mut(context) - .operation_queue - .push(Operation::UnloadScript(id as usize)); - Ok(()) -} - fn activate_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> { puffin::profile_function!(); context_mut(context) @@ -444,7 +643,7 @@ fn storage_size(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> puffin::profile_function!(); Ok(context_ref(context) .storage - .get() + .data() .map(|data| data.len()) .unwrap_or_default() as u32) } @@ -456,7 +655,7 @@ fn load_storage( puffin::profile_function!(); let memory = caller.memory("memory").unwrap(); let context = context_ref(context); - let state = context.storage.get().unwrap_or(&[]); + let state = context.storage.data().unwrap_or(&[]); memory.write(&mut caller, offset as usize, state).unwrap(); Ok(()) } @@ -480,6 +679,79 @@ fn save_storage( Ok(()) } +fn is_saving_storage(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok(if context_ref(context).storage.is_saving_data() { + 1 + } else { + 0 + }) +} + +fn saving_storage_failed(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok(if context_ref(context).storage.saving_data_failed() { + 1 + } else { + 0 + }) +} + +fn config_size(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok(context_ref(context) + .storage + .config() + .map(|data| data.len()) + .unwrap_or_default() as u32) +} + +fn load_config(mut caller: FuncContext<'_>, (context, offset): (u64, u32)) -> tinywasm::Result<()> { + puffin::profile_function!(); + let memory = caller.memory("memory").unwrap(); + let context = context_ref(context); + let state = context.storage.config().unwrap_or(&[]); + memory.write(&mut caller, offset as usize, state).unwrap(); + Ok(()) +} + +fn save_config( + mut caller: FuncContext<'_>, + (context, offset, length): (u64, u32, u32), +) -> tinywasm::Result<()> { + puffin::profile_function!(); + let offset = offset as usize; + let length = length as usize; + let memory = caller.memory("memory").unwrap(); + let new_save = memory + .read_vec(caller.store_mut(), offset, length) + .unwrap() + .into_boxed_slice(); + let context = context_mut(context); + context + .operation_queue + .push(Operation::UpdateConfig { new_save }); + Ok(()) +} + +fn is_saving_config(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok(if context_ref(context).storage.is_saving_config() { + 1 + } else { + 0 + }) +} + +fn saving_config_failed(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> { + puffin::profile_function!(); + Ok(if context_ref(context).storage.saving_config_failed() { + 1 + } else { + 0 + }) +} + fn system_now(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u64> { puffin::profile_function!(); Ok(context_ref(context) @@ -514,18 +786,29 @@ pub fn linker(store: &mut Store) -> Result<Imports, tinywasm::Error> { [ add_cell, window_size, + set_framerate_limit, is_key_pressed_this_frame, + is_mouse_clicked_this_frame, + mouse_location, + scroll_direction_this_frame, + is_focused, + is_button_pressed_this_frame, + is_button_released_this_frame, + is_gamepad_connected_this_frame, + is_gamepad_connected, + is_button_down, + axis_value, + gamepad_power_info, + was_focused_this_frame, + was_unfocused_this_frame, log, exit, random_u64, + script_name_size, + load_script_name, scene_id_by_name, - preload_scene, switch_scene, - is_scene_loaded, script_id_by_name, - preload_script, - is_script_loaded, - unload_script, activate_script, deactivate_script, state_size, @@ -537,6 +820,13 @@ pub fn linker(store: &mut Store) -> Result<Imports, tinywasm::Error> { storage_size, load_storage, save_storage, + is_saving_storage, + saving_storage_failed, + config_size, + load_config, + save_config, + is_saving_config, + saving_config_failed, system_now, monotonic_now, delta_time diff --git a/src/storage.rs b/src/storage.rs index 90abf05..c099481 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,41 +1,123 @@ +use std::{ + path::PathBuf, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; + use etcetera::{AppStrategy, AppStrategyArgs, app_strategy::Xdg}; use smol::unblock; pub struct StorageManager { xdg: Xdg, + config: Option<Box<[u8]>>, data: Option<Box<[u8]>>, + saving_config: Arc<AtomicBool>, + saving_data: Arc<AtomicBool>, + saving_config_failed: Arc<AtomicBool>, + saving_data_failed: Arc<AtomicBool>, } impl StorageManager { pub fn load(app: AppStrategyArgs) -> std::io::Result<Self> { let xdg = etcetera::choose_app_strategy(app).map_err(std::io::Error::other)?; + let config = std::fs::read(xdg.in_config_dir("config")) + .or_else(|_| std::fs::read(xdg.in_config_dir("config.tmp"))) + .map(|config| config.into_boxed_slice()) + .ok(); let data = std::fs::read(xdg.in_data_dir("save")) .or_else(|_| std::fs::read(xdg.in_data_dir("save.tmp"))) .map(|data| data.into_boxed_slice()) .ok(); - Ok(Self { data, xdg }) + Ok(Self { + xdg, + config, + data, + saving_config: Arc::new(AtomicBool::new(false)), + saving_data: Arc::new(AtomicBool::new(false)), + saving_config_failed: Arc::new(AtomicBool::new(false)), + saving_data_failed: Arc::new(AtomicBool::new(false)), + }) + } + + pub fn config(&self) -> Option<&[u8]> { + self.config.as_deref() } - pub fn get(&self) -> Option<&[u8]> { + pub fn data(&self) -> Option<&[u8]> { self.data.as_deref() } - pub fn background_save(&mut self, data: Box<[u8]>) { - let tmp_file = self.xdg.in_data_dir("save.tmp"); - let save_file = self.xdg.in_data_dir("save"); + pub fn is_saving_config(&self) -> bool { + self.saving_config.load(Ordering::Acquire) + } + + pub fn is_saving_data(&self) -> bool { + self.saving_data.load(Ordering::Acquire) + } + + pub fn saving_config_failed(&self) -> bool { + self.saving_config_failed.load(Ordering::Acquire) + } + + pub fn saving_data_failed(&self) -> bool { + self.saving_data_failed.load(Ordering::Acquire) + } + + fn background_save( + &mut self, + file: PathBuf, + data: Box<[u8]>, + state: Arc<AtomicBool>, + failed: Arc<AtomicBool>, + ) { + let tmp_file = file.with_added_extension("tmp"); self.data = Some(data.clone()); unblock(move || { - std::fs::write(&tmp_file, &data)?; + if let Err(e) = { + std::fs::write(&tmp_file, &data)?; - let saved_data = std::fs::read(&tmp_file)?; - if *saved_data != *data { - return Err(std::io::Error::other("Failed to verify file integrity")); + let saved_data = std::fs::read(&tmp_file)?; + if *saved_data != *data { + return Err(std::io::Error::other("Failed to verify file integrity")); + } + + std::fs::rename(tmp_file, &file)?; + state.store(false, Ordering::Release); + Ok(()) + } { + log::error!("{e}"); + failed.store(true, Ordering::Release); + Err(e) + } else { + Ok(()) } + }) + .detach(); + } + + pub fn background_save_config(&mut self, data: Box<[u8]>) { + self.saving_config_failed.store(false, Ordering::Release); + self.saving_config.store(true, Ordering::Release); + self.background_save( + self.xdg.in_data_dir("config"), + data, + self.saving_config.clone(), + self.saving_config_failed.clone(), + ) + } - std::fs::rename(tmp_file, save_file)?; - Ok(()) - }); + pub fn background_save_data(&mut self, data: Box<[u8]>) { + self.saving_data_failed.store(false, Ordering::Release); + self.saving_data.store(true, Ordering::Release); + self.background_save( + self.xdg.in_data_dir("save"), + data, + self.saving_data.clone(), + self.saving_config_failed.clone(), + ) } } |
