From 5e898858ae69de4e6648be0029b5354996e03c88 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 25 Mar 2023 20:54:15 +0100 Subject: [PATCH] Implement roblox instance tags & CollectionService --- .../src/instance/collection_service.rs | 72 +++++++++++ .../lib-roblox/src/instance/data_model.rs | 6 +- packages/lib-roblox/src/instance/mod.rs | 116 +++++++++++++++--- packages/lib-roblox/src/shared/instance.rs | 7 ++ 4 files changed, 184 insertions(+), 17 deletions(-) create mode 100644 packages/lib-roblox/src/instance/collection_service.rs diff --git a/packages/lib-roblox/src/instance/collection_service.rs b/packages/lib-roblox/src/instance/collection_service.rs new file mode 100644 index 0000000..74066bb --- /dev/null +++ b/packages/lib-roblox/src/instance/collection_service.rs @@ -0,0 +1,72 @@ +use mlua::prelude::*; + +use crate::shared::classes::add_class_restricted_method; + +use super::Instance; + +pub const CLASS_NAME: &str = "CollectionService"; + +pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) { + add_class_restricted_method(m, CLASS_NAME, "AddTag", collection_service_add_tag); + add_class_restricted_method(m, CLASS_NAME, "GetTags", collection_service_get_tags); + add_class_restricted_method(m, CLASS_NAME, "HasTag", collection_service_has_tag); + add_class_restricted_method(m, CLASS_NAME, "RemoveTag", collection_service_remove_tag); +} + +/** + Adds a tag to the instance. + + ### See Also + * [`AddTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#AddTag) + on the Roblox Developer Hub +*/ +fn collection_service_add_tag( + _: &Lua, + _: &Instance, + (object, tag_name): (Instance, String), +) -> LuaResult<()> { + object.add_tag(tag_name); + Ok(()) +} + +/** + Gets all current tags for the instance. + + ### See Also + * [`GetTags`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#GetTags) + on the Roblox Developer Hub +*/ +fn collection_service_get_tags(_: &Lua, _: &Instance, object: Instance) -> LuaResult> { + Ok(object.get_tags()) +} + +/** + Checks if the instance has a specific tag. + + ### See Also + * [`HasTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#HasTag) + on the Roblox Developer Hub +*/ +fn collection_service_has_tag( + _: &Lua, + _: &Instance, + (object, tag_name): (Instance, String), +) -> LuaResult { + Ok(object.has_tag(tag_name)) +} + +/** + Removes a tag from the instance. + + ### See Also + * [`RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#RemoveTag) + on the Roblox Developer Hub +*/ +fn collection_service_remove_tag( + _: &Lua, + _: &Instance, + (object, tag_name): (Instance, String), +) -> LuaResult<()> { + object.remove_tag(tag_name); + Ok(()) +} diff --git a/packages/lib-roblox/src/instance/data_model.rs b/packages/lib-roblox/src/instance/data_model.rs index 7f83be0..672d48f 100644 --- a/packages/lib-roblox/src/instance/data_model.rs +++ b/packages/lib-roblox/src/instance/data_model.rs @@ -6,9 +6,9 @@ use super::Instance; pub const CLASS_NAME: &str = "DataModel"; -pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M) { - add_class_restricted_method(methods, CLASS_NAME, "GetService", data_model_get_service); - add_class_restricted_method(methods, CLASS_NAME, "FindService", data_model_find_service); +pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) { + add_class_restricted_method(m, CLASS_NAME, "GetService", data_model_get_service); + add_class_restricted_method(m, CLASS_NAME, "FindService", data_model_find_service); } /** diff --git a/packages/lib-roblox/src/instance/mod.rs b/packages/lib-roblox/src/instance/mod.rs index c6d336c..a164ff2 100644 --- a/packages/lib-roblox/src/instance/mod.rs +++ b/packages/lib-roblox/src/instance/mod.rs @@ -21,6 +21,7 @@ use crate::{ shared::instance::{class_exists, class_is_a, find_property_info}, }; +pub(crate) mod collection_service; pub(crate) mod data_model; static INTERNAL_DOM: Lazy> = @@ -478,7 +479,7 @@ impl Instance { pub fn set_attribute(&self, name: impl AsRef, value: DomValue) { let mut dom = INTERNAL_DOM .try_write() - .expect("Failed to get read access to document"); + .expect("Failed to get write access to document"); let inst = dom .get_by_ref_mut(self.dom_ref) .expect("Failed to find instance in document"); @@ -487,6 +488,96 @@ impl Instance { } } + /** + Adds a tag to the instance. + + ### See Also + * [`AddTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#AddTag) + on the Roblox Developer Hub + */ + pub fn add_tag(&self, name: impl AsRef) { + let mut dom = INTERNAL_DOM + .try_write() + .expect("Failed to get write access to document"); + let inst = dom + .get_by_ref_mut(self.dom_ref) + .expect("Failed to find instance in document"); + if let Some(DomValue::Tags(tags)) = inst.properties.get_mut("Tags") { + tags.push(name.as_ref()); + } else { + inst.properties.insert( + "Tags".to_string(), + DomValue::Tags(vec![name.as_ref().to_string()].into()), + ); + } + } + + /** + Gets all current tags for the instance. + + ### See Also + * [`GetTags`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#GetTags) + on the Roblox Developer Hub + */ + pub fn get_tags(&self) -> Vec { + let dom = INTERNAL_DOM + .try_read() + .expect("Failed to get read access to document"); + let inst = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document"); + if let Some(DomValue::Tags(tags)) = inst.properties.get("Tags") { + tags.iter().map(ToString::to_string).collect() + } else { + Vec::new() + } + } + + /** + Checks if the instance has a specific tag. + + ### See Also + * [`HasTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#HasTag) + on the Roblox Developer Hub + */ + pub fn has_tag(&self, name: impl AsRef) -> bool { + let dom = INTERNAL_DOM + .try_read() + .expect("Failed to get read access to document"); + let inst = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document"); + if let Some(DomValue::Tags(tags)) = inst.properties.get("Tags") { + let name = name.as_ref(); + tags.iter().any(|tag| tag == name) + } else { + false + } + } + + /** + Removes a tag from the instance. + + ### See Also + * [`RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#RemoveTag) + on the Roblox Developer Hub + */ + pub fn remove_tag(&self, name: impl AsRef) { + let mut dom = INTERNAL_DOM + .try_write() + .expect("Failed to get write access to document"); + let inst = dom + .get_by_ref_mut(self.dom_ref) + .expect("Failed to find instance in document"); + if let Some(DomValue::Tags(tags)) = inst.properties.get_mut("Tags") { + let name = name.as_ref(); + let mut new_tags = tags.iter().map(ToString::to_string).collect::>(); + new_tags.retain(|tag| tag != name); + inst.properties + .insert("Tags".to_string(), DomValue::Tags(new_tags.into())); + } + } + /** Gets all of the current children of this `Instance`. @@ -728,11 +819,6 @@ impl LuaUserData for Instance { "Parent" => { return this.get_parent().to_lua(lua); } - // These are stored as properties in Rojo but are actually not, so we block them - "Attributes" | "Tags" => return Err(LuaError::RuntimeError(format!( - "{} is not a valid member of {}", - prop_name, this - ))), _ => {} } @@ -828,13 +914,6 @@ impl LuaUserData for Instance { this.set_parent(parent); return Ok(()); } - // These are stored as properties in Rojo but are actually not, so we block them - "Attributes" | "Tags" => { - return Err(LuaError::RuntimeError(format!( - "{} is not a valid member of {}", - prop_name, this - ))) - } _ => {} } @@ -995,12 +1074,21 @@ impl LuaUserData for Instance { // Here we add inheritance-like behavior for instances by creating // methods that are restricted to specific classnames / base classes data_model::add_methods(methods); + collection_service::add_methods(methods); } } impl fmt::Display for Instance { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.get_name()) + write!( + f, + "{}", + if self.is_destroyed() { + "<>".to_string() + } else { + self.get_name() + } + ) } } diff --git a/packages/lib-roblox/src/shared/instance.rs b/packages/lib-roblox/src/shared/instance.rs index fe925f3..a685ffb 100644 --- a/packages/lib-roblox/src/shared/instance.rs +++ b/packages/lib-roblox/src/shared/instance.rs @@ -28,6 +28,13 @@ pub(crate) fn find_property_info( let instance_class = instance_class.as_ref(); let property_name = property_name.as_ref(); + // Attributes and tags are *technically* properties but we don't + // want to treat them as such when looking up property info, any + // reading or modification of these should always be explicit + if matches!(property_name, "Attributes" | "Tags") { + return None; + } + // FUTURE: We can probably cache the result of calling this // function, if property access is being used in a tight loop // in a build step or something similar then it would be beneficial