From 0f4c36219f73734e1119b64cb44258642163a3f3 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Thu, 9 Mar 2023 17:31:44 +0100 Subject: [PATCH] Implement auto detection of document kind for roblox lib --- Cargo.lock | 2 + packages/lib-roblox/Cargo.toml | 2 + packages/lib-roblox/src/document/kind.rs | 68 +++++++++++++++++++++--- packages/lib-roblox/src/document/mod.rs | 42 ++++++++++++--- packages/lib-roblox/src/instance/mod.rs | 66 +++++++++++++++++++++++ packages/lib-roblox/src/lib.rs | 2 + 6 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 packages/lib-roblox/src/instance/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 3343a9a..adbc934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -858,6 +858,8 @@ dependencies = [ "mlua", "rbx_binary", "rbx_dom_weak", + "rbx_reflection", + "rbx_reflection_database", "rbx_xml", "thiserror", ] diff --git a/packages/lib-roblox/Cargo.toml b/packages/lib-roblox/Cargo.toml index 5009e56..5e2c6cc 100644 --- a/packages/lib-roblox/Cargo.toml +++ b/packages/lib-roblox/Cargo.toml @@ -21,6 +21,8 @@ thiserror = "1.0" rbx_binary = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" } rbx_dom_weak = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" } +rbx_reflection = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" } +rbx_reflection_database = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" } rbx_xml = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" } # TODO: Split lune lib out into something like lune-core so diff --git a/packages/lib-roblox/src/document/kind.rs b/packages/lib-roblox/src/document/kind.rs index b1e5da1..5a63903 100644 --- a/packages/lib-roblox/src/document/kind.rs +++ b/packages/lib-roblox/src/document/kind.rs @@ -1,5 +1,9 @@ use std::path::Path; +use rbx_dom_weak::WeakDom; + +use crate::instance::instance_is_a_service; + /** A document kind specifier. @@ -51,13 +55,29 @@ impl DocumentKind { } /** - Try to detect a document kind specifier from file contents. + Try to detect a document kind specifier from a weak dom. - Returns `None` if the file contents do not seem to be from a valid roblox file. + Returns `None` if the given dom is empty and as such can not have its kind inferred. */ - pub fn from_bytes(_bytes: impl AsRef<[u8]>) -> Option { - // TODO: Implement this, read comment below - todo!("Investigate if it is possible to detect document kind from contents") + pub fn from_weak_dom(dom: &WeakDom) -> Option { + let mut has_top_level_child = false; + let mut has_top_level_service = false; + for child_ref in dom.root().children() { + if let Some(child_inst) = dom.get_by_ref(*child_ref) { + has_top_level_child = true; + if instance_is_a_service(&child_inst.class).unwrap_or(false) { + has_top_level_service = true; + break; + } + } + } + if has_top_level_service { + Some(Self::Place) + } else if has_top_level_child { + Some(Self::Model) + } else { + None + } } } @@ -65,6 +85,8 @@ impl DocumentKind { mod tests { use std::path::PathBuf; + use rbx_dom_weak::InstanceBuilder; + use super::*; #[test] @@ -149,5 +171,39 @@ mod tests { ); } - // TODO: Add tests here for the from_bytes implementation + #[test] + fn from_weak_dom() { + let empty = WeakDom::new(InstanceBuilder::new("Instance")); + assert_eq!(DocumentKind::from_weak_dom(&empty), None); + + let with_services = WeakDom::new( + InstanceBuilder::new("Instance") + .with_child(InstanceBuilder::new("Workspace")) + .with_child(InstanceBuilder::new("ReplicatedStorage")), + ); + assert_eq!( + DocumentKind::from_weak_dom(&with_services), + Some(DocumentKind::Place) + ); + + let with_children = WeakDom::new( + InstanceBuilder::new("Instance") + .with_child(InstanceBuilder::new("Model")) + .with_child(InstanceBuilder::new("Part")), + ); + assert_eq!( + DocumentKind::from_weak_dom(&with_children), + Some(DocumentKind::Model) + ); + + let with_mixed = WeakDom::new( + InstanceBuilder::new("Instance") + .with_child(InstanceBuilder::new("Workspace")) + .with_child(InstanceBuilder::new("Part")), + ); + assert_eq!( + DocumentKind::from_weak_dom(&with_mixed), + Some(DocumentKind::Place) + ); + } } diff --git a/packages/lib-roblox/src/document/mod.rs b/packages/lib-roblox/src/document/mod.rs index 2bff4c5..4c0fd6c 100644 --- a/packages/lib-roblox/src/document/mod.rs +++ b/packages/lib-roblox/src/document/mod.rs @@ -51,15 +51,10 @@ impl Document { } } - /** - Decodes and creates a new document from a byte buffer. - - This will automatically handle and detect if the document should be decoded - using a roblox binary or roblox xml format, and if it is a model or place file. - */ - pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result { + fn from_bytes_inner( + bytes: impl AsRef<[u8]>, + ) -> Result<(DocumentFormat, WeakDom), DocumentError> { let bytes = bytes.as_ref(); - let kind = DocumentKind::from_bytes(bytes).ok_or(DocumentError::UnknownKind)?; let format = DocumentFormat::from_bytes(bytes).ok_or(DocumentError::UnknownFormat)?; let dom = match format { DocumentFormat::InternalRoot => Err(DocumentError::InternalRootReadWrite), @@ -72,6 +67,37 @@ impl Document { .map_err(|err| DocumentError::ReadError(err.to_string())) } }?; + Ok((format, dom)) + } + + /** + Decodes and creates a new document from a byte buffer. + + This will automatically handle and detect if the document should be decoded + using a roblox binary or roblox xml format, and if it is a model or place file. + + Note that detection of model vs place file is heavily dependent on the structure + of the file, and a model file with services in it will detect as a place file, so + if possible using [`Document::from_bytes`] with an explicit kind should be preferred. + */ + pub fn from_bytes_auto(bytes: impl AsRef<[u8]>) -> Result { + let (format, dom) = Self::from_bytes_inner(bytes)?; + let kind = DocumentKind::from_weak_dom(&dom).ok_or(DocumentError::UnknownKind)?; + Ok(Self { + kind, + format, + dom: Arc::new(RwLock::new(dom)), + }) + } + + /** + Decodes and creates a new document from a byte buffer. + + This will automatically handle and detect if the document + should be decoded using a roblox binary or roblox xml format. + */ + pub fn from_bytes(bytes: impl AsRef<[u8]>, kind: DocumentKind) -> Result { + let (format, dom) = Self::from_bytes_inner(bytes)?; Ok(Self { kind, format, diff --git a/packages/lib-roblox/src/instance/mod.rs b/packages/lib-roblox/src/instance/mod.rs new file mode 100644 index 0000000..40d4d95 --- /dev/null +++ b/packages/lib-roblox/src/instance/mod.rs @@ -0,0 +1,66 @@ +use std::borrow::Borrow; + +use rbx_reflection::ClassTag; + +/** + Checks if an instance class matches a given class or superclass, similar to + [Instance::IsA](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA) + from the Roblox standard library. + + Note that this function may return `None` if it encounters a class or superclass + that does not exist in the currently known class reflection database. +*/ +#[allow(dead_code)] +pub fn instance_is_a( + instance_class_name: impl AsRef, + class_name: impl AsRef, +) -> Option { + let instance_class_name = instance_class_name.as_ref(); + let class_name = class_name.as_ref(); + + if class_name == "Instance" || instance_class_name == class_name { + Some(true) + } else { + let db = rbx_reflection_database::get(); + + let mut super_class_name = instance_class_name; + while super_class_name != class_name { + let class_descriptor = db.classes.get(super_class_name)?; + if let Some(sup) = &class_descriptor.superclass { + super_class_name = sup.borrow(); + } else { + return Some(false); + } + } + + Some(true) + } +} + +/** + Checks if an instance class is a service. + + This is separate from [`instance_is_a`] since services do not share a + common base class, and are instead determined through reflection tags. + + Note that this function may return `None` if it encounters a class or superclass + that does not exist in the currently known class reflection database. +*/ +pub fn instance_is_a_service(class_name: impl AsRef) -> Option { + let mut class_name = class_name.as_ref(); + + let db = rbx_reflection_database::get(); + + loop { + let class_descriptor = db.classes.get(class_name)?; + if class_descriptor.tags.contains(&ClassTag::Service) { + return Some(true); + } else if let Some(sup) = &class_descriptor.superclass { + class_name = sup.borrow(); + } else { + break; + } + } + + Some(false) +} diff --git a/packages/lib-roblox/src/lib.rs b/packages/lib-roblox/src/lib.rs index 82bd2e0..44777ba 100644 --- a/packages/lib-roblox/src/lib.rs +++ b/packages/lib-roblox/src/lib.rs @@ -1,5 +1,7 @@ use mlua::prelude::*; +mod instance; + pub mod document; pub fn module(lua: &Lua) -> LuaResult {