Implement support for Roblox's Content type (#305)

This commit is contained in:
Micah 2025-04-03 11:40:08 -07:00 committed by GitHub
parent bb8c4bce82
commit 8bd1a9b77d
Signed by: DevComp
GPG key ID: B5690EEEBB952194
7 changed files with 191 additions and 2 deletions

View file

@ -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),

View file

@ -19,6 +19,7 @@ impl DomValueExt for DomType {
Color3uint8 => "Color3uint8",
ColorSequence => "ColorSequence",
Content => "Content",
ContentId => "ContentId",
Enum => "Enum",
Faces => "Faces",
Float32 => "Float32",

View file

@ -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<LuaTable<'_>> {
let from_uri = |_, uri: String| Ok(Self(ContentType::Uri(uri)));
let from_object = |_, obj: LuaUserDataRef<Instance>| {
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<DomContent> for Content {
fn from(value: DomContent) -> Self {
Self(value.value().clone())
}
}
impl From<Content> 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:?}"),
}
}
}

View file

@ -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;

View file

@ -25,6 +25,7 @@ fn create_all_exports(lua: &Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
export::<Color3>(lua)?,
export::<ColorSequence>(lua)?,
export::<ColorSequenceKeypoint>(lua)?,
export::<Content>(lua)?,
export::<Faces>(lua)?,
export::<Font>(lua)?,
export::<NumberRange>(lua)?,

View file

@ -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",

View file

@ -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}`
)