summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2026-07-30 07:34:24 -0400
committerMica White <botahamec@outlook.com>2026-07-30 07:34:24 -0400
commite3c5839159903658a667f789963f20a31567b25c (patch)
tree1e358a5fb43fd4c8f8e06374667583250111d6e6 /src
Initial commitHEADmain
Diffstat (limited to 'src')
-rw-r--r--src/allocator.rs204
-rw-r--r--src/buffer.rs235
-rw-r--r--src/entity.rs133
-rw-r--r--src/input.rs84
-rw-r--r--src/lib.rs29
-rw-r--r--src/log.rs36
-rw-r--r--src/process.rs14
-rw-r--r--src/random.rs73
-rw-r--r--src/scene.rs32
-rw-r--r--src/script.rs44
-rw-r--r--src/state.rs99
-rw-r--r--src/storage.rs84
-rw-r--r--src/time.rs142
13 files changed, 1209 insertions, 0 deletions
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)
+ }
+}