summaryrefslogtreecommitdiff
path: root/src/script/wasm.rs
blob: a1b923b9867142d1074f093f2d2ad3a468a94f6e (plain)
use core::slice;
use std::time::UNIX_EPOCH;

use crossterm::{
	event::KeyCode,
	style::{Attribute, Attributes, Color, ContentStyle},
};
use wasmi::{Caller, Engine, Linker};

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

use super::Operation;

unsafe fn str_from_memory<'a>(
	caller: &'a Caller<FullScriptContext<'_>>,
	offset: u32,
	length: u32,
) -> &'a str {
	let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
	unsafe {
		str::from_utf8_unchecked(slice::from_raw_parts(
			memory.data_ptr(caller).byte_add(offset as usize),
			length as usize,
		))
	}
}

#[allow(clippy::too_many_arguments)]
fn add_cell(
	mut caller: Caller<FullScriptContext<'_>>,
	character: u32,
	location: u32,
	z: u32,
	foreground_color: u32,
	background_color: u32,
	underline_color: u32,
	attributes: u32,
) {
	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 & 2048 != 0 {
		attribute_set.set(Attribute::CrossedOut);
	}
	if attributes & 4096 != 0 {
		attribute_set.set(Attribute::Fraktur);
	}
	if attributes & 8192 != 0 {
		attribute_set.set(Attribute::Framed);
	}
	if attributes & 16384 != 0 {
		attribute_set.set(Attribute::Encircled);
	}
	if attributes & 32768 != 0 {
		attribute_set.set(Attribute::OverLined);
	}

	caller
		.data_mut()
		.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,
		}));
}

fn window_size(caller: Caller<FullScriptContext<'_>>) -> u32 {
	let window_size = &caller.data().buffer.window_size;
	((window_size.rows as u32) << 16) | (window_size.columns as u32)
}

fn is_key_pressed_this_frame(caller: Caller<FullScriptContext<'_>>, key_code: u32) -> u32 {
	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,
	};

	if caller.data().input.is_key_pressed_this_frame(key_code) {
		1
	} else {
		0
	}
}

fn log(caller: Caller<FullScriptContext<'_>>, severity: u32, offset: u32, length: u32) {
	let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
	let message = unsafe {
		str::from_utf8_unchecked(slice::from_raw_parts(
			memory.data_ptr(&caller).byte_add(offset as usize),
			length as usize,
		))
	};
	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}");
}

fn exit(mut caller: Caller<FullScriptContext<'_>>) {
	caller.data_mut().operation_queue.push(Operation::Exit)
}

fn random_u64() -> u64 {
	getrandom::u64().unwrap_or(4)
}

fn scene_id_by_name(caller: Caller<FullScriptContext<'_>>, offset: u32, length: u32) -> u32 {
	let name = unsafe { str_from_memory(&caller, offset, length) };
	crate::scene::scene_id_by_name(&caller.data().manifest.scenes, name)
		.map(|u| u as u32)
		.unwrap_or(u32::MAX)
}

fn preload_scene(mut caller: Caller<FullScriptContext<'_>>, id: u32) {
	caller
		.data_mut()
		.operation_queue
		.push(Operation::PreloadScene(id as usize))
}

fn is_scene_loaded(caller: Caller<FullScriptContext<'_>>, id: u32) -> u32 {
	let context = caller.data();
	crate::scene::is_scene_loaded(
		&context.manifest.scenes,
		context.scripts,
		context.files,
		id as usize,
	) as u32
}

fn switch_scene(mut caller: Caller<FullScriptContext<'_>>, id: u32) {
	caller
		.data_mut()
		.operation_queue
		.push(Operation::SwitchScene(id as usize));
}

fn script_id_by_name(caller: Caller<FullScriptContext<'_>>, offset: u32, length: u32) -> u32 {
	let context = caller.data();
	let name = unsafe { str_from_memory(&caller, offset, length) };
	context
		.scripts
		.script_id_by_name(name)
		.map(|u| u as u32)
		.unwrap_or(u32::MAX)
}

fn preload_script(mut caller: Caller<FullScriptContext<'_>>, id: u32) {
	caller
		.data_mut()
		.operation_queue
		.push(Operation::PreloadScript(id as usize));
}

fn is_script_loaded(caller: Caller<FullScriptContext<'_>>, id: u32) -> u32 {
	let context = caller.data();
	context.scripts.is_script_loaded(id as usize, context.files) as u32
}

fn unload_script(mut caller: Caller<FullScriptContext<'_>>, id: u32) {
	caller
		.data_mut()
		.operation_queue
		.push(Operation::UnloadScript(id as usize));
}

fn activate_script(mut caller: Caller<FullScriptContext<'_>>, id: u32) {
	caller
		.data_mut()
		.operation_queue
		.push(Operation::ActivateScript(id as usize));
}

fn deactivate_script(mut caller: Caller<FullScriptContext<'_>>, id: u32) {
	caller
		.data_mut()
		.operation_queue
		.push(Operation::DeactivateScript(id as usize));
}

fn state_size(caller: Caller<FullScriptContext<'_>>) -> u32 {
	let context = caller.data();
	context
		.scripts
		.script_states
		.get(&context.script_id)
		.map(|state| state.len())
		.unwrap_or_default() as u32
}

fn load_state(mut caller: Caller<FullScriptContext<'_>>, offset: u32) {
	let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
	let context = caller.data();
	let state = context
		.scripts
		.script_states
		.get(&context.script_id)
		.map(|state| &**state)
		.unwrap_or(&[]);
	memory.write(&mut caller, offset as usize, state).unwrap();
}

fn save_state(mut caller: Caller<FullScriptContext<'_>>, offset: u32, length: u32) {
	let offset = offset as usize;
	let length = length as usize;
	let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
	let new_state = Box::from(&memory.data(&caller)[offset..(offset + length)]);
	let context = caller.data_mut();
	context.operation_queue.push(Operation::UpdateState {
		script_id: context.script_id,
		new_state,
	})
}

fn system_now(caller: Caller<FullScriptContext<'_>>) -> u64 {
	caller
		.data()
		.time
		.system_now()
		.duration_since(UNIX_EPOCH)
		.unwrap()
		.as_nanos() as u64
}

fn monotonic_now(caller: Caller<FullScriptContext<'_>>) -> u64 {
	caller.data().time.monotonic_now().as_nanos() as u64
}

fn delta_time(caller: Caller<FullScriptContext<'_>>) -> u64 {
	caller.data().time.delta_time().as_nanos() as u64
}

pub fn linker<'ctx>(
	engine: &Engine,
) -> Result<wasmi::Linker<FullScriptContext<'ctx>>, wasmi::Error> {
	macro_rules! add_fns {
		($linker: expr, [$($func: ident),*]) => {
			$($linker.func_wrap("tortuise", stringify!($func), $func)?;)*
		};
	}

	let mut linker = Linker::new(engine);
	add_fns!(
		linker,
		[
			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,
			system_now,
			monotonic_now,
			delta_time
		]
	);
	Ok(linker)
}