summaryrefslogtreecommitdiff
path: root/platforms/tortuise-script-wasm/src
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2026-10-26 10:52:04 -0400
committerMica White <botahamec@outlook.com>2026-10-26 10:58:46 -0400
commitec40f6e3acc7874b82709ab81b254c741529fdff (patch)
treee3dfdc315bd0c6f1d2efcf442c2cfb3317d6810f /platforms/tortuise-script-wasm/src
parente3c5839159903658a667f789963f20a31567b25c (diff)
Finish engineHEADmain
Diffstat (limited to 'platforms/tortuise-script-wasm/src')
-rw-r--r--platforms/tortuise-script-wasm/src/allocator.rs204
-rw-r--r--platforms/tortuise-script-wasm/src/buffer.rs240
-rw-r--r--platforms/tortuise-script-wasm/src/entity.rs157
-rw-r--r--platforms/tortuise-script-wasm/src/input.rs264
-rw-r--r--platforms/tortuise-script-wasm/src/lib.rs239
-rw-r--r--platforms/tortuise-script-wasm/src/log.rs36
-rw-r--r--platforms/tortuise-script-wasm/src/process.rs14
-rw-r--r--platforms/tortuise-script-wasm/src/random.rs73
-rw-r--r--platforms/tortuise-script-wasm/src/scene.rs22
-rw-r--r--platforms/tortuise-script-wasm/src/script.rs48
-rw-r--r--platforms/tortuise-script-wasm/src/state.rs102
-rw-r--r--platforms/tortuise-script-wasm/src/storage.rs194
-rw-r--r--platforms/tortuise-script-wasm/src/time.rs142
13 files changed, 1735 insertions, 0 deletions
diff --git a/platforms/tortuise-script-wasm/src/allocator.rs b/platforms/tortuise-script-wasm/src/allocator.rs
new file mode 100644
index 0000000..b4e3dec
--- /dev/null
+++ b/platforms/tortuise-script-wasm/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_unchecked(new_size, layout.align()))
+ }
+ }
+}
diff --git a/platforms/tortuise-script-wasm/src/buffer.rs b/platforms/tortuise-script-wasm/src/buffer.rs
new file mode 100644
index 0000000..bbcb3ca
--- /dev/null
+++ b/platforms/tortuise-script-wasm/src/buffer.rs
@@ -0,0 +1,240 @@
+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;
+ pub safe fn set_framerate_limit(context: u64, limit: 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(&mut 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,
+ }
+ }
+
+ pub fn set_framerate_limit(&mut self, limit: Option<u32>) {
+ tortuise::set_framerate_limit(self.0, limit.unwrap_or_default());
+ }
+}
diff --git a/platforms/tortuise-script-wasm/src/entity.rs b/platforms/tortuise-script-wasm/src/entity.rs
new file mode 100644
index 0000000..e66e2fe
--- /dev/null
+++ b/platforms/tortuise-script-wasm/src/entity.rs
@@ -0,0 +1,157 @@
+use core::{
+ alloc::Layout,
+ ops::{Deref, DerefMut},
+};
+
+use alloc::{
+ alloc::{alloc, dealloc},
+ borrow::Cow,
+ string::String,
+};
+use serde::{Deserialize, Serialize};
+use spin::lock_api::{MappedRwLockWriteGuard, RwLock, RwLockWriteGuard};
+
+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!();
+ };
+ if layout.size() == 0 {
+ return None;
+ }
+ 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 = match messagepack_serde::from_slice(data) {
+ Ok(data) => data,
+ Err(e) => {
+ log::error!("Failed to parse entity: {e}");
+ return None;
+ }
+ };
+ dealloc(ptr, layout);
+ Some(data)
+ }
+}
+
+fn save_entity<T: Serialize>(context: u64, name: &str, state: &T) {
+ unsafe {
+ let data = match messagepack_serde::to_vec(state) {
+ Ok(data) => data,
+ Err(e) => {
+ log::error!("Failed to save entity: {e}");
+ panic!();
+ }
+ };
+ tortuise::save_entity(
+ context,
+ name.as_ptr(),
+ name.len(),
+ data.as_ptr(),
+ data.len(),
+ );
+ }
+}
+
+pub struct Entity<T> {
+ name: Cow<'static, str>,
+ data: RwLock<Option<T>>,
+ default: fn(&ScriptContext) -> T,
+}
+
+pub struct EntityRef<'entity, T> {
+ data: MappedRwLockWriteGuard<'entity, T>,
+}
+
+pub struct EntityMut<'entity, T: Serialize> {
+ context: u64,
+ name: &'entity str,
+ data: MappedRwLockWriteGuard<'entity, T>,
+}
+
+impl<'de, T: Serialize + Deserialize<'de>> Entity<T> {
+ pub const fn new(name: &'static str, default: fn(&ScriptContext) -> T) -> Self {
+ Self {
+ name: Cow::Borrowed(name),
+ data: RwLock::new(None),
+ default,
+ }
+ }
+
+ pub const fn from_string(name: String, default: fn(&ScriptContext) -> T) -> Self {
+ Self {
+ name: Cow::Owned(name),
+ data: RwLock::new(None),
+ default,
+ }
+ }
+
+ pub fn get(&self, context: &ScriptContext) -> EntityRef<'_, T> {
+ let data = RwLockWriteGuard::map(self.data.write(), |data| {
+ data.get_or_insert_with(|| {
+ load_entity(context.0, &self.name).unwrap_or_else(|| (self.default)(context))
+ })
+ });
+ EntityRef { data }
+ }
+
+ pub fn get_mut(&self, context: &ScriptContext) -> EntityMut<'_, T> {
+ let data = RwLockWriteGuard::map(self.data.write(), |data| {
+ data.get_or_insert_with(|| {
+ load_entity(context.0, &self.name).unwrap_or_else(|| (self.default)(context))
+ })
+ });
+ EntityMut {
+ context: context.0,
+ name: &self.name,
+ data,
+ }
+ }
+}
+
+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.name, &*self.data);
+ }
+}
diff --git a/platforms/tortuise-script-wasm/src/input.rs b/platforms/tortuise-script-wasm/src/input.rs
new file mode 100644
index 0000000..07cbe62
--- /dev/null
+++ b/platforms/tortuise-script-wasm/src/input.rs
@@ -0,0 +1,264 @@
+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;
+ pub safe fn is_mouse_clicked_this_frame(context: u64) -> u32;
+ pub safe fn mouse_location(context: u64) -> u32;
+ pub safe fn scroll_direction_this_frame(context: u64) -> u32;
+ pub safe fn is_button_pressed_this_frame(context: u64, gamepad_id: u32, button: u32)
+ -> u32;
+ pub safe fn is_button_released_this_frame(
+ context: u64,
+ gamepad_id: u32,
+ button: u32,
+ ) -> u32;
+ pub safe fn is_button_down(context: u64, gamepad_id: u32, button: u32) -> u32;
+ pub safe fn axis_value(context: u64, gamepad_id: u32, axis: u32) -> f32;
+ pub safe fn is_gamepad_connected_this_frame(context: u64) -> u32;
+ pub safe fn is_gamepad_connected(context: u64, gamepad_id: u32) -> u32;
+ pub safe fn gamepad_power_info(context: u64, gamepad_id: u32) -> u32;
+ pub safe fn is_focused(context: u64) -> u32;
+ pub safe fn was_focused_this_frame(context: u64) -> u32;
+ pub safe fn was_unfocused_this_frame(context: u64) -> u32;
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Position {
+ pub x: u16,
+ pub y: u16,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Direction {
+ Up = 1,
+ Down,
+ Left,
+ Right,
+}
+
+/// 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,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum Button {
+ // Action Pad
+ South = 1,
+ East = 2,
+ North = 4,
+ West = 5,
+ C = 3,
+ Z = 6,
+ // Triggers
+ LeftTrigger = 7,
+ LeftTrigger2 = 9,
+ RightTrigger = 8,
+ RightTrigger2 = 10,
+ // Menu Pad
+ Select = 11,
+ Start = 12,
+ Mode = 13,
+ // Sticks
+ LeftThumb = 14,
+ RightThumb = 15,
+ // D-Pad
+ DPadUp = 16,
+ DPadDown = 17,
+ DPadLeft = 18,
+ DPadRight = 19,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum Axis {
+ LeftStickX = 1,
+ LeftStickY = 2,
+ LeftZ = 3,
+ RightStickX = 4,
+ RightStickY = 5,
+ RightZ = 6,
+ DPadX = 7,
+ DPadY = 8,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum PowerInfo {
+ Wired,
+ Discharging(u8),
+ Charging(u8),
+ Charged,
+ Unknown,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct Gamepad(u32);
+
+impl From<u32> for Position {
+ fn from(value: u32) -> Self {
+ Self {
+ x: (value & 0xffff) as u16,
+ y: ((value >> 16) & 0xffff) as u16,
+ }
+ }
+}
+
+impl TryFrom<u32> for Direction {
+ type Error = ();
+
+ fn try_from(value: u32) -> Result<Self, Self::Error> {
+ match value {
+ 1 => Ok(Direction::Up),
+ 2 => Ok(Direction::Down),
+ 3 => Ok(Direction::Left),
+ 4 => Ok(Direction::Right),
+ _ => Err(()),
+ }
+ }
+}
+
+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 Gamepad {
+ pub fn is_button_pressed_this_frame(&self, context: &ScriptContext, button: Button) -> bool {
+ tortuise::is_button_pressed_this_frame(context.0, self.0, button as u32) > 0
+ }
+
+ pub fn is_button_released_this_frame(&self, context: &ScriptContext, button: Button) -> bool {
+ tortuise::is_button_released_this_frame(context.0, self.0, button as u32) > 0
+ }
+
+ pub fn is_button_down(&self, context: &ScriptContext, button: Button) -> bool {
+ tortuise::is_button_down(context.0, self.0, button as u32) > 0
+ }
+
+ pub fn axis_value(&self, context: &ScriptContext, axis: Axis) -> f32 {
+ tortuise::axis_value(context.0, self.0, axis as u32)
+ }
+
+ pub fn is_connected(&self, context: &ScriptContext) -> bool {
+ tortuise::is_gamepad_connected(context.0, self.0) > 0
+ }
+
+ pub fn power_info(&self, context: &ScriptContext) -> PowerInfo {
+ const WIRED: u32 = 1 << 16;
+ const DISCHARGING: u32 = 2 << 16;
+ const CHARGING: u32 = 3 << 16;
+ const CHARGED: u32 = 4 << 16;
+
+ match tortuise::gamepad_power_info(context.0, self.0) {
+ WIRED => PowerInfo::Wired,
+ power @ DISCHARGING..CHARGING => PowerInfo::Discharging((power & 0xff) as u8),
+ power @ CHARGING..CHARGED => PowerInfo::Charging((power & 0xff) as u8),
+ CHARGED => PowerInfo::Charged,
+ _ => PowerInfo::Unknown,
+ }
+ }
+}
+
+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
+ }
+
+ pub fn is_mouse_clicked_this_frame(&self) -> bool {
+ tortuise::is_mouse_clicked_this_frame(self.0) > 0
+ }
+
+ pub fn mouse_location(&self) -> Option<Position> {
+ let location = tortuise::mouse_location(self.0);
+ (location != u32::MAX).then(|| location.into())
+ }
+
+ pub fn scroll_direction_this_frame(&self) -> Option<Direction> {
+ tortuise::scroll_direction_this_frame(self.0)
+ .try_into()
+ .ok()
+ }
+
+ pub fn is_gamepad_connected_this_frame(&self) -> Option<Gamepad> {
+ let gamepad_id = tortuise::is_gamepad_connected_this_frame(self.0);
+ if gamepad_id == 0 {
+ return None;
+ }
+
+ Some(Gamepad(gamepad_id))
+ }
+
+ pub fn is_focused(&self) -> bool {
+ tortuise::is_focused(self.0) > 0
+ }
+
+ pub fn was_focused_this_frame(&self) -> bool {
+ tortuise::was_focused_this_frame(self.0) > 0
+ }
+
+ pub fn was_unfocused_this_frame(&self) -> bool {
+ tortuise::was_unfocused_this_frame(self.0) > 0
+ }
+}
diff --git a/platforms/tortuise-script-wasm/src/lib.rs b/platforms/tortuise-script-wasm/src/lib.rs
new file mode 100644
index 0000000..6394b8f
--- /dev/null
+++ b/platforms/tortuise-script-wasm/src/lib.rs
@@ -0,0 +1,239 @@
+#![no_std]
+
+use core::marker::PhantomData;
+
+use alloc::vec::Vec;
+use serde::Serialize;
+
+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<'a>(u64, PhantomData<&'a ()>);
+
+pub struct TortuiseScript {
+ pub start: fn(cx: ScriptContext),
+ pub update: fn(cx: ScriptContext),
+ pub end: fn(cx: ScriptContext),
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct TortuiseScene {
+ pub scripts: &'static [&'static str],
+}
+
+fn default_script(_cx: ScriptContext) {}
+
+impl Default for TortuiseScript {
+ fn default() -> Self {
+ Self {
+ start: default_script,
+ update: default_script,
+ end: default_script,
+ }
+ }
+}
+
+impl TortuiseScript {
+ pub const fn empty() -> Self {
+ Self {
+ start: default_script,
+ update: default_script,
+ end: default_script,
+ }
+ }
+}
+
+#[doc(hidden)]
+pub fn to_vec<T: Serialize>(val: &T) -> &'static [u8] {
+ Vec::leak(messagepack_serde::to_vec(val).unwrap())
+}
+
+#[doc(hidden)]
+pub type Lazy<T> = spin::LazyLock<T>;
+
+#[macro_export]
+macro_rules! game {
+ {
+ name: $name: literal,
+ author: $author: literal,
+ tld: $tld: literal,
+ first_scene: $first_scene: literal,
+ scenes: { $($scene_name: literal => $scene: expr),* },
+ scripts: { $($script_name: literal => $script: expr),* }
+ } => {
+ use alloc::boxed::Box;
+
+ #[derive(Debug, Clone, serde::Serialize)]
+ struct GameManifest {
+ name: Box<str>,
+ author: Box<str>,
+ tld: Box<str>,
+ first_scene: Box<str>,
+ scene: Box<[ManifestScene]>,
+ script: Box<[Box<str>]>,
+ }
+
+ #[derive(Debug, Clone, serde::Serialize)]
+ struct ManifestScene {
+ name: Box<str>,
+ scripts: Box<[Box<str>]>,
+ }
+
+ #[global_allocator]
+ static ALLOCATOR: $crate::allocator::TalcAllocator =
+ $crate::allocator::TalcAllocator::new();
+
+ static SCRIPTS: phf::Map<&'static str, $crate::TortuiseScript> = phf::phf_map! {
+ $($script_name => $script,)*
+ };
+
+ static MANIFEST: $crate::Lazy<&'static [u8]> = $crate::Lazy::new(|| {
+ fn slice_to_boxed_slice(names: &[&str]) -> Box<[Box<str>]> {
+ names.iter().cloned().map(Into::into).collect()
+ }
+
+ let manifest = GameManifest {
+ name: $name.into(),
+ author: $author.into(),
+ tld: $tld.into(),
+ first_scene: $first_scene.into(),
+ script: slice_to_boxed_slice([$($script_name,)*].as_slice()),
+ scene: Box::from([$(ManifestScene {
+ name: $scene_name.into(),
+ scripts: slice_to_boxed_slice($scene.scripts),
+ },)*].as_slice())
+ };
+
+ $crate::to_vec(&manifest)
+ });
+
+ #[unsafe(no_mangle)]
+ extern "C" fn manifest_addr() -> *const u8 {
+ MANIFEST.as_ptr()
+ }
+
+ #[unsafe(no_mangle)]
+ extern "C" fn manifest_len() -> usize {
+ MANIFEST.len()
+ }
+
+ #[unsafe(no_mangle)]
+ extern "C" fn start_script(cx: $crate::ScriptContext) {
+ let _ = $crate::log::TortuiseLogger::init();
+ let Some(script) = SCRIPTS.get(&*cx.current_script_name()) else { return };
+ (script.start)(cx);
+ }
+
+ #[unsafe(no_mangle)]
+ extern "C" fn update_script(cx: $crate::ScriptContext) {
+ let _ = $crate::log::TortuiseLogger::init();
+ let Some(script) = SCRIPTS.get(&*cx.current_script_name()) else { return };
+ (script.update)(cx);
+ }
+
+ #[unsafe(no_mangle)]
+ extern "C" fn end_script(cx: $crate::ScriptContext) {
+ let _ = $crate::log::TortuiseLogger::init();
+ let Some(script) = SCRIPTS.get(&*cx.current_script_name()) else { return };
+ (script.end)(cx);
+ }
+ };
+}
+
+#[macro_export]
+macro_rules! patch {
+ {
+ scenes: { $($scene_name: literal => $scene: expr),* },
+ scripts: { $($script_name: literal => $script: expr),* }
+ } => {
+ use alloc::boxed::Box;
+
+ #[derive(Debug, Clone, serde::Serialize)]
+ struct GameManifestPatch {
+ new_scenes: Box<[ManifestScene]>,
+ new_scripts: Box<[Box<str>]>,
+ patched_scenes: Box<[ScenePatch]>,
+ }
+
+ #[derive(Debug, Clone, serde::Serialize)]
+ struct ManifestScene {
+ name: Box<str>,
+ scripts: Box<[Box<str>]>,
+ }
+
+ #[derive(Debug, Clone, serde::Serialize)]
+ struct ScenePatch {
+ add_scripts: Box<[Box<str>]>,
+ remove_scripts: Box<[Box<str>]>,
+ }
+
+ #[global_allocator]
+ static ALLOCATOR: $crate::allocator::TalcAllocator =
+ $crate::allocator::TalcAllocator::new();
+
+ static SCRIPTS: phf::Map<&'static str, $crate::TortuiseScript> = phf::phf_map! {
+ $($script_name => $script,)*
+ };
+
+ static MANIFEST: $crate::Lazy<&'static [u8]> = $crate::Lazy::new(|| {
+ fn slice_to_boxed_slice(names: &[&str]) -> Box<[Box<str>]> {
+ names.iter().cloned().map(Into::into).collect()
+ }
+
+ let manifest = GameManifestPatch {
+ added_scripts: slice_to_boxed_slice([$($script_name,)*].as_slice()),
+ added_scenes: Box::from([$(ManifestScene {
+ name: $scene_name.into(),
+ scripts: slice_to_boxed_slice($scene.scripts),
+ },)*].as_slice())
+ };
+
+ $crate::to_vec(&manifest)
+ });
+
+ #[unsafe(no_mangle)]
+ extern "C" fn manifest_addr() -> *const u8 {
+ MANIFEST.as_ptrhttps://botahamec.dev/cgit/shake/plain/shake()
+ }
+
+ #[unsafe(no_mangle)]
+ extern "C" fn manifest_len() -> usize {
+ MANIFEST.len()
+ }
+
+ #[unsafe(no_mangle)]
+ extern "C" fn start_script(cx: $crate::ScriptContext) {
+ let _ = $crate::log::TortuiseLogger::init();
+ let Some(script) = SCRIPTS.get(&*cx.current_script_name()) else { return };
+ (script.start)(cx);
+ }
+
+ #[unsafe(no_mangle)]
+ extern "C" fn update_script(cx: $crate::ScriptContext) {
+ let _ = $crate::log::TortuiseLogger::init();
+ let Some(script) = SCRIPTS.get(&*cx.current_script_name()) else { return };
+ (script.update)(cx);
+ }
+
+ #[unsafe(no_mangle)]
+ extern "C" fn end_script(cx: $crate::ScriptContext) {
+ let _ = $crate::log::TortuiseLogger::init();
+ let Some(script) = SCRIPTS.get(&*cx.current_script_name()) else { return };
+ (script.end)(cx);
+ }
+ };
+}
diff --git a/platforms/tortuise-script-wasm/src/log.rs b/platforms/tortuise-script-wasm/src/log.rs
new file mode 100644
index 0000000..306a0d5
--- /dev/null
+++ b/platforms/tortuise-script-wasm/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/platforms/tortuise-script-wasm/src/process.rs b/platforms/tortuise-script-wasm/src/process.rs
new file mode 100644
index 0000000..fe2af19
--- /dev/null
+++ b/platforms/tortuise-script-wasm/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(&mut self) {
+ tortuise::exit(self.0)
+ }
+}
diff --git a/platforms/tortuise-script-wasm/src/random.rs b/platforms/tortuise-script-wasm/src/random.rs
new file mode 100644
index 0000000..334b6bf
--- /dev/null
+++ b/platforms/tortuise-script-wasm/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/platforms/tortuise-script-wasm/src/scene.rs b/platforms/tortuise-script-wasm/src/scene.rs
new file mode 100644
index 0000000..93889a7
--- /dev/null
+++ b/platforms/tortuise-script-wasm/src/scene.rs
@@ -0,0 +1,22 @@
+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 switch_scene(context: u64, id: usize);
+ }
+}
+
+impl ScriptContext<'_> {
+ pub fn scene_id(&self, name: &str) -> Option<usize> {
+ let id = unsafe { tortuise::scene_id_by_name(self.0, name.as_ptr(), name.len()) };
+ (id < usize::MAX).then_some(id)
+ }
+
+ pub fn switch_scene(&mut self, name: &str) -> Option<()> {
+ let id = self.scene_id(name)?;
+ tortuise::switch_scene(self.0, id);
+ Some(())
+ }
+}
diff --git a/platforms/tortuise-script-wasm/src/script.rs b/platforms/tortuise-script-wasm/src/script.rs
new file mode 100644
index 0000000..ee4d91d
--- /dev/null
+++ b/platforms/tortuise-script-wasm/src/script.rs
@@ -0,0 +1,48 @@
+use core::alloc::Layout;
+
+use alloc::string::String;
+
+use crate::ScriptContext;
+
+mod tortuise {
+ #[link(wasm_import_module = "tortuise")]
+ unsafe extern "C" {
+ pub safe fn script_name_size(context: u64) -> usize;
+ pub unsafe fn load_script_name(context: u64, offset: *const u8);
+ pub unsafe fn script_id_by_name(context: u64, name: *const u8, length: usize) -> usize;
+ pub safe fn activate_script(context: u64, id: usize);
+ pub safe fn deactivate_script(context: u64, id: usize);
+ }
+}
+
+impl ScriptContext<'_> {
+ pub fn current_script_name(&self) -> String {
+ // safety: the length is correct and the string will fit into offset
+ unsafe {
+ let len = tortuise::script_name_size(self.0);
+ if len == 0 {
+ return String::new();
+ }
+ let offset = alloc::alloc::alloc(Layout::array::<u8>(len).unwrap());
+ tortuise::load_script_name(self.0, offset);
+ String::from_raw_parts(offset, len, len)
+ }
+ }
+
+ pub fn script_id(&self, name: &str) -> Option<usize> {
+ let id = unsafe { tortuise::script_id_by_name(self.0, name.as_ptr(), name.len()) };
+ (id < usize::MAX).then_some(id)
+ }
+
+ pub fn activate_script(&mut self, name: &str) -> Option<()> {
+ let id = self.script_id(name)?;
+ tortuise::activate_script(self.0, id);
+ Some(())
+ }
+
+ pub fn deactivate_script(&mut self, name: &str) -> Option<()> {
+ let id = self.script_id(name)?;
+ tortuise::deactivate_script(self.0, id);
+ Some(())
+ }
+}
diff --git a/platforms/tortuise-script-wasm/src/state.rs b/platforms/tortuise-script-wasm/src/state.rs
new file mode 100644
index 0000000..ffcdb3e
--- /dev/null
+++ b/platforms/tortuise-script-wasm/src/state.rs
@@ -0,0 +1,102 @@
+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();
+ if layout.size() == 0 {
+ return None;
+ }
+ 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/platforms/tortuise-script-wasm/src/storage.rs b/platforms/tortuise-script-wasm/src/storage.rs
new file mode 100644
index 0000000..2606e2b
--- /dev/null
+++ b/platforms/tortuise-script-wasm/src/storage.rs
@@ -0,0 +1,194 @@
+use core::{
+ alloc::Layout,
+ marker::PhantomData,
+ 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 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);
+ pub safe fn is_saving_storage(context: u64) -> usize;
+ pub safe fn saving_storage_failed(context: u64) -> usize;
+ pub safe fn config_size(context: u64) -> usize;
+ pub unsafe fn load_config(context: u64, ptr: *mut u8);
+ pub unsafe fn save_config(context: u64, ptr: *const u8, size: usize);
+ pub safe fn is_saving_config(context: u64) -> usize;
+ pub safe fn saving_config_failed(context: u64) -> usize;
+ }
+}
+
+fn load_storage<T: DeserializeOwned>(context: u64) -> Option<T> {
+ unsafe {
+ let size = tortuise::storage_size(context);
+ let layout = Layout::array::<u8>(size).unwrap();
+ if layout.size() == 0 {
+ return None;
+ }
+ 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());
+ }
+}
+
+fn load_config<T: DeserializeOwned>(context: u64) -> Option<T> {
+ unsafe {
+ let size = tortuise::config_size(context);
+ let layout = Layout::array::<u8>(size).unwrap();
+ if layout.size() == 0 {
+ return None;
+ }
+ let ptr = alloc(layout);
+ tortuise::load_config(context, ptr);
+ let data = core::slice::from_raw_parts(ptr, size);
+ let data = toml::from_slice(data).ok()?;
+ dealloc(ptr, layout);
+ Some(data)
+ }
+}
+
+fn save_config<T: Serialize>(context: u64, state: &T) {
+ unsafe {
+ let data = toml::to_string(state).unwrap();
+ tortuise::save_config(context, data.as_ptr(), data.len());
+ }
+}
+
+pub struct Storage<T> {
+ initial_state: fn() -> T,
+}
+
+pub struct Config<T> {
+ initial_state: fn() -> T,
+}
+
+pub struct StorageMut<'a, 'cx, T: Serialize + Eq> {
+ context: u64,
+ original: T,
+ data: T,
+ _phantom: PhantomData<(&'a (), &'cx ())>,
+}
+
+pub struct ConfigMut<'a, 'cx, T: Serialize + Eq> {
+ context: u64,
+ original: T,
+ data: T,
+ _phantom: PhantomData<(&'a (), &'cx ())>,
+}
+
+impl<T: Serialize + DeserializeOwned + Clone + Eq> 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<'a, 'cx>(&self, context: &'cx mut ScriptContext<'a>) -> StorageMut<'a, 'cx, T> {
+ let data = load_storage(context.0).unwrap_or_else(self.initial_state);
+ StorageMut {
+ context: context.0,
+ original: data.clone(),
+ data,
+ _phantom: PhantomData,
+ }
+ }
+
+ pub fn is_saving(&self, context: &ScriptContext) -> bool {
+ tortuise::is_saving_storage(context.0) > 0
+ }
+
+ pub fn save_failed(&self, context: &ScriptContext) -> bool {
+ tortuise::saving_storage_failed(context.0) > 0
+ }
+}
+
+impl<T: Serialize + DeserializeOwned + Clone + Eq> Config<T> {
+ pub const fn new(initial_state: fn() -> T) -> Self {
+ Self { initial_state }
+ }
+
+ pub fn load(&self, context: &ScriptContext) -> T {
+ load_config(context.0).unwrap_or_else(self.initial_state)
+ }
+
+ pub fn load_mut<'a, 'cx>(&self, context: &'cx mut ScriptContext<'a>) -> StorageMut<'a, 'cx, T> {
+ let data = load_config(context.0).unwrap_or_else(self.initial_state);
+ StorageMut {
+ context: context.0,
+ original: data.clone(),
+ data,
+ _phantom: PhantomData,
+ }
+ }
+
+ pub fn is_saving(&self, context: &ScriptContext) -> bool {
+ tortuise::is_saving_config(context.0) > 0
+ }
+
+ pub fn save_failed(&self, context: &ScriptContext) -> bool {
+ tortuise::saving_config_failed(context.0) > 0
+ }
+}
+
+impl<T: Serialize + Eq> Deref for StorageMut<'_, '_, T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.data
+ }
+}
+
+impl<T: Serialize + Eq> DerefMut for StorageMut<'_, '_, T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.data
+ }
+}
+
+impl<T: Serialize + Eq> Drop for StorageMut<'_, '_, T> {
+ fn drop(&mut self) {
+ if self.data != self.original {
+ save_storage(self.context, &self.data);
+ }
+ }
+}
+
+impl<T: Serialize + Eq> Deref for ConfigMut<'_, '_, T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.data
+ }
+}
+
+impl<T: Serialize + Eq> DerefMut for ConfigMut<'_, '_, T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.data
+ }
+}
+
+impl<T: Serialize + Eq> Drop for ConfigMut<'_, '_, T> {
+ fn drop(&mut self) {
+ if self.data != self.original {
+ save_config(self.context, &self.data);
+ }
+ }
+}
diff --git a/platforms/tortuise-script-wasm/src/time.rs b/platforms/tortuise-script-wasm/src/time.rs
new file mode 100644
index 0000000..a7c8b72
--- /dev/null
+++ b/platforms/tortuise-script-wasm/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)
+ }
+}