From b8b39eb0b8b23f1099aadbc42437e0680c81f514 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Tue, 21 Mar 2023 21:53:10 +0100 Subject: [PATCH] Implement lua APIs for reading & writing roblox files --- .cargo/config.toml | 6 -- packages/lib-roblox/src/document/error.rs | 15 ++++ packages/lib-roblox/src/document/format.rs | 6 ++ packages/lib-roblox/src/document/mod.rs | 87 +++++++++++++++++++++- packages/lib/src/globals/roblox.rs | 82 +++++++++++++++++++- 5 files changed, 185 insertions(+), 11 deletions(-) delete mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index d3d204d..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,6 +0,0 @@ -[env] -# We only use these blocking::unblock threads for prompting -# for interactive input using stdin, the default amount of -# threads is 500 which is unnecessarily high, we probably -# only need one thread here but lets do 10 just in case -BLOCKING_MAX_THREADS = "10" diff --git a/packages/lib-roblox/src/document/error.rs b/packages/lib-roblox/src/document/error.rs index ccb79d4..de1b5e2 100644 --- a/packages/lib-roblox/src/document/error.rs +++ b/packages/lib-roblox/src/document/error.rs @@ -1,3 +1,4 @@ +use mlua::prelude::*; use thiserror::Error; #[derive(Debug, Clone, Error)] @@ -12,4 +13,18 @@ pub enum DocumentError { ReadError(String), #[error("Failed to write document to buffer")] WriteError(String), + #[error("Failed to convert into a DataModel - the given document is not a place")] + IntoDataModelInvalidArgs, + #[error("Failed to convert into array of Instances - the given document is a place")] + IntoInstanceArrayInvalidArgs, + #[error("Failed to convert into a document - the given instance is not a DataModel")] + FromDataModelInvalidArgs, + #[error("Failed to convert into a document - a given instances is a DataModel")] + FromInstanceArrayInvalidArgs, +} + +impl From for LuaError { + fn from(value: DocumentError) -> Self { + Self::RuntimeError(value.to_string()) + } } diff --git a/packages/lib-roblox/src/document/format.rs b/packages/lib-roblox/src/document/format.rs index e1eb1ea..4fa66b0 100644 --- a/packages/lib-roblox/src/document/format.rs +++ b/packages/lib-roblox/src/document/format.rs @@ -73,6 +73,12 @@ impl DocumentFormat { } } +impl Default for DocumentFormat { + fn default() -> Self { + Self::Binary + } +} + #[cfg(test)] mod tests { use std::path::PathBuf; diff --git a/packages/lib-roblox/src/document/mod.rs b/packages/lib-roblox/src/document/mod.rs index baac1b5..4cbd152 100644 --- a/packages/lib-roblox/src/document/mod.rs +++ b/packages/lib-roblox/src/document/mod.rs @@ -1,6 +1,6 @@ use std::sync::{Arc, RwLock}; -use rbx_dom_weak::WeakDom; +use rbx_dom_weak::{InstanceBuilder as DomInstanceBuilder, WeakDom}; use rbx_xml::{ DecodeOptions as XmlDecodeOptions, DecodePropertyBehavior as XmlDecodePropertyBehavior, EncodeOptions as XmlEncodeOptions, EncodePropertyBehavior as XmlEncodePropertyBehavior, @@ -14,6 +14,8 @@ pub use error::*; pub use format::*; pub use kind::*; +use crate::instance::Instance; + pub type DocumentResult = Result; /** @@ -189,9 +191,86 @@ impl Document { } /** - Gets the underlying weak dom for this document. + Creates a DataModel instance out of this document. + + Will error if the document is not a place. */ - pub fn dom(&self) -> Arc> { - Arc::clone(&self.dom) + pub fn into_data_model_instance(self) -> DocumentResult { + if self.kind != DocumentKind::Place { + return Err(DocumentError::IntoDataModelInvalidArgs); + } + + // NOTE: We create a new scope here to avoid deadlocking, + // creating a new instance will try to get the dom rwlock + let data_model_ref = { + let mut dom_handle = self.dom.write().unwrap(); + let dom_root = dom_handle.root_ref(); + + let data_model_ref = dom_handle.insert(dom_root, DomInstanceBuilder::new("DataModel")); + let data_model_child_refs = dom_handle.root().children().to_vec(); + + for child_ref in data_model_child_refs { + if child_ref != data_model_ref { + dom_handle.transfer_within(child_ref, data_model_ref); + } + } + + data_model_ref + }; + + Ok(Instance::new(&self.dom, data_model_ref)) + } + + /** + Creates an array of instances out of this document. + + Will error if the document is not a model. + */ + pub fn into_instance_array(self) -> DocumentResult> { + if self.kind != DocumentKind::Model { + return Err(DocumentError::IntoInstanceArrayInvalidArgs); + } + + // NOTE: We create a new scope here to avoid deadlocking, + // creating a new instance will try to get the dom rwlock + let root_child_refs = { + let dom_handle = self.dom.read().unwrap(); + dom_handle.root().children().to_vec() + }; + + let root_child_instances = root_child_refs + .into_iter() + .map(|child_ref| Instance::new(&self.dom, child_ref)) + .collect(); + + Ok(root_child_instances) + } + + /** + Creates a Document out of a DataModel instance. + + Will error if the instance is not a DataModel. + */ + pub fn from_data_model_instance(instance: Instance) -> DocumentResult { + if instance.class_name != "DataModel" { + return Err(DocumentError::FromDataModelInvalidArgs); + } + + todo!() + } + + /** + Creates an array of instances out of this document. + + Will error if the document is not a model. + */ + pub fn from_instance_array(instances: Vec) -> DocumentResult { + for instance in &instances { + if instance.class_name == "DataModel" { + return Err(DocumentError::FromInstanceArrayInvalidArgs); + } + } + + todo!() } } diff --git a/packages/lib/src/globals/roblox.rs b/packages/lib/src/globals/roblox.rs index e99ee36..7a55fa5 100644 --- a/packages/lib/src/globals/roblox.rs +++ b/packages/lib/src/globals/roblox.rs @@ -1,4 +1,13 @@ +use std::path::PathBuf; + +use blocking::unblock; use mlua::prelude::*; +use tokio::fs; + +use lune_roblox::{ + document::{Document, DocumentError, DocumentFormat, DocumentKind}, + instance::Instance, +}; use crate::lua::table::TableBuilder; @@ -8,8 +17,79 @@ pub fn create(lua: &'static Lua) -> LuaResult { for pair in roblox_module.pairs::() { roblox_constants.push(pair?); } - // TODO: Add async functions for reading & writing documents, creating instances TableBuilder::new(lua)? .with_values(roblox_constants)? + .with_async_function("readPlaceFile", read_place_file)? + .with_async_function("readModelFile", read_model_file)? + .with_async_function("writePlaceFile", write_place_file)? + .with_async_function("writeModelFile", write_model_file)? .build_readonly() } + +fn parse_file_path(path: String) -> LuaResult<(PathBuf, DocumentFormat)> { + let file_path = PathBuf::from(path); + let file_ext = file_path + .extension() + .ok_or_else(|| { + LuaError::RuntimeError(format!( + "Missing file extension for file path: '{}'", + file_path.display() + )) + })? + .to_string_lossy(); + let doc_format = DocumentFormat::from_extension(&file_ext).ok_or_else(|| { + LuaError::RuntimeError(format!( + "Invalid file extension for writing place file: '{}'", + file_ext + )) + })?; + Ok((file_path, doc_format)) +} + +async fn read_place_file(lua: &Lua, path: String) -> LuaResult { + let bytes = fs::read(path).await.map_err(LuaError::external)?; + let fut = unblock(move || { + let doc = Document::from_bytes(bytes, DocumentKind::Place)?; + let data_model = doc.into_data_model_instance()?; + Ok::<_, DocumentError>(data_model) + }); + fut.await?.to_lua(lua) +} + +async fn read_model_file(lua: &Lua, path: String) -> LuaResult { + let bytes = fs::read(path).await.map_err(LuaError::external)?; + let fut = unblock(move || { + let doc = Document::from_bytes(bytes, DocumentKind::Model)?; + let instance_array = doc.into_instance_array()?; + Ok::<_, DocumentError>(instance_array) + }); + fut.await?.to_lua(lua) +} + +async fn write_place_file(_: &Lua, (path, data_model): (String, Instance)) -> LuaResult<()> { + let (file_path, doc_format) = parse_file_path(path)?; + let fut = unblock(move || { + let doc = Document::from_data_model_instance(data_model)?; + let bytes = doc.to_bytes_with_format(doc_format)?; + Ok::<_, DocumentError>(bytes) + }); + let bytes = fut.await?; + fs::write(file_path, bytes) + .await + .map_err(LuaError::external)?; + Ok(()) +} + +async fn write_model_file(_: &Lua, (path, instances): (String, Vec)) -> LuaResult<()> { + let (file_path, doc_format) = parse_file_path(path)?; + let fut = unblock(move || { + let doc = Document::from_instance_array(instances)?; + let bytes = doc.to_bytes_with_format(doc_format)?; + Ok::<_, DocumentError>(bytes) + }); + let bytes = fut.await?; + fs::write(file_path, bytes) + .await + .map_err(LuaError::external)?; + Ok(()) +}