summaryrefslogtreecommitdiff
path: root/platforms/tortuise-script-compiled/src
diff options
context:
space:
mode:
Diffstat (limited to 'platforms/tortuise-script-compiled/src')
-rw-r--r--platforms/tortuise-script-compiled/src/buffer.rs296
-rw-r--r--platforms/tortuise-script-compiled/src/entity.rs73
-rw-r--r--platforms/tortuise-script-compiled/src/input.rs260
-rw-r--r--platforms/tortuise-script-compiled/src/lib.rs119
-rw-r--r--platforms/tortuise-script-compiled/src/process.rs10
-rw-r--r--platforms/tortuise-script-compiled/src/random.rs70
-rw-r--r--platforms/tortuise-script-compiled/src/scene.rs17
-rw-r--r--platforms/tortuise-script-compiled/src/script.rs29
-rw-r--r--platforms/tortuise-script-compiled/src/state.rs65
-rw-r--r--platforms/tortuise-script-compiled/src/storage.rs181
-rw-r--r--platforms/tortuise-script-compiled/src/time.rs132
11 files changed, 1252 insertions, 0 deletions
diff --git a/platforms/tortuise-script-compiled/src/buffer.rs b/platforms/tortuise-script-compiled/src/buffer.rs
new file mode 100644
index 0000000..c60ee40
--- /dev/null
+++ b/platforms/tortuise-script-compiled/src/buffer.rs
@@ -0,0 +1,296 @@
+use std::time::Duration;
+
+use crossterm::style::ContentStyle;
+use serde::{Deserialize, Serialize};
+use tortuise::script::Operation;
+
+use crate::ScriptContext;
+
+#[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,
+}
+
+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 From<Color> for crossterm::style::Color {
+ fn from(value: Color) -> Self {
+ match value {
+ Color::Reset => crossterm::style::Color::Reset,
+ Color::Black => crossterm::style::Color::Black,
+ Color::DarkGrey => crossterm::style::Color::DarkGrey,
+ Color::Red => crossterm::style::Color::Red,
+ Color::DarkRed => crossterm::style::Color::DarkRed,
+ Color::Green => crossterm::style::Color::Green,
+ Color::DarkGreen => crossterm::style::Color::DarkGreen,
+ Color::Yellow => crossterm::style::Color::Yellow,
+ Color::DarkYellow => crossterm::style::Color::DarkYellow,
+ Color::Blue => crossterm::style::Color::Blue,
+ Color::DarkBlue => crossterm::style::Color::DarkBlue,
+ Color::Magenta => crossterm::style::Color::Magenta,
+ Color::DarkMagenta => crossterm::style::Color::DarkMagenta,
+ Color::Cyan => crossterm::style::Color::Cyan,
+ Color::DarkCyan => crossterm::style::Color::DarkCyan,
+ Color::White => crossterm::style::Color::White,
+ Color::Grey => crossterm::style::Color::Grey,
+ Color::Rgb { r, g, b } => crossterm::style::Color::Rgb { r, g, b },
+ Color::AnsiValue(v) => crossterm::style::Color::AnsiValue(v),
+ }
+ }
+}
+
+impl From<Attributes> for crossterm::style::Attributes {
+ fn from(value: Attributes) -> Self {
+ let mut new = crossterm::style::Attributes::none();
+
+ if value.has(Attribute::Bold) {
+ new.set(crossterm::style::Attribute::Bold);
+ }
+ if value.has(Attribute::Dim) {
+ new.set(crossterm::style::Attribute::Dim);
+ }
+ if value.has(Attribute::Italic) {
+ new.set(crossterm::style::Attribute::Italic);
+ }
+ if value.has(Attribute::Underlined) {
+ new.set(crossterm::style::Attribute::Underlined);
+ }
+ if value.has(Attribute::DoubleUnderlined) {
+ new.set(crossterm::style::Attribute::DoubleUnderlined);
+ }
+ if value.has(Attribute::Undercurled) {
+ new.set(crossterm::style::Attribute::Undercurled);
+ }
+ if value.has(Attribute::Underdotted) {
+ new.set(crossterm::style::Attribute::Underdotted);
+ }
+ if value.has(Attribute::Underdashed) {
+ new.set(crossterm::style::Attribute::Underdashed);
+ }
+ if value.has(Attribute::SlowBlink) {
+ new.set(crossterm::style::Attribute::SlowBlink);
+ }
+ if value.has(Attribute::RapidBlink) {
+ new.set(crossterm::style::Attribute::RapidBlink);
+ }
+ if value.has(Attribute::Reverse) {
+ new.set(crossterm::style::Attribute::Reverse);
+ }
+ if value.has(Attribute::Hidden) {
+ new.set(crossterm::style::Attribute::Hidden);
+ }
+ if value.has(Attribute::CrossedOut) {
+ new.set(crossterm::style::Attribute::CrossedOut);
+ }
+ if value.has(Attribute::Fraktur) {
+ new.set(crossterm::style::Attribute::Fraktur);
+ }
+ if value.has(Attribute::Framed) {
+ new.set(crossterm::style::Attribute::Framed);
+ }
+ if value.has(Attribute::Encircled) {
+ new.set(crossterm::style::Attribute::Encircled);
+ }
+ if value.has(Attribute::OverLined) {
+ new.set(crossterm::style::Attribute::OverLined);
+ }
+
+ new
+ }
+}
+
+impl ScriptContext<'_> {
+ pub fn add_cell(&mut self, cell: Cell) {
+ puffin::profile_function!();
+ self.0
+ .operation_queue
+ .push(Operation::AddCell(tortuise::buffer::Cell {
+ character: cell.character,
+ style: ContentStyle {
+ foreground_color: Some(cell.foreground_color.into()),
+ background_color: Some(cell.background_color.into()),
+ underline_color: Some(cell.underline_color.into()),
+ attributes: cell.attributes.into(),
+ },
+ x: cell.x,
+ y: cell.y,
+ z: cell.z,
+ }));
+ }
+
+ pub fn window_size(&self) -> WindowSize {
+ puffin::profile_function!();
+ let size = &self.0.buffer.window_size;
+ WindowSize {
+ rows: size.rows,
+ columns: size.columns,
+ }
+ }
+
+ pub fn set_framerate_limit(&mut self, limit: Option<u32>) {
+ puffin::profile_function!();
+ let limit = limit.map(|limit| Duration::from_nanos(1_000_000_000 / limit as u64));
+ self.0.operation_queue.push(Operation::SetFrameLimit(limit));
+ }
+}
diff --git a/platforms/tortuise-script-compiled/src/entity.rs b/platforms/tortuise-script-compiled/src/entity.rs
new file mode 100644
index 0000000..507690d
--- /dev/null
+++ b/platforms/tortuise-script-compiled/src/entity.rs
@@ -0,0 +1,73 @@
+use core::ops::{Deref, DerefMut};
+
+use spin::lock_api::{MappedRwLockWriteGuard, RwLock, RwLockWriteGuard};
+
+use crate::ScriptContext;
+
+pub struct Entity<T> {
+ data: RwLock<Option<T>>,
+ default: fn(&ScriptContext) -> T,
+}
+
+pub struct EntityRef<'entity, T> {
+ data: MappedRwLockWriteGuard<'entity, T>,
+}
+
+pub struct EntityMut<'entity, T> {
+ data: MappedRwLockWriteGuard<'entity, T>,
+}
+
+impl<T> Entity<T> {
+ pub const fn new(_name: &'static str, default: fn(&ScriptContext) -> T) -> Self {
+ Self {
+ data: RwLock::new(None),
+ default,
+ }
+ }
+
+ pub const fn from_string(name: String, default: fn(&ScriptContext) -> T) -> Self {
+ core::mem::forget(name);
+ Self {
+ data: RwLock::new(None),
+ default,
+ }
+ }
+
+ pub fn get(&self, context: &ScriptContext) -> EntityRef<'_, T> {
+ puffin::profile_function!();
+ let data = RwLockWriteGuard::map(self.data.write(), |data| {
+ data.get_or_insert_with(|| (self.default)(context))
+ });
+ EntityRef { data }
+ }
+
+ pub fn get_mut(&self, context: &ScriptContext) -> EntityMut<'_, T> {
+ puffin::profile_function!();
+ let data = RwLockWriteGuard::map(self.data.write(), |data| {
+ data.get_or_insert_with(|| (self.default)(context))
+ });
+ EntityMut { data }
+ }
+}
+
+impl<T> Deref for EntityRef<'_, T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.data
+ }
+}
+
+impl<T> Deref for EntityMut<'_, T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.data
+ }
+}
+
+impl<T> DerefMut for EntityMut<'_, T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.data
+ }
+}
diff --git a/platforms/tortuise-script-compiled/src/input.rs b/platforms/tortuise-script-compiled/src/input.rs
new file mode 100644
index 0000000..a7d7340
--- /dev/null
+++ b/platforms/tortuise-script-compiled/src/input.rs
@@ -0,0 +1,260 @@
+use serde::{Deserialize, Serialize};
+
+use crate::ScriptContext;
+
+/// 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,
+}
+
+pub type Position = tortuise::input::Position;
+pub type Direction = tortuise::input::Direction;
+
+#[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(usize);
+
+impl From<Button> for gilrs::Button {
+ fn from(value: Button) -> Self {
+ match value {
+ Button::South => gilrs::Button::South,
+ Button::East => gilrs::Button::East,
+ Button::North => gilrs::Button::North,
+ Button::West => gilrs::Button::West,
+ Button::C => gilrs::Button::C,
+ Button::Z => gilrs::Button::Z,
+ Button::LeftTrigger => gilrs::Button::LeftTrigger,
+ Button::LeftTrigger2 => gilrs::Button::LeftTrigger2,
+ Button::RightTrigger => gilrs::Button::RightTrigger,
+ Button::RightTrigger2 => gilrs::Button::RightTrigger2,
+ Button::Select => gilrs::Button::Select,
+ Button::Start => gilrs::Button::Start,
+ Button::Mode => gilrs::Button::Mode,
+ Button::LeftThumb => gilrs::Button::LeftThumb,
+ Button::RightThumb => gilrs::Button::RightThumb,
+ Button::DPadUp => gilrs::Button::DPadUp,
+ Button::DPadDown => gilrs::Button::DPadDown,
+ Button::DPadLeft => gilrs::Button::DPadLeft,
+ Button::DPadRight => gilrs::Button::DPadRight,
+ }
+ }
+}
+
+impl From<Axis> for gilrs::Axis {
+ fn from(value: Axis) -> Self {
+ match value {
+ Axis::LeftStickX => gilrs::Axis::LeftStickX,
+ Axis::LeftStickY => gilrs::Axis::LeftStickY,
+ Axis::LeftZ => gilrs::Axis::LeftZ,
+ Axis::RightStickX => gilrs::Axis::RightStickX,
+ Axis::RightStickY => gilrs::Axis::RightStickY,
+ Axis::RightZ => gilrs::Axis::RightZ,
+ Axis::DPadX => gilrs::Axis::DPadX,
+ Axis::DPadY => gilrs::Axis::DPadY,
+ }
+ }
+}
+
+impl From<Option<gilrs::PowerInfo>> for PowerInfo {
+ fn from(value: Option<gilrs::PowerInfo>) -> Self {
+ match value {
+ Some(gilrs::PowerInfo::Wired) => PowerInfo::Wired,
+ Some(gilrs::PowerInfo::Discharging(p)) => PowerInfo::Discharging(p),
+ Some(gilrs::PowerInfo::Charging(p)) => PowerInfo::Charging(p),
+ Some(gilrs::PowerInfo::Charged) => PowerInfo::Charged,
+ Some(gilrs::PowerInfo::Unknown) => PowerInfo::Unknown,
+ None => PowerInfo::Unknown,
+ }
+ }
+}
+
+impl From<KeyCode> for crossterm::event::KeyCode {
+ fn from(value: KeyCode) -> Self {
+ match value {
+ KeyCode::Backspace => crossterm::event::KeyCode::Backspace,
+ KeyCode::Enter => crossterm::event::KeyCode::Enter,
+ KeyCode::Left => crossterm::event::KeyCode::Left,
+ KeyCode::Right => crossterm::event::KeyCode::Right,
+ KeyCode::Up => crossterm::event::KeyCode::Up,
+ KeyCode::Down => crossterm::event::KeyCode::Down,
+ KeyCode::Home => crossterm::event::KeyCode::Home,
+ KeyCode::End => crossterm::event::KeyCode::End,
+ KeyCode::PageUp => crossterm::event::KeyCode::PageUp,
+ KeyCode::PageDown => crossterm::event::KeyCode::PageDown,
+ KeyCode::Tab => crossterm::event::KeyCode::Tab,
+ KeyCode::BackTab => crossterm::event::KeyCode::BackTab,
+ KeyCode::Delete => crossterm::event::KeyCode::Delete,
+ KeyCode::Insert => crossterm::event::KeyCode::Insert,
+ KeyCode::F(f) => crossterm::event::KeyCode::F(f),
+ KeyCode::Char(c) => crossterm::event::KeyCode::Char(c),
+ KeyCode::Null => crossterm::event::KeyCode::Null,
+ KeyCode::Esc => crossterm::event::KeyCode::Esc,
+ }
+ }
+}
+
+impl Gamepad {
+ pub fn is_button_pressed_this_frame(&self, context: &ScriptContext, button: Button) -> bool {
+ context
+ .0
+ .input
+ .is_button_pressed_this_frame(self.0, button.into())
+ }
+
+ pub fn is_button_released_this_frame(&self, context: &ScriptContext, button: Button) -> bool {
+ context
+ .0
+ .input
+ .is_button_released_this_frame(self.0, button.into())
+ }
+
+ pub fn is_button_down(&self, context: &ScriptContext, button: Button) -> bool {
+ context.0.input.is_button_down(self.0, button.into())
+ }
+
+ pub fn axis_value(&self, context: &ScriptContext, axis: Axis) -> f32 {
+ context
+ .0
+ .input
+ .axis_value(self.0, axis.into())
+ .unwrap_or_default()
+ }
+
+ pub fn is_connected(&self, context: &ScriptContext) -> bool {
+ context.0.input.is_gamepad_connected(self.0)
+ }
+
+ pub fn power_info(&self, context: &ScriptContext) -> PowerInfo {
+ context.0.input.gamepad_power_info(self.0).into()
+ }
+}
+
+impl ScriptContext<'_> {
+ pub fn is_key_pressed_this_frame(&self, key_code: KeyCode) -> bool {
+ puffin::profile_function!();
+ self.0.input.is_key_pressed_this_frame(key_code.into())
+ }
+
+ pub fn is_mouse_clicked_this_frame(&self) -> bool {
+ puffin::profile_function!();
+ self.0.input.is_mouse_clicked_this_frame()
+ }
+
+ pub fn mouse_location(&self) -> Option<Position> {
+ puffin::profile_function!();
+ self.0.input.mouse_location()
+ }
+
+ pub fn scroll_direction_this_frame(&self) -> Option<Direction> {
+ puffin::profile_function!();
+ self.0.input.scroll_direction_this_frame()
+ }
+
+ pub fn is_gamepad_connected_this_frame(&self) -> Option<Gamepad> {
+ puffin::profile_function!();
+ self.0.input.is_gamepad_connected_this_frame().map(Gamepad)
+ }
+
+ pub fn is_focused(&self) -> bool {
+ puffin::profile_function!();
+ self.0.input.is_focused()
+ }
+
+ pub fn was_focused_this_frame(&self) -> bool {
+ puffin::profile_function!();
+ self.0.input.was_focused_this_frame()
+ }
+
+ pub fn was_unfocused_this_frame(&self) -> bool {
+ puffin::profile_function!();
+ self.0.input.was_unfocused_this_frame()
+ }
+}
diff --git a/platforms/tortuise-script-compiled/src/lib.rs b/platforms/tortuise-script-compiled/src/lib.rs
new file mode 100644
index 0000000..47bc926
--- /dev/null
+++ b/platforms/tortuise-script-compiled/src/lib.rs
@@ -0,0 +1,119 @@
+use std::sync::Arc;
+
+use tortuise::{file::FileManager, script::WasmRunner};
+
+pub mod buffer;
+pub mod entity;
+pub mod input;
+pub mod process;
+pub mod random;
+pub mod scene;
+pub mod script;
+pub mod state;
+pub mod storage;
+pub mod time;
+
+pub struct ScriptContext<'a>(tortuise::script::FullScriptContext<'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 run_game(
+ name: &str,
+ author: &str,
+ tld: &str,
+ first_scene: &str,
+ scenes: &[(&str, TortuiseScene)],
+ scripts: &[(&str, TortuiseScript)],
+) {
+ use tortuise::manifest::HumaneGameManifest;
+
+ fn wrap_script_fn(f: fn(ScriptContext)) -> tortuise::script::StaticScriptFunction {
+ Some(Arc::new(move |c| f(ScriptContext(c))))
+ }
+
+ fn slice_to_boxed_slice(names: &[&str]) -> Box<[Box<str>]> {
+ names.iter().cloned().map(Into::into).collect()
+ }
+
+ let manifest = HumaneGameManifest {
+ name: Box::from(name),
+ author: Box::from(author),
+ tld: Box::from(tld),
+ first_scene: Box::from(first_scene),
+ scene: scenes
+ .iter()
+ .map(|(name, scene)| tortuise::manifest::PartialScene {
+ name: (*name).into(),
+ scripts: slice_to_boxed_slice(scene.scripts),
+ })
+ .collect(),
+ script: scripts
+ .iter()
+ .map(
+ |(name, script)| tortuise::script::ScriptDeclaration::Static {
+ name: (*name).into(),
+ start: wrap_script_fn(script.start),
+ update: wrap_script_fn(script.update),
+ end: wrap_script_fn(script.end),
+ },
+ )
+ .collect(),
+ };
+
+ let mut files = FileManager::new();
+ let wasm_runner = WasmRunner::new();
+ let mut manifest = manifest.into();
+ tortuise::init_logger(&manifest);
+ if let Err(e) = tortuise::apply_patches(&mut manifest, &mut files, &wasm_runner) {
+ log::error!("{e}");
+ }
+ tortuise::run_game(files, wasm_runner, manifest)
+}
+
+#[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),* }
+ } => {
+ fn main() {
+ $crate::run_game($name, $author, $tld, $first_scene, &[$(($scene_name, $scene),)*], &[$(($script_name, $script),)*]);
+ }
+ };
+}
diff --git a/platforms/tortuise-script-compiled/src/process.rs b/platforms/tortuise-script-compiled/src/process.rs
new file mode 100644
index 0000000..1ee39f2
--- /dev/null
+++ b/platforms/tortuise-script-compiled/src/process.rs
@@ -0,0 +1,10 @@
+use tortuise::script::Operation;
+
+use crate::ScriptContext;
+
+impl ScriptContext<'_> {
+ pub fn exit(&mut self) {
+ puffin::profile_function!();
+ self.0.operation_queue.push(Operation::Exit)
+ }
+}
diff --git a/platforms/tortuise-script-compiled/src/random.rs b/platforms/tortuise-script-compiled/src/random.rs
new file mode 100644
index 0000000..148f152
--- /dev/null
+++ b/platforms/tortuise-script-compiled/src/random.rs
@@ -0,0 +1,70 @@
+use rand::{
+ Rng, SeedableRng, TryRng,
+ rngs::{SmallRng, StdRng},
+};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct TortuiseRng(SmallRng);
+
+impl TortuiseRng {
+ pub fn new() -> Self {
+ puffin::profile_function!();
+ Self::seed_from_u64(rand::make_rng::<StdRng>().next_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-compiled/src/scene.rs b/platforms/tortuise-script-compiled/src/scene.rs
new file mode 100644
index 0000000..fc83d6b
--- /dev/null
+++ b/platforms/tortuise-script-compiled/src/scene.rs
@@ -0,0 +1,17 @@
+use tortuise::script::Operation;
+
+use crate::ScriptContext;
+
+impl ScriptContext<'_> {
+ pub fn scene_id(&self, name: &str) -> Option<usize> {
+ puffin::profile_function!();
+ tortuise::scene::scene_id_by_name(&self.0.manifest.scenes, name)
+ }
+
+ pub fn switch_scene(&mut self, name: &str) -> Option<()> {
+ puffin::profile_function!();
+ let id = self.scene_id(name)?;
+ self.0.operation_queue.push(Operation::SwitchScene(id));
+ Some(())
+ }
+}
diff --git a/platforms/tortuise-script-compiled/src/script.rs b/platforms/tortuise-script-compiled/src/script.rs
new file mode 100644
index 0000000..d316fe5
--- /dev/null
+++ b/platforms/tortuise-script-compiled/src/script.rs
@@ -0,0 +1,29 @@
+use tortuise::script::Operation;
+
+use crate::ScriptContext;
+
+impl ScriptContext<'_> {
+ pub fn current_script_name(&self) -> String {
+ puffin::profile_function!();
+ self.0.manifest.scripts[self.0.script_id].name().to_owned()
+ }
+
+ pub fn script_id(&self, name: &str) -> Option<usize> {
+ puffin::profile_function!();
+ self.0.scripts.script_id_by_name(name)
+ }
+
+ pub fn activate_script(&mut self, name: &str) -> Option<()> {
+ puffin::profile_function!();
+ let id = self.script_id(name)?;
+ self.0.operation_queue.push(Operation::ActivateScript(id));
+ Some(())
+ }
+
+ pub fn deactivate_script(&mut self, name: &str) -> Option<()> {
+ puffin::profile_function!();
+ let id = self.script_id(name)?;
+ self.0.operation_queue.push(Operation::DeactivateScript(id));
+ Some(())
+ }
+}
diff --git a/platforms/tortuise-script-compiled/src/state.rs b/platforms/tortuise-script-compiled/src/state.rs
new file mode 100644
index 0000000..896b836
--- /dev/null
+++ b/platforms/tortuise-script-compiled/src/state.rs
@@ -0,0 +1,65 @@
+use core::ops::{Deref, DerefMut};
+
+use spin::lock_api::{MappedMutexGuard, Mutex, MutexGuard};
+
+use crate::ScriptContext;
+
+pub struct ScriptState<T> {
+ data: Mutex<Option<T>>,
+ default: fn(&ScriptContext) -> T,
+}
+
+pub struct StateRef<'state, T> {
+ data: MappedMutexGuard<'state, T>,
+}
+
+pub struct StateMut<'state, T> {
+ data: MappedMutexGuard<'state, T>,
+}
+
+impl<T> ScriptState<T> {
+ pub const fn new(default: fn(&ScriptContext) -> T) -> Self {
+ Self {
+ data: Mutex::new(None),
+ default,
+ }
+ }
+
+ pub fn get(&self, context: &ScriptContext) -> StateRef<'_, T> {
+ puffin::profile_function!();
+ let data = MutexGuard::map(self.data.lock(), |data| {
+ data.get_or_insert_with(|| (self.default)(context))
+ });
+ StateRef { data }
+ }
+
+ pub fn get_mut(&self, context: &ScriptContext) -> StateMut<'_, T> {
+ puffin::profile_function!();
+ let data = MutexGuard::map(self.data.lock(), |data| {
+ data.get_or_insert_with(|| (self.default)(context))
+ });
+ StateMut { data }
+ }
+}
+
+impl<T> Deref for StateRef<'_, T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.data
+ }
+}
+
+impl<T> Deref for StateMut<'_, T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.data
+ }
+}
+
+impl<T> DerefMut for StateMut<'_, T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.data
+ }
+}
diff --git a/platforms/tortuise-script-compiled/src/storage.rs b/platforms/tortuise-script-compiled/src/storage.rs
new file mode 100644
index 0000000..d028a91
--- /dev/null
+++ b/platforms/tortuise-script-compiled/src/storage.rs
@@ -0,0 +1,181 @@
+use core::ops::{Deref, DerefMut};
+
+use serde::{Serialize, de::DeserializeOwned};
+use spin::Mutex;
+use tortuise::script::Operation;
+
+use crate::ScriptContext;
+
+pub struct Storage<T> {
+ initial_state: fn() -> T,
+ data: Mutex<Option<T>>,
+}
+
+pub struct Config<T> {
+ initial_state: fn() -> T,
+ data: Mutex<Option<T>>,
+}
+
+pub struct StorageMut<'a, 'cx, T: Serialize + Eq> {
+ context: &'cx mut ScriptContext<'a>,
+ data: T,
+ original: T,
+}
+
+pub struct ConfigMut<'a, 'cx, T: Serialize + Eq> {
+ context: &'cx mut ScriptContext<'a>,
+ data: T,
+ original: T,
+}
+
+impl<T: Serialize + DeserializeOwned + Clone + Eq> Storage<T> {
+ pub const fn new(initial_state: fn() -> T) -> Self {
+ Self {
+ initial_state,
+ data: Mutex::new(None),
+ }
+ }
+
+ pub fn load(&self, context: &ScriptContext) -> T {
+ puffin::profile_function!();
+ let mut data = self.data.lock();
+ data.get_or_insert_with(|| {
+ context
+ .0
+ .storage
+ .data()
+ .map(messagepack_serde::from_slice)
+ .and_then(|r| r.ok())
+ .unwrap_or_else(self.initial_state)
+ })
+ .clone()
+ }
+
+ pub fn load_mut<'a, 'cx>(&self, context: &'cx mut ScriptContext<'a>) -> StorageMut<'a, 'cx, T> {
+ let data = self.load(context);
+ StorageMut {
+ original: data.clone(),
+ data,
+ context,
+ }
+ }
+
+ pub fn is_saving(&self, context: &ScriptContext) -> bool {
+ context.0.storage.is_saving_data()
+ }
+
+ pub fn save_failed(&self, context: &ScriptContext) -> bool {
+ context.0.storage.saving_data_failed()
+ }
+}
+
+impl<T: Serialize + DeserializeOwned + Clone + Eq> Config<T> {
+ pub const fn new(initial_state: fn() -> T) -> Self {
+ Self {
+ initial_state,
+ data: Mutex::new(None),
+ }
+ }
+
+ pub fn load(&self, context: &ScriptContext) -> T {
+ puffin::profile_function!();
+ let mut data = self.data.lock();
+ data.get_or_insert_with(|| {
+ context
+ .0
+ .storage
+ .config()
+ .map(toml::from_slice)
+ .and_then(|r| r.ok())
+ .unwrap_or_else(self.initial_state)
+ })
+ .clone()
+ }
+
+ pub fn load_mut<'a, 'cx>(&self, context: &'cx mut ScriptContext<'a>) -> StorageMut<'a, 'cx, T> {
+ let data = self.load(context);
+ StorageMut {
+ original: data.clone(),
+ data,
+ context,
+ }
+ }
+
+ pub fn is_saving(&self, context: &ScriptContext) -> bool {
+ context.0.storage.is_saving_config()
+ }
+
+ pub fn save_failed(&self, context: &ScriptContext) -> bool {
+ context.0.storage.saving_config_failed()
+ }
+}
+
+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) {
+ puffin::profile_function!();
+ if self.data == self.original {
+ return;
+ }
+
+ let new_save = match messagepack_serde::to_vec(&self.data) {
+ Ok(new_save) => new_save.into_boxed_slice(),
+ Err(e) => {
+ log::error!("{e}");
+ return;
+ }
+ };
+ self.context
+ .0
+ .operation_queue
+ .push(Operation::UpdateStorage { new_save });
+ }
+}
+
+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) {
+ puffin::profile_function!();
+ if self.data == self.original {
+ return;
+ }
+
+ let new_save = match toml::to_string(&self.data) {
+ Ok(new_save) => new_save.as_bytes().into(),
+ Err(e) => {
+ log::error!("{e}");
+ return;
+ }
+ };
+ self.context
+ .0
+ .operation_queue
+ .push(Operation::UpdateConfig { new_save });
+ }
+}
diff --git a/platforms/tortuise-script-compiled/src/time.rs b/platforms/tortuise-script-compiled/src/time.rs
new file mode 100644
index 0000000..72a2f68
--- /dev/null
+++ b/platforms/tortuise-script-compiled/src/time.rs
@@ -0,0 +1,132 @@
+use core::{
+ ops::{Add, AddAssign, Sub, SubAssign},
+ time::Duration,
+};
+
+use serde::{Deserialize, Serialize};
+
+use crate::ScriptContext;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+#[repr(transparent)]
+#[serde(transparent)]
+pub struct SystemTime(std::time::SystemTime);
+
+#[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 {
+ puffin::profile_function!();
+ Duration::from_nanos_u128(self.0.time.delta_time().as_nanos())
+ }
+}
+
+impl SystemTime {
+ pub const UNIX_EPOCH: Self = Self(std::time::SystemTime::UNIX_EPOCH);
+
+ pub fn now(context: &ScriptContext) -> Self {
+ puffin::profile_function!();
+ Self(context.0.time.system_now())
+ }
+
+ pub fn duration_since(self, earlier: SystemTime) -> Option<Duration> {
+ puffin::profile_function!();
+ self.0.duration_since(earlier.0).ok()
+ }
+
+ pub fn checked_add(self, duration: Duration) -> Option<Self> {
+ Some(Self(self.0.checked_add(duration)?))
+ }
+
+ pub fn checked_sub(self, duration: Duration) -> Option<Self> {
+ Some(Self(self.0.checked_sub(duration)?))
+ }
+}
+
+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 {
+ puffin::profile_function!();
+ Self(context.0.time.monotonic_now().as_nanos())
+ }
+
+ 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)
+ }
+}