Merge branch 'main' into datetime-rfc2822-support

This commit is contained in:
Ryan 2025-04-07 23:35:45 -04:00 committed by GitHub
commit f5d0967c24
Signed by: DevComp
GPG key ID: B5690EEEBB952194
20 changed files with 318 additions and 66 deletions

55
Cargo.lock generated
View file

@ -28,6 +28,19 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if 1.0.0",
"getrandom 0.2.15",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@ -2258,10 +2271,11 @@ dependencies = [
[[package]]
name = "rbx_binary"
version = "0.7.7"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b85057e8ff75a1ce99248200c4b3c7b481a3d52f921f1053ecd67921dcc7930"
checksum = "9573fee5e073d7b303f475c285197fdc8179468de66ca60ee115a58fbac99296"
dependencies = [
"ahash",
"log",
"lz4",
"profiling",
@ -2269,6 +2283,7 @@ dependencies = [
"rbx_reflection",
"rbx_reflection_database",
"thiserror",
"zstd",
]
[[package]]
@ -2288,19 +2303,21 @@ dependencies = [
[[package]]
name = "rbx_dom_weak"
version = "2.9.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcd2a17d09e46af0805f8b311a926402172b97e8d9388745c9adf8f448901841"
checksum = "04425cf6e9376e5486f4fb35906c120d1b1b45618a490318cf563fab1fa230a9"
dependencies = [
"ahash",
"rbx_types",
"serde",
"ustr",
]
[[package]]
name = "rbx_reflection"
version = "4.7.0"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8118ac6021d700e8debe324af6b40ecfd2cef270a00247849dbdfeebb0802677"
checksum = "1b6d0d62baa613556b058a5f94a53b01cf0ccde0ea327ce03056e335b982e77e"
dependencies = [
"rbx_types",
"serde",
@ -2309,9 +2326,9 @@ dependencies = [
[[package]]
name = "rbx_reflection_database"
version = "0.2.12+roblox-638"
version = "1.0.1+roblox-666"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e29381d675420e841f8c02db5755cbb2545ed3e13f56c539546dc58702b512a"
checksum = "1ea11f26cfddc57a136781baed2e68eda5a3aa0735152d1562d65b1909c8d65d"
dependencies = [
"lazy_static",
"rbx_reflection",
@ -2321,9 +2338,9 @@ dependencies = [
[[package]]
name = "rbx_types"
version = "1.10.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e30f49b2a3bb667e4074ba73c2dfb8ca0873f610b448ccf318a240acfdec6c73"
checksum = "78e4fdde46493def107e5f923d82e813dec9b3eef52c2f75fbad3a716023eda2"
dependencies = [
"base64 0.13.1",
"bitflags 1.3.2",
@ -2336,10 +2353,11 @@ dependencies = [
[[package]]
name = "rbx_xml"
version = "0.13.5"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b14b3027bc9ccd82e2fc854c8bcd25ed58318e570c355bf2cf63df9cdbd5ba8"
checksum = "bb623833c31cc43bbdaeb32f5e91db8ecd63fc46e438d0d268baf9e61539cf1c"
dependencies = [
"ahash",
"base64 0.13.1",
"log",
"rbx_dom_weak",
@ -3483,6 +3501,19 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "ustr"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18b19e258aa08450f93369cf56dd78063586adf19e92a75b338a800f799a0208"
dependencies = [
"ahash",
"byteorder 1.5.0",
"lazy_static",
"parking_lot",
"serde",
]
[[package]]
name = "utf-8"
version = "0.7.6"

View file

@ -20,10 +20,10 @@ rand = "0.8"
thiserror = "1.0"
once_cell = "1.17"
rbx_binary = "0.7.7"
rbx_dom_weak = "2.9.0"
rbx_reflection = "4.7.0"
rbx_reflection_database = "0.2.12"
rbx_xml = "0.13.5"
rbx_binary = "1.0.0"
rbx_dom_weak = "3.0.0"
rbx_reflection = "5.0.0"
rbx_reflection_database = "1.0.0"
rbx_xml = "1.0.0"
lune-utils = { version = "0.1.3", path = "../lune-utils" }

View file

@ -47,6 +47,7 @@ pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> {
| DomType::CFrame
| DomType::Color3
| DomType::ColorSequence
| DomType::EnumItem
| DomType::Float32
| DomType::Float64
| DomType::Font

View file

@ -51,7 +51,7 @@ impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> {
DomValue::Float32(n) => Ok(LuaValue::Number(*n as f64)),
DomValue::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
DomValue::BinaryString(s) => Ok(LuaValue::String(lua.create_string(s)?)),
DomValue::Content(s) => Ok(LuaValue::String(
DomValue::ContentId(s) => Ok(LuaValue::String(
lua.create_string(AsRef::<str>::as_ref(s))?,
)),
@ -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,8 @@ 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::EnumItem(value) => dom_to_userdata!(lua, value => EnumItem),
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,7 +258,8 @@ 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::Enum => userdata_to_dom!(self as EnumItem => dom::Enum),
DomType::Content => userdata_to_dom!(self as Content => dom::Content),
DomType::EnumItem => userdata_to_dom!(self as EnumItem => dom::EnumItem),
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),
@ -314,7 +317,7 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
value if value.is::<CFrame>() => userdata_to_dom!(value as CFrame => dom::CFrame),
value if value.is::<Color3>() => userdata_to_dom!(value as Color3 => dom::Color3),
value if value.is::<ColorSequence>() => userdata_to_dom!(value as ColorSequence => dom::ColorSequence),
value if value.is::<Enum>() => userdata_to_dom!(value as EnumItem => dom::Enum),
value if value.is::<EnumItem>() => userdata_to_dom!(value as EnumItem => dom::EnumItem),
value if value.is::<Faces>() => userdata_to_dom!(value as Faces => dom::Faces),
value if value.is::<Font>() => userdata_to_dom!(value as Font => dom::Font),
value if value.is::<Instance>() => userdata_to_dom!(value as Instance => dom::Ref),

View file

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

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

@ -1,7 +1,7 @@
use core::fmt;
use mlua::prelude::*;
use rbx_dom_weak::types::Enum as DomEnum;
use rbx_dom_weak::types::EnumItem as DomEnumItem;
use super::{super::*, Enum};
@ -100,8 +100,18 @@ impl PartialEq for EnumItem {
}
}
impl From<EnumItem> for DomEnum {
impl From<EnumItem> for DomEnumItem {
fn from(v: EnumItem) -> Self {
DomEnum::from_u32(v.value)
DomEnumItem {
ty: v.parent.desc.name.to_string(),
value: v.value,
}
}
}
impl From<DomEnumItem> for EnumItem {
fn from(value: DomEnumItem) -> Self {
EnumItem::from_enum_name_and_value(value.ty, value.value)
.expect("cannot convert rbx_type::EnumItem with unknown type into EnumItem")
}
}

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

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

View file

@ -1,6 +1,6 @@
use rbx_dom_weak::{
types::{Ref as DomRef, VariantType as DomType},
Instance as DomInstance, WeakDom,
ustr, Instance as DomInstance, WeakDom,
};
use crate::shared::instance::class_is_a;
@ -18,8 +18,8 @@ pub fn postprocess_dom_for_model(dom: &mut WeakDom) {
remove_matching_prop(inst, DomType::UniqueId, "HistoryId");
// Similar story with ScriptGuid - this is used
// in the studio-only cloud script drafts feature
if class_is_a(&inst.class, "LuaSourceContainer").unwrap_or(false) {
inst.properties.remove("ScriptGuid");
if class_is_a(inst.class, "LuaSourceContainer").unwrap_or(false) {
inst.properties.remove(&ustr("ScriptGuid"));
}
});
}
@ -41,6 +41,7 @@ where
}
fn remove_matching_prop(inst: &mut DomInstance, ty: DomType, name: &'static str) {
let name = &ustr(name);
if inst.properties.get(name).is_some_and(|u| u.ty() == ty) {
inst.properties.remove(name);
}

View file

@ -71,7 +71,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
"FindFirstAncestorWhichIsA",
|lua, this, class_name: String| {
ensure_not_destroyed(this)?;
this.find_ancestor(|child| class_is_a(&child.class, &class_name).unwrap_or(false))
this.find_ancestor(|child| class_is_a(child.class, &class_name).unwrap_or(false))
.into_lua(lua)
},
);
@ -104,7 +104,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|lua, this, (class_name, recursive): (String, Option<bool>)| {
ensure_not_destroyed(this)?;
let predicate =
|child: &DomInstance| class_is_a(&child.class, &class_name).unwrap_or(false);
|child: &DomInstance| class_is_a(child.class, &class_name).unwrap_or(false);
if matches!(recursive, Some(true)) {
this.find_descendant(predicate).into_lua(lua)
} else {
@ -113,7 +113,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
},
);
m.add_method("IsA", |_, this, class_name: String| {
Ok(class_is_a(&this.class_name, class_name).unwrap_or(false))
Ok(class_is_a(this.class_name, class_name).unwrap_or(false))
});
m.add_method(
"IsAncestorOf",
@ -230,7 +230,7 @@ fn instance_property_get<'lua>(
return this.get_name().into_lua(lua);
}
if let Some(info) = find_property_info(&this.class_name, &prop_name) {
if let Some(info) = find_property_info(this.class_name, &prop_name) {
if let Some(prop) = this.get_property(&prop_name) {
if let DomValue::Enum(enum_value) = prop {
let enum_name = info.enum_name.ok_or_else(|| {
@ -275,7 +275,7 @@ fn instance_property_get<'lua>(
} else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
Ok(LuaValue::UserData(lua.create_userdata(inst)?))
} else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) {
getter.call(this.clone())
getter.call(*this)
} else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) {
Ok(LuaValue::Function(method))
} else {
@ -321,17 +321,17 @@ fn instance_property_set<'lua>(
}
type Parent<'lua> = Option<LuaUserDataRef<'lua, Instance>>;
let parent = Parent::from_lua(prop_value, lua)?;
this.set_parent(parent.map(|p| p.clone()));
this.set_parent(parent.map(|p| *p));
return Ok(());
}
_ => {}
}
if let Some(info) = find_property_info(&this.class_name, &prop_name) {
if let Some(info) = find_property_info(this.class_name, &prop_name) {
if let Some(enum_name) = info.enum_name {
match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) {
Ok(given_enum) if given_enum.parent.desc.name == enum_name => {
this.set_property(prop_name, DomValue::Enum((*given_enum).clone().into()));
this.set_property(prop_name, DomValue::EnumItem((*given_enum).clone().into()));
Ok(())
}
Ok(given_enum) => Err(LuaError::RuntimeError(format!(
@ -354,7 +354,7 @@ fn instance_property_set<'lua>(
)))
}
} else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) {
setter.call((this.clone(), prop_value))
setter.call((*this, prop_value))
} else {
Err(LuaError::RuntimeError(format!(
"{prop_name} is not a valid member of {this}",

View file

@ -48,7 +48,7 @@ fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> Lua
Ok(service)
} else {
let service = Instance::new_orphaned(service_name);
service.set_parent(Some(this.clone()));
service.set_parent(Some(*this));
Ok(service)
}
}

View file

@ -11,7 +11,7 @@ use mlua::prelude::*;
use once_cell::sync::Lazy;
use rbx_dom_weak::{
types::{Attributes as DomAttributes, Ref as DomRef, Variant as DomValue},
Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom,
ustr, Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, Ustr, WeakDom,
};
use lune_utils::TableBuilder;
@ -34,10 +34,10 @@ const PROPERTY_NAME_TAGS: &str = "Tags";
static INTERNAL_DOM: Lazy<Mutex<WeakDom>> =
Lazy::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT"))));
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct Instance {
pub(crate) dom_ref: DomRef,
pub(crate) class_name: String,
pub(crate) class_name: Ustr,
}
impl Instance {
@ -75,7 +75,7 @@ impl Instance {
Some(Self {
dom_ref,
class_name: instance.class.clone(),
class_name: instance.class,
})
} else {
None
@ -96,14 +96,14 @@ impl Instance {
let class_name = class_name.as_ref();
let instance = DomInstanceBuilder::new(class_name.to_string());
let instance = DomInstanceBuilder::new(class_name);
let dom_root = dom.root_ref();
let dom_ref = dom.insert(dom_root, instance);
Self {
dom_ref,
class_name: class_name.to_string(),
class_name: ustr(class_name),
}
}
@ -244,7 +244,7 @@ impl Instance {
on the Roblox Developer Hub
*/
pub fn is_a(&self, class_name: impl AsRef<str>) -> bool {
class_is_a(&self.class_name, class_name).unwrap_or(false)
class_is_a(self.class_name, class_name).unwrap_or(false)
}
/**
@ -341,7 +341,7 @@ impl Instance {
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.properties
.get(name.as_ref())
.get(&ustr(name.as_ref()))
.cloned()
}
@ -358,7 +358,7 @@ impl Instance {
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document")
.properties
.insert(name.as_ref().to_string(), value);
.insert(ustr(name.as_ref()), value);
}
/**
@ -374,7 +374,7 @@ impl Instance {
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get(PROPERTY_NAME_ATTRIBUTES)
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
{
attributes.get(name.as_ref()).cloned()
} else {
@ -395,7 +395,7 @@ impl Instance {
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get(PROPERTY_NAME_ATTRIBUTES)
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
{
attributes.clone().into_iter().collect()
} else {
@ -422,14 +422,14 @@ impl Instance {
value => value,
};
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES)
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
{
attributes.insert(name.as_ref().to_string(), value);
} else {
let mut attributes = DomAttributes::new();
attributes.insert(name.as_ref().to_string(), value);
inst.properties.insert(
PROPERTY_NAME_ATTRIBUTES.to_string(),
ustr(PROPERTY_NAME_ATTRIBUTES),
DomValue::Attributes(attributes),
);
}
@ -449,11 +449,11 @@ impl Instance {
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES)
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
{
attributes.remove(name.as_ref());
if attributes.is_empty() {
inst.properties.remove(PROPERTY_NAME_ATTRIBUTES);
inst.properties.remove(&ustr(PROPERTY_NAME_ATTRIBUTES));
}
}
}
@ -470,11 +470,11 @@ impl Instance {
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(PROPERTY_NAME_TAGS) {
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
tags.push(name.as_ref());
} else {
inst.properties.insert(
PROPERTY_NAME_TAGS.to_string(),
ustr(PROPERTY_NAME_TAGS),
DomValue::Tags(vec![name.as_ref().to_string()].into()),
);
}
@ -492,7 +492,7 @@ impl Instance {
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(PROPERTY_NAME_TAGS) {
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
tags.iter().map(ToString::to_string).collect()
} else {
Vec::new()
@ -511,7 +511,7 @@ impl Instance {
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(PROPERTY_NAME_TAGS) {
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
let name = name.as_ref();
tags.iter().any(|tag| tag == name)
} else {
@ -531,14 +531,12 @@ impl Instance {
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(PROPERTY_NAME_TAGS) {
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
let name = name.as_ref();
let mut new_tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
new_tags.retain(|tag| tag != name);
inst.properties.insert(
PROPERTY_NAME_TAGS.to_string(),
DomValue::Tags(new_tags.into()),
);
inst.properties
.insert(ustr(PROPERTY_NAME_TAGS), DomValue::Tags(new_tags.into()));
}
}

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

@ -122,7 +122,7 @@ pub(crate) fn get_or_create_property_ref_instance(
Ok(inst)
} else {
let inst = Instance::new_orphaned(class_name);
inst.set_parent(Some(this.clone()));
inst.set_parent(Some(*this));
this.set_property(prop_name, DomValue::Ref(inst.dom_ref));
Ok(inst)
}

View file

@ -77,7 +77,7 @@ async fn serialize_place<'lua>(
lua: &'lua Lua,
(data_model, as_xml): (LuaUserDataRef<'lua, Instance>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
let data_model = (*data_model).clone();
let data_model = *data_model;
let fut = lua.spawn_blocking(move || {
let doc = Document::from_data_model_instance(data_model)?;
let bytes = doc.to_bytes_with_format(match as_xml {
@ -94,7 +94,7 @@ async fn serialize_model<'lua>(
lua: &'lua Lua,
(instances, as_xml): (Vec<LuaUserDataRef<'lua, Instance>>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
let instances = instances.iter().map(|i| (*i).clone()).collect();
let instances = instances.iter().map(|i| **i).collect();
let fut = lua.spawn_blocking(move || {
let doc = Document::from_instance_array(instances)?;
let bytes = doc.to_bytes_with_format(match as_xml {

View file

@ -169,6 +169,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}`
)

View file

@ -16,6 +16,7 @@ local UDim2 = roblox.UDim2
local Vector2 = roblox.Vector2
local Vector3 = roblox.Vector3
local Instance = roblox.Instance
local Enum = roblox.Enum
local modelFile = fs.readFile("tests/roblox/rbx-test-files/models/attributes/binary.rbxm")
local model = roblox.deserializeModel(modelFile)[1]
@ -114,3 +115,12 @@ model.Parent = game
local placeFile = roblox.serializePlace(game)
fs.writeDir("bin/roblox")
fs.writeFile("bin/roblox/attributes.rbxl", placeFile)
local enum_attr = Instance.new("Folder")
enum_attr:SetAttribute("Foo", Enum.NormalId.Front)
assert(enum_attr:GetAttribute("Foo") == Enum.NormalId.Front)
local enum_attr_ser = roblox.serializeModel({ enum_attr })
local enum_attr_de = roblox.deserializeModel(enum_attr_ser)
assert(enum_attr_de[1]:GetAttribute("Foo") == Enum.NormalId.Front)

View file

@ -33,6 +33,16 @@ part.Shape = Enum.PartType.Ball
assert(part.Shape == Enum.PartType.Ball)
-- Enums should roundtrip through serde without problem
local decal = Instance.new("Decal")
decal.Face = Enum.NormalId.Top
local decal_ser = roblox.serializeModel({ decal })
local decal_de = roblox.deserializeModel(decal_ser)
assert(decal_de[1].Face == Enum.NormalId.Top)
-- Properties that don't exist for a class should error
local meshPart = Instance.new("MeshPart")