Implement lua APIs for reading & writing roblox files

This commit is contained in:
Filip Tibell 2023-03-21 21:53:10 +01:00
parent 531c579f2d
commit b8b39eb0b8
No known key found for this signature in database
5 changed files with 185 additions and 11 deletions

View file

@ -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"

View file

@ -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<DocumentError> for LuaError {
fn from(value: DocumentError) -> Self {
Self::RuntimeError(value.to_string())
}
}

View file

@ -73,6 +73,12 @@ impl DocumentFormat {
}
}
impl Default for DocumentFormat {
fn default() -> Self {
Self::Binary
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;

View file

@ -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<T> = Result<T, DocumentError>;
/**
@ -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<RwLock<WeakDom>> {
Arc::clone(&self.dom)
pub fn into_data_model_instance(self) -> DocumentResult<Instance> {
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<Vec<Instance>> {
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<Self> {
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<Instance>) -> DocumentResult<Self> {
for instance in &instances {
if instance.class_name == "DataModel" {
return Err(DocumentError::FromInstanceArrayInvalidArgs);
}
}
todo!()
}
}

View file

@ -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<LuaTable> {
for pair in roblox_module.pairs::<LuaValue, LuaValue>() {
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<LuaValue> {
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<LuaValue> {
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<Instance>)) -> 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(())
}