summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/buffer.rs46
-rw-r--r--src/file.rs154
-rw-r--r--src/input.rs27
-rw-r--r--src/lib.rs4
-rw-r--r--src/main.rs133
-rw-r--r--src/scene.rs43
-rw-r--r--src/script.rs168
7 files changed, 521 insertions, 54 deletions
diff --git a/src/buffer.rs b/src/buffer.rs
index fa0feaa..9e91ae7 100644
--- a/src/buffer.rs
+++ b/src/buffer.rs
@@ -2,8 +2,8 @@ use std::io::Write;
use crossterm::{
cursor::{Hide, MoveTo, Show},
- event::{self, Event},
- execute,
+ event::{self, KeyEvent},
+ execute, queue,
style::{ContentStyle, PrintStyledContent, StyledContent},
terminal::{
BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen,
@@ -20,15 +20,31 @@ pub struct Cell {
pub z: u8,
}
-#[derive(Debug)]
-pub struct Buffer<'a, W: Write> {
- stdout: &'a mut W,
+pub struct Buffer {
+ stdout: Box<dyn Write>,
pub cells: Vec<Cell>,
pub window_size: WindowSize,
}
-impl<'a, W: Write> Buffer<'a, W> {
- pub fn new(stdout: &'a mut W) -> std::io::Result<Self> {
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum Event {
+ Key(KeyEvent),
+ Render,
+}
+
+impl TryFrom<crossterm::event::Event> for Event {
+ type Error = ();
+
+ fn try_from(value: crossterm::event::Event) -> Result<Self, Self::Error> {
+ match value {
+ crossterm::event::Event::Key(event) => Ok(Event::Key(event)),
+ _ => Err(()),
+ }
+ }
+}
+
+impl Buffer {
+ pub fn new(stdout: Box<dyn Write>) -> std::io::Result<Self> {
Ok(Self {
stdout,
cells: Vec::new(),
@@ -44,15 +60,16 @@ impl<'a, W: Write> Buffer<'a, W> {
fn render(&mut self) -> std::io::Result<()> {
self.cells.sort_unstable_by_key(|cell| cell.z);
- execute!(self.stdout, BeginSynchronizedUpdate, Clear(ClearType::All))?;
+ queue!(self.stdout, BeginSynchronizedUpdate, Clear(ClearType::All))?;
for cell in &self.cells {
- execute!(
+ queue!(
self.stdout,
MoveTo(cell.x, cell.y),
PrintStyledContent(StyledContent::new(cell.style, cell.character))
)?;
}
- execute!(self.stdout, EndSynchronizedUpdate)?;
+ queue!(self.stdout, EndSynchronizedUpdate)?;
+ self.stdout.flush()?;
Ok(())
}
@@ -65,9 +82,8 @@ impl<'a, W: Write> Buffer<'a, W> {
pub fn render_loop(
&mut self,
mut event_handler: impl FnMut(&mut Self, Event),
- mut update: impl FnMut(&mut Self),
) -> std::io::Result<()> {
- Self::initialize(self.stdout)?;
+ Self::initialize(&mut self.stdout)?;
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::scope(|scope| -> std::io::Result<()> {
scope.spawn(|| {
@@ -80,9 +96,11 @@ impl<'a, W: Write> Buffer<'a, W> {
});
loop {
while let Ok(event) = receiver.try_recv() {
- event_handler(self, event);
+ if let Ok(event) = event.try_into() {
+ event_handler(self, event);
+ }
}
- update(self);
+ event_handler(self, Event::Render);
self.render()?;
}
})?;
diff --git a/src/file.rs b/src/file.rs
new file mode 100644
index 0000000..8e96fa9
--- /dev/null
+++ b/src/file.rs
@@ -0,0 +1,154 @@
+use std::{ffi::OsStr, path::Path, sync::Arc};
+
+use rustc_hash::FxHashMap;
+use serde::{Deserialize, Serialize};
+use smol::{Task, block_on, unblock};
+
+use crate::scene::{Scene, scene_id_by_name};
+
+type FileTask = Task<std::io::Result<FileData>>;
+
+pub struct FileManager {
+ pub wasm_compiler: Arc<wasmi::Engine>,
+ active_tasks: FxHashMap<Box<Path>, FileTask>,
+ cache: FxHashMap<Box<Path>, FileData>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct GameManifest {
+ pub name: Box<str>,
+ pub first_scene: usize,
+ pub scenes: Box<[Scene]>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct TomlGameManifest {
+ name: Box<str>,
+ first_scene: Box<str>,
+ scene: Box<[Scene]>,
+}
+
+pub enum FileData {
+ GameManifest(GameManifest),
+ WasmModule(wasmi::Module),
+}
+
+fn normalize_game_manifest(manifest: TomlGameManifest) -> std::io::Result<GameManifest> {
+ let first_scene = scene_id_by_name(&manifest.scene, &manifest.first_scene).ok_or(
+ std::io::Error::other("Invalid first scene: scene not found"),
+ )?;
+ Ok(GameManifest {
+ name: manifest.name,
+ first_scene,
+ scenes: manifest.scene,
+ })
+}
+
+fn compile_data(path: &Path, data: &[u8], compiler: &wasmi::Engine) -> std::io::Result<FileData> {
+ if path == "manifest.postcard" {
+ return Ok(FileData::GameManifest(
+ postcard::from_bytes(data).map_err(std::io::Error::other)?,
+ ));
+ } else if path == "manifest.toml" {
+ return Ok(FileData::GameManifest(normalize_game_manifest(
+ toml::from_slice(data).map_err(std::io::Error::other)?,
+ )?));
+ }
+ let extension = path
+ .extension()
+ .and_then(OsStr::to_str)
+ .map(|ext| ext.to_lowercase());
+ if extension.as_deref() == Some("wasm") {
+ return Ok(FileData::WasmModule(
+ wasmi::Module::new(compiler, data).map_err(std::io::Error::other)?,
+ ));
+ }
+ Err(std::io::Error::other("Unrecognized file extension"))
+}
+
+impl FileData {
+ pub fn unwrap_manifest(&self) -> &GameManifest {
+ if let FileData::GameManifest(manifest) = self {
+ manifest
+ } else {
+ panic!("Not a game manifest");
+ }
+ }
+
+ pub fn unwrap_wasm_module(&self) -> &wasmi::Module {
+ if let FileData::WasmModule(module) = self {
+ module
+ } else {
+ panic!("Not a WASM module")
+ }
+ }
+}
+
+impl FileManager {
+ pub fn new() -> Self {
+ Self {
+ wasm_compiler: Arc::new(wasmi::Engine::default()),
+ active_tasks: FxHashMap::default(),
+ cache: FxHashMap::default(),
+ }
+ }
+
+ pub fn load_file(&mut self, path: Box<Path>) -> std::io::Result<&FileData> {
+ let data = compile_data(
+ path.as_ref(),
+ &std::fs::read(path.as_ref()).map(Vec::into_boxed_slice)?,
+ &self.wasm_compiler.clone(),
+ )?;
+ self.cache.entry(path.clone()).insert_entry(data);
+ Ok(self.cache.get(&path).unwrap())
+ }
+
+ pub fn get_loaded_file(&mut self, path: Box<Path>) -> Option<std::io::Result<&FileData>> {
+ if let Some(task) = self.active_tasks.get(path.as_ref())
+ && task.is_finished()
+ {
+ let data = match block_on(self.active_tasks.remove(&path).unwrap()) {
+ Ok(data) => data,
+ Err(e) => return Some(Err(e)),
+ };
+ self.cache.entry(path.clone()).insert_entry(data);
+ Some(Ok(self.cache.get(&path).unwrap()))
+ } else if let Some(data) = self.cache.get(&path) {
+ Some(Ok(data))
+ } else {
+ None
+ }
+ }
+
+ pub fn start_loading_file(&mut self, path: impl AsRef<Path>) {
+ let boxed_path = Box::from(path.as_ref());
+ let compiler = self.wasm_compiler.clone();
+ let task = unblock(move || {
+ compile_data(
+ &boxed_path,
+ &std::fs::read(&boxed_path).map(Vec::into_boxed_slice)?,
+ &compiler,
+ )
+ });
+ let boxed_path = Box::from(path.as_ref());
+ self.active_tasks.insert(boxed_path, task);
+ }
+
+ pub fn is_file_loaded(&self, path: impl AsRef<Path>) -> bool {
+ self.active_tasks
+ .get(path.as_ref())
+ .is_some_and(|task| task.is_finished())
+ || self.cache.contains_key(path.as_ref())
+ }
+
+ pub fn unload_file(&mut self, path: impl AsRef<Path>) {
+ self.active_tasks.remove(path.as_ref());
+ self.cache.remove(path.as_ref());
+ }
+}
+
+impl Default for FileManager {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/src/input.rs b/src/input.rs
new file mode 100644
index 0000000..c86daf4
--- /dev/null
+++ b/src/input.rs
@@ -0,0 +1,27 @@
+use crossterm::event::KeyCode;
+use rustc_hash::FxHashSet;
+
+#[derive(Debug, Default, Clone)]
+pub struct InputManager {
+ pressed_keys: FxHashSet<KeyCode>,
+}
+
+impl InputManager {
+ pub fn new() -> Self {
+ Self {
+ pressed_keys: FxHashSet::default(),
+ }
+ }
+
+ pub fn press_key(&mut self, key_code: KeyCode) {
+ self.pressed_keys.insert(key_code);
+ }
+
+ pub fn reset_next_frame(&mut self) {
+ self.pressed_keys.clear();
+ }
+
+ pub fn is_key_pressed_this_frame(&self, key_code: KeyCode) -> bool {
+ self.pressed_keys.contains(&key_code)
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 86b6256..3d76b0d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1 +1,5 @@
pub mod buffer;
+pub mod file;
+pub mod input;
+pub mod scene;
+pub mod script;
diff --git a/src/main.rs b/src/main.rs
index e8c9f23..0cbb7c8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,48 +1,101 @@
-use std::time::Instant;
+use std::time::SystemTime;
-use crossterm::{
- event::{Event, KeyCode},
- style::ContentStyle,
+use crossterm::{event::KeyCode, style::ContentStyle};
+use mimalloc::MiMalloc;
+use tortuise::{
+ buffer::{Buffer, Cell, Event},
+ file::FileManager,
+ input::InputManager,
+ scene::switch_scenes,
+ script::{ScriptContext, ScriptDeclaration, ScriptManager},
};
-use tortuise::buffer::{Buffer, Cell};
+
+#[global_allocator]
+static GLOBAL: MiMalloc = MiMalloc;
+
+fn get_timestamp() -> u128 {
+ SystemTime::UNIX_EPOCH.elapsed().unwrap().as_nanos()
+}
+
+fn init_first_frame<'a>(_: &mut ScriptContext<'a>, state: &mut Box<[u8]>) {
+ *state = Box::from(get_timestamp().to_ne_bytes());
+}
+
+fn render_frame_rate<'a>(ctx: &mut ScriptContext<'a>, state: &mut Box<[u8]>) {
+ ctx.buffer.cells.clear();
+ let end = get_timestamp();
+ let last_frame_start = u128::from_ne_bytes(*state.as_array().unwrap());
+ let frame_time = end - last_frame_start + 1;
+ let digits = [
+ frame_time % 1_000_000 / 100_000,
+ frame_time % 100_000 / 10_000,
+ frame_time % 10_000 / 1_000,
+ frame_time % 1_000 / 100,
+ frame_time % 100 / 10,
+ frame_time % 10,
+ ];
+ for (x, digit) in digits.into_iter().enumerate() {
+ ctx.buffer.cells.push(Cell {
+ character: char::from_digit(digit as u32, 10).unwrap(),
+ x: x as u16,
+ y: 0,
+ z: 0,
+ style: ContentStyle::new(),
+ });
+ }
+ *state = Box::from(end.to_ne_bytes());
+}
+
+fn exit_on_escape(ctx: &mut ScriptContext, _: &mut Box<[u8]>) {
+ if ctx.input.is_key_pressed_this_frame(KeyCode::Esc) {
+ ctx.exit();
+ }
+}
fn main() {
- let mut stdout = std::io::stdout().lock();
- let mut buffer = Buffer::new(&mut stdout).unwrap();
- let mut last_frame_start = Instant::now();
+ let stdout = Box::new(std::io::stdout().lock());
+ let mut buffer = Buffer::new(stdout).unwrap();
+ let mut input = InputManager::new();
+ let mut context = ScriptContext {
+ buffer: &mut buffer,
+ input: &input,
+ };
+ let scripts = [
+ ScriptDeclaration::Static {
+ start: Some(init_first_frame),
+ update: Some(render_frame_rate),
+ end: None,
+ },
+ ScriptDeclaration::Static {
+ start: None,
+ update: Some(exit_on_escape),
+ end: None,
+ },
+ ];
+ let mut scripts = ScriptManager::new(&scripts);
+
+ let mut files = FileManager::default();
+ let manifest = files
+ .load_file(Box::from("manifest.toml".as_ref()))
+ .unwrap()
+ .unwrap_manifest();
+ switch_scenes(&manifest.scenes, &mut scripts, &mut context, 0);
+
buffer
- .render_loop(
- |renderer, event| {
- if let Event::Key(event) = event
- && event.code == KeyCode::Esc
- {
- let _ = renderer.finish();
- std::process::exit(0);
- }
- },
- |renderer| {
- renderer.cells.clear();
- let end = Instant::now();
- let frame_time = (end - last_frame_start).as_micros();
- let digits = [
- frame_time % 1_000_000 / 100_000,
- frame_time % 100_000 / 10_000,
- frame_time % 10_000 / 1_000,
- frame_time % 1_000 / 100,
- frame_time % 100 / 10,
- frame_time % 10,
- ];
- for (x, digit) in digits.into_iter().enumerate() {
- renderer.cells.push(Cell {
- character: char::from_digit(digit as u32, 10).unwrap(),
- x: x as u16,
- y: 0,
- z: 0,
- style: ContentStyle::new(),
- });
+ .render_loop(|buffer, event| match event {
+ Event::Key(event) => {
+ if event.is_press() {
+ input.press_key(event.code);
}
- last_frame_start = Instant::now();
- },
- )
+ }
+ Event::Render => {
+ let mut context = ScriptContext {
+ buffer,
+ input: &input,
+ };
+ scripts.run_scripts(&mut context);
+ input.reset_next_frame();
+ }
+ })
.unwrap();
}
diff --git a/src/scene.rs b/src/scene.rs
new file mode 100644
index 0000000..143eb0b
--- /dev/null
+++ b/src/scene.rs
@@ -0,0 +1,43 @@
+use rustc_hash::FxHashSet;
+use serde::{Deserialize, Serialize};
+
+use crate::script::{ScriptContext, ScriptManager};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Scene {
+ pub name: Box<str>,
+ pub scripts: ResourceRequirements,
+}
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+pub struct ResourceRequirements {
+ pub required: Box<[usize]>,
+ pub preload: Box<[usize]>,
+}
+
+pub fn scene_id_by_name(scenes: &[Scene], name: &str) -> Option<usize> {
+ for (i, scene) in scenes.iter().enumerate() {
+ if &*scene.name == name {
+ return Some(i);
+ }
+ }
+
+ None
+}
+
+pub fn switch_scenes(
+ scenes: &[Scene],
+ scripts: &mut ScriptManager,
+ context: &mut ScriptContext<'_>,
+ new: usize,
+) {
+ let active_scripts = FxHashSet::from_iter(scripts.active_scripts.iter().cloned());
+ let scripts_to_load = &scenes[new].scripts.required;
+ for script in scripts_to_load {
+ if !active_scripts.contains(script) {
+ scripts.activate_script(*script, context);
+ } else {
+ scripts.reset_script(*script, context);
+ }
+ }
+}
diff --git a/src/script.rs b/src/script.rs
new file mode 100644
index 0000000..dd65106
--- /dev/null
+++ b/src/script.rs
@@ -0,0 +1,168 @@
+use std::path::Path;
+
+use rustc_hash::FxHashMap;
+use wasmer::{Imports, Instance, Module, Store};
+
+use crate::{buffer::Buffer, file::FileManager, input::InputManager};
+
+pub struct ScriptManager<'a> {
+ scripts: &'a [ScriptDeclaration],
+ pub active_scripts: Vec<usize>,
+ script_states: FxHashMap<usize, Box<[u8]>>,
+}
+
+pub struct ScriptContext<'a> {
+ pub buffer: &'a mut Buffer,
+ pub input: &'a InputManager,
+}
+
+type StaticScriptFunction = Option<fn(&mut ScriptContext, &mut Box<[u8]>)>;
+
+pub enum ScriptDeclaration {
+ Static {
+ start: StaticScriptFunction,
+ update: StaticScriptFunction,
+ end: StaticScriptFunction,
+ },
+ Wasm {
+ path: Box<Path>,
+ },
+}
+
+pub struct WasmScript {
+ pub module: Module,
+ pub instance: Instance,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Profile {
+ Debug,
+ Release,
+}
+
+impl<'a> ScriptContext<'a> {
+ pub fn exit(&mut self) -> ! {
+ let _ = self.buffer.finish();
+ std::process::exit(0);
+ }
+}
+
+fn get_script_state(states: &mut FxHashMap<usize, Box<[u8]>>, script_id: usize) -> &mut Box<[u8]> {
+ states.entry(script_id).or_insert(Box::from([]))
+}
+
+impl<'a> ScriptManager<'a> {
+ pub fn new(scripts: &'a [ScriptDeclaration]) -> Self {
+ Self {
+ scripts,
+ active_scripts: Vec::new(),
+ script_states: FxHashMap::default(),
+ }
+ }
+
+ pub fn activate_script(
+ &mut self,
+ script_id: usize,
+ context: &mut ScriptContext<'_>,
+ files: &mut FileManager,
+ ) {
+ let Some(script_declaration) = self.scripts.get(script_id) else {
+ unreachable!("The active script does not exist");
+ };
+ match script_declaration {
+ ScriptDeclaration::Static { start, .. } => {
+ start.map(|s| {
+ s(
+ context,
+ get_script_state(&mut self.script_states, script_id),
+ )
+ });
+ }
+ ScriptDeclaration::Wasm { path } => {
+ let module = files.load_file(path.clone()).unwrap().unwrap_wasm_module();
+ let mut store = wasmi::Store::new(&files.wasm_compiler, context);
+ let instance = self
+ .linker
+ .instantiate_and_start(&mut store, &module)
+ .unwrap();
+ }
+ }
+ self.active_scripts.push(script_id);
+ }
+
+ pub fn deactivate_script(&mut self, script_id: usize, context: &mut ScriptContext<'_>) {
+ self.active_scripts.retain(|element| {
+ if *element != script_id {
+ let Some(script) = self.scripts.get(script_id) else {
+ unreachable!("The active script does not exist");
+ };
+ match script {
+ ScriptDeclaration::Static { end, .. } => {
+ end.map(|s| {
+ s(
+ context,
+ get_script_state(&mut self.script_states, script_id),
+ );
+ });
+ }
+ ScriptDeclaration::Wasm { .. } => todo!("wasm scripts"),
+ };
+
+ true
+ } else {
+ false
+ }
+ });
+ }
+
+ pub fn run_scripts(&mut self, context: &mut ScriptContext<'_>) {
+ for script_id in &self.active_scripts {
+ let Some(script) = self.scripts.get(*script_id) else {
+ unreachable!("The active script does not exist");
+ };
+ match *script {
+ ScriptDeclaration::Static { update, .. } => {
+ if let Some(s) = update {
+ s(
+ context,
+ get_script_state(&mut self.script_states, *script_id),
+ )
+ }
+ }
+ ScriptDeclaration::Wasm { .. } => todo!("wasm scripts"),
+ }
+ }
+ }
+
+ pub fn reset_script(&mut self, script_id: usize, context: &mut ScriptContext<'_>) {
+ let script = &self.scripts[script_id];
+ match script {
+ ScriptDeclaration::Static { start, end, .. } => {
+ if let Some(s) = start {
+ s(
+ context,
+ get_script_state(&mut self.script_states, script_id),
+ )
+ }
+ if let Some(s) = end {
+ s(
+ context,
+ get_script_state(&mut self.script_states, script_id),
+ )
+ }
+ }
+ ScriptDeclaration::Wasm { .. } => todo!("wasm scripts"),
+ }
+ }
+}
+
+impl WasmScript {
+ fn call_function(&self, name: &str, profile: Profile, store: &mut Store, imports: &Imports) {
+ let instance = match profile {
+ Profile::Debug => &Instance::new(store, &self.module, imports).unwrap(),
+ Profile::Release => &self.instance,
+ };
+ let function = instance.exports.get_function(name).unwrap();
+ function.call(store, &[]).unwrap();
+ }
+}