summaryrefslogtreecommitdiff
path: root/src/script/wasm.rs
blob: 9b9cd915883d9fe804397a6c73b98df7bc8ad4d0 (plain)
use std::time::UNIX_EPOCH;

use crossterm::{
	event::KeyCode,
	style::{Attribute, Attributes, Color, ContentStyle},
};
use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store};

use crate::{buffer::Cell, script::FullScriptContext};

use super::Operation;

pub struct WasmModule {
	pub instance: ModuleInstance,
}

fn str_from_memory(caller: &FuncContext<'_>, offset: u32, length: u32) -> tinywasm::Result<String> {
	let memory = caller.memory("memory").unwrap();
	memory.read_string(caller.store(), offset as usize, length as usize)
}

fn context_mut<'b>(address: u64) -> &'b mut FullScriptContext<'b> {
	unsafe {
		std::ptr::with_exposed_provenance_mut::<FullScriptContext<'b>>(address as usize)
			.as_mut()
			.unwrap_unchecked()
	}
}

fn context_ref<'b>(address: u64) -> &'b FullScriptContext<'b> {
	unsafe {
		std::ptr::with_exposed_provenance::<FullScriptContext<'b>>(address as usize)
			.as_ref()
			.unwrap_unchecked()
	}
}

fn add_cell(
	_caller: FuncContext<'_>,
	(
		context,
		character,
		location,
		z,
		foreground_color,
		background_color,
		underline_color,
		attributes,
	): (u64, u32, u32, u32, u32, u32, u32, u32),
) -> tinywasm::Result<()> {
	puffin::profile_function!();
	const STANDARD_COLOR: u32 = 1 << 24;
	const ANSI_COLOR: u32 = 2 << 24;
	const RGB_COLOR: u32 = 3 << 24;
	const UNSUPPORTED: u32 = 4 << 24;

	fn get_color(color: u32) -> Option<Color> {
		match color {
			0 => None,
			STANDARD_COLOR..ANSI_COLOR => Some(match color & 0xff {
				0 => Color::Reset,
				1 => Color::Black,
				2 => Color::DarkGrey,
				3 => Color::Red,
				4 => Color::DarkRed,
				5 => Color::Green,
				6 => Color::DarkGreen,
				7 => Color::Yellow,
				8 => Color::DarkYellow,
				9 => Color::Blue,
				10 => Color::DarkBlue,
				11 => Color::Magenta,
				12 => Color::DarkMagenta,
				13 => Color::Cyan,
				14 => Color::DarkCyan,
				15 => Color::White,
				16 => Color::Grey,
				_ => Color::Reset,
			}),
			ANSI_COLOR..RGB_COLOR => Some(Color::AnsiValue((color & 0xff) as u8)),
			RGB_COLOR..UNSUPPORTED => Some(Color::Rgb {
				r: ((color >> 16) & 0xff) as u8,
				g: ((color >> 8) & 0xff) as u8,
				b: (color & 0xff) as u8,
			}),
			_ => None,
		}
	}

	let mut attribute_set = Attributes::none();
	if attributes & 1 != 0 {
		attribute_set.set(Attribute::Bold);
	}
	if attributes & 2 != 0 {
		attribute_set.set(Attribute::Dim);
	}
	if attributes & 4 != 0 {
		attribute_set.set(Attribute::Italic);
	}
	if attributes & 8 != 0 {
		attribute_set.set(Attribute::Underlined);
	}
	if attributes & 16 != 0 {
		attribute_set.set(Attribute::DoubleUnderlined);
	}
	if attributes & 32 != 0 {
		attribute_set.set(Attribute::Undercurled);
	}
	if attributes & 64 != 0 {
		attribute_set.set(Attribute::Underdotted);
	}
	if attributes & 128 != 0 {
		attribute_set.set(Attribute::Underdashed);
	}
	if attributes & 256 != 0 {
		attribute_set.set(Attribute::SlowBlink);
	}
	if attributes & 512 != 0 {
		attribute_set.set(Attribute::RapidBlink);
	}
	if attributes & 1024 != 0 {
		attribute_set.set(Attribute::Reverse);
	}
	if attributes & 2048 != 0 {
		attribute_set.set(Attribute::Hidden);
	}
	if attributes & 4096 != 0 {
		attribute_set.set(Attribute::CrossedOut);
	}
	if attributes & 8192 != 0 {
		attribute_set.set(Attribute::Fraktur);
	}
	if attributes & 16384 != 0 {
		attribute_set.set(Attribute::Framed);
	}
	if attributes & 32768 != 0 {
		attribute_set.set(Attribute::Encircled);
	}
	if attributes & 65536 != 0 {
		attribute_set.set(Attribute::OverLined);
	}

	context_mut(context)
		.operation_queue
		.push(Operation::AddCell(Cell {
			character: char::from_u32(character).unwrap_or_default(),
			style: ContentStyle {
				foreground_color: get_color(foreground_color),
				background_color: get_color(background_color),
				underline_color: get_color(underline_color),
				attributes: attribute_set,
			},
			x: (location >> 16) as u16,
			y: (location & 0xffff) as u16,
			z: z as u8,
		}));
	Ok(())
}

fn window_size(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> {
	puffin::profile_function!();
	let window_size = &context_ref(context).buffer.window_size;
	Ok(((window_size.rows as u32) << 16) | (window_size.columns as u32))
}

fn is_key_pressed_this_frame(
	_caller: FuncContext<'_>,
	(context, key_code): (u64, u32),
) -> tinywasm::Result<u32> {
	puffin::profile_function!();
	const BACKSPACE: u32 = 1 << 24;
	const ENTER: u32 = 2 << 24;
	const LEFT: u32 = 3 << 24;
	const RIGHT: u32 = 4 << 24;
	const UP: u32 = 5 << 24;
	const DOWN: u32 = 6 << 24;
	const HOME: u32 = 7 << 24;
	const END: u32 = 8 << 24;
	const PAGE_UP: u32 = 9 << 24;
	const PAGE_DOWN: u32 = 10 << 24;
	const TAB: u32 = 11 << 24;
	const BACK_TAB: u32 = 12 << 24;
	const DELETE: u32 = 13 << 24;
	const INSERT: u32 = 14 << 24;
	const F: u32 = 15 << 24;
	const CHAR: u32 = 16 << 24;
	const ESC: u32 = 17 << 24;
	let key_code = match key_code {
		0 => KeyCode::Null,
		BACKSPACE => KeyCode::Backspace,
		ENTER => KeyCode::Enter,
		LEFT => KeyCode::Left,
		RIGHT => KeyCode::Right,
		UP => KeyCode::Up,
		DOWN => KeyCode::Down,
		HOME => KeyCode::Home,
		END => KeyCode::End,
		PAGE_UP => KeyCode::PageUp,
		PAGE_DOWN => KeyCode::PageDown,
		TAB => KeyCode::Tab,
		BACK_TAB => KeyCode::BackTab,
		DELETE => KeyCode::Delete,
		INSERT => KeyCode::Insert,
		F..CHAR => KeyCode::F((key_code & 0xff) as u8),
		CHAR..ESC => KeyCode::Char(char::from_u32(key_code & 0xffffff).unwrap_or_default()),
		ESC => KeyCode::Esc,
		_ => KeyCode::Null,
	};

	Ok(
		if context_ref(context)
			.input
			.is_key_pressed_this_frame(key_code)
		{
			1
		} else {
			0
		},
	)
}

fn log(
	caller: FuncContext<'_>,
	(severity, offset, length): (u32, u32, u32),
) -> tinywasm::Result<()> {
	puffin::profile_function!();
	let message = str_from_memory(&caller, offset, length)?;
	let severity = match severity {
		1 => log::Level::Error,
		2 => log::Level::Warn,
		3 => log::Level::Info,
		4 => log::Level::Debug,
		5 => log::Level::Trace,
		_ => log::Level::Error,
	};
	log::log!(severity, "{message}");
	Ok(())
}

fn exit(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<()> {
	puffin::profile_function!();
	context_mut(context).operation_queue.push(Operation::Exit);
	Ok(())
}

fn random_u64(_caller: FuncContext<'_>, (): ()) -> tinywasm::Result<u64> {
	puffin::profile_function!();
	Ok(getrandom::u64().unwrap_or(4))
}

fn scene_id_by_name(
	caller: FuncContext<'_>,
	(context, offset, length): (u64, u32, u32),
) -> tinywasm::Result<u32> {
	puffin::profile_function!();
	let name = str_from_memory(&caller, offset, length)?;
	Ok(
		crate::scene::scene_id_by_name(&context_ref(context).manifest.scenes, &name)
			.map(|u| u as u32)
			.unwrap_or(u32::MAX),
	)
}

fn preload_scene(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> {
	puffin::profile_function!();
	context_mut(context)
		.operation_queue
		.push(Operation::PreloadScene(id as usize));
	Ok(())
}

fn is_scene_loaded(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<u32> {
	puffin::profile_function!();
	let context = context_ref(context);
	Ok(crate::scene::is_scene_loaded(
		&context.manifest.scenes,
		context.scripts,
		&context.files,
		id as usize,
	) as u32)
}

fn switch_scene(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> {
	puffin::profile_function!();
	context_mut(context)
		.operation_queue
		.push(Operation::SwitchScene(id as usize));
	Ok(())
}

fn script_id_by_name(
	caller: FuncContext<'_>,
	(context, offset, length): (u64, u32, u32),
) -> tinywasm::Result<u32> {
	puffin::profile_function!();
	let name = str_from_memory(&caller, offset, length)?;
	let context = context_ref(context);
	Ok(context
		.scripts
		.script_id_by_name(&name)
		.map(|u| u as u32)
		.unwrap_or(u32::MAX))
}

fn preload_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> {
	puffin::profile_function!();
	context_mut(context)
		.operation_queue
		.push(Operation::PreloadScript(id as usize));
	Ok(())
}

fn is_script_loaded(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<u32> {
	puffin::profile_function!();
	let context = context_ref(context);
	Ok(context
		.scripts
		.is_script_loaded(id as usize, &context.files) as u32)
}

fn unload_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> {
	puffin::profile_function!();
	context_mut(context)
		.operation_queue
		.push(Operation::UnloadScript(id as usize));
	Ok(())
}

fn activate_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> {
	puffin::profile_function!();
	context_mut(context)
		.operation_queue
		.push(Operation::ActivateScript(id as usize));
	Ok(())
}

fn deactivate_script(_caller: FuncContext<'_>, (context, id): (u64, u32)) -> tinywasm::Result<()> {
	puffin::profile_function!();
	context_mut(context)
		.operation_queue
		.push(Operation::DeactivateScript(id as usize));
	Ok(())
}

fn state_size(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> {
	puffin::profile_function!();
	let context = context_ref(context);
	Ok(context
		.scripts
		.script_states
		.get(&context.script_id)
		.map(|state| state.len())
		.unwrap_or_default() as u32)
}

fn load_state(mut caller: FuncContext<'_>, (context, offset): (u64, u32)) -> tinywasm::Result<()> {
	puffin::profile_function!();
	let memory = caller.memory("memory").unwrap();
	let context = context_ref(context);
	let state = context
		.scripts
		.script_states
		.get(&context.script_id)
		.map(|state| &**state)
		.unwrap_or(&[]);
	memory.write(&mut caller, offset as usize, state).unwrap();
	Ok(())
}

fn save_state(
	mut caller: FuncContext<'_>,
	(context, offset, length): (u64, u32, u32),
) -> tinywasm::Result<()> {
	puffin::profile_function!();
	let offset = offset as usize;
	let length = length as usize;
	let memory = caller.memory("memory").unwrap();
	let new_state = memory
		.read_vec(caller.store_mut(), offset, length)
		.unwrap()
		.into_boxed_slice();
	let context = context_mut(context);
	context.operation_queue.push(Operation::UpdateState {
		script_id: context.script_id,
		new_state,
	});
	Ok(())
}

fn entity_size(
	caller: FuncContext<'_>,
	(context, name, name_len): (u64, u32, u32),
) -> tinywasm::Result<u32> {
	puffin::profile_function!();
	let name = str_from_memory(&caller, name, name_len)?;
	let context = context_ref(context);
	Ok(context
		.scripts
		.entities
		.get(&*name)
		.map(|entity| entity.len())
		.unwrap_or_default() as u32)
}

fn load_entity(
	mut caller: FuncContext<'_>,
	(context, name, name_len, offset): (u64, u32, u32, u32),
) -> tinywasm::Result<()> {
	puffin::profile_function!();
	let name = str_from_memory(&caller, name, name_len)?;
	let memory = caller.memory("memory").unwrap();
	let context = context_ref(context);
	let entity = context
		.scripts
		.entities
		.get(&*name)
		.map(|entity| &**entity)
		.unwrap_or(&[]);
	memory.write(&mut caller, offset as usize, entity).unwrap();
	Ok(())
}

fn save_entity(
	mut caller: FuncContext<'_>,
	(context, name, name_len, offset, length): (u64, u32, u32, u32, u32),
) -> tinywasm::Result<()> {
	puffin::profile_function!();
	let offset = offset as usize;
	let length = length as usize;
	let memory = caller.memory("memory").unwrap();
	let new_state = memory
		.read_vec(caller.store_mut(), offset, length)
		.unwrap()
		.into_boxed_slice();
	let name = str_from_memory(&caller, name, name_len)?.into_boxed_str();
	let context = context_mut(context);
	context
		.operation_queue
		.push(Operation::UpdateEntity { name, new_state });
	Ok(())
}

fn storage_size(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u32> {
	puffin::profile_function!();
	Ok(context_ref(context)
		.storage
		.get()
		.map(|data| data.len())
		.unwrap_or_default() as u32)
}

fn load_storage(
	mut caller: FuncContext<'_>,
	(context, offset): (u64, u32),
) -> tinywasm::Result<()> {
	puffin::profile_function!();
	let memory = caller.memory("memory").unwrap();
	let context = context_ref(context);
	let state = context.storage.get().unwrap_or(&[]);
	memory.write(&mut caller, offset as usize, state).unwrap();
	Ok(())
}

fn save_storage(
	mut caller: FuncContext<'_>,
	(context, offset, length): (u64, u32, u32),
) -> tinywasm::Result<()> {
	puffin::profile_function!();
	let offset = offset as usize;
	let length = length as usize;
	let memory = caller.memory("memory").unwrap();
	let new_save = memory
		.read_vec(caller.store_mut(), offset, length)
		.unwrap()
		.into_boxed_slice();
	let context = context_mut(context);
	context
		.operation_queue
		.push(Operation::UpdateStorage { new_save });
	Ok(())
}

fn system_now(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u64> {
	puffin::profile_function!();
	Ok(context_ref(context)
		.time
		.system_now()
		.duration_since(UNIX_EPOCH)
		.unwrap()
		.as_nanos() as u64)
}

fn monotonic_now(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u64> {
	puffin::profile_function!();
	Ok(context_ref(context).time.monotonic_now().as_nanos() as u64)
}

fn delta_time(_caller: FuncContext<'_>, context: u64) -> tinywasm::Result<u64> {
	puffin::profile_function!();
	Ok(context_ref(context).time.delta_time().as_nanos() as u64)
}

pub fn linker(store: &mut Store) -> Result<Imports, tinywasm::Error> {
	macro_rules! add_fns {
		($linker: expr, $store: expr, [$($func: ident),*]) => {
			$($linker.define("tortuise", stringify!($func), HostFunction::from($store, $func));)*
		};
	}

	let mut linker = Imports::new();
	add_fns!(
		linker,
		store,
		[
			add_cell,
			window_size,
			is_key_pressed_this_frame,
			log,
			exit,
			random_u64,
			scene_id_by_name,
			preload_scene,
			switch_scene,
			is_scene_loaded,
			script_id_by_name,
			preload_script,
			is_script_loaded,
			unload_script,
			activate_script,
			deactivate_script,
			state_size,
			load_state,
			save_state,
			entity_size,
			load_entity,
			save_entity,
			storage_size,
			load_storage,
			save_storage,
			system_now,
			monotonic_now,
			delta_time
		]
	);
	Ok(linker)
}