From 22ab18026bb3f81f80fd77469ece7075e72bc2cf Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 25 Mar 2023 10:48:14 +0100 Subject: [PATCH] Implement attributes & instance ref properties for roblox lib --- docs/pages/roblox/Api-Status.md | 7 +- .../lib-roblox/src/datatypes/conversion.rs | 373 ++++++++++-------- .../lib-roblox/src/datatypes/extension.rs | 2 +- packages/lib-roblox/src/instance/mod.rs | 152 ++++++- 4 files changed, 358 insertions(+), 176 deletions(-) diff --git a/docs/pages/roblox/Api-Status.md b/docs/pages/roblox/Api-Status.md index 5d90500..674d71c 100644 --- a/docs/pages/roblox/Api-Status.md +++ b/docs/pages/roblox/Api-Status.md @@ -25,17 +25,14 @@ Currently implemented APIs: - [`FindFirstChildOfClass`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChildOfClass) - [`FindFirstChildWhichIsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChildWhichIsA) - [`FindFirstDescendant`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstDescendant) +- [`GetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttribute) +- [`GetAttributes`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttributes) - [`GetChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetChildren) - [`GetDescendants`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetDescendants) - [`GetFullName`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetFullName) - [`IsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA) - [`IsAncestorOf`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsAncestorOf) - [`IsDescendantOf`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsDescendantOf) - -Not yet implemented, but planned: - -- [`GetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttribute) -- [`GetAttributes`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttributes) - [`SetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#SetAttribute) ### `DataModel` diff --git a/packages/lib-roblox/src/datatypes/conversion.rs b/packages/lib-roblox/src/datatypes/conversion.rs index 07b91ad..2e5016f 100644 --- a/packages/lib-roblox/src/datatypes/conversion.rs +++ b/packages/lib-roblox/src/datatypes/conversion.rs @@ -2,19 +2,28 @@ use mlua::prelude::*; use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType}; -use crate::datatypes::extension::DomValueExt; +use crate::{datatypes::extension::DomValueExt, instance::Instance}; use super::*; pub(crate) trait LuaToDomValue<'lua> { + /** + Converts a lua value into a weak dom value. + + If a `variant_type` is given the conversion will be more strict + and also more accurate, it should be given whenever possible. + */ fn lua_to_dom_value( &self, lua: &'lua Lua, - variant_type: DomType, + variant_type: Option, ) -> DomConversionResult; } pub(crate) trait DomValueToLua<'lua>: Sized { + /** + Converts a weak dom value into a lua value. + */ fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult; } @@ -52,9 +61,9 @@ impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> { Ok(LuaValue::String(lua.create_string(&encoded)?)) } - // NOTE: We need this special case here to handle default (nil) - // physical properties since our PhysicalProperties datatype - // implementation does not handle default at all, only custom + // NOTE: Some values are either optional or default and we should handle + // that properly here since the userdata conversion above will always fail + DomValue::OptionalCFrame(None) => Ok(LuaValue::Nil), DomValue::PhysicalProperties(dom::PhysicalProperties::Default) => Ok(LuaValue::Nil), _ => Err(e), @@ -67,48 +76,65 @@ impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> { fn lua_to_dom_value( &self, lua: &'lua Lua, - variant_type: DomType, + variant_type: Option, ) -> DomConversionResult { use base64::engine::general_purpose::STANDARD_NO_PAD; use base64::engine::Engine as _; use rbx_dom_weak::types as dom; - match (self, variant_type) { - (LuaValue::Boolean(b), DomType::Bool) => Ok(DomValue::Bool(*b)), + if let Some(variant_type) = variant_type { + match (self, variant_type) { + (LuaValue::Boolean(b), DomType::Bool) => Ok(DomValue::Bool(*b)), - (LuaValue::Integer(i), DomType::Int64) => Ok(DomValue::Int64(*i as i64)), - (LuaValue::Integer(i), DomType::Int32) => Ok(DomValue::Int32(*i)), - (LuaValue::Integer(i), DomType::Float64) => Ok(DomValue::Float64(*i as f64)), - (LuaValue::Integer(i), DomType::Float32) => Ok(DomValue::Float32(*i as f32)), + (LuaValue::Integer(i), DomType::Int64) => Ok(DomValue::Int64(*i as i64)), + (LuaValue::Integer(i), DomType::Int32) => Ok(DomValue::Int32(*i)), + (LuaValue::Integer(i), DomType::Float64) => Ok(DomValue::Float64(*i as f64)), + (LuaValue::Integer(i), DomType::Float32) => Ok(DomValue::Float32(*i as f32)), - (LuaValue::Number(n), DomType::Int64) => Ok(DomValue::Int64(*n as i64)), - (LuaValue::Number(n), DomType::Int32) => Ok(DomValue::Int32(*n as i32)), - (LuaValue::Number(n), DomType::Float64) => Ok(DomValue::Float64(*n)), - (LuaValue::Number(n), DomType::Float32) => Ok(DomValue::Float32(*n as f32)), + (LuaValue::Number(n), DomType::Int64) => Ok(DomValue::Int64(*n as i64)), + (LuaValue::Number(n), DomType::Int32) => Ok(DomValue::Int32(*n as i32)), + (LuaValue::Number(n), DomType::Float64) => Ok(DomValue::Float64(*n)), + (LuaValue::Number(n), DomType::Float32) => Ok(DomValue::Float32(*n as f32)), - (LuaValue::String(s), DomType::String) => Ok(DomValue::String(s.to_str()?.to_string())), - (LuaValue::String(s), DomType::Content) => { - Ok(DomValue::Content(s.to_str()?.to_string().into())) + (LuaValue::String(s), DomType::String) => { + Ok(DomValue::String(s.to_str()?.to_string())) + } + (LuaValue::String(s), DomType::Content) => { + Ok(DomValue::Content(s.to_str()?.to_string().into())) + } + (LuaValue::String(s), DomType::BinaryString) => { + Ok(DomValue::BinaryString(STANDARD_NO_PAD.decode(s)?.into())) + } + + // NOTE: Some values are either optional or default and we + // should handle that here before trying to convert as userdata + (LuaValue::Nil, DomType::OptionalCFrame) => Ok(DomValue::OptionalCFrame(None)), + (LuaValue::Nil, DomType::PhysicalProperties) => Ok(DomValue::PhysicalProperties( + dom::PhysicalProperties::Default, + )), + + (LuaValue::UserData(u), d) => u.lua_to_dom_value(lua, Some(d)), + + (v, d) => Err(DomConversionError::ToDomValue { + to: d.variant_name(), + from: v.type_name(), + detail: None, + }), } - (LuaValue::String(s), DomType::BinaryString) => { - Ok(DomValue::BinaryString(STANDARD_NO_PAD.decode(s)?.into())) + } else { + match self { + LuaValue::Boolean(b) => Ok(DomValue::Bool(*b)), + LuaValue::Integer(i) => Ok(DomValue::Int32(*i)), + LuaValue::Number(n) => Ok(DomValue::Float64(*n)), + LuaValue::String(s) => Ok(DomValue::String(s.to_str()?.to_string())), + LuaValue::UserData(u) => u.lua_to_dom_value(lua, None), + v => Err(DomConversionError::ToDomValue { + to: "unknown", + from: v.type_name(), + detail: None, + }), } - - // NOTE: We need this special case here to handle default (nil) - // physical properties since our PhysicalProperties datatype - // implementation does not handle default at all, only custom - (LuaValue::Nil, DomType::PhysicalProperties) => Ok(DomValue::PhysicalProperties( - dom::PhysicalProperties::Default, - )), - - (LuaValue::UserData(u), d) => u.lua_to_dom_value(lua, d), - - (v, d) => Err(DomConversionError::ToDomValue { - to: d.variant_name(), - from: v.type_name(), - detail: None, - }), } } } @@ -123,6 +149,32 @@ impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> { */ +macro_rules! dom_to_userdata { + ($lua:expr, $value:ident => $to_type:ty) => { + Ok($lua.create_userdata(Into::<$to_type>::into($value.clone()))?) + }; +} + +macro_rules! userdata_to_dom { + ($userdata:ident as $from_type:ty => $to_type:ty) => { + match $userdata.borrow::<$from_type>() { + Ok(value) => Ok(From::<$to_type>::from(value.clone().into())), + Err(error) => match error { + LuaError::UserDataTypeMismatch => Err(DomConversionError::ToDomValue { + to: stringify!($to_type), + from: "userdata", + detail: Some("Type mismatch".to_string()), + }), + e => Err(DomConversionError::ToDomValue { + to: stringify!($to_type), + from: "userdata", + detail: Some(format!("Internal error: {e}")), + }), + }, + } + }; +} + impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> { #[rustfmt::skip] fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult { @@ -130,64 +182,44 @@ impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> { use rbx_dom_weak::types as dom; - /* - NOTES: - - 1. Enum is intentionally left out here, it has a custom - conversion going from instance property > datatype instead, - check `EnumItem::from_instance_property` for specifics - - 2. PhysicalProperties can only be converted if they are custom - physical properties, since default physical properties values - depend on what other related properties an instance might have - - */ - Ok(match variant.clone() { - DomValue::Axes(value) => lua.create_userdata(Axes::from(value))?, - DomValue::Faces(value) => lua.create_userdata(Faces::from(value))?, - - DomValue::CFrame(value) => lua.create_userdata(CFrame::from(value))?, - - DomValue::BrickColor(value) => lua.create_userdata(BrickColor::from(value))?, - DomValue::Color3(value) => lua.create_userdata(Color3::from(value))?, - DomValue::Color3uint8(value) => lua.create_userdata(Color3::from(value))?, - DomValue::ColorSequence(value) => lua.create_userdata(ColorSequence::from(value))?, - - DomValue::Font(value) => lua.create_userdata(Font::from(value))?, - - DomValue::NumberRange(value) => lua.create_userdata(NumberRange::from(value))?, - DomValue::NumberSequence(value) => lua.create_userdata(NumberSequence::from(value))?, - - DomValue::Ray(value) => lua.create_userdata(Ray::from(value))?, - - DomValue::Rect(value) => lua.create_userdata(Rect::from(value))?, - DomValue::UDim(value) => lua.create_userdata(UDim::from(value))?, - DomValue::UDim2(value) => lua.create_userdata(UDim2::from(value))?, - - DomValue::Region3(value) => lua.create_userdata(Region3::from(value))?, - DomValue::Region3int16(value) => lua.create_userdata(Region3int16::from(value))?, - DomValue::Vector2(value) => lua.create_userdata(Vector2::from(value))?, - DomValue::Vector2int16(value) => lua.create_userdata(Vector2int16::from(value))?, - DomValue::Vector3(value) => lua.create_userdata(Vector3::from(value))?, - DomValue::Vector3int16(value) => lua.create_userdata(Vector3int16::from(value))?, - - DomValue::OptionalCFrame(value) => match value { - Some(value) => lua.create_userdata(CFrame::from(value))?, - None => lua.create_userdata(CFrame::IDENTITY)? - }, + match variant { + DomValue::Axes(value) => dom_to_userdata!(lua, value => Axes), + DomValue::BrickColor(value) => dom_to_userdata!(lua, value => BrickColor), + DomValue::CFrame(value) => dom_to_userdata!(lua, value => CFrame), + DomValue::Color3(value) => dom_to_userdata!(lua, value => Color3), + DomValue::Color3uint8(value) => dom_to_userdata!(lua, value => Color3), + DomValue::ColorSequence(value) => dom_to_userdata!(lua, value => ColorSequence), + DomValue::Faces(value) => dom_to_userdata!(lua, value => Faces), + DomValue::Font(value) => dom_to_userdata!(lua, value => Font), + DomValue::NumberRange(value) => dom_to_userdata!(lua, value => NumberRange), + DomValue::NumberSequence(value) => dom_to_userdata!(lua, value => NumberSequence), + DomValue::Ray(value) => dom_to_userdata!(lua, value => Ray), + DomValue::Rect(value) => dom_to_userdata!(lua, value => Rect), + DomValue::Ref(value) => dom_to_userdata!(lua, value => Instance), + DomValue::Region3(value) => dom_to_userdata!(lua, value => Region3), + DomValue::Region3int16(value) => dom_to_userdata!(lua, value => Region3int16), + DomValue::UDim(value) => dom_to_userdata!(lua, value => UDim), + DomValue::UDim2(value) => dom_to_userdata!(lua, value => UDim2), + DomValue::Vector2(value) => dom_to_userdata!(lua, value => Vector2), + DomValue::Vector2int16(value) => dom_to_userdata!(lua, value => Vector2int16), + DomValue::Vector3(value) => dom_to_userdata!(lua, value => Vector3), + DomValue::Vector3int16(value) => dom_to_userdata!(lua, value => Vector3int16), + // NOTE: The none and default variants of these types are handled in + // DomValueToLua for the LuaValue type instead, allowing for nil/default + DomValue::OptionalCFrame(Some(value)) => dom_to_userdata!(lua, value => CFrame), DomValue::PhysicalProperties(dom::PhysicalProperties::Custom(value)) => { - lua.create_userdata(PhysicalProperties::from(value))? + dom_to_userdata!(lua, value => PhysicalProperties) }, v => { - return Err(DomConversionError::FromDomValue { + Err(DomConversionError::FromDomValue { from: v.variant_name(), to: "userdata", detail: Some("Type not supported".to_string()), }) } - }) + } } } @@ -196,96 +228,105 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> { fn lua_to_dom_value( &self, _: &'lua Lua, - variant_type: DomType, + variant_type: Option, ) -> DomConversionResult { use super::types::*; use rbx_dom_weak::types as dom; - let f = match variant_type { - DomType::Axes => convert::, - DomType::Faces => convert::, + if let Some(variant_type) = variant_type { + /* + Strict target type, use it to skip checking the actual + type of the userdata and try to just do a pure conversion + */ + match variant_type { + DomType::Axes => userdata_to_dom!(self as Axes => dom::Axes), + DomType::BrickColor => userdata_to_dom!(self as BrickColor => dom::BrickColor), + DomType::CFrame => userdata_to_dom!(self as CFrame => dom::CFrame), + DomType::Color3 => userdata_to_dom!(self as Color3 => dom::Color3), + DomType::Color3uint8 => userdata_to_dom!(self as Color3 => dom::Color3uint8), + DomType::ColorSequence => userdata_to_dom!(self as ColorSequence => dom::ColorSequence), + DomType::Enum => userdata_to_dom!(self as EnumItem => dom::Enum), + DomType::Faces => userdata_to_dom!(self as Faces => dom::Faces), + DomType::Font => userdata_to_dom!(self as Font => dom::Font), + DomType::NumberRange => userdata_to_dom!(self as NumberRange => dom::NumberRange), + DomType::NumberSequence => userdata_to_dom!(self as NumberSequence => dom::NumberSequence), + DomType::Ray => userdata_to_dom!(self as Ray => dom::Ray), + DomType::Rect => userdata_to_dom!(self as Rect => dom::Rect), + DomType::Ref => userdata_to_dom!(self as Instance => dom::Ref), + DomType::Region3 => userdata_to_dom!(self as Region3 => dom::Region3), + DomType::Region3int16 => userdata_to_dom!(self as Region3int16 => dom::Region3int16), + DomType::UDim => userdata_to_dom!(self as UDim => dom::UDim), + DomType::UDim2 => userdata_to_dom!(self as UDim2 => dom::UDim2), + DomType::Vector2 => userdata_to_dom!(self as Vector2 => dom::Vector2), + DomType::Vector2int16 => userdata_to_dom!(self as Vector2int16 => dom::Vector2int16), + DomType::Vector3 => userdata_to_dom!(self as Vector3 => dom::Vector3), + DomType::Vector3int16 => userdata_to_dom!(self as Vector3int16 => dom::Vector3int16), - DomType::CFrame => convert::, + // NOTE: The none and default variants of these types are handled in + // LuaToDomValue for the LuaValue type instead, allowing for nil/default + DomType::OptionalCFrame => { + return match self.borrow::() { + Err(_) => unreachable!("Invalid use of conversion method, should be using LuaValue"), + Ok(value) => Ok(DomValue::OptionalCFrame(Some(dom::CFrame::from(*value)))), + } + } + DomType::PhysicalProperties => { + return match self.borrow::() { + Err(_) => unreachable!("Invalid use of conversion method, should be using LuaValue"), + Ok(value) => { + let props = dom::CustomPhysicalProperties::from(*value); + let custom = dom::PhysicalProperties::Custom(props); + Ok(DomValue::PhysicalProperties(custom)) + } + } + } - DomType::BrickColor => convert::, - DomType::Color3 => convert::, - DomType::Color3uint8 => convert::, - DomType::ColorSequence => convert::, + ty => { + return Err(DomConversionError::ToDomValue { + to: ty.variant_name(), + from: "userdata", + detail: Some("Type not supported".to_string()), + }) + } + } + } else { + /* + Non-strict target type, here we need to do manual typechecks + on the userdata to see what we should be converting it into - DomType::Enum => convert::, + This is used for example for attributes, where the wanted + type is not known by the dom and instead determined by the user + */ + match self { + value if value.is::() => userdata_to_dom!(value as Axes => dom::Axes), + value if value.is::() => userdata_to_dom!(value as BrickColor => dom::BrickColor), + value if value.is::() => userdata_to_dom!(value as CFrame => dom::CFrame), + value if value.is::() => userdata_to_dom!(value as Color3 => dom::Color3), + value if value.is::() => userdata_to_dom!(value as ColorSequence => dom::ColorSequence), + value if value.is::() => userdata_to_dom!(value as EnumItem => dom::Enum), + value if value.is::() => userdata_to_dom!(value as Faces => dom::Faces), + value if value.is::() => userdata_to_dom!(value as Font => dom::Font), + value if value.is::() => userdata_to_dom!(value as NumberRange => dom::NumberRange), + value if value.is::() => userdata_to_dom!(value as NumberSequence => dom::NumberSequence), + value if value.is::() => userdata_to_dom!(value as Ray => dom::Ray), + value if value.is::() => userdata_to_dom!(value as Rect => dom::Rect), + value if value.is::() => userdata_to_dom!(value as Instance => dom::Ref), + value if value.is::() => userdata_to_dom!(value as Region3 => dom::Region3), + value if value.is::() => userdata_to_dom!(value as Region3int16 => dom::Region3int16), + value if value.is::() => userdata_to_dom!(value as UDim => dom::UDim), + value if value.is::() => userdata_to_dom!(value as UDim2 => dom::UDim2), + value if value.is::() => userdata_to_dom!(value as Vector2 => dom::Vector2), + value if value.is::() => userdata_to_dom!(value as Vector2int16 => dom::Vector2int16), + value if value.is::() => userdata_to_dom!(value as Vector3 => dom::Vector3), + value if value.is::() => userdata_to_dom!(value as Vector3int16 => dom::Vector3int16), - DomType::Font => convert::, - - DomType::NumberRange => convert::, - DomType::NumberSequence => convert::, - - DomType::Rect => convert::, - DomType::UDim => convert::, - DomType::UDim2 => convert::, - - DomType::Ray => convert::, - - DomType::Region3 => convert::, - DomType::Region3int16 => convert::, - DomType::Vector2 => convert::, - DomType::Vector2int16 => convert::, - DomType::Vector3 => convert::, - DomType::Vector3int16 => convert::, - - DomType::OptionalCFrame => return match self.borrow::() { - Ok(value) => Ok(DomValue::OptionalCFrame(Some(dom::CFrame::from(*value)))), - Err(e) => Err(lua_userdata_error_to_conversion_error(variant_type, e)), - }, - - DomType::PhysicalProperties => return match self.borrow::() { - Ok(value) => { - let props = dom::CustomPhysicalProperties::from(*value); - let custom = dom::PhysicalProperties::Custom(props); - Ok(DomValue::PhysicalProperties(custom)) - }, - Err(e) => Err(lua_userdata_error_to_conversion_error(variant_type, e)), - }, - - _ => return Err(DomConversionError::ToDomValue { - to: variant_type.variant_name(), - from: "userdata", - detail: Some("Type not supported".to_string()), - }), - }; - - f(self, variant_type) - } -} - -fn convert( - userdata: &LuaAnyUserData, - variant_type: DomType, -) -> DomConversionResult -where - TypeFrom: LuaUserData + Clone + 'static, - TypeTo: From + Into, -{ - match userdata.borrow::() { - Ok(value) => Ok(TypeTo::from(value.clone()).into()), - Err(e) => Err(lua_userdata_error_to_conversion_error(variant_type, e)), - } -} - -fn lua_userdata_error_to_conversion_error( - variant_type: DomType, - error: LuaError, -) -> DomConversionError { - match error { - LuaError::UserDataTypeMismatch => DomConversionError::ToDomValue { - to: variant_type.variant_name(), - from: "userdata", - detail: Some("Type mismatch".to_string()), - }, - e => DomConversionError::ToDomValue { - to: variant_type.variant_name(), - from: "userdata", - detail: Some(format!("Internal error: {e}")), - }, + _ => Err(DomConversionError::ToDomValue { + to: "unknown", + from: "userdata", + detail: Some("Type not supported".to_string()), + }) + } + } } } diff --git a/packages/lib-roblox/src/datatypes/extension.rs b/packages/lib-roblox/src/datatypes/extension.rs index ed66a3a..4328ef0 100644 --- a/packages/lib-roblox/src/datatypes/extension.rs +++ b/packages/lib-roblox/src/datatypes/extension.rs @@ -1,6 +1,6 @@ use super::*; -pub(super) trait DomValueExt { +pub(crate) trait DomValueExt { fn variant_name(&self) -> &'static str; } diff --git a/packages/lib-roblox/src/instance/mod.rs b/packages/lib-roblox/src/instance/mod.rs index 7692fad..fde459a 100644 --- a/packages/lib-roblox/src/instance/mod.rs +++ b/packages/lib-roblox/src/instance/mod.rs @@ -1,14 +1,19 @@ -use std::{collections::VecDeque, fmt, sync::RwLock}; +use std::{ + collections::{BTreeMap, VecDeque}, + fmt, + sync::RwLock, +}; use mlua::prelude::*; use rbx_dom_weak::{ - types::{Ref as DomRef, Variant as DomValue}, + types::{Ref as DomRef, Variant as DomValue, VariantType as DomType}, Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom, }; use crate::{ datatypes::{ conversion::{DomValueToLua, LuaToDomValue}, + extension::DomValueExt, types::EnumItem, userdata_impl_eq, userdata_impl_to_string, }, @@ -141,6 +146,10 @@ impl Instance { .parent() }; + // TODO: We should keep track of a map from old ref -> new ref + // for each instance so that we can then transform properties + // that are instance refs into ones pointing at the new instances + let new_ref = Self::clone_inner(self.dom_ref, parent_ref); let new_inst = Self::new(new_ref); @@ -391,6 +400,87 @@ impl Instance { .insert(name.as_ref().to_string(), value); } + fn ensure_valid_attribute_value(&self, value: &DomValue) -> LuaResult<()> { + let is_valid = matches!( + value.ty(), + DomType::Bool + | DomType::BrickColor + | DomType::CFrame + | DomType::Color3 + | DomType::ColorSequence + | DomType::Float32 + | DomType::Float64 + | DomType::Int32 + | DomType::Int64 + | DomType::NumberRange + | DomType::NumberSequence + | DomType::Rect + | DomType::String + | DomType::UDim + | DomType::UDim2 + | DomType::Vector2 + | DomType::Vector3 + | DomType::Font + ); + if is_valid { + Ok(()) + } else { + Err(LuaError::RuntimeError(format!( + "'{}' is not a valid attribute type", + value.ty().variant_name() + ))) + } + } + + /** + Gets an attribute for the instance, if it exists. + */ + pub fn get_attribute(&self, name: impl AsRef) -> Option { + 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::Attributes(attributes)) = inst.properties.get("Attributes") { + attributes.get(name.as_ref()).cloned() + } else { + None + } + } + + /** + Gets all known attributes for the instance. + */ + pub fn get_attributes(&self) -> BTreeMap { + 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::Attributes(attributes)) = inst.properties.get("Attributes") { + attributes.clone().into_iter().collect() + } else { + BTreeMap::new() + } + } + + /** + Sets an attribute for the 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"); + let inst = dom + .get_by_ref_mut(self.dom_ref) + .expect("Failed to find instance in document"); + if let Some(DomValue::Attributes(attributes)) = inst.properties.get_mut("Attributes") { + attributes.insert(name.as_ref().to_string(), value); + } + } + /** Gets all of the current children of this `Instance`. @@ -629,6 +719,11 @@ 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 + ))), _ => {} } @@ -668,7 +763,7 @@ impl LuaUserData for Instance { "Failed to get property '{}' - missing default value", prop_name ))) - } else { + } else { Err(LuaError::RuntimeError(format!( "Failed to get property '{}' - malformed property info", prop_name @@ -720,6 +815,13 @@ 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 + ))) + } _ => {} } @@ -746,7 +848,7 @@ impl LuaUserData for Instance { Err(e) => Err(e), } } else if let Some(dom_type) = info.value_type { - match prop_value.lua_to_dom_value(lua, dom_type) { + match prop_value.lua_to_dom_value(lua, Some(dom_type)) { Ok(dom_value) => { this.set_property(prop_name, dom_value); Ok(()) @@ -846,6 +948,36 @@ impl LuaUserData for Instance { .find_ancestor(|ancestor| ancestor.referent() == instance.dom_ref) .is_some()) }); + methods.add_method("GetAttribute", |lua, this, name: String| { + this.ensure_not_destroyed()?; + match this.get_attribute(name) { + Some(attribute) => Ok(LuaValue::dom_value_to_lua(lua, &attribute)?), + None => Ok(LuaValue::Nil), + } + }); + methods.add_method("GetAttributes", |lua, this, ()| { + this.ensure_not_destroyed()?; + let attributes = this.get_attributes(); + let tab = lua.create_table_with_capacity(0, attributes.len() as i32)?; + for (key, value) in attributes.into_iter() { + tab.set(key, LuaValue::dom_value_to_lua(lua, &value)?)?; + } + Ok(tab) + }); + methods.add_method( + "SetAttribute", + |lua, this, (attribute_name, lua_value): (String, LuaValue)| { + this.ensure_not_destroyed()?; + match lua_value.lua_to_dom_value(lua, None) { + Ok(dom_value) => { + this.ensure_valid_attribute_value(&dom_value)?; + this.set_attribute(attribute_name, dom_value); + Ok(()) + } + Err(e) => Err(e.into()), + } + }, + ); // Here we add inheritance-like behavior for instances by creating // methods that are restricted to specific classnames / base classes data_model::add_methods(methods); @@ -863,3 +995,15 @@ impl PartialEq for Instance { self.dom_ref == other.dom_ref } } + +impl From for DomRef { + fn from(value: Instance) -> Self { + value.dom_ref + } +} + +impl From for Instance { + fn from(value: DomRef) -> Self { + Instance::new(value) + } +}