mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Implement auto detection of document kind for roblox lib
This commit is contained in:
parent
5bdc968ffe
commit
0f4c36219f
6 changed files with 168 additions and 14 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -858,6 +858,8 @@ dependencies = [
|
||||||
"mlua",
|
"mlua",
|
||||||
"rbx_binary",
|
"rbx_binary",
|
||||||
"rbx_dom_weak",
|
"rbx_dom_weak",
|
||||||
|
"rbx_reflection",
|
||||||
|
"rbx_reflection_database",
|
||||||
"rbx_xml",
|
"rbx_xml",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
|
@ -21,6 +21,8 @@ thiserror = "1.0"
|
||||||
|
|
||||||
rbx_binary = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" }
|
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_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" }
|
rbx_xml = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" }
|
||||||
|
|
||||||
# TODO: Split lune lib out into something like lune-core so
|
# TODO: Split lune lib out into something like lune-core so
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use rbx_dom_weak::WeakDom;
|
||||||
|
|
||||||
|
use crate::instance::instance_is_a_service;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
A document kind specifier.
|
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<Self> {
|
pub fn from_weak_dom(dom: &WeakDom) -> Option<Self> {
|
||||||
// TODO: Implement this, read comment below
|
let mut has_top_level_child = false;
|
||||||
todo!("Investigate if it is possible to detect document kind from contents")
|
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 {
|
mod tests {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use rbx_dom_weak::InstanceBuilder;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[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)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,15 +51,10 @@ impl Document {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fn from_bytes_inner(
|
||||||
Decodes and creates a new document from a byte buffer.
|
bytes: impl AsRef<[u8]>,
|
||||||
|
) -> Result<(DocumentFormat, WeakDom), DocumentError> {
|
||||||
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<Self, DocumentError> {
|
|
||||||
let bytes = bytes.as_ref();
|
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 format = DocumentFormat::from_bytes(bytes).ok_or(DocumentError::UnknownFormat)?;
|
||||||
let dom = match format {
|
let dom = match format {
|
||||||
DocumentFormat::InternalRoot => Err(DocumentError::InternalRootReadWrite),
|
DocumentFormat::InternalRoot => Err(DocumentError::InternalRootReadWrite),
|
||||||
|
@ -72,6 +67,37 @@ impl Document {
|
||||||
.map_err(|err| DocumentError::ReadError(err.to_string()))
|
.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<Self, DocumentError> {
|
||||||
|
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<Self, DocumentError> {
|
||||||
|
let (format, dom) = Self::from_bytes_inner(bytes)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
kind,
|
kind,
|
||||||
format,
|
format,
|
||||||
|
|
66
packages/lib-roblox/src/instance/mod.rs
Normal file
66
packages/lib-roblox/src/instance/mod.rs
Normal file
|
@ -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<str>,
|
||||||
|
class_name: impl AsRef<str>,
|
||||||
|
) -> Option<bool> {
|
||||||
|
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<str>) -> Option<bool> {
|
||||||
|
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)
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
|
mod instance;
|
||||||
|
|
||||||
pub mod document;
|
pub mod document;
|
||||||
|
|
||||||
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||||
|
|
Loading…
Reference in a new issue