summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2026-10-26 10:51:49 -0400
committerMica White <botahamec@outlook.com>2026-10-26 10:51:49 -0400
commit0c128e472b1b49e32c313874eaeccc8c5f1b1202 (patch)
tree2ce06455440e85ec3387f7348b7bd69bf2119634 /src
parentc687e2aba30ca231b03288108b632f1bad464028 (diff)
Finish engineHEADmain
Diffstat (limited to 'src')
-rw-r--r--src/buffer.rs63
-rw-r--r--src/file.rs255
-rw-r--r--src/input.rs220
-rw-r--r--src/lib.rs156
-rw-r--r--src/main.rs161
-rw-r--r--src/scene.rs58
-rw-r--r--src/script.rs230
-rw-r--r--src/script/lua.rs67
-rw-r--r--src/script/wasm.rs380
-rw-r--r--src/storage.rs106
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()
+ }
}
diff --git a/src/lib.rs b/src/lib.rs
index 767eff9..1418ebd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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(),
+ )
}
}