diff --git a/crates/lune-roblox/src/datatypes/conversion.rs b/crates/lune-roblox/src/datatypes/conversion.rs index f4e85fb..dbd4249 100644 --- a/crates/lune-roblox/src/datatypes/conversion.rs +++ b/crates/lune-roblox/src/datatypes/conversion.rs @@ -104,8 +104,8 @@ impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> { (LuaValue::String(s), DomType::BinaryString) => { Ok(DomValue::BinaryString(s.as_ref().into())) } - (LuaValue::String(s), DomType::Content) => { - Ok(DomValue::Content(s.to_str()?.to_string().into())) + (LuaValue::String(s), DomType::ContentId) => { + Ok(DomValue::ContentId(s.to_str()?.to_string().into())) } // NOTE: Some values are either optional or default and we @@ -200,6 +200,7 @@ impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> { 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::Content(value) => dom_to_userdata!(lua, value => Content), 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), @@ -256,6 +257,7 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> { 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::Content => userdata_to_dom!(self as Content => dom::Content), 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), diff --git a/crates/lune-roblox/src/datatypes/extension.rs b/crates/lune-roblox/src/datatypes/extension.rs index 6f83242..30bb916 100644 --- a/crates/lune-roblox/src/datatypes/extension.rs +++ b/crates/lune-roblox/src/datatypes/extension.rs @@ -19,6 +19,7 @@ impl DomValueExt for DomType { Color3uint8 => "Color3uint8", ColorSequence => "ColorSequence", Content => "Content", + ContentId => "ContentId", Enum => "Enum", Faces => "Faces", Float32 => "Float32", diff --git a/crates/lune-roblox/src/datatypes/types/content.rs b/crates/lune-roblox/src/datatypes/types/content.rs new file mode 100644 index 0000000..226ea92 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/content.rs @@ -0,0 +1,120 @@ +use core::fmt; + +use mlua::prelude::*; +use rbx_dom_weak::types::{Content as DomContent, ContentType}; + +use lune_utils::TableBuilder; + +use crate::{exports::LuaExportsTable, instance::Instance}; + +use super::{super::*, EnumItem}; + +/** + An implementation of the [Content](https://create.roblox.com/docs/reference/engine/datatypes/Content) Roblox datatype. + + This implements all documented properties, methods & constructors of the Content type as of April 2025. +*/ +#[derive(Debug, Clone, PartialEq)] +pub struct Content(ContentType); + +impl LuaExportsTable<'_> for Content { + const EXPORT_NAME: &'static str = "Content"; + + fn create_exports_table(lua: &'_ Lua) -> LuaResult> { + let from_uri = |_, uri: String| Ok(Self(ContentType::Uri(uri))); + + let from_object = |_, obj: LuaUserDataRef| { + let database = rbx_reflection_database::get(); + let instance_descriptor = database + .classes + .get("Instance") + .expect("the reflection database should always have Instance in it"); + let param_descriptor = database.classes.get(obj.get_class_name()).expect( + "you should not be able to construct an Instance that is not known to Lune", + ); + if database.has_superclass(param_descriptor, instance_descriptor) { + Err(LuaError::runtime("the provided object is a descendant class of 'Instance', expected one that was only an 'Object'")) + } else { + Ok(Content(ContentType::Object(obj.dom_ref))) + } + }; + + TableBuilder::new(lua)? + .with_value("none", Content(ContentType::None))? + .with_function("fromUri", from_uri)? + .with_function("fromObject", from_object)? + .build_readonly() + } +} + +impl LuaUserData for Content { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("SourceType", |_, this| { + let variant_name = match &this.0 { + ContentType::None => "None", + ContentType::Uri(_) => "Uri", + ContentType::Object(_) => "Object", + other => { + return Err(LuaError::runtime(format!( + "cannot get SourceType: unknown ContentType variant '{other:?}'" + ))) + } + }; + Ok(EnumItem::from_enum_name_and_name( + "ContentSourceType", + variant_name, + )) + }); + fields.add_field_method_get("Uri", |_, this| { + if let ContentType::Uri(uri) = &this.0 { + Ok(Some(uri.to_owned())) + } else { + Ok(None) + } + }); + fields.add_field_method_get("Object", |_, this| { + if let ContentType::Object(referent) = &this.0 { + Ok(Instance::new_opt(*referent)) + } else { + Ok(None) + } + }); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for Content { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Regardless of the actual content of the Content, Roblox just emits + // `Content` when casting it to a string. We do not do that. + write!(f, "Content(")?; + match &self.0 { + ContentType::None => write!(f, "None")?, + ContentType::Uri(uri) => write!(f, "Uri={uri}")?, + ContentType::Object(_) => write!(f, "Object")?, + other => write!(f, "UnknownType({other:?})")?, + } + write!(f, ")") + } +} + +impl From for Content { + fn from(value: DomContent) -> Self { + Self(value.value().clone()) + } +} + +impl From for DomContent { + fn from(value: Content) -> Self { + match value.0 { + ContentType::None => Self::none(), + ContentType::Uri(uri) => Self::from_uri(uri), + ContentType::Object(referent) => Self::from_referent(referent), + other => unimplemented!("unknown variant of ContentType: {other:?}"), + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/mod.rs b/crates/lune-roblox/src/datatypes/types/mod.rs index 394b210..abc07a7 100644 --- a/crates/lune-roblox/src/datatypes/types/mod.rs +++ b/crates/lune-roblox/src/datatypes/types/mod.rs @@ -4,6 +4,7 @@ mod cframe; mod color3; mod color_sequence; mod color_sequence_keypoint; +mod content; mod r#enum; mod r#enum_item; mod r#enums; @@ -30,6 +31,7 @@ pub use cframe::CFrame; pub use color3::Color3; pub use color_sequence::ColorSequence; pub use color_sequence_keypoint::ColorSequenceKeypoint; +pub use content::Content; pub use faces::Faces; pub use font::Font; pub use number_range::NumberRange; diff --git a/crates/lune-roblox/src/lib.rs b/crates/lune-roblox/src/lib.rs index 878a2f2..eefa73b 100644 --- a/crates/lune-roblox/src/lib.rs +++ b/crates/lune-roblox/src/lib.rs @@ -25,6 +25,7 @@ fn create_all_exports(lua: &Lua) -> LuaResult> { export::(lua)?, export::(lua)?, export::(lua)?, + export::(lua)?, export::(lua)?, export::(lua)?, export::(lua)?, diff --git a/crates/lune/src/tests.rs b/crates/lune/src/tests.rs index ecf01b2..04e3cca 100644 --- a/crates/lune/src/tests.rs +++ b/crates/lune/src/tests.rs @@ -167,6 +167,7 @@ create_tests! { roblox_datatype_color3: "roblox/datatypes/Color3", roblox_datatype_color_sequence: "roblox/datatypes/ColorSequence", roblox_datatype_color_sequence_keypoint: "roblox/datatypes/ColorSequenceKeypoint", + roblox_datatype_content: "roblox/datatypes/Content", roblox_datatype_enum: "roblox/datatypes/Enum", roblox_datatype_faces: "roblox/datatypes/Faces", roblox_datatype_font: "roblox/datatypes/Font", diff --git a/tests/roblox/datatypes/Content.luau b/tests/roblox/datatypes/Content.luau new file mode 100644 index 0000000..2ca90d6 --- /dev/null +++ b/tests/roblox/datatypes/Content.luau @@ -0,0 +1,62 @@ +local roblox = require("@lune/roblox") :: any +local Content = roblox.Content +local Instance = roblox.Instance +local Enum = roblox.Enum + +assert(Content.none, "Content.none did not exist") +assert( + Content.none.SourceType == Enum.ContentSourceType.None, + "Content.none's SourceType was wrong" +) +assert(Content.none.Uri == nil, "Content.none's Uri field was wrong") +assert(Content.none.Object == nil, "Content.none's Object field was wrong") + +local uri = Content.fromUri("test uri") +assert(uri.SourceType == Enum.ContentSourceType.Uri, "URI Content's SourceType was wrong") +assert(uri.Uri == "test uri", "URI Content's Uri field was wrong") +assert(uri.Object == nil, "URI Content's Object field was wrong") + +assert(not pcall(Content.fromUri), "Content.fromUri accepted no argument") +assert(not pcall(Content.fromUri, false), "Content.fromUri accepted a boolean argument") +assert(not pcall(Content.fromUri, Enum), "Content.fromUri accepted a UserData as an argument") +assert( + not pcall(Content.fromUri, buffer.create(0)), + "Content.fromUri accepted a buffer as an argument" +) + +-- It feels weird that this is allowed because `EditableImage` is very much +-- not an Instance. But what can you do? +local target = Instance.new("EditableImage") +local object = Content.fromObject(target) +assert(object.SourceType == Enum.ContentSourceType.Object, "Object Content's SourceType was wrong") +assert(object.Uri == nil, "Object Content's Uri field was wrong") +assert(object.Object == target, "Object Content's Object field was wrong") + +assert(not pcall(Content.fromObject), "Content.fromObject accepted no argument") +assert(not pcall(Content.fromObject, false), "Content.fromObject accepted a boolean argument") +assert( + not pcall(Content.fromObject, Enum), + "Content.fromObject accepted a non-Instance/Object UserData as an argument" +) +assert( + not pcall(Content.fromObject, buffer.create(0)), + "Content.fromObject accepted a buffer as an argument" +) + +assert( + not pcall(Content.fromObject, Instance.new("Folder")), + "Content.fromObject accepted an Instance as an argument" +) + +assert( + tostring(Content.none) == "Content(None)", + `expected tostring(Content.none) to be Content(None), it was actually {Content.none}` +) +assert( + tostring(uri) == "Content(Uri=test uri)", + `expected tostring(URI Content) to be Content(Uri=...), it was actually {uri}` +) +assert( + tostring(object) == "Content(Object)", + `expected tostring(Object Content) to be Content(Object), it was actually {object}` +)