diff options
| -rw-r--r-- | .cargo/config.toml | 2 | ||||
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.lock | 166 | ||||
| -rw-r--r-- | Cargo.toml | 11 | ||||
| -rw-r--r-- | config.toml | 2 | ||||
| -rw-r--r-- | rustfmt.toml | 3 | ||||
| -rw-r--r-- | src/allocator.rs | 204 | ||||
| -rw-r--r-- | src/buffer.rs | 235 | ||||
| -rw-r--r-- | src/entity.rs | 133 | ||||
| -rw-r--r-- | src/input.rs | 84 | ||||
| -rw-r--r-- | src/lib.rs | 29 | ||||
| -rw-r--r-- | src/log.rs | 36 | ||||
| -rw-r--r-- | src/process.rs | 14 | ||||
| -rw-r--r-- | src/random.rs | 73 | ||||
| -rw-r--r-- | src/scene.rs | 32 | ||||
| -rw-r--r-- | src/script.rs | 44 | ||||
| -rw-r--r-- | src/state.rs | 99 | ||||
| -rw-r--r-- | src/storage.rs | 84 | ||||
| -rw-r--r-- | src/time.rs | 142 |
19 files changed, 1394 insertions, 0 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..f4e8c00 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..45fd3d4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,166 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "messagepack-core" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fe590f7b7d58bfefd83d9995c0b09eaecb8607e4bb66a658c95c0bd691f876" +dependencies = [ + "num-traits", +] + +[[package]] +name = "messagepack-serde" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a630d3ff4e4c893267925baffa3f8e75f0cdb20d212170ee7d8576a45ac1869" +dependencies = [ + "messagepack-core", + "num-traits", + "serde", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "talc" +version = "5.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a5888b3189a89207eea4190e74387b5db3150d213f84186cac637dcd55259d" +dependencies = [ + "allocator-api2", + "lock_api", +] + +[[package]] +name = "tortuise-script" +version = "0.1.0" +dependencies = [ + "log", + "messagepack-serde", + "rand", + "serde", + "talc", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9e3073e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "tortuise-script" +version = "0.1.0" +edition = "2024" + +[dependencies] +log = "0.4" +messagepack-serde = "0.2" +rand = { version = "0.10", default-features = false } +serde = "1" +talc = "5" diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..f4e8c00 --- /dev/null +++ b/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..d3d0d1e --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +edition = "2024" +hard_tabs = true +newline_style = "Unix" diff --git a/src/allocator.rs b/src/allocator.rs new file mode 100644 index 0000000..eaaba5f --- /dev/null +++ b/src/allocator.rs @@ -0,0 +1,204 @@ +use core::{ + alloc::{GlobalAlloc, Layout}, + sync::atomic::{AtomicUsize, Ordering::Relaxed}, +}; + +pub struct TalcAllocator( + talc::cell::TalcSyncCell<talc::wasm::WasmGrowAndExtend, talc::wasm::WasmBinning>, +); + +impl TalcAllocator { + pub const fn new() -> Self { + Self(talc::cell::TalcSyncCell::new_wasm( + talc::wasm::WasmGrowAndExtend::new(), + )) + } +} + +impl Default for TalcAllocator { + fn default() -> Self { + Self::new() + } +} + +unsafe impl GlobalAlloc for TalcAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + unsafe { self.0.alloc(layout) } + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + unsafe { + self.0.dealloc(ptr, layout); + } + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + unsafe { self.0.alloc_zeroed(layout) } + } + + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + unsafe { self.0.realloc(ptr, layout, new_size) } + } +} + +const CHUNK_SIZE: usize = 64; +const PAGE_SIZE: usize = 64 * 1024; + +unsafe extern "C" { + safe static __heap_base: usize; +} + +pub struct BumpAllocator { + offset: AtomicUsize, + capacity: AtomicUsize, + free_8: AtomicUsize, + free_16: AtomicUsize, + free_32: AtomicUsize, + free_64: AtomicUsize, + free_128: AtomicUsize, + free_256: AtomicUsize, + free_512: AtomicUsize, + free_1024: AtomicUsize, + free_2048: AtomicUsize, + free_4096: AtomicUsize, + free_8192: AtomicUsize, + free_16384: AtomicUsize, + free_32768: AtomicUsize, +} + +fn space_for(layout: Layout) -> usize { + usize::div_ceil(layout.size(), CHUNK_SIZE) * CHUNK_SIZE +} + +fn pop_free_list(list: &AtomicUsize) -> Option<*mut u8> { + let offset = list.swap(0, Relaxed); + if offset != 0 { + // safety: offset is a valid pointer for these operations when non-zero + unsafe { + let ptr = offset as *mut u8; + let next = *ptr.cast::<usize>(); + list.swap(next, Relaxed); + Some(ptr) + } + } else { + None + } +} + +unsafe fn push_free_list(list: &AtomicUsize, ptr: *mut u8) { + // safety: all of the pointers here are valid + unsafe { + let new_offset = ptr.addr(); + let current_head = list.swap(new_offset, Relaxed); + *ptr.cast::<usize>() = current_head; + } +} + +impl BumpAllocator { + pub const fn new() -> Self { + Self { + offset: AtomicUsize::new(8), + capacity: AtomicUsize::new(PAGE_SIZE - 8), + free_8: AtomicUsize::new(0), + free_16: AtomicUsize::new(0), + free_32: AtomicUsize::new(0), + free_64: AtomicUsize::new(0), + free_128: AtomicUsize::new(0), + free_256: AtomicUsize::new(0), + free_512: AtomicUsize::new(0), + free_1024: AtomicUsize::new(0), + free_2048: AtomicUsize::new(0), + free_4096: AtomicUsize::new(0), + free_8192: AtomicUsize::new(0), + free_16384: AtomicUsize::new(0), + free_32768: AtomicUsize::new(0), + } + } +} + +impl Default for BumpAllocator { + fn default() -> Self { + Self::new() + } +} + +unsafe impl GlobalAlloc for BumpAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + self.offset.fetch_max(__heap_base, Relaxed); + let space_needed = space_for(layout); + let ptr = match space_needed { + 8 => pop_free_list(&self.free_8), + 16 => pop_free_list(&self.free_16), + 32 => pop_free_list(&self.free_32), + 64 => pop_free_list(&self.free_64), + 128 => pop_free_list(&self.free_128), + 256 => pop_free_list(&self.free_256), + 512 => pop_free_list(&self.free_512), + 1024 => pop_free_list(&self.free_1024), + 2048 => pop_free_list(&self.free_2048), + 4096 => pop_free_list(&self.free_4096), + 8192 => pop_free_list(&self.free_8192), + 16384 => pop_free_list(&self.free_16384), + 32768 => pop_free_list(&self.free_32768), + _ => None, + }; + if let Some(ptr) = ptr { + return ptr; + } + + let offset = self.offset.fetch_add(space_needed, Relaxed); + let pointer = offset as *mut u8; + let available_space = self.capacity.load(Relaxed) - offset; + if available_space < layout.size() { + let pages = layout.size().div_ceil(PAGE_SIZE); + core::arch::wasm32::memory_grow(0, pages); + self.capacity.fetch_add(pages * PAGE_SIZE, Relaxed); + } + + pointer + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + let space_used = space_for(layout); + let ptr_offset = ptr.addr(); + if self + .offset + .compare_exchange(ptr_offset + space_used, ptr_offset, Relaxed, Relaxed) + .is_ok() + { + return; + } + + // safety: ptr must be valid and on the heap + unsafe { + match space_used { + 8 => push_free_list(&self.free_8, ptr), + 16 => push_free_list(&self.free_16, ptr), + 32 => push_free_list(&self.free_32, ptr), + 64 => push_free_list(&self.free_64, ptr), + 128 => push_free_list(&self.free_128, ptr), + 256 => push_free_list(&self.free_256, ptr), + 512 => push_free_list(&self.free_512, ptr), + 1024 => push_free_list(&self.free_1024, ptr), + 2048 => push_free_list(&self.free_2048, ptr), + 4096 => push_free_list(&self.free_4096, ptr), + 8192 => push_free_list(&self.free_8192, ptr), + 16384 => push_free_list(&self.free_16384, ptr), + 32768 => push_free_list(&self.free_32768, ptr), + _ => {} + }; + } + } + + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + let space_used = space_for(layout); + if new_size <= space_used { + return ptr; + } + + unsafe { + self.dealloc(ptr, layout); + self.alloc(Layout::from_size_align(new_size, layout.align()).unwrap_unchecked()) + } + } +} diff --git a/src/buffer.rs b/src/buffer.rs new file mode 100644 index 0000000..78b6bd5 --- /dev/null +++ b/src/buffer.rs @@ -0,0 +1,235 @@ +use serde::{Deserialize, Serialize}; + +use crate::ScriptContext; + +mod tortuise { + #[link(wasm_import_module = "tortuise")] + unsafe extern "C" { + pub safe fn add_cell( + context: u64, + character: u32, + location: u32, + z: u32, + foreground_color: u32, + background_color: u32, + underline_color: u32, + attributes: u32, + ); + pub safe fn window_size(context: u64) -> u32; + } +} + +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct Cell { + pub character: char, + pub foreground_color: Color, + pub background_color: Color, + pub underline_color: Color, + pub attributes: Attributes, + pub x: u16, + pub y: u16, + pub z: u8, +} + +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct WindowSize { + pub rows: u16, + pub columns: u16, +} + +#[derive( + Copy, Clone, Debug, Default, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize, +)] +pub enum Color { + /// Resets the terminal color. + #[default] + Reset, + + /// Black color. + Black, + + /// Dark grey color. + DarkGrey, + + /// Light red color. + Red, + + /// Dark red color. + DarkRed, + + /// Light green color. + Green, + + /// Dark green color. + DarkGreen, + + /// Light yellow color. + Yellow, + + /// Dark yellow color. + DarkYellow, + + /// Light blue color. + Blue, + + /// Dark blue color. + DarkBlue, + + /// Light magenta color. + Magenta, + + /// Dark magenta color. + DarkMagenta, + + /// Light cyan color. + Cyan, + + /// Dark cyan color. + DarkCyan, + + /// White color. + White, + + /// Grey color. + Grey, + + /// An RGB color. See [RGB color model](https://en.wikipedia.org/wiki/RGB_color_model) for more info. + /// + /// Most UNIX terminals and Windows 10 supported only. + /// See [Platform-specific notes](enum.Color.html#platform-specific-notes) for more info. + Rgb { r: u8, g: u8, b: u8 }, + + /// An ANSI color. See [256 colors - cheat sheet](https://jonasjacek.github.io/colors/) for more info. + /// + /// Most UNIX terminals and Windows 10 supported only. + /// See [Platform-specific notes](enum.Color.html#platform-specific-notes) for more info. + AnsiValue(u8), +} + +#[derive( + Copy, Clone, Debug, Default, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize, +)] +pub struct Attributes(u32); + +pub enum Attribute { + /// Increases the text intensity. + Bold = 1, + /// Decreases the text intensity. + Dim = 2, + /// Emphasises the text. + Italic = 4, + /// Underlines the text. + Underlined = 8, + + // Other types of underlining + /// Double underlines the text. + DoubleUnderlined = 16, + /// Undercurls the text. + Undercurled = 32, + /// Underdots the text. + Underdotted = 64, + /// Underdashes the text. + Underdashed = 128, + + /// Makes the text blinking (< 150 per minute). + SlowBlink = 256, + /// Makes the text blinking (>= 150 per minute). + RapidBlink = 512, + /// Swaps foreground and background colors. + Reverse = 1024, + /// Hides the text (also known as Conceal). + Hidden = 2048, + /// Crosses the text. + CrossedOut = 4096, + /// Sets the [Fraktur](https://en.wikipedia.org/wiki/Fraktur) typeface. + /// + /// Mostly used for [mathematical alphanumeric symbols](https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols). + Fraktur = 8192, + /// Makes the text framed. + Framed = 16384, + /// Makes the text encircled. + Encircled = 32768, + /// Draws a line at the top of the text. + OverLined = 65536, +} + +fn color_to_u32(color: Color) -> u32 { + match color { + Color::Reset => 1 << 24, + Color::Black => 1 << 24 | 1, + Color::DarkGrey => 1 << 24 | 2, + Color::Red => 1 << 24 | 3, + Color::DarkRed => 1 << 24 | 4, + Color::Green => 1 << 24 | 5, + Color::DarkGreen => 1 << 24 | 6, + Color::Yellow => 1 << 24 | 7, + Color::DarkYellow => 1 << 24 | 8, + Color::Blue => 1 << 24 | 9, + Color::DarkBlue => 1 << 24 | 10, + Color::Magenta => 1 << 24 | 11, + Color::DarkMagenta => 1 << 24 | 12, + Color::Cyan => 1 << 24 | 13, + Color::DarkCyan => 1 << 24 | 14, + Color::White => 1 << 24 | 15, + Color::Grey => 1 << 24 | 16, + Color::AnsiValue(num) => 2 << 24 | num as u32, + Color::Rgb { r, g, b } => 3 << 24 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32), + } +} + +impl Attributes { + pub fn none() -> Self { + Self(0) + } + + pub fn with(self, attribute: Attribute) -> Self { + Self(self.0 | attribute as u32) + } + + pub fn without(self, attribute: Attribute) -> Self { + Self(self.0 & !(attribute as u32)) + } + + pub fn set(&mut self, attribute: Attribute) { + self.0 |= attribute as u32; + } + + pub fn unset(&mut self, attribute: Attribute) { + self.0 &= !(attribute as u32) + } + + pub fn has(self, attribute: Attribute) -> bool { + self.0 & (attribute as u32) != 0 + } + + pub fn extend(&mut self, attributes: Attributes) { + self.0 |= attributes.0 + } + + pub fn is_empty(self) -> bool { + self.0 == 0 + } +} + +impl ScriptContext { + pub fn add_cell(&self, cell: Cell) { + tortuise::add_cell( + self.0, + cell.character as u32, + (cell.x as u32) << 16 | cell.y as u32, + cell.z as u32, + color_to_u32(cell.foreground_color), + color_to_u32(cell.background_color), + color_to_u32(cell.underline_color), + cell.attributes.0, + ); + } + + pub fn window_size(&self) -> WindowSize { + let size = tortuise::window_size(self.0); + WindowSize { + rows: (size >> 16) as u16, + columns: (size & 0xffff) as u16, + } + } +} diff --git a/src/entity.rs b/src/entity.rs new file mode 100644 index 0000000..9cf6f17 --- /dev/null +++ b/src/entity.rs @@ -0,0 +1,133 @@ +use core::{ + alloc::Layout, + marker::PhantomData, + ops::{Deref, DerefMut}, +}; + +use alloc::{ + alloc::{alloc, dealloc}, + borrow::Cow, + string::String, +}; +use serde::{Deserialize, Serialize}; + +use crate::ScriptContext; + +mod tortuise { + #[link(wasm_import_module = "tortuise")] + unsafe extern "C" { + pub unsafe fn entity_size(context: u64, name: *const u8, name_len: usize) -> usize; + pub unsafe fn load_entity(context: u64, name: *const u8, name_len: usize, offset: *mut u8); + pub unsafe fn save_entity( + context: u64, + name: *const u8, + name_len: usize, + offset: *const u8, + size: usize, + ); + } +} + +fn load_entity<'de, T: Deserialize<'de>>(context: u64, name: &str) -> Option<T> { + unsafe { + let size = tortuise::entity_size(context, name.as_ptr(), name.len()); + let Ok(layout) = Layout::array::<u8>(size) else { + log::error!("Failed to create layout for size: {size}"); + panic!(); + }; + let ptr = alloc(layout); + tortuise::load_entity(context, name.as_ptr(), name.len(), ptr); + let data = core::slice::from_raw_parts(ptr, size); + let data = messagepack_serde::from_slice(data).ok()?; + dealloc(ptr, layout); + Some(data) + } +} + +fn save_entity<T: Serialize>(context: u64, name: &str, state: &T) { + unsafe { + let data = messagepack_serde::to_vec(state).unwrap(); + tortuise::save_entity( + context, + name.as_ptr(), + name.len(), + data.as_ptr(), + data.len(), + ); + } +} + +pub struct Entity<T> { + name: Cow<'static, str>, + default: fn() -> T, +} + +pub struct EntityRef<'entity, T> { + entity: PhantomData<&'entity Entity<T>>, + data: T, +} + +pub struct EntityMut<'entity, T: Serialize> { + context: u64, + entity: &'entity Entity<T>, + data: T, +} + +impl<'de, T: Serialize + Deserialize<'de>> Entity<T> { + pub const fn new(name: &'static str, default: fn() -> T) -> Self { + Self { + name: Cow::Borrowed(name), + default, + } + } + + pub const fn from_string(name: String, default: fn() -> T) -> Self { + Self { + name: Cow::Owned(name), + default, + } + } + + pub fn get(&self, context: &ScriptContext) -> EntityRef<'_, T> { + EntityRef { + entity: PhantomData, + data: load_entity(context.0, &self.name).unwrap_or_else(self.default), + } + } + + pub fn get_mut(&mut self, context: &ScriptContext) -> EntityMut<'_, T> { + EntityMut { + context: context.0, + entity: self, + data: load_entity(context.0, &self.name).unwrap_or_else(self.default), + } + } +} + +impl<T> Deref for EntityRef<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl<T: Serialize> Deref for EntityMut<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl<T: Serialize> DerefMut for EntityMut<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.data + } +} + +impl<T: Serialize> Drop for EntityMut<'_, T> { + fn drop(&mut self) { + save_entity(self.context, &self.entity.name, &self.data); + } +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..a72597d --- /dev/null +++ b/src/input.rs @@ -0,0 +1,84 @@ +use serde::{Deserialize, Serialize}; + +use crate::ScriptContext; + +mod tortuise { + #[link(wasm_import_module = "tortuise")] + unsafe extern "C" { + pub safe fn is_key_pressed_this_frame(context: u64, keycode: u32) -> u32; + } +} + +/// Represents a key. +#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash, Serialize, Deserialize)] +pub enum KeyCode { + /// Backspace key (Delete on macOS, Backspace on other platforms). + Backspace, + /// Enter key. + Enter, + /// Left arrow key. + Left, + /// Right arrow key. + Right, + /// Up arrow key. + Up, + /// Down arrow key. + Down, + /// Home key. + Home, + /// End key. + End, + /// Page up key. + PageUp, + /// Page down key. + PageDown, + /// Tab key. + Tab, + /// Shift + Tab key. + BackTab, + /// Delete key. (Fn+Delete on macOS, Delete on other platforms) + Delete, + /// Insert key. + Insert, + /// F key. + /// + /// `KeyCode::F(1)` represents F1 key, etc. + F(u8), + /// A character. + /// + /// `KeyCode::Char('c')` represents `c` character, etc. + Char(char), + /// Null. + Null, + /// Escape key. + Esc, +} + +fn key_code_to_number(key_code: KeyCode) -> u32 { + match key_code { + KeyCode::Null => 0, + KeyCode::Backspace => 1 << 24, + KeyCode::Enter => 2 << 24, + KeyCode::Left => 3 << 24, + KeyCode::Right => 4 << 24, + KeyCode::Up => 5 << 24, + KeyCode::Down => 6 << 24, + KeyCode::Home => 7 << 24, + KeyCode::End => 8 << 24, + KeyCode::PageUp => 9 << 24, + KeyCode::PageDown => 10 << 24, + KeyCode::Tab => 11 << 24, + KeyCode::BackTab => 12 << 24, + KeyCode::Delete => 13 << 24, + KeyCode::Insert => 14 << 24, + KeyCode::F(f) => 15 << 24 | f as u32, + KeyCode::Char(c) => 16 << 24 | c as u32, + KeyCode::Esc => 17 << 24, + } +} + +impl ScriptContext { + pub fn is_key_pressed_this_frame(&self, key_code: KeyCode) -> bool { + tortuise::is_key_pressed_this_frame(self.0, key_code_to_number(key_code)) > 0 + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..bc94658 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,29 @@ +#![no_std] + +extern crate alloc; + +pub mod allocator; +pub mod buffer; +pub mod entity; +pub mod input; +pub mod log; +pub mod process; +pub mod random; +pub mod scene; +pub mod script; +pub mod state; +pub mod storage; +pub mod time; + +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct ScriptContext(u64); + +#[macro_export] +macro_rules! prelude { + () => { + #[global_allocator] + static ALLOCATOR: $crate::allocator::BumpAllocator = + $crate::allocator::BumpAllocator::new(); + }; +} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..306a0d5 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,36 @@ +use alloc::string::ToString; +use log::{LevelFilter, Log, Metadata, Record, SetLoggerError, set_logger, set_max_level}; + +mod tortuise { + use log::Level; + + #[link(wasm_import_module = "tortuise")] + unsafe extern "C" { + pub unsafe fn log(severity: Level, message: *const u8, length: usize); + } +} + +static LOG: TortuiseLogger = TortuiseLogger; + +pub struct TortuiseLogger; + +impl TortuiseLogger { + pub fn init() -> Result<(), SetLoggerError> { + set_logger(&LOG).map(|()| set_max_level(LevelFilter::Trace)) + } +} + +impl Log for TortuiseLogger { + fn enabled(&self, _: &Metadata) -> bool { + true + } + + fn log(&self, record: &Record) { + let message = record.args().to_string(); + unsafe { + tortuise::log(record.level(), message.as_ptr(), message.len()); + } + } + + fn flush(&self) {} +} diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 0000000..ed8c542 --- /dev/null +++ b/src/process.rs @@ -0,0 +1,14 @@ +use crate::ScriptContext; + +mod tortuise { + #[link(wasm_import_module = "tortuise")] + unsafe extern "C" { + pub safe fn exit(context: u64) -> !; + } +} + +impl ScriptContext { + pub fn exit(&self) -> ! { + tortuise::exit(self.0) + } +} diff --git a/src/random.rs b/src/random.rs new file mode 100644 index 0000000..334b6bf --- /dev/null +++ b/src/random.rs @@ -0,0 +1,73 @@ +use rand::{Rng, SeedableRng, TryRng, rngs::SmallRng}; + +mod tortuise { + #[link(wasm_import_module = "tortuise")] + unsafe extern "C" { + pub safe fn random_u64() -> u64; + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TortuiseRng(SmallRng); + +impl TortuiseRng { + pub fn new() -> Self { + Self::seed_from_u64(tortuise::random_u64()) + } +} + +impl Default for TortuiseRng { + fn default() -> Self { + Self::new() + } +} + +impl SeedableRng for TortuiseRng { + type Seed = <SmallRng as SeedableRng>::Seed; + + fn from_seed(seed: Self::Seed) -> Self { + Self(SmallRng::from_seed(seed)) + } + + fn seed_from_u64(state: u64) -> Self { + Self(SmallRng::seed_from_u64(state)) + } + + fn from_rng<R: Rng + ?Sized>(rng: &mut R) -> Self { + Self(SmallRng::from_rng(rng)) + } + + fn try_from_rng<R: TryRng + ?Sized>(rng: &mut R) -> Result<Self, R::Error> { + Ok(Self(SmallRng::try_from_rng(rng)?)) + } + + fn fork(&mut self) -> Self + where + Self: Rng, + { + Self(self.0.fork()) + } + + fn try_fork(&mut self) -> Result<Self, <Self as TryRng>::Error> + where + Self: TryRng, + { + Ok(Self(self.0.try_fork()?)) + } +} + +impl TryRng for TortuiseRng { + type Error = <SmallRng as TryRng>::Error; + + fn try_next_u32(&mut self) -> Result<u32, Self::Error> { + self.0.try_next_u32() + } + + fn try_next_u64(&mut self) -> Result<u64, Self::Error> { + self.0.try_next_u64() + } + + fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> { + self.0.try_fill_bytes(dst) + } +} diff --git a/src/scene.rs b/src/scene.rs new file mode 100644 index 0000000..95284e9 --- /dev/null +++ b/src/scene.rs @@ -0,0 +1,32 @@ +use crate::ScriptContext; + +mod tortuise { + #[link(wasm_import_module = "tortuise")] + unsafe extern "C" { + pub unsafe fn scene_id_by_name(context: u64, name: *const u8, length: usize) -> usize; + pub safe fn preload_scene(context: u64, id: usize); + pub safe fn is_scene_loaded(context: u64, id: usize) -> u32; + pub safe fn switch_scene(context: u64, id: usize); + } +} + +impl ScriptContext { + pub fn scene_id(&self, name: &str) -> usize { + unsafe { tortuise::scene_id_by_name(self.0, name.as_ptr(), name.len()) } + } + + pub fn preload_scene(&self, name: &str) { + let id = self.scene_id(name); + tortuise::preload_scene(self.0, id); + } + + pub fn is_scene_loaded(&self, name: &str) -> bool { + let id = self.scene_id(name); + tortuise::is_scene_loaded(self.0, id) > 0 + } + + pub fn switch_scene(&self, name: &str) { + let id = self.scene_id(name); + tortuise::switch_scene(self.0, id); + } +} diff --git a/src/script.rs b/src/script.rs new file mode 100644 index 0000000..0de2b54 --- /dev/null +++ b/src/script.rs @@ -0,0 +1,44 @@ +use crate::ScriptContext; + +mod tortuise { + #[link(wasm_import_module = "tortuise")] + unsafe extern "C" { + pub unsafe fn script_id_by_name(context: u64, name: *const u8, length: usize) -> usize; + pub safe fn preload_script(context: u64, id: usize); + pub safe fn is_script_loaded(context: u64, id: usize) -> u32; + pub safe fn unload_script(context: u64, id: usize); + pub safe fn activate_script(context: u64, id: usize); + pub safe fn deactivate_script(context: u64, id: usize); + } +} + +impl ScriptContext { + pub fn script_id(&self, name: &str) -> usize { + unsafe { tortuise::script_id_by_name(self.0, name.as_ptr(), name.len()) } + } + + pub fn preload_script(&self, name: &str) { + let id = self.script_id(name); + tortuise::preload_script(self.0, id) + } + + pub fn is_script_loaded(&self, name: &str) -> bool { + let id = self.script_id(name); + tortuise::is_script_loaded(self.0, id) > 0 + } + + pub fn unload_script(&self, name: &str) { + let id = self.script_id(name); + tortuise::unload_script(self.0, id) + } + + pub fn activate_script(&self, name: &str) { + let id = self.script_id(name); + tortuise::activate_script(self.0, id) + } + + pub fn deactivate_script(&self, name: &str) { + let id = self.script_id(name); + tortuise::deactivate_script(self.0, id) + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..8b4de18 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,99 @@ +use core::{ + alloc::Layout, + ops::{Deref, DerefMut}, +}; + +use alloc::alloc::{alloc, dealloc}; +use serde::{Serialize, de::DeserializeOwned}; + +use crate::ScriptContext; + +mod tortuise { + #[link(wasm_import_module = "tortuise")] + unsafe extern "C" { + pub safe fn state_size(context: u64) -> usize; + pub unsafe fn load_state(context: u64, offset: *mut u8); + pub unsafe fn save_state(context: u64, offset: *const u8, size: usize); + + } +} + +impl ScriptContext { + pub fn load_state<T: DeserializeOwned>(&self) -> Option<T> { + unsafe { + let size = tortuise::state_size(self.0); + let layout = Layout::array::<u8>(size).unwrap(); + let ptr = alloc(layout); + tortuise::load_state(self.0, ptr); + let data = core::slice::from_raw_parts(ptr, size); + let data = messagepack_serde::from_reader(data).ok()?; + dealloc(ptr, layout); + Some(data) + } + } +} + +fn save_state<T: Serialize>(context: u64, state: &T) { + unsafe { + let data = messagepack_serde::to_vec(state).unwrap(); + tortuise::save_state(context, data.as_ptr(), data.len()); + } +} + +pub struct StateMut<T: Serialize> { + context: u64, + data: T, +} + +impl<T: Serialize + DeserializeOwned> StateMut<T> { + pub fn load(context: &ScriptContext) -> Option<Self> { + Some(Self { + context: context.0, + data: context.load_state()?, + }) + } + + pub fn load_or(context: &ScriptContext, default: T) -> Self { + Self { + context: context.0, + data: context.load_state().unwrap_or(default), + } + } + + pub fn load_or_else(context: &ScriptContext, default: impl FnOnce() -> T) -> Self { + Self { + context: context.0, + data: context.load_state().unwrap_or_else(default), + } + } + + pub fn load_or_default(context: &ScriptContext) -> Self + where + T: Default, + { + Self { + context: context.0, + data: context.load_state().unwrap_or_default(), + } + } +} + +impl<T: Serialize> Deref for StateMut<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl<T: Serialize> DerefMut for StateMut<T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.data + } +} + +impl<T: Serialize> Drop for StateMut<T> { + fn drop(&mut self) { + save_state(self.context, &self.data); + } +} diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..48fdcb4 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,84 @@ +use core::{ + alloc::Layout, + ops::{Deref, DerefMut}, +}; + +use alloc::alloc::{alloc, dealloc}; +use serde::{Deserialize, Serialize}; + +use crate::ScriptContext; + +mod tortuise { + #[link(wasm_import_module = "tortuise")] + unsafe extern "C" { + pub safe fn storage_size(context: u64) -> usize; + pub unsafe fn load_storage(context: u64, ptr: *mut u8); + pub unsafe fn save_storage(context: u64, ptr: *const u8, size: usize); + } +} + +fn load_storage<'de, T: Deserialize<'de>>(context: u64) -> Option<T> { + unsafe { + let size = tortuise::storage_size(context); + let layout = Layout::array::<u8>(size).unwrap(); + let ptr = alloc(layout); + tortuise::load_storage(context, ptr); + let data = core::slice::from_raw_parts(ptr, size); + let data = messagepack_serde::from_slice(data).ok()?; + dealloc(ptr, layout); + Some(data) + } +} + +fn save_storage<T: Serialize>(context: u64, state: &T) { + unsafe { + let data = messagepack_serde::to_vec(state).unwrap(); + tortuise::save_storage(context, data.as_ptr(), data.len()); + } +} + +pub struct Storage<T> { + initial_state: fn() -> T, +} + +pub struct StorageMut<T: Serialize> { + context: u64, + data: T, +} + +impl<'de, T: Serialize + Deserialize<'de>> Storage<T> { + pub const fn new(initial_state: fn() -> T) -> Self { + Self { initial_state } + } + + pub fn load(&self, context: &ScriptContext) -> T { + load_storage(context.0).unwrap_or_else(self.initial_state) + } + + pub fn load_mut(&self, context: &ScriptContext) -> StorageMut<T> { + StorageMut { + context: context.0, + data: load_storage(context.0).unwrap_or_else(self.initial_state), + } + } +} + +impl<T: Serialize> Deref for StorageMut<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl<T: Serialize> DerefMut for StorageMut<T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.data + } +} + +impl<T: Serialize> Drop for StorageMut<T> { + fn drop(&mut self) { + save_storage(self.context, &self.data); + } +} diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..35e1a86 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,142 @@ +use core::{ + ops::{Add, AddAssign, Sub, SubAssign}, + time::Duration, +}; + +use serde::{Deserialize, Serialize}; + +use crate::ScriptContext; + +mod tortuise { + + #[link(wasm_import_module = "tortuise")] + unsafe extern "C" { + pub safe fn system_now(context: u64) -> u64; + pub safe fn monotonic_now(context: u64) -> u64; + pub safe fn delta_time(context: u64) -> u64; + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[repr(transparent)] +#[serde(transparent)] +pub struct SystemTime(u128); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[repr(transparent)] +#[serde(transparent)] +pub struct Instant(u128); + +impl ScriptContext { + pub fn delta_time(&self) -> Duration { + Duration::from_nanos(tortuise::delta_time(self.0)) + } +} + +impl SystemTime { + pub const UNIX_EPOCH: Self = Self(0); + + pub fn now(context: &ScriptContext) -> Self { + Self(tortuise::system_now(context.0) as u128) + } + + pub fn duration_since(self, earlier: SystemTime) -> Option<Duration> { + if self < earlier { + return None; + } + + Some(Duration::from_nanos_u128(self.0 - earlier.0)) + } + + pub fn checked_add(self, duration: Duration) -> Option<Self> { + Some(Self(self.0.checked_add(duration.as_nanos())?)) + } + + pub fn checked_sub(self, duration: Duration) -> Option<Self> { + Some(Self(self.0.checked_sub(duration.as_nanos())?)) + } +} + +impl Add<Duration> for SystemTime { + type Output = Self; + + fn add(self, rhs: Duration) -> Self::Output { + self.checked_add(rhs).unwrap() + } +} + +impl AddAssign<Duration> for SystemTime { + fn add_assign(&mut self, rhs: Duration) { + *self = self.add(rhs); + } +} + +impl Sub<Duration> for SystemTime { + type Output = Self; + + fn sub(self, rhs: Duration) -> Self::Output { + self.checked_sub(rhs).unwrap() + } +} + +impl SubAssign<Duration> for SystemTime { + fn sub_assign(&mut self, rhs: Duration) { + *self = self.sub(rhs) + } +} + +impl Instant { + pub fn now(context: &ScriptContext) -> Self { + Self(tortuise::monotonic_now(context.0) as u128) + } + + pub fn duration_since(self, earlier: Instant) -> Option<Duration> { + if earlier > self { + return None; + } + + Some(Duration::from_nanos_u128(self.0 - earlier.0)) + } + + pub fn elapsed(self, context: &ScriptContext) -> Duration { + Self::now(context) + .duration_since(self) + .unwrap_or(Duration::ZERO) + } + + pub fn checked_add(self, duration: Duration) -> Option<Self> { + Some(Self(self.0.checked_add(duration.as_nanos())?)) + } + + pub fn checked_sub(self, duration: Duration) -> Option<Self> { + Some(Self(self.0.checked_sub(duration.as_nanos())?)) + } +} + +impl Add<Duration> for Instant { + type Output = Self; + + fn add(self, rhs: Duration) -> Self::Output { + self.checked_add(rhs).unwrap() + } +} + +impl AddAssign<Duration> for Instant { + fn add_assign(&mut self, rhs: Duration) { + *self = self.add(rhs); + } +} + +impl Sub<Duration> for Instant { + type Output = Self; + + fn sub(self, rhs: Duration) -> Self::Output { + self.checked_sub(rhs).unwrap() + } +} + +impl SubAssign<Duration> for Instant { + fn sub_assign(&mut self, rhs: Duration) { + *self = self.sub(rhs) + } +} |
