From 68515dc40a08bd386a3548a64b425557f719a99c Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sun, 19 Mar 2023 13:23:25 +0100 Subject: [PATCH] Initial implementation of instances for roblox lib --- packages/lib-roblox/src/datatypes/mod.rs | 11 +- packages/lib-roblox/src/datatypes/result.rs | 13 + .../lib-roblox/src/datatypes/types/enums.rs | 6 - packages/lib-roblox/src/document/kind.rs | 4 +- packages/lib-roblox/src/document/mod.rs | 44 +- packages/lib-roblox/src/instance/mod.rs | 436 +++++++++++++++++- packages/lib-roblox/src/instance/util.rs | 107 ----- packages/lib-roblox/src/lib.rs | 63 ++- packages/lib-roblox/src/shared/instance.rs | 171 +++++++ packages/lib-roblox/src/shared/mod.rs | 2 + .../shared.rs => shared/userdata.rs} | 20 +- packages/lib/Cargo.toml | 2 +- 12 files changed, 683 insertions(+), 196 deletions(-) delete mode 100644 packages/lib-roblox/src/instance/util.rs create mode 100644 packages/lib-roblox/src/shared/instance.rs create mode 100644 packages/lib-roblox/src/shared/mod.rs rename packages/lib-roblox/src/{datatypes/shared.rs => shared/userdata.rs} (81%) diff --git a/packages/lib-roblox/src/datatypes/mod.rs b/packages/lib-roblox/src/datatypes/mod.rs index 9381c53..86bb01f 100644 --- a/packages/lib-roblox/src/datatypes/mod.rs +++ b/packages/lib-roblox/src/datatypes/mod.rs @@ -1,11 +1,10 @@ pub(crate) use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType}; -mod conversion; -mod extension; -mod result; -mod shared; - +pub mod conversion; +pub mod extension; +pub mod result; pub mod types; use result::*; -use shared::*; + +pub use crate::shared::userdata::*; diff --git a/packages/lib-roblox/src/datatypes/result.rs b/packages/lib-roblox/src/datatypes/result.rs index 036f6fb..5f43dd3 100644 --- a/packages/lib-roblox/src/datatypes/result.rs +++ b/packages/lib-roblox/src/datatypes/result.rs @@ -45,6 +45,19 @@ impl fmt::Display for DomConversionError { impl Error for DomConversionError {} +impl From for LuaError { + fn from(value: DomConversionError) -> Self { + use DomConversionError as E; + match value { + E::LuaError(e) => e, + E::External { message } => LuaError::external(message), + E::FromDomValue { .. } | E::ToDomValue { .. } => { + LuaError::RuntimeError(value.to_string()) + } + } + } +} + impl From for DomConversionError { fn from(value: LuaError) -> Self { Self::LuaError(value) diff --git a/packages/lib-roblox/src/datatypes/types/enums.rs b/packages/lib-roblox/src/datatypes/types/enums.rs index b8f584d..5ce78d7 100644 --- a/packages/lib-roblox/src/datatypes/types/enums.rs +++ b/packages/lib-roblox/src/datatypes/types/enums.rs @@ -12,12 +12,6 @@ use super::{super::*, Enum}; #[derive(Debug, Clone, Copy, PartialEq)] pub struct Enums; -impl Enums { - pub(crate) fn make_singleton(lua: &Lua) -> LuaResult { - lua.create_userdata(Self) - } -} - impl LuaUserData for Enums { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { // Methods diff --git a/packages/lib-roblox/src/document/kind.rs b/packages/lib-roblox/src/document/kind.rs index b8e928f..4afc93f 100644 --- a/packages/lib-roblox/src/document/kind.rs +++ b/packages/lib-roblox/src/document/kind.rs @@ -2,7 +2,7 @@ use std::path::Path; use rbx_dom_weak::WeakDom; -use crate::instance::util::instance_is_a_service; +use crate::shared::instance::class_is_a_service; /** A document kind specifier. @@ -65,7 +65,7 @@ impl DocumentKind { 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) { + if class_is_a_service(&child_inst.class).unwrap_or(false) { has_top_level_service = true; break; } diff --git a/packages/lib-roblox/src/document/mod.rs b/packages/lib-roblox/src/document/mod.rs index 162ec52..baac1b5 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, RwLockReadGuard, RwLockWriteGuard}; +use std::sync::{Arc, RwLock}; -use rbx_dom_weak::{types::Ref, WeakDom}; +use rbx_dom_weak::WeakDom; use rbx_xml::{ DecodeOptions as XmlDecodeOptions, DecodePropertyBehavior as XmlDecodePropertyBehavior, EncodeOptions as XmlEncodeOptions, EncodePropertyBehavior as XmlEncodePropertyBehavior, @@ -189,43 +189,9 @@ impl Document { } /** - Retrieves the root referent of the underlying weak dom. + Gets the underlying weak dom for this document. */ - pub fn get_root_ref(&self) -> Ref { - let dom = self.dom.try_read().expect("Failed to lock dom"); - dom.root_ref() - } - - /** - Retrieves all root child referents of the underlying weak dom. - */ - pub fn get_root_child_refs(&self) -> Vec { - let dom = self.dom.try_read().expect("Failed to lock dom"); - dom.root().children().to_vec() - } - - /** - Retrieves a reference to the underlying weak dom. - */ - pub fn get_dom(&self) -> RwLockReadGuard { - self.dom.try_read().expect("Failed to lock dom") - } - - /** - Retrieves a mutable reference to the underlying weak dom. - */ - pub fn get_dom_mut(&mut self) -> RwLockWriteGuard { - self.dom.try_write().expect("Failed to lock dom") - } - - /** - Consumes the document, returning the underlying weak dom. - - This may panic if the document has been cloned - and still has another owner in memory. - */ - pub fn into_dom(self) -> WeakDom { - let lock = Arc::try_unwrap(self.dom).expect("Document has multiple owners in memory"); - lock.into_inner().expect("Failed to lock dom") + pub fn dom(&self) -> Arc> { + Arc::clone(&self.dom) } } diff --git a/packages/lib-roblox/src/instance/mod.rs b/packages/lib-roblox/src/instance/mod.rs index 812d1ed..4275221 100644 --- a/packages/lib-roblox/src/instance/mod.rs +++ b/packages/lib-roblox/src/instance/mod.rs @@ -1 +1,435 @@ -pub mod util; +use std::{ + fmt, + sync::{Arc, RwLock}, +}; + +use mlua::prelude::*; +use rbx_dom_weak::{ + types::{Ref as DomRef, Variant as DomValue}, + Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom, +}; + +use crate::{ + datatypes::{ + conversion::{DomValueToLua, LuaToDomValue}, + types::EnumItem, + userdata_impl_to_string, + }, + shared::instance::{ + class_exists, class_is_a, find_property_enum, find_property_type, property_is_enum, + }, +}; + +#[derive(Debug, Clone)] +pub struct Instance { + dom: Arc>, + dom_ref: DomRef, + class_name: String, +} + +impl Instance { + /** + Creates a new `Instance` from a document and dom object ref. + */ + pub fn new(dom: &Arc>, dom_ref: DomRef) -> Self { + let class_name = dom + .read() + .expect("Failed to get read access to document") + .get_by_ref(dom_ref) + .expect("Failed to find instance in document") + .class + .clone(); + Self { + dom: Arc::clone(dom), + dom_ref, + class_name, + } + } + + /** + Creates a new orphaned `Instance` with a given class name. + + An orphaned instance does not belong to any particular document and + is instead part of the internal weak dom for orphaned lua instances, + it can however be re-parented to a "real" document and weak dom. + */ + pub fn new_orphaned(lua: &Lua, class_name: impl AsRef) -> Self { + let dom_lua = lua + .app_data_mut::>>() + .expect("Failed to find internal lua weak dom"); + let mut dom = dom_lua + .write() + .expect("Failed to get write access to document"); + + let class_name = class_name.as_ref(); + let dom_root = dom.root_ref(); + let dom_ref = dom.insert(dom_root, DomInstanceBuilder::new(class_name.to_string())); + + Self { + dom: Arc::clone(&dom_lua), + dom_ref, + class_name: class_name.to_string(), + } + } + + /** + Checks if the instance matches or inherits a given class name. + */ + pub fn is_a(&self, class_name: impl AsRef) -> bool { + class_is_a(&self.class_name, class_name).unwrap_or(false) + } + + /** + Checks if the instance has been destroyed. + */ + pub fn is_destroyed(&self) -> bool { + self.dom + .read() + .expect("Failed to get read access to document") + .get_by_ref(self.dom_ref) + .is_none() + } + + /** + Checks if the instance is the root instance. + */ + pub fn is_root(&self) -> bool { + self.dom + .read() + .expect("Failed to get read access to document") + .root_ref() + == self.dom_ref + } + + /** + Gets the name of the instance, if it exists. + */ + pub fn get_name(&self) -> String { + let dom = self + .dom + .read() + .expect("Failed to get read access to document"); + dom.get_by_ref(self.dom_ref) + .expect("Failed to find instance in document") + .name + .clone() + } + + /** + Sets the name of the instance, if it exists. + */ + pub fn set_name(&self, name: impl Into) { + let mut dom = self + .dom + .write() + .expect("Failed to get write access to document"); + dom.get_by_ref_mut(self.dom_ref) + .expect("Failed to find instance in document") + .name = name.into() + } + + /** + Gets the parent of the instance, if it exists. + */ + pub fn get_parent(&self) -> Option { + let dom = self + .dom + .read() + .expect("Failed to get read access to document"); + let parent_ref = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document") + .parent(); + if parent_ref == dom.root_ref() { + None + } else { + Some(Self::new(&self.dom, parent_ref)) + } + } + + /** + Sets the parent of the instance, if it exists. + + Note that this can transfer between different weak doms, + and assumes that separate doms always have unique root referents. + + If doms do not have unique root referents then this operation may panic. + */ + pub fn set_parent(&self, parent: Instance) { + let mut dom_source = self + .dom + .write() + .expect("Failed to get read access to source document"); + let dom_target = parent + .dom + .read() + .expect("Failed to get read access to target document"); + let target_ref = dom_target + .get_by_ref(parent.dom_ref) + .expect("Failed to find instance in target document") + .parent(); + if dom_source.root_ref() == dom_target.root_ref() { + dom_source.transfer_within(self.dom_ref, target_ref); + } else { + // NOTE: We must drop the previous dom_target read handle here first so + // that we can get exclusive write access for transferring across doms + drop(dom_target); + let mut dom_target = parent + .dom + .try_write() + .expect("Failed to get write access to target document"); + dom_source.transfer(self.dom_ref, &mut dom_target, target_ref) + } + } + + /** + Sets the parent of the instance, if it exists, to nil, making it orphaned. + + An orphaned instance does not belong to any particular document and + is instead part of the internal weak dom for orphaned lua instances, + it can however be re-parented to a "real" document and weak dom. + */ + pub fn set_parent_to_nil(&self, lua: &Lua) { + let mut dom_source = self + .dom + .write() + .expect("Failed to get read access to source document"); + let dom_lua = lua + .app_data_mut::>>() + .expect("Failed to find internal lua weak dom"); + let mut dom_target = dom_lua + .write() + .expect("Failed to get write access to target document"); + let target_ref = dom_target.root_ref(); + dom_source.transfer(self.dom_ref, &mut dom_target, target_ref) + } + + /** + Gets a property for the instance, if it exists. + */ + pub fn get_property(&self, name: impl AsRef) -> Option { + self.dom + .read() + .expect("Failed to get read access to document") + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document") + .properties + .get(name.as_ref()) + .cloned() + } + + /** + Sets a property for the instance. + + Note that setting a property here will not fail even if the + property does not actually exist for the instance class. + */ + pub fn set_property(&self, name: impl AsRef, value: DomValue) { + self.dom + .write() + .expect("Failed to get read access to document") + .get_by_ref_mut(self.dom_ref) + .expect("Failed to find instance in document") + .properties + .insert(name.as_ref().to_string(), value); + } + + /** + Finds a child of the instance using the given predicate callback. + */ + pub fn find_child(&self, predicate: F) -> Option + where + F: Fn(&DomInstance) -> bool, + { + let dom = self + .dom + .read() + .expect("Failed to get read access to document"); + let children = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document") + .children(); + children.iter().find_map(|child_ref| { + if let Some(child_inst) = dom.get_by_ref(*child_ref) { + if predicate(child_inst) { + Some(Self::new(&self.dom, *child_ref)) + } else { + None + } + } else { + None + } + }) + } +} + +impl Instance { + pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { + datatype_table.set( + "new", + lua.create_function(|lua, class_name: String| { + if class_exists(&class_name) { + Instance::new_orphaned(lua, class_name).to_lua(lua) + } else { + Err(LuaError::RuntimeError(format!( + "{} is not a valid class name", + class_name + ))) + } + })?, + ) + } +} + +impl LuaUserData for Instance { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + /* + Getting a value does the following: + + 1. Check if it is a special property like "ClassName", "Name" or "Parent" + 2. Try to get a known instance property + 3. Try to get a current child of the instance + 4. No valid property or instance found, throw error + */ + methods.add_meta_method(LuaMetaMethod::Index, |lua, this, prop_name: String| { + match prop_name.as_str() { + "ClassName" => return this.class_name.clone().to_lua(lua), + "Name" => { + return this.get_name().to_lua(lua); + } + "Parent" => { + return this.get_parent().to_lua(lua); + } + _ => {} + } + + if let Some(prop) = this.get_property(&prop_name) { + match LuaValue::dom_value_to_lua(lua, &prop) { + Ok(value) => Ok(value), + Err(e) => Err(e.into()), + } + } else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) { + Ok(LuaValue::UserData(lua.create_userdata(inst)?)) + } else { + Err(LuaError::RuntimeError(format!( + "{} is not a valid member of {}", + prop_name, this + ))) + } + }); + /* + Setting a value does the following: + + 1. Check if it is a special property like "ClassName", "Name" or "Parent" + 2. Check if a property exists for the wanted name + 3a. Set a strict enum from a given EnumItem OR + 3b. Set a normal property from a given value + */ + methods.add_meta_method_mut( + LuaMetaMethod::NewIndex, + |lua, this, (prop_name, prop_value): (String, LuaValue)| { + match prop_name.as_str() { + "ClassName" => { + return Err(LuaError::RuntimeError( + "ClassName can not be written to".to_string(), + )) + } + "Name" => { + let name = String::from_lua(prop_value, lua)?; + this.set_name(name); + return Ok(()); + } + "Parent" => { + type Parent = Option; + match Parent::from_lua(prop_value, lua)? { + Some(parent) => this.set_parent(parent), + None => this.set_parent_to_nil(lua), + } + return Ok(()); + } + _ => {} + } + + let is_enum = match property_is_enum(&this.class_name, &prop_name) { + Some(b) => b, + None => { + return Err(LuaError::RuntimeError(format!( + "{} is not a valid member of {}", + prop_name, this + ))) + } + }; + + if is_enum { + let enum_name = find_property_enum(&this.class_name, &prop_name).unwrap(); + match EnumItem::from_lua(prop_value, lua) { + Ok(given_enum) if given_enum.name == enum_name => { + this.set_property(prop_name, DomValue::Enum(given_enum.into())); + Ok(()) + } + Ok(given_enum) => Err(LuaError::RuntimeError(format!( + "Expected Enum.{}, got Enum.{}", + enum_name, given_enum.name + ))), + Err(e) => Err(e), + } + } else { + let dom_type = find_property_type(&this.class_name, &prop_name).unwrap(); + match prop_value.lua_to_dom_value(lua, dom_type) { + Ok(dom_value) => { + this.set_property(prop_name, dom_value); + Ok(()) + } + Err(e) => Err(e.into()), + } + } + }, + ); + /* + Implementations of base methods on the Instance class + + Currently implemented: + + * FindFirstChild + * FindFirstChildOfClass + * FindFirstChildWhichIsA + + Not yet implemented, but planned: + + * Clone + * Destroy + * FindFirstDescendant + * FindFirstAncestor + * FindFirstAncestorOfClass + * FindFirstAncestorWhichIsA + * IsAncestorOf + * IsDescendantOf + * GetChildren + * GetDescendants + * GetFullName + * GetAttribute + * GetAttributes + * SetAttribute + */ + methods.add_method("FindFirstChild", |lua, this, name: String| { + this.find_child(|child| child.name == name).to_lua(lua) + }); + methods.add_method("FindFirstChildOfClass", |lua, this, class_name: String| { + this.find_child(|child| child.class == class_name) + .to_lua(lua) + }); + methods.add_method("FindFirstChildWhichIsA", |lua, this, class_name: String| { + this.find_child(|child| class_is_a(&child.class, &class_name).unwrap_or(false)) + .to_lua(lua) + }); + // FUTURE: We could pass the "methods" struct to some other functions + // here to add inheritance-like behavior and class-specific methods + } +} + +impl fmt::Display for Instance { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.get_name()) + } +} diff --git a/packages/lib-roblox/src/instance/util.rs b/packages/lib-roblox/src/instance/util.rs deleted file mode 100644 index 08c9926..0000000 --- a/packages/lib-roblox/src/instance/util.rs +++ /dev/null @@ -1,107 +0,0 @@ -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: impl AsRef, class_name: impl AsRef) -> Option { - let mut instance_class = instance_class.as_ref(); - let class_name = class_name.as_ref(); - - if class_name == "Instance" || instance_class == class_name { - Some(true) - } else { - let db = rbx_reflection_database::get(); - - while instance_class != class_name { - let class_descriptor = db.classes.get(instance_class)?; - if let Some(sup) = &class_descriptor.superclass { - instance_class = 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(instance_class: impl AsRef) -> Option { - let mut instance_class = instance_class.as_ref(); - - let db = rbx_reflection_database::get(); - - loop { - let class_descriptor = db.classes.get(instance_class)?; - if class_descriptor.tags.contains(&ClassTag::Service) { - return Some(true); - } else if let Some(sup) = &class_descriptor.superclass { - instance_class = sup.borrow(); - } else { - break; - } - } - - Some(false) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn is_a_class_valid() { - assert_eq!(instance_is_a("Part", "Part"), Some(true)); - assert_eq!(instance_is_a("Part", "BasePart"), Some(true)); - assert_eq!(instance_is_a("Part", "PVInstance"), Some(true)); - assert_eq!(instance_is_a("Part", "Instance"), Some(true)); - - assert_eq!(instance_is_a("Workspace", "Workspace"), Some(true)); - assert_eq!(instance_is_a("Workspace", "Model"), Some(true)); - assert_eq!(instance_is_a("Workspace", "Instance"), Some(true)); - } - - #[test] - fn is_a_class_invalid() { - assert_eq!(instance_is_a("Part", "part"), Some(false)); - assert_eq!(instance_is_a("Part", "Base-Part"), Some(false)); - assert_eq!(instance_is_a("Part", "Model"), Some(false)); - assert_eq!(instance_is_a("Part", "Paart"), Some(false)); - - assert_eq!(instance_is_a("Workspace", "Service"), Some(false)); - assert_eq!(instance_is_a("Workspace", "."), Some(false)); - assert_eq!(instance_is_a("Workspace", ""), Some(false)); - } - - #[test] - fn is_a_service_valid() { - assert_eq!(instance_is_a_service("Workspace"), Some(true)); - assert_eq!(instance_is_a_service("PhysicsService"), Some(true)); - assert_eq!(instance_is_a_service("ReplicatedFirst"), Some(true)); - assert_eq!(instance_is_a_service("CSGDictionaryService"), Some(true)); - } - - #[test] - fn is_a_service_invalid() { - assert_eq!(instance_is_a_service("Camera"), Some(false)); - assert_eq!(instance_is_a_service("Terrain"), Some(false)); - assert_eq!(instance_is_a_service("Work-space"), None); - assert_eq!(instance_is_a_service("CSG Dictionary Service"), None); - } -} diff --git a/packages/lib-roblox/src/lib.rs b/packages/lib-roblox/src/lib.rs index eb741f9..5d8c3df 100644 --- a/packages/lib-roblox/src/lib.rs +++ b/packages/lib-roblox/src/lib.rs @@ -1,10 +1,17 @@ +use std::sync::{Arc, RwLock}; + use mlua::prelude::*; +use rbx_dom_weak::{InstanceBuilder as DomInstanceBuilder, WeakDom}; + +use crate::instance::Instance; pub mod datatypes; pub mod document; pub mod instance; -fn make_dt(lua: &Lua, f: F) -> LuaResult +pub(crate) mod shared; + +fn make(lua: &Lua, f: F) -> LuaResult where F: Fn(&Lua, &LuaTable) -> LuaResult<()>, { @@ -18,35 +25,43 @@ where fn make_all_datatypes(lua: &Lua) -> LuaResult> { use datatypes::types::*; Ok(vec![ + // Datatypes + ("Axes", make(lua, Axes::make_table)?), + ("BrickColor", make(lua, BrickColor::make_table)?), + ("CFrame", make(lua, CFrame::make_table)?), + ("Color3", make(lua, Color3::make_table)?), + ("ColorSequence", make(lua, ColorSequence::make_table)?), + ("ColorSequenceKeypoint", make(lua, ColorSequenceKeypoint::make_table)?), + ("Faces", make(lua, Faces::make_table)?), + ("Font", make(lua, Font::make_table)?), + ("NumberRange", make(lua, NumberRange::make_table)?), + ("NumberSequence", make(lua, NumberSequence::make_table)?), + ("NumberSequenceKeypoint", make(lua, NumberSequenceKeypoint::make_table)?), + ("PhysicalProperties", make(lua, PhysicalProperties::make_table)?), + ("Ray", make(lua, Ray::make_table)?), + ("Rect", make(lua, Rect::make_table)?), + ("UDim", make(lua, UDim::make_table)?), + ("UDim2", make(lua, UDim2::make_table)?), + ("Region3", make(lua, Region3::make_table)?), + ("Region3int16", make(lua, Region3int16::make_table)?), + ("Vector2", make(lua, Vector2::make_table)?), + ("Vector2int16", make(lua, Vector2int16::make_table)?), + ("Vector3", make(lua, Vector3::make_table)?), + ("Vector3int16", make(lua, Vector3int16::make_table)?), // Classes - ("Axes", make_dt(lua, Axes::make_table)?), - ("BrickColor", make_dt(lua, BrickColor::make_table)?), - ("CFrame", make_dt(lua, CFrame::make_table)?), - ("Color3", make_dt(lua, Color3::make_table)?), - ("ColorSequence", make_dt(lua, ColorSequence::make_table)?), - ("ColorSequenceKeypoint", make_dt(lua, ColorSequenceKeypoint::make_table)?), - ("Faces", make_dt(lua, Faces::make_table)?), - ("Font", make_dt(lua, Font::make_table)?), - ("NumberRange", make_dt(lua, NumberRange::make_table)?), - ("NumberSequence", make_dt(lua, NumberSequence::make_table)?), - ("NumberSequenceKeypoint", make_dt(lua, NumberSequenceKeypoint::make_table)?), - ("PhysicalProperties", make_dt(lua, PhysicalProperties::make_table)?), - ("Ray", make_dt(lua, Ray::make_table)?), - ("Rect", make_dt(lua, Rect::make_table)?), - ("UDim", make_dt(lua, UDim::make_table)?), - ("UDim2", make_dt(lua, UDim2::make_table)?), - ("Region3", make_dt(lua, Region3::make_table)?), - ("Region3int16", make_dt(lua, Region3int16::make_table)?), - ("Vector2", make_dt(lua, Vector2::make_table)?), - ("Vector2int16", make_dt(lua, Vector2int16::make_table)?), - ("Vector3", make_dt(lua, Vector3::make_table)?), - ("Vector3int16", make_dt(lua, Vector3int16::make_table)?), + ("Instance", make(lua, Instance::make_table)?), // Singletons - ("Enum", LuaValue::UserData(Enums::make_singleton(lua)?)), + ("Enum", Enums.to_lua(lua)?), ]) } pub fn module(lua: &Lua) -> LuaResult { + // Create an internal weak dom that will be used + // for any instance that does not yet have a parent + let internal_root = DomInstanceBuilder::new("<<>>"); + let internal_dom = Arc::new(RwLock::new(WeakDom::new(internal_root))); + lua.set_app_data(internal_dom); + // Create all datatypes and singletons and export them let exports = lua.create_table()?; for (name, tab) in make_all_datatypes(lua)? { exports.set(name, tab)?; diff --git a/packages/lib-roblox/src/shared/instance.rs b/packages/lib-roblox/src/shared/instance.rs new file mode 100644 index 0000000..1a71cb6 --- /dev/null +++ b/packages/lib-roblox/src/shared/instance.rs @@ -0,0 +1,171 @@ +use std::borrow::{Borrow, Cow}; + +use rbx_dom_weak::types::VariantType as DomType; +use rbx_reflection::{ClassTag, DataType}; + +/** + Checks if the given property is an enum. + + Returns `None` if the class or property does not exist. +*/ +pub fn property_is_enum( + instance_class: impl AsRef, + property_name: impl AsRef, +) -> Option { + let db = rbx_reflection_database::get(); + let class = db.classes.get(instance_class.as_ref())?; + let prop = class.properties.get(property_name.as_ref())?; + + Some(matches!(prop.data_type, DataType::Enum(_))) +} + +/** + Finds the type of a property of the given class. + + Returns `None` if the class or property does not exist or if the property is an enum. +*/ +pub fn find_property_type( + instance_class: impl AsRef, + property_name: impl AsRef, +) -> Option { + let db = rbx_reflection_database::get(); + let class = db.classes.get(instance_class.as_ref())?; + let prop = class.properties.get(property_name.as_ref())?; + + if let DataType::Value(typ) = prop.data_type { + Some(typ) + } else { + None + } +} + +/** + Finds the enum name of a property of the given class. + + Returns `None` if the class or property does not exist or if the property is *not* an enum. +*/ +pub fn find_property_enum( + instance_class: impl AsRef, + property_name: impl AsRef, +) -> Option> { + let db = rbx_reflection_database::get(); + let class = db.classes.get(instance_class.as_ref())?; + let prop = class.properties.get(property_name.as_ref())?; + + if let DataType::Enum(name) = &prop.data_type { + Some(Cow::Borrowed(name)) + } else { + None + } +} + +/** + Checks if an instance class exists in the reflection database. +*/ +pub fn class_exists(class_name: impl AsRef) -> bool { + let db = rbx_reflection_database::get(); + db.classes.contains_key(class_name.as_ref()) +} + +/** + 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. +*/ +pub fn class_is_a(instance_class: impl AsRef, class_name: impl AsRef) -> Option { + let mut instance_class = instance_class.as_ref(); + let class_name = class_name.as_ref(); + + if class_name == "Instance" || instance_class == class_name { + Some(true) + } else { + let db = rbx_reflection_database::get(); + + while instance_class != class_name { + let class_descriptor = db.classes.get(instance_class)?; + if let Some(sup) = &class_descriptor.superclass { + instance_class = sup.borrow(); + } else { + return Some(false); + } + } + + Some(true) + } +} + +/** + Checks if an instance class is a service. + + This is separate from [`class_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 class_is_a_service(instance_class: impl AsRef) -> Option { + let mut instance_class = instance_class.as_ref(); + + let db = rbx_reflection_database::get(); + + loop { + let class_descriptor = db.classes.get(instance_class)?; + if class_descriptor.tags.contains(&ClassTag::Service) { + return Some(true); + } else if let Some(sup) = &class_descriptor.superclass { + instance_class = sup.borrow(); + } else { + break; + } + } + + Some(false) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_a_class_valid() { + assert_eq!(class_is_a("Part", "Part"), Some(true)); + assert_eq!(class_is_a("Part", "BasePart"), Some(true)); + assert_eq!(class_is_a("Part", "PVInstance"), Some(true)); + assert_eq!(class_is_a("Part", "Instance"), Some(true)); + + assert_eq!(class_is_a("Workspace", "Workspace"), Some(true)); + assert_eq!(class_is_a("Workspace", "Model"), Some(true)); + assert_eq!(class_is_a("Workspace", "Instance"), Some(true)); + } + + #[test] + fn is_a_class_invalid() { + assert_eq!(class_is_a("Part", "part"), Some(false)); + assert_eq!(class_is_a("Part", "Base-Part"), Some(false)); + assert_eq!(class_is_a("Part", "Model"), Some(false)); + assert_eq!(class_is_a("Part", "Paart"), Some(false)); + + assert_eq!(class_is_a("Workspace", "Service"), Some(false)); + assert_eq!(class_is_a("Workspace", "."), Some(false)); + assert_eq!(class_is_a("Workspace", ""), Some(false)); + } + + #[test] + fn is_a_service_valid() { + assert_eq!(class_is_a_service("Workspace"), Some(true)); + assert_eq!(class_is_a_service("PhysicsService"), Some(true)); + assert_eq!(class_is_a_service("ReplicatedFirst"), Some(true)); + assert_eq!(class_is_a_service("CSGDictionaryService"), Some(true)); + } + + #[test] + fn is_a_service_invalid() { + assert_eq!(class_is_a_service("Camera"), Some(false)); + assert_eq!(class_is_a_service("Terrain"), Some(false)); + assert_eq!(class_is_a_service("Work-space"), None); + assert_eq!(class_is_a_service("CSG Dictionary Service"), None); + } +} diff --git a/packages/lib-roblox/src/shared/mod.rs b/packages/lib-roblox/src/shared/mod.rs new file mode 100644 index 0000000..10d13e0 --- /dev/null +++ b/packages/lib-roblox/src/shared/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod instance; +pub(crate) mod userdata; diff --git a/packages/lib-roblox/src/datatypes/shared.rs b/packages/lib-roblox/src/shared/userdata.rs similarity index 81% rename from packages/lib-roblox/src/datatypes/shared.rs rename to packages/lib-roblox/src/shared/userdata.rs index 033f203..71e48ce 100644 --- a/packages/lib-roblox/src/datatypes/shared.rs +++ b/packages/lib-roblox/src/shared/userdata.rs @@ -5,7 +5,7 @@ use mlua::prelude::*; // Utility functions type ListWriter = dyn Fn(&mut fmt::Formatter<'_>, bool, &str) -> fmt::Result; -pub(super) fn make_list_writer() -> Box { +pub fn make_list_writer() -> Box { let first = RefCell::new(true); Box::new(move |f, flag, literal| { if flag { @@ -21,14 +21,14 @@ pub(super) fn make_list_writer() -> Box { // Userdata metamethod implementations -pub(super) fn userdata_impl_to_string(_: &Lua, datatype: &D, _: ()) -> LuaResult +pub fn userdata_impl_to_string(_: &Lua, datatype: &D, _: ()) -> LuaResult where D: LuaUserData + ToString + 'static, { Ok(datatype.to_string()) } -pub(super) fn userdata_impl_eq(_: &Lua, datatype: &D, value: LuaValue) -> LuaResult +pub fn userdata_impl_eq(_: &Lua, datatype: &D, value: LuaValue) -> LuaResult where D: LuaUserData + PartialEq + 'static, { @@ -43,28 +43,28 @@ where } } -pub(super) fn userdata_impl_unm(_: &Lua, datatype: &D, _: ()) -> LuaResult +pub fn userdata_impl_unm(_: &Lua, datatype: &D, _: ()) -> LuaResult where D: LuaUserData + ops::Neg + Copy, { Ok(-*datatype) } -pub(super) fn userdata_impl_add(_: &Lua, datatype: &D, value: D) -> LuaResult +pub fn userdata_impl_add(_: &Lua, datatype: &D, value: D) -> LuaResult where D: LuaUserData + ops::Add + Copy, { Ok(*datatype + value) } -pub(super) fn userdata_impl_sub(_: &Lua, datatype: &D, value: D) -> LuaResult +pub fn userdata_impl_sub(_: &Lua, datatype: &D, value: D) -> LuaResult where D: LuaUserData + ops::Sub + Copy, { Ok(*datatype - value) } -pub(super) fn userdata_impl_mul_f32(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult +pub fn userdata_impl_mul_f32(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult where D: LuaUserData + ops::Mul + ops::Mul + Copy + 'static, { @@ -89,7 +89,7 @@ where }) } -pub(super) fn userdata_impl_mul_i32(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult +pub fn userdata_impl_mul_i32(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult where D: LuaUserData + ops::Mul + ops::Mul + Copy + 'static, { @@ -114,7 +114,7 @@ where }) } -pub(super) fn userdata_impl_div_f32(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult +pub fn userdata_impl_div_f32(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult where D: LuaUserData + ops::Div + ops::Div + Copy + 'static, { @@ -139,7 +139,7 @@ where }) } -pub(super) fn userdata_impl_div_i32(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult +pub fn userdata_impl_div_i32(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult where D: LuaUserData + ops::Div + ops::Div + Copy + 'static, { diff --git a/packages/lib/Cargo.toml b/packages/lib/Cargo.toml index 12ff91e..2e10754 100644 --- a/packages/lib/Cargo.toml +++ b/packages/lib/Cargo.toml @@ -36,13 +36,13 @@ async-trait = "0.1" blocking = "1.3" dialoguer = "0.10" directories = "4.0" +dunce = "1.0" pin-project = "1.0" os_str_bytes = "6.4" hyper = { version = "0.14", features = ["full"] } hyper-tungstenite = { version = "0.9" } tokio-tungstenite = { version = "0.18" } -dunce = "1.0" [dev-dependencies] anyhow = "1.0"