summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/file.rs67
-rw-r--r--src/main.rs199
-rw-r--r--src/scene.rs6
-rw-r--r--src/script.rs249
-rw-r--r--src/script/lua.rs495
-rw-r--r--src/script/wasm.rs361
-rw-r--r--src/storage.rs41
7 files changed, 1124 insertions, 294 deletions
diff --git a/src/file.rs b/src/file.rs
index 9b9888c..4aedd48 100644
--- a/src/file.rs
+++ b/src/file.rs
@@ -6,7 +6,7 @@ use smol::{Task, unblock};
use crate::{
scene::{ResourceRequirements, Scene, scene_id_by_name},
- script::ScriptDeclaration,
+ script::{ScriptDeclaration, WasmRunner, lua::LuaModule, wasm::WasmModule},
};
type FileTask = Task<std::io::Result<FileData>>;
@@ -25,7 +25,7 @@ pub struct GameManifest {
}
#[derive(Debug, Serialize, Deserialize)]
-struct TomlGameManifest {
+pub struct TomlGameManifest {
name: Box<str>,
first_scene: Box<str>,
#[serde(default)]
@@ -49,14 +49,16 @@ struct TomlScene {
#[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(wasmi::Module),
+ WasmModule(Box<WasmModule>),
+ LuaModule(Box<LuaModule>),
}
-fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameManifest> {
+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
@@ -69,6 +71,13 @@ fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameMa
});
(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
@@ -102,8 +111,8 @@ fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameMa
})
}
-fn compile_data(path: &Path, data: &[u8], compiler: &wasmi::Engine) -> std::io::Result<FileData> {
- if path == "manifest.toml" {
+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)?,
)?));
@@ -113,9 +122,14 @@ fn compile_data(path: &Path, data: &[u8], compiler: &wasmi::Engine) -> std::io::
.and_then(OsStr::to_str)
.map(|ext| ext.to_lowercase());
if extension.as_deref() == Some("wasm") {
- return Ok(FileData::WasmModule(
- wasmi::Module::new(compiler, data).map_err(std::io::Error::other)?,
- ));
+ 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)?,
+ )));
}
Err(std::io::Error::other("Unrecognized file extension"))
}
@@ -129,7 +143,23 @@ impl FileData {
}
}
- pub fn unwrap_wasm_module(&self) -> &wasmi::Module {
+ pub fn unwrap_wasm_module(&self) -> &WasmModule {
+ if let FileData::WasmModule(module) = self {
+ module
+ } else {
+ panic!("Not a WASM module")
+ }
+ }
+
+ pub fn unwrap_lua_module(&self) -> &LuaModule {
+ if let FileData::LuaModule(module) = self {
+ module
+ } else {
+ panic!("Not a Lua module")
+ }
+ }
+
+ pub fn unwrap_wasm_module_mut(&mut self) -> &mut WasmModule {
if let FileData::WasmModule(module) = self {
module
} else {
@@ -149,7 +179,7 @@ impl FileManager {
pub fn load_file(
&mut self,
path: Box<Path>,
- wasm_compiler: &wasmi::Engine,
+ wasm_compiler: &WasmRunner,
) -> std::io::Result<&FileData> {
if !self.cache.contains_key(&path) {
let data = compile_data(
@@ -166,7 +196,11 @@ impl FileManager {
self.cache.get(path)
}
- pub fn start_loading_file(&mut self, path: impl AsRef<Path>, wasm_compiler: &wasmi::Engine) {
+ 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());
let compiler = wasm_compiler.clone();
let task = unblock(move || {
@@ -180,6 +214,15 @@ impl FileManager {
self.active_tasks.insert(boxed_path, task);
}
+ pub fn loaded_files(&self) -> Box<[Box<Path>]> {
+ self.active_tasks
+ .iter()
+ .filter(|&(_, v)| v.is_finished())
+ .map(|(k, _)| k.clone())
+ .chain(self.cache.keys().cloned())
+ .collect()
+ }
+
pub fn is_file_loaded(&self, path: impl AsRef<Path>) -> bool {
self.active_tasks
.get(path.as_ref())
diff --git a/src/main.rs b/src/main.rs
index 66e4cf4..75068fd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,9 +1,11 @@
use std::io::BufWriter;
+use bpaf::Bpaf;
use mimalloc::MiMalloc;
+use prologger::{ColorMode, ProLogger, RotationConfig};
use tortuise::{
buffer::{Buffer, Event},
- file::FileManager,
+ file::{FileManager, TomlGameManifest, normalize_game_manifest},
input::InputManager,
scene::switch_scenes,
script::{ScriptContext, ScriptManager, WasmRunner},
@@ -11,70 +13,143 @@ use tortuise::{
time::TimeContext,
};
-#[global_allocator]
-static GLOBAL: MiMalloc = MiMalloc;
+// #[global_allocator]
+// static GLOBAL: MiMalloc = MiMalloc;
+
+#[derive(Debug, Clone, Bpaf)]
+#[bpaf(options)]
+enum Options {
+ #[bpaf(command)]
+ Run,
+ #[bpaf(command)]
+ Compile,
+}
fn main() {
- let mut files = FileManager::default();
- let wasm_runner = WasmRunner::new();
- let manifest = files
- .load_file(Box::from("manifest.toml".as_ref()), &wasm_runner.engine)
- .unwrap()
- .unwrap_manifest()
- .clone();
+ 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 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(),
- };
+ 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,
- );
+ 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 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();
+ }
+ }
}
diff --git a/src/scene.rs b/src/scene.rs
index 89a0d99..807628d 100644
--- a/src/scene.rs
+++ b/src/scene.rs
@@ -1,3 +1,5 @@
+use std::path::Path;
+
use rustc_hash::FxHashSet;
use serde::{Deserialize, Serialize};
@@ -45,7 +47,7 @@ pub fn preload_scene(
pub fn is_scene_loaded(
scenes: &[Scene],
scripts: &ScriptManager,
- files: &FileManager,
+ loaded_files: &[Box<Path>],
id: usize,
) -> bool {
let scene = &scenes[id];
@@ -54,7 +56,7 @@ pub fn is_scene_loaded(
.required
.iter()
.cloned()
- .all(|script_id| scripts.is_script_loaded(script_id, files))
+ .all(|script_id| scripts.is_script_loaded(script_id, loaded_files))
}
pub fn switch_scenes(
diff --git a/src/script.rs b/src/script.rs
index 8539c92..a2cb76d 100644
--- a/src/script.rs
+++ b/src/script.rs
@@ -1,4 +1,4 @@
-use std::path::Path;
+use std::{path::Path, sync::Arc};
use rustc_hash::FxHashMap;
@@ -7,11 +7,13 @@ use crate::{
file::{FileManager, GameManifest},
input::InputManager,
scene::{preload_scene, switch_scenes},
+ script::wasm::WasmModule,
storage::StorageManager,
time::TimeContext,
};
-mod wasm;
+pub mod lua;
+pub mod wasm;
pub struct ScriptManager<'a> {
scripts: &'a [ScriptDeclaration],
@@ -20,9 +22,11 @@ pub struct ScriptManager<'a> {
script_states: FxHashMap<usize, Box<[u8]>>,
}
+#[derive(Clone)]
pub struct WasmRunner {
- pub engine: wasmi::Engine,
- pub linker: wasmi::Linker<FullScriptContext<'static>>,
+ pub engine: tinywasm::Engine,
+ pub store: Arc<spin::Mutex<tinywasm::Store>>,
+ pub linker: tinywasm::Imports,
}
pub struct ScriptContext<'a> {
@@ -41,7 +45,7 @@ pub struct FullScriptContext<'a> {
pub input: &'a InputManager,
pub time: &'a TimeContext,
pub scripts: &'a ScriptManager<'a>,
- pub files: &'a FileManager,
+ pub files: Box<[Box<Path>]>,
pub storage: &'a StorageManager,
pub wasm_runner: &'a WasmRunner,
pub operation_queue: &'a mut Vec<Operation>,
@@ -79,6 +83,10 @@ pub enum ScriptDeclaration {
update: StaticScriptFunction,
end: StaticScriptFunction,
},
+ Lua {
+ name: Box<str>,
+ path: Box<Path>,
+ },
Wasm {
name: Box<str>,
path: Box<Path>,
@@ -104,66 +112,62 @@ impl Default for WasmRunner {
impl WasmRunner {
pub fn new() -> Self {
- let engine = wasmi::Engine::default();
- let linker = unsafe {
- std::mem::transmute::<
- wasmi::Linker<FullScriptContext<'_>>,
- wasmi::Linker<FullScriptContext<'_>>,
- >(wasm::linker(&engine).unwrap())
- };
- Self { engine, linker }
+ let engine = tinywasm::Engine::default();
+ let mut store = tinywasm::Store::new(engine.clone());
+ let linker = wasm::linker(&mut store).unwrap();
+ let store = Arc::new(spin::Mutex::new(store));
+ Self {
+ engine,
+ store,
+ linker,
+ }
}
- pub fn compile(&self, data: &[u8]) -> Result<wasmi::Module, wasmi::Error> {
- wasmi::Module::new(&self.engine, data)
+ pub fn compile(&self, data: &[u8]) -> Result<WasmModule, tinywasm::Error> {
+ let module = tinywasm::parse_bytes(data)?;
+ let instance = tinywasm::ModuleInstance::instantiate(
+ &mut self.store.lock(),
+ &module,
+ Some(self.linker.clone()),
+ )?;
+ Ok(WasmModule { instance })
}
- #[allow(clippy::too_many_arguments)]
+ #[expect(clippy::too_many_arguments)]
pub fn run_function(
&self,
script_id: usize,
- module: &wasmi::Module,
+ module: &mut WasmModule,
scripts: &ScriptManager,
- files: &FileManager,
+ files: Box<[Box<Path>]>,
wasm_runner: &WasmRunner,
context: &mut ScriptContext<'_>,
function_name: &str,
- ) -> Result<(), wasmi::Error> {
- // safety: the store won't be kept around after this function ends. Only the
- // lifetimes are being transmuted
- let mut store = {
- puffin::profile_scope!("create store");
- wasmi::Store::new(&self.engine, unsafe {
- std::mem::transmute::<FullScriptContext<'_>, FullScriptContext<'_>>(
- FullScriptContext {
- script_id,
- manifest: context.manifest,
- buffer: context.buffer,
- input: context.input,
- time: context.time,
- storage: context.storage,
- scripts,
- files,
- wasm_runner,
- operation_queue: &mut context.operation_queue,
- },
- )
- })
- };
- let instance = {
- puffin::profile_scope!("instantiate");
- self.linker.instantiate_and_start(&mut store, module)?
- };
- let function = {
- puffin::profile_scope!("get function");
- instance.get_func(&store, function_name).ok_or_else(|| {
- wasmi::Error::new(format!("No function named {function_name} was exported"))
- })?
- };
- {
- puffin::profile_scope!("call function");
- function.call(&mut store, &[], &mut [])
- }
+ ) -> Result<(), tinywasm::Error> {
+ puffin::profile_function!();
+
+ let context = std::ptr::from_mut(Box::leak(Box::new(FullScriptContext {
+ script_id,
+ manifest: context.manifest,
+ buffer: context.buffer,
+ input: context.input,
+ time: context.time,
+ storage: context.storage,
+ scripts,
+ files,
+ wasm_runner,
+ operation_queue: &mut context.operation_queue,
+ })));
+
+ let function = module
+ .instance
+ .func::<u64, ()>(&wasm_runner.store.lock(), function_name)?;
+ let r = function.call(
+ &mut wasm_runner.store.lock(),
+ context.expose_provenance() as u64,
+ );
+ drop(unsafe { Box::from_raw(context) });
+ r
}
}
@@ -185,6 +189,11 @@ 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,17 +214,17 @@ impl<'a> ScriptManager<'a> {
let script = &self.scripts[script_id];
match script {
ScriptDeclaration::Static { .. } => (),
- ScriptDeclaration::Wasm { path, .. } => {
- files.start_loading_file(path, &wasm_runner.engine)
- }
+ 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, files: &FileManager) -> bool {
+ 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::Wasm { path, .. } => files.is_file_loaded(path),
+ ScriptDeclaration::Lua { path, .. } => loaded_files.contains(path),
+ ScriptDeclaration::Wasm { path, .. } => loaded_files.contains(path),
}
}
@@ -223,6 +232,7 @@ impl<'a> ScriptManager<'a> {
let script = &self.scripts[script_id];
match script {
ScriptDeclaration::Static { .. } => (),
+ ScriptDeclaration::Lua { path, .. } => files.unload_file(path),
ScriptDeclaration::Wasm { path, .. } => files.unload_file(path),
}
}
@@ -284,7 +294,9 @@ impl<'a> ScriptManager<'a> {
Operation::UpdateState {
script_id,
new_state,
- } => *get_script_state(&mut self.script_states, *script_id) = new_state.clone(),
+ } => {
+ self.script_states.insert(*script_id, new_state.clone());
+ }
Operation::UpdateEntity { name, new_state } => {
self.entities.insert(name.clone(), new_state.clone());
}
@@ -316,18 +328,46 @@ impl<'a> ScriptManager<'a> {
)
});
}
+ 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 {
+ script_id,
+ manifest: context.manifest,
+ buffer: context.buffer,
+ input: context.input,
+ time: context.time,
+ scripts: self,
+ files: loaded_files,
+ storage: context.storage,
+ wasm_runner,
+ operation_queue: &mut context.operation_queue,
+ },
+ ) {
+ log::error!("{e}");
+ }
+ }
ScriptDeclaration::Wasm { path, .. } => {
- files.load_file(path.clone(), &wasm_runner.engine).unwrap();
- let module = files.get_cached_file(path).unwrap().unwrap_wasm_module();
- wasm_runner.run_function(
+ files.load_file(path.clone(), wasm_runner).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,
self,
- files,
+ loaded_files,
wasm_runner,
context,
"start",
- );
+ ) {
+ log::error!("{e}");
+ }
}
}
self.active_scripts.push(script_id);
@@ -360,18 +400,46 @@ impl<'a> ScriptManager<'a> {
);
});
}
+ 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 {
+ script_id,
+ manifest: context.manifest,
+ buffer: context.buffer,
+ input: context.input,
+ time: context.time,
+ scripts: self,
+ files: loaded_files,
+ storage: context.storage,
+ wasm_runner,
+ operation_queue: &mut context.operation_queue,
+ },
+ ) {
+ log::error!("{e}");
+ }
+ }
ScriptDeclaration::Wasm { path, .. } => {
- files.load_file(path.clone(), &wasm_runner.engine).unwrap();
- let module = files.get_cached_file(path).unwrap().unwrap_wasm_module();
- wasm_runner.run_function(
+ files.load_file(path.clone(), wasm_runner).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,
self,
- files,
+ loaded_files,
wasm_runner,
context,
"end",
- );
+ ) {
+ log::error!("{e}");
+ }
}
};
}
@@ -399,21 +467,46 @@ impl<'a> ScriptManager<'a> {
)
}
}
+ 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 {
+ script_id: *script_id,
+ manifest: context.manifest,
+ buffer: context.buffer,
+ input: context.input,
+ time: context.time,
+ scripts: self,
+ files: loaded_files,
+ storage: context.storage,
+ wasm_runner,
+ operation_queue: &mut context.operation_queue,
+ },
+ ) {
+ log::error!("{e}");
+ }
+ }
ScriptDeclaration::Wasm { path, .. } => {
- let module = {
- puffin::profile_scope!("load module");
- files.load_file(path.clone(), &wasm_runner.engine).unwrap();
- files.get_cached_file(path).unwrap().unwrap_wasm_module()
- };
- wasm_runner.run_function(
+ files.load_file(path.clone(), wasm_runner).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,
self,
- files,
+ loaded_files,
wasm_runner,
context,
"update",
- );
+ ) {
+ log::error!("Error while running scripts: {e}");
+ }
}
}
}
diff --git a/src/script/lua.rs b/src/script/lua.rs
new file mode 100644
index 0000000..a5c7f6a
--- /dev/null
+++ b/src/script/lua.rs
@@ -0,0 +1,495 @@
+use std::time::SystemTime;
+
+use crossterm::{
+ event::KeyCode,
+ style::{Attribute, Attributes, Color, ContentStyle},
+};
+use hex_color::HexColor;
+use mlua::{FromLua, IntoLua, Lua, LuaOptions, StdLib, Table, Value};
+use serde::{Deserialize, Serialize};
+
+use crate::{
+ buffer::Cell,
+ scene::scene_id_by_name,
+ script::{FullScriptContext, Operation},
+};
+
+pub struct LuaModule {
+ interpreter: Lua,
+}
+
+impl LuaModule {
+ pub fn new(data: &[u8]) -> mlua::Result<Self> {
+ macro_rules! add_fn {
+ ($lua: expr, $table: expr, $fn: ident) => {
+ $table.set(stringify!($fn), $lua.create_function($fn)?)?;
+ };
+ }
+
+ macro_rules! add_fns {
+ ($lua: expr, [$($fn: ident),*]) => {
+ let tortuise_module = $lua.create_table()?;
+ $(add_fn!($lua, tortuise_module, $fn);)*
+ let globals = $lua.globals();
+ globals.set("tortuise", tortuise_module)?;
+ $lua.set_globals(globals)?;
+ };
+ }
+
+ let interpreter = Lua::new_with(
+ StdLib::TABLE | StdLib::STRING | StdLib::MATH,
+ LuaOptions::new().catch_rust_panics(true),
+ )?;
+ add_fns!(
+ interpreter,
+ [
+ add_cell,
+ window_size,
+ is_key_pressed_this_frame,
+ log,
+ warn,
+ error,
+ exit,
+ is_scene_loaded,
+ preload_scene,
+ switch_scene,
+ is_script_loaded,
+ preload_script,
+ unload_script,
+ activate_script,
+ deactivate_script,
+ state,
+ set_state,
+ entity,
+ set_entity,
+ storage,
+ set_storage,
+ clock,
+ time,
+ delta_time
+ ]
+ );
+ interpreter.load(data).exec()?;
+ Ok(Self { interpreter })
+ }
+
+ pub fn run_function(&self, func: &str, ctx: FullScriptContext) -> mlua::Result<()> {
+ unsafe {
+ let ctx = std::ptr::from_mut(Box::leak(Box::new(ctx)));
+ self.interpreter.set_app_data(ctx.expose_provenance());
+ self.interpreter
+ .load(format!("if {func} then {func}() end"))
+ .exec()?;
+ drop(Box::from_raw(ctx));
+ Ok(())
+ }
+ }
+}
+
+#[allow(clippy::mut_from_ref)]
+unsafe fn context(lua: &Lua) -> &mut FullScriptContext<'_> {
+ unsafe {
+ std::ptr::with_exposed_provenance_mut::<FullScriptContext>(*lua.app_data_ref().unwrap())
+ .as_mut_unchecked()
+ }
+}
+
+fn add_cell(lua: &Lua, cell: Table) -> mlua::Result<()> {
+ fn parse_color(mut color: String) -> Option<Color> {
+ color.make_ascii_uppercase();
+ color.retain(|c| c != '-' && c != ' ' && c != '_');
+
+ match &*color {
+ "RESET" => return Some(Color::Reset),
+ "BLACK" => return Some(Color::Black),
+ "DARKGREY" => return Some(Color::DarkGrey),
+ "DARKGRAY" => return Some(Color::DarkGrey),
+ "RED" => return Some(Color::Red),
+ "DARKRED" => return Some(Color::DarkRed),
+ "GREEN" => return Some(Color::Green),
+ "DARKGREEN" => return Some(Color::DarkGreen),
+ "YELLOW" => return Some(Color::Yellow),
+ "DARKYELLOW" => return Some(Color::DarkYellow),
+ "BLUE" => return Some(Color::Blue),
+ "DARKBLUE" => return Some(Color::DarkBlue),
+ "MAGENTA" => return Some(Color::Magenta),
+ "DARKMAGENTA" => return Some(Color::DarkMagenta),
+ "CYAN" => return Some(Color::Cyan),
+ "DARKCYAN" => return Some(Color::DarkCyan),
+ "WHITE" => return Some(Color::White),
+ "GREY" => return Some(Color::Grey),
+ "GRAY" => return Some(Color::Grey),
+ _ => (),
+ };
+ if color.starts_with('#') {
+ let color = HexColor::parse(&color).ok()?;
+ return Some(Color::Rgb {
+ r: color.r,
+ g: color.g,
+ b: color.b,
+ });
+ }
+
+ None
+ }
+
+ fn parse_attributes(value: Vec<String>) -> Attributes {
+ let mut attributes = Attributes::none();
+ for mut value in value {
+ value.make_ascii_uppercase();
+ value.retain(|c| c != '-' && c != ' ' && c != '_');
+ match &*value {
+ "BOLD" => attributes.set(Attribute::Bold),
+ "DIM" => attributes.set(Attribute::Dim),
+ "ITALIC" => attributes.set(Attribute::Italic),
+ "UNDERLINED" => attributes.set(Attribute::Underlined),
+ "DOUBLEUNDERLINED" => attributes.set(Attribute::DoubleUnderlined),
+ "UNDERCURLED" => attributes.set(Attribute::Undercurled),
+ "UNDERDOTTED" => attributes.set(Attribute::Underdotted),
+ "SLOWBLINK" => attributes.set(Attribute::SlowBlink),
+ "RAPIDBLINK" => attributes.set(Attribute::RapidBlink),
+ "REVERSE" => attributes.set(Attribute::Reverse),
+ "HIDDEN" => attributes.set(Attribute::Hidden),
+ "CROSSEDOUT" => attributes.set(Attribute::CrossedOut),
+ "FRAKTUR" => attributes.set(Attribute::Fraktur),
+ "FRAMED" => attributes.set(Attribute::Framed),
+ "ENCIRCLED" => attributes.set(Attribute::Encircled),
+ "OVERLINED" => attributes.set(Attribute::OverLined),
+ _ => (),
+ }
+ }
+
+ attributes
+ }
+
+ puffin::profile_function!();
+ unsafe { context(lua) }
+ .operation_queue
+ .push(Operation::AddCell(Cell {
+ character: cell.get("character")?,
+ style: ContentStyle {
+ foreground_color: parse_color(cell.get("foreground_color").unwrap_or_default()),
+ background_color: parse_color(cell.get("background_color").unwrap_or_default()),
+ underline_color: parse_color(cell.get("underline_color").unwrap_or_default()),
+ attributes: parse_attributes(cell.get("attributes").unwrap_or_default()),
+ },
+ x: cell.get("x")?,
+ y: cell.get("y")?,
+ z: cell.get("z")?,
+ }));
+ Ok(())
+}
+
+fn window_size(lua: &Lua, _: ()) -> mlua::Result<Table> {
+ puffin::profile_function!();
+ let window_size = &unsafe { context(lua) }.buffer.window_size;
+ let value = lua.create_table()?;
+ value.set("rows", window_size.rows)?;
+ value.set("columns", window_size.columns)?;
+ value.set("width", window_size.width)?;
+ value.set("height", window_size.height)?;
+ Ok(value)
+}
+
+fn is_key_pressed_this_frame(lua: &Lua, mut key: String) -> mlua::Result<bool> {
+ puffin::profile_function!();
+ key.make_ascii_uppercase();
+ key.retain(|c| c != '-' && c != '_' && c != ' ');
+ let key = match &*key {
+ "BACKSPACE" => KeyCode::Backspace,
+ "ENTER" => KeyCode::Enter,
+ "LEFT" => KeyCode::Left,
+ "RIGHT" => KeyCode::Right,
+ "UP" => KeyCode::Up,
+ "DOWN" => KeyCode::Down,
+ "HOME" => KeyCode::Home,
+ "END" => KeyCode::End,
+ "PAGEUP" => KeyCode::PageUp,
+ "PAGEDOWN" => KeyCode::PageDown,
+ "TAB" => KeyCode::Tab,
+ "BACKTAB" => KeyCode::BackTab,
+ "DELETE" => KeyCode::Delete,
+ "INSERT" => KeyCode::Insert,
+ "ESC" => KeyCode::Esc,
+ "ESCAPE" => KeyCode::Esc,
+ f if f.starts_with('F') => KeyCode::F(f[1..].parse::<u8>().unwrap()),
+ c if c.len() == 1 => KeyCode::Char(c.chars().next().unwrap()),
+ _ => return Err(mlua::Error::runtime(format!("Invalid key code: {key}"))),
+ };
+
+ Ok(unsafe { context(lua).input.is_key_pressed_this_frame(key) })
+}
+
+fn log(_: &Lua, value: Value) -> mlua::Result<()> {
+ puffin::profile_function!();
+ log::info!("{}", value.to_string()?);
+ Ok(())
+}
+
+fn warn(_: &Lua, value: Value) -> mlua::Result<()> {
+ puffin::profile_function!();
+ log::warn!("{}", value.to_string()?);
+ Ok(())
+}
+
+fn error(_: &Lua, value: Value) -> mlua::Result<()> {
+ puffin::profile_function!();
+ log::error!("{}", value.to_string()?);
+ Ok(())
+}
+
+fn exit(lua: &Lua, _: ()) -> mlua::Result<()> {
+ puffin::profile_function!();
+ unsafe { context(lua) }
+ .operation_queue
+ .push(Operation::Exit);
+ 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) };
+ 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::SwitchScene(scene));
+ 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) };
+ 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::ActivateScript(script));
+ Ok(())
+}
+
+fn deactivate_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::DeactivateScript(script));
+ Ok(())
+}
+
+fn state(lua: &Lua, default: MyLuaValue) -> mlua::Result<MyLuaValue> {
+ puffin::profile_function!();
+ let context = unsafe { context(lua) };
+ Ok(context
+ .scripts
+ .script_states
+ .get(&context.script_id)
+ .map(|state| postcard::from_bytes(state).unwrap())
+ .unwrap_or(default))
+}
+
+fn set_state(lua: &Lua, new_state: MyLuaValue) -> mlua::Result<()> {
+ puffin::profile_function!();
+ let context = unsafe { context(lua) };
+ context.operation_queue.push(Operation::UpdateState {
+ script_id: context.script_id,
+ new_state: postcard::to_stdvec(&new_state).unwrap().into_boxed_slice(),
+ });
+ Ok(())
+}
+
+fn entity(lua: &Lua, (name, default): (String, MyLuaValue)) -> mlua::Result<MyLuaValue> {
+ puffin::profile_function!();
+ let context = unsafe { context(lua) };
+ Ok(context
+ .scripts
+ .entities
+ .get(&*name)
+ .map(|state| postcard::from_bytes(state).unwrap())
+ .unwrap_or(default))
+}
+
+fn set_entity(lua: &Lua, (name, new_state): (String, MyLuaValue)) -> mlua::Result<()> {
+ puffin::profile_function!();
+ let context = unsafe { context(lua) };
+ context.operation_queue.push(Operation::UpdateEntity {
+ name: name.into_boxed_str(),
+ new_state: postcard::to_stdvec(&new_state).unwrap().into_boxed_slice(),
+ });
+ Ok(())
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+enum MyLuaValue {
+ Nil,
+ Boolean(bool),
+ Number(f64),
+ String(String),
+ Table(Vec<(MyLuaValue, MyLuaValue)>),
+}
+
+impl FromLua for MyLuaValue {
+ fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
+ Ok(match value {
+ Value::Nil => MyLuaValue::Nil,
+ Value::Boolean(b) => MyLuaValue::Boolean(b),
+ Value::Number(n) => MyLuaValue::Number(n),
+ Value::Integer(n) => MyLuaValue::Number(n as f64),
+ Value::String(s) => MyLuaValue::String(s.to_string_lossy()),
+ Value::Table(t) => {
+ let mut pairs = Vec::new();
+ t.for_each(|k, v| {
+ pairs.push((k, v));
+ Ok(())
+ })?;
+ MyLuaValue::Table(pairs)
+ }
+ _ => return Err(mlua::Error::runtime("Invalid storage type")),
+ })
+ }
+}
+
+impl IntoLua for MyLuaValue {
+ fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
+ Ok(match self {
+ MyLuaValue::Nil => Value::Nil,
+ MyLuaValue::Boolean(b) => Value::Boolean(b),
+ MyLuaValue::Number(n) => Value::Number(n),
+ MyLuaValue::String(s) => Value::String(lua.create_string(s)?),
+ MyLuaValue::Table(items) => {
+ let table = lua.create_table()?;
+ for (k, v) in items {
+ table.set(k.into_lua(lua)?, v.into_lua(lua)?)?;
+ }
+ Value::Table(table)
+ }
+ })
+ }
+}
+
+fn storage(lua: &Lua, default: MyLuaValue) -> mlua::Result<MyLuaValue> {
+ puffin::profile_function!();
+ let context = unsafe { context(lua) };
+ Ok(context
+ .storage
+ .get()
+ .map(|state| postcard::from_bytes(state).unwrap())
+ .unwrap_or(default))
+}
+
+fn set_storage(lua: &Lua, new_state: MyLuaValue) -> mlua::Result<()> {
+ puffin::profile_function!();
+ let context = unsafe { context(lua) };
+ context.operation_queue.push(Operation::UpdateStorage {
+ new_save: postcard::to_stdvec(&new_state).unwrap().into_boxed_slice(),
+ });
+ Ok(())
+}
+
+fn clock(lua: &Lua, _: ()) -> mlua::Result<f64> {
+ puffin::profile_function!();
+ let context = unsafe { context(lua) };
+ Ok(context.time.monotonic_now().as_secs_f64())
+}
+
+fn time(lua: &Lua, input: Option<Table>) -> mlua::Result<f64> {
+ puffin::profile_function!();
+ let context = unsafe { context(lua) };
+ if let Some(input) = input {
+ let date = chrono::NaiveDate::from_ymd_opt(
+ input.get("year")?,
+ input.get("month")?,
+ input.get("day")?,
+ )
+ .unwrap();
+ let time = chrono::NaiveTime::from_hms_opt(
+ input.get("hour").unwrap_or_default(),
+ input.get("min").unwrap_or_default(),
+ input.get("sec").unwrap_or_default(),
+ )
+ .unwrap();
+ let datetime = chrono::NaiveDateTime::new(date, time)
+ .and_local_timezone(chrono::Local)
+ .unwrap();
+ Ok(datetime
+ .signed_duration_since(chrono::DateTime::UNIX_EPOCH)
+ .as_seconds_f64())
+ } else {
+ Ok(context
+ .time
+ .system_now()
+ .duration_since(SystemTime::UNIX_EPOCH)
+ .unwrap()
+ .as_secs_f64())
+ }
+}
+
+fn delta_time(lua: &Lua, _: ()) -> mlua::Result<f64> {
+ puffin::profile_function!();
+ let context = unsafe { context(lua) };
+ Ok(context.time.delta_time().as_secs_f64())
+}
diff --git a/src/script/wasm.rs b/src/script/wasm.rs
index 25632fa..9b9cd91 100644
--- a/src/script/wasm.rs
+++ b/src/script/wasm.rs
@@ -1,41 +1,54 @@
-use core::slice;
use std::time::UNIX_EPOCH;
use crossterm::{
event::KeyCode,
style::{Attribute, Attributes, Color, ContentStyle},
};
-use wasmi::{Caller, Engine, Linker};
+use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store};
use crate::{buffer::Cell, script::FullScriptContext};
use super::Operation;
-unsafe fn str_from_memory<'a>(
- caller: &'a Caller<FullScriptContext<'_>>,
- offset: u32,
- length: u32,
-) -> &'a str {
- let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
+pub struct WasmModule {
+ pub instance: ModuleInstance,
+}
+
+fn str_from_memory(caller: &FuncContext<'_>, offset: u32, length: u32) -> tinywasm::Result<String> {
+ let memory = caller.memory("memory").unwrap();
+ memory.read_string(caller.store(), offset as usize, length as usize)
+}
+
+fn context_mut<'b>(address: u64) -> &'b mut FullScriptContext<'b> {
unsafe {
- str::from_utf8_unchecked(slice::from_raw_parts(
- memory.data_ptr(caller).byte_add(offset as usize),
- length as usize,
- ))
+ std::ptr::with_exposed_provenance_mut::<FullScriptContext<'b>>(address as usize)
+ .as_mut()
+ .unwrap_unchecked()
+ }
+}
+
+fn context_ref<'b>(address: u64) -> &'b FullScriptContext<'b> {
+ unsafe {
+ std::ptr::with_exposed_provenance::<FullScriptContext<'b>>(address as usize)
+ .as_ref()
+ .unwrap_unchecked()
}
}
-#[allow(clippy::too_many_arguments)]
fn add_cell(
- mut caller: Caller<FullScriptContext<'_>>,
- character: u32,
- location: u32,
- z: u32,
- foreground_color: u32,
- background_color: u32,
- underline_color: u32,
- attributes: u32,
-) {
+ _caller: FuncContext<'_>,
+ (
+ context,
+ character,
+ location,
+ z,
+ foreground_color,
+ background_color,
+ underline_color,
+ attributes,
+ ): (u64, u32, u32, u32, u32, u32, u32, u32),
+) -> tinywasm::Result<()> {
+ puffin::profile_function!();
const STANDARD_COLOR: u32 = 1 << 24;
const ANSI_COLOR: u32 = 2 << 24;
const RGB_COLOR: u32 = 3 << 24;
@@ -127,8 +140,7 @@ fn add_cell(
attribute_set.set(Attribute::OverLined);
}
- caller
- .data_mut()
+ context_mut(context)
.operation_queue
.push(Operation::AddCell(Cell {
character: char::from_u32(character).unwrap_or_default(),
@@ -142,14 +154,20 @@ fn add_cell(
y: (location & 0xffff) as u16,
z: z as u8,
}));
+ Ok(())
}
-fn window_size(caller: Caller<FullScriptContext<'_>>) -> u32 {
- let window_size = &caller.data().buffer.window_size;
- ((window_size.rows as u32) << 16) | (window_size.columns as u32)
+fn window_size(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> {
+ puffin::profile_function!();
+ let window_size = &context_ref(context).buffer.window_size;
+ Ok(((window_size.rows as u32) << 16) | (window_size.columns as u32))
}
-fn is_key_pressed_this_frame(caller: Caller<FullScriptContext<'_>>, key_code: u32) -> u32 {
+fn is_key_pressed_this_frame(
+ _caller: FuncContext<'_>,
+ (context, key_code): (u64, u32),
+) -> tinywasm::Result<u32> {
+ puffin::profile_function!();
const BACKSPACE: u32 = 1 << 24;
const ENTER: u32 = 2 << 24;
const LEFT: u32 = 3 << 24;
@@ -189,21 +207,24 @@ fn is_key_pressed_this_frame(caller: Caller<FullScriptContext<'_>>, key_code: u3
_ => KeyCode::Null,
};
- if caller.data().input.is_key_pressed_this_frame(key_code) {
- 1
- } else {
- 0
- }
+ Ok(
+ if context_ref(context)
+ .input
+ .is_key_pressed_this_frame(key_code)
+ {
+ 1
+ } else {
+ 0
+ },
+ )
}
-fn log(caller: Caller<FullScriptContext<'_>>, severity: u32, offset: u32, length: u32) {
- let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
- let message = unsafe {
- str::from_utf8_unchecked(slice::from_raw_parts(
- memory.data_ptr(&caller).byte_add(offset as usize),
- length as usize,
- ))
- };
+fn log(
+ caller: FuncContext<'_>,
+ (severity, offset, length): (u32, u32, u32),
+) -> tinywasm::Result<()> {
+ puffin::profile_function!();
+ let message = str_from_memory(&caller, offset, length)?;
let severity = match severity {
1 => log::Level::Error,
2 => log::Level::Warn,
@@ -213,103 +234,129 @@ fn log(caller: Caller<FullScriptContext<'_>>, severity: u32, offset: u32, length
_ => log::Level::Error,
};
log::log!(severity, "{message}");
+ Ok(())
}
-fn exit(mut caller: Caller<FullScriptContext<'_>>) {
- caller.data_mut().operation_queue.push(Operation::Exit)
+fn exit(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<()> {
+ puffin::profile_function!();
+ context_mut(context).operation_queue.push(Operation::Exit);
+ Ok(())
}
-fn random_u64() -> u64 {
- getrandom::u64().unwrap_or(4)
+fn random_u64(_caller: FuncContext<'_>, (): ()) -> tinywasm::Result<u64> {
+ puffin::profile_function!();
+ Ok(getrandom::u64().unwrap_or(4))
}
-fn scene_id_by_name(caller: Caller<FullScriptContext<'_>>, offset: u32, length: u32) -> u32 {
- let name = unsafe { str_from_memory(&caller, offset, length) };
- crate::scene::scene_id_by_name(&caller.data().manifest.scenes, name)
- .map(|u| u as u32)
- .unwrap_or(u32::MAX)
+fn scene_id_by_name(
+ caller: FuncContext<'_>,
+ (context, offset, length): (u64, u32, u32),
+) -> tinywasm::Result<u32> {
+ puffin::profile_function!();
+ let name = str_from_memory(&caller, offset, length)?;
+ Ok(
+ crate::scene::scene_id_by_name(&context_ref(context).manifest.scenes, &name)
+ .map(|u| u as u32)
+ .unwrap_or(u32::MAX),
+ )
}
-fn preload_scene(mut caller: Caller<FullScriptContext<'_>>, id: u32) {
- caller
- .data_mut()
+fn preload_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::PreloadScene(id as usize));
+ Ok(())
}
-fn is_scene_loaded(caller: Caller<FullScriptContext<'_>>, id: u32) -> u32 {
- let context = caller.data();
- crate::scene::is_scene_loaded(
+fn is_scene_loaded(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<u32> {
+ puffin::profile_function!();
+ let context = context_ref(context);
+ Ok(crate::scene::is_scene_loaded(
&context.manifest.scenes,
context.scripts,
- context.files,
+ &context.files,
id as usize,
- ) as u32
+ ) as u32)
}
-fn switch_scene(mut caller: Caller<FullScriptContext<'_>>, id: u32) {
- caller
- .data_mut()
+fn switch_scene(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> {
+ puffin::profile_function!();
+ context_mut(context)
.operation_queue
.push(Operation::SwitchScene(id as usize));
+ Ok(())
}
-fn script_id_by_name(caller: Caller<FullScriptContext<'_>>, offset: u32, length: u32) -> u32 {
- let context = caller.data();
- let name = unsafe { str_from_memory(&caller, offset, length) };
- context
+fn script_id_by_name(
+ caller: FuncContext<'_>,
+ (context, offset, length): (u64, u32, u32),
+) -> tinywasm::Result<u32> {
+ puffin::profile_function!();
+ let name = str_from_memory(&caller, offset, length)?;
+ let context = context_ref(context);
+ Ok(context
.scripts
- .script_id_by_name(name)
+ .script_id_by_name(&name)
.map(|u| u as u32)
- .unwrap_or(u32::MAX)
+ .unwrap_or(u32::MAX))
}
-fn preload_script(mut caller: Caller<FullScriptContext<'_>>, id: u32) {
- caller
- .data_mut()
+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: Caller<FullScriptContext<'_>>, id: u32) -> u32 {
- let context = caller.data();
- context.scripts.is_script_loaded(id as usize, context.files) as u32
+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(mut caller: Caller<FullScriptContext<'_>>, id: u32) {
- caller
- .data_mut()
+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(mut caller: Caller<FullScriptContext<'_>>, id: u32) {
- caller
- .data_mut()
+fn activate_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> {
+ puffin::profile_function!();
+ context_mut(context)
.operation_queue
.push(Operation::ActivateScript(id as usize));
+ Ok(())
}
-fn deactivate_script(mut caller: Caller<FullScriptContext<'_>>, id: u32) {
- caller
- .data_mut()
+fn deactivate_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> {
+ puffin::profile_function!();
+ context_mut(context)
.operation_queue
.push(Operation::DeactivateScript(id as usize));
+ Ok(())
}
-fn state_size(caller: Caller<FullScriptContext<'_>>) -> u32 {
- let context = caller.data();
- context
+fn state_size(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> {
+ puffin::profile_function!();
+ let context = context_ref(context);
+ Ok(context
.scripts
.script_states
.get(&context.script_id)
.map(|state| state.len())
- .unwrap_or_default() as u32
+ .unwrap_or_default() as u32)
}
-fn load_state(mut caller: Caller<FullScriptContext<'_>>, offset: u32) {
- let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
- let context = caller.data();
+fn load_state(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
.scripts
.script_states
@@ -317,119 +364,153 @@ fn load_state(mut caller: Caller<FullScriptContext<'_>>, offset: u32) {
.map(|state| &**state)
.unwrap_or(&[]);
memory.write(&mut caller, offset as usize, state).unwrap();
+ Ok(())
}
-fn save_state(mut caller: Caller<FullScriptContext<'_>>, offset: u32, length: u32) {
+fn save_state(
+ 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.get_export("memory").unwrap().into_memory().unwrap();
- let new_state = Box::from(&memory.data(&caller)[offset..(offset + length)]);
- let context = caller.data_mut();
+ let memory = caller.memory("memory").unwrap();
+ let new_state = memory
+ .read_vec(caller.store_mut(), offset, length)
+ .unwrap()
+ .into_boxed_slice();
+ let context = context_mut(context);
context.operation_queue.push(Operation::UpdateState {
script_id: context.script_id,
new_state,
});
+ Ok(())
}
-fn entity_size(caller: Caller<FullScriptContext<'_>>, name: u32, name_len: u32) -> u32 {
- let context = caller.data();
- let name = unsafe { str_from_memory(&caller, name, name_len) };
- context
+fn entity_size(
+ caller: FuncContext<'_>,
+ (context, name, name_len): (u64, u32, u32),
+) -> tinywasm::Result<u32> {
+ puffin::profile_function!();
+ let name = str_from_memory(&caller, name, name_len)?;
+ let context = context_ref(context);
+ Ok(context
.scripts
.entities
- .get(name)
+ .get(&*name)
.map(|entity| entity.len())
- .unwrap_or_default() as u32
+ .unwrap_or_default() as u32)
}
-fn load_entity(mut caller: Caller<FullScriptContext<'_>>, name: u32, name_len: u32, offset: u32) {
- let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
- let name = unsafe { str_from_memory(&caller, name, name_len) };
- let context = caller.data();
+fn load_entity(
+ mut caller: FuncContext<'_>,
+ (context, name, name_len, offset): (u64, u32, u32, u32),
+) -> tinywasm::Result<()> {
+ puffin::profile_function!();
+ let name = str_from_memory(&caller, name, name_len)?;
+ let memory = caller.memory("memory").unwrap();
+ let context = context_ref(context);
let entity = context
.scripts
.entities
- .get(name)
+ .get(&*name)
.map(|entity| &**entity)
.unwrap_or(&[]);
memory.write(&mut caller, offset as usize, entity).unwrap();
+ Ok(())
}
fn save_entity(
- mut caller: Caller<FullScriptContext<'_>>,
- name: u32,
- name_len: u32,
- offset: u32,
- length: u32,
-) {
+ mut caller: FuncContext<'_>,
+ (context, name, name_len, offset, length): (u64, u32, u32, u32, u32),
+) -> tinywasm::Result<()> {
+ puffin::profile_function!();
let offset = offset as usize;
let length = length as usize;
- let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
- let new_state = Box::from(&memory.data(&caller)[offset..(offset + length)]);
- let name = Box::from(unsafe { str_from_memory(&caller, name, name_len) });
- let context = caller.data_mut();
+ let memory = caller.memory("memory").unwrap();
+ let new_state = memory
+ .read_vec(caller.store_mut(), offset, length)
+ .unwrap()
+ .into_boxed_slice();
+ let name = str_from_memory(&caller, name, name_len)?.into_boxed_str();
+ let context = context_mut(context);
context
.operation_queue
.push(Operation::UpdateEntity { name, new_state });
+ Ok(())
}
-fn storage_size(caller: Caller<FullScriptContext<'_>>) -> u32 {
- caller
- .data()
+fn storage_size(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> {
+ puffin::profile_function!();
+ Ok(context_ref(context)
.storage
.get()
.map(|data| data.len())
- .unwrap_or_default() as u32
+ .unwrap_or_default() as u32)
}
-fn load_storage(mut caller: Caller<FullScriptContext<'_>>, offset: u32) {
- let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
- let context = caller.data();
+fn load_storage(
+ 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.get().unwrap_or(&[]);
memory.write(&mut caller, offset as usize, state).unwrap();
+ Ok(())
}
-fn save_storage(mut caller: Caller<FullScriptContext<'_>>, offset: u32, length: u32) {
+fn save_storage(
+ 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.get_export("memory").unwrap().into_memory().unwrap();
- let new_save = Box::from(&memory.data(&caller)[offset..(offset + length)]);
- let context = caller.data_mut();
+ 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::UpdateStorage { new_save });
+ Ok(())
}
-fn system_now(caller: Caller<FullScriptContext<'_>>) -> u64 {
- caller
- .data()
+fn system_now(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u64> {
+ puffin::profile_function!();
+ Ok(context_ref(context)
.time
.system_now()
.duration_since(UNIX_EPOCH)
.unwrap()
- .as_nanos() as u64
+ .as_nanos() as u64)
}
-fn monotonic_now(caller: Caller<FullScriptContext<'_>>) -> u64 {
- caller.data().time.monotonic_now().as_nanos() as u64
+fn monotonic_now(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u64> {
+ puffin::profile_function!();
+ Ok(context_ref(context).time.monotonic_now().as_nanos() as u64)
}
-fn delta_time(caller: Caller<FullScriptContext<'_>>) -> u64 {
- caller.data().time.delta_time().as_nanos() as u64
+fn delta_time(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u64> {
+ puffin::profile_function!();
+ Ok(context_ref(context).time.delta_time().as_nanos() as u64)
}
-pub fn linker<'ctx>(
- engine: &Engine,
-) -> Result<wasmi::Linker<FullScriptContext<'ctx>>, wasmi::Error> {
+pub fn linker(store: &mut Store) -> Result<Imports, tinywasm::Error> {
macro_rules! add_fns {
- ($linker: expr, [$($func: ident),*]) => {
- $($linker.func_wrap("tortuise", stringify!($func), $func)?;)*
+ ($linker: expr, $store: expr, [$($func: ident),*]) => {
+ $($linker.define("tortuise", stringify!($func), HostFunction::from($store, $func));)*
};
}
- let mut linker = Linker::new(engine);
+ let mut linker = Imports::new();
add_fns!(
linker,
+ store,
[
add_cell,
window_size,
diff --git a/src/storage.rs b/src/storage.rs
new file mode 100644
index 0000000..90abf05
--- /dev/null
+++ b/src/storage.rs
@@ -0,0 +1,41 @@
+use etcetera::{AppStrategy, AppStrategyArgs, app_strategy::Xdg};
+use smol::unblock;
+
+pub struct StorageManager {
+ xdg: Xdg,
+ data: Option<Box<[u8]>>,
+}
+
+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 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 })
+ }
+
+ pub fn get(&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");
+
+ self.data = Some(data.clone());
+ unblock(move || {
+ 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"));
+ }
+
+ std::fs::rename(tmp_file, save_file)?;
+ Ok(())
+ });
+ }
+}