lune/src/roblox/shared/instance.rs
2023-07-20 09:47:00 +02:00

209 lines
7.4 KiB
Rust

use std::borrow::{Borrow, BorrowMut, Cow};
use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType};
use rbx_reflection::{ClassTag, DataType};
#[derive(Debug, Clone, Default)]
pub(crate) struct PropertyInfo {
pub enum_name: Option<Cow<'static, str>>,
pub enum_default: Option<u32>,
pub value_type: Option<DomType>,
pub value_default: Option<&'static DomValue>,
}
/**
Finds the info of a property of the given class.
This will also check superclasses if the property
was not directly found for the given class.
Returns `None` if the class or property does not exist.
*/
pub(crate) fn find_property_info(
instance_class: impl AsRef<str>,
property_name: impl AsRef<str>,
) -> Option<PropertyInfo> {
let db = rbx_reflection_database::get();
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
let mut class_name = Cow::Borrowed(instance_class);
let mut class_info = None;
while let Some(class) = db.classes.get(class_name.as_ref()) {
if let Some(prop_definition) = class.properties.get(property_name) {
/*
We found a property, create a property info containing name/type
Note that we might have found the property in the
base class but the default value can be part of
some separate class, it will be checked below
*/
class_info = Some(match &prop_definition.data_type {
DataType::Enum(enum_name) => PropertyInfo {
enum_name: Some(Cow::Borrowed(enum_name)),
..Default::default()
},
DataType::Value(value_type) => PropertyInfo {
value_type: Some(*value_type),
..Default::default()
},
_ => Default::default(),
});
break;
} else if let Some(sup) = &class.superclass {
// No property found, we should look at the superclass
class_name = Cow::Borrowed(sup)
} else {
break;
}
}
if let Some(class_info) = class_info.borrow_mut() {
class_name = Cow::Borrowed(instance_class);
while let Some(class) = db.classes.get(class_name.as_ref()) {
if let Some(default) = class.default_properties.get(property_name) {
// We found a default value, map it to a more useful value for us
if class_info.enum_name.is_some() {
class_info.enum_default = match default {
DomValue::Enum(enum_default) => Some(enum_default.to_u32()),
_ => None,
};
} else if class_info.value_type.is_some() {
class_info.value_default = Some(default);
}
break;
} else if let Some(sup) = &class.superclass {
// No default value found, we should look at the superclass
class_name = Cow::Borrowed(sup)
} else {
break;
}
}
}
class_info
}
/**
Checks if an instance class exists in the reflection database.
*/
pub fn class_exists(class_name: impl AsRef<str>) -> bool {
let db = rbx_reflection_database::get();
db.classes.contains_key(class_name.as_ref())
}
/**
Checks if an instance class matches a given class or superclass, similar to
[Instance::IsA](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA)
from the Roblox standard library.
Note that this function may return `None` if it encounters a class or superclass
that does not exist in the currently known class reflection database.
*/
pub fn class_is_a(instance_class: impl AsRef<str>, class_name: impl AsRef<str>) -> Option<bool> {
let mut instance_class = instance_class.as_ref();
let class_name = class_name.as_ref();
if class_name == "Instance" || instance_class == class_name {
Some(true)
} else {
let db = rbx_reflection_database::get();
while instance_class != class_name {
let class_descriptor = db.classes.get(instance_class)?;
if let Some(sup) = &class_descriptor.superclass {
instance_class = sup.borrow();
} else {
return Some(false);
}
}
Some(true)
}
}
/**
Checks if an instance class is a service.
This is separate from [`class_is_a`] since services do not share a
common base class, and are instead determined through reflection tags.
Note that this function may return `None` if it encounters a class or superclass
that does not exist in the currently known class reflection database.
*/
pub fn class_is_a_service(instance_class: impl AsRef<str>) -> Option<bool> {
let mut instance_class = instance_class.as_ref();
let db = rbx_reflection_database::get();
loop {
let class_descriptor = db.classes.get(instance_class)?;
if class_descriptor.tags.contains(&ClassTag::Service) {
return Some(true);
} else if let Some(sup) = &class_descriptor.superclass {
instance_class = sup.borrow();
} else {
break;
}
}
Some(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_a_class_valid() {
assert_eq!(class_is_a("Part", "Part"), Some(true));
assert_eq!(class_is_a("Part", "BasePart"), Some(true));
assert_eq!(class_is_a("Part", "PVInstance"), Some(true));
assert_eq!(class_is_a("Part", "Instance"), Some(true));
assert_eq!(class_is_a("Workspace", "Workspace"), Some(true));
assert_eq!(class_is_a("Workspace", "Model"), Some(true));
assert_eq!(class_is_a("Workspace", "Instance"), Some(true));
}
#[test]
fn is_a_class_invalid() {
assert_eq!(class_is_a("Part", "part"), Some(false));
assert_eq!(class_is_a("Part", "Base-Part"), Some(false));
assert_eq!(class_is_a("Part", "Model"), Some(false));
assert_eq!(class_is_a("Part", "Paart"), Some(false));
assert_eq!(class_is_a("Workspace", "Service"), Some(false));
assert_eq!(class_is_a("Workspace", "."), Some(false));
assert_eq!(class_is_a("Workspace", ""), Some(false));
}
#[test]
fn is_a_service_valid() {
assert_eq!(class_is_a_service("Workspace"), Some(true));
assert_eq!(class_is_a_service("PhysicsService"), Some(true));
assert_eq!(class_is_a_service("ReplicatedFirst"), Some(true));
assert_eq!(class_is_a_service("CSGDictionaryService"), Some(true));
}
#[test]
fn is_a_service_invalid() {
assert_eq!(class_is_a_service("Camera"), Some(false));
assert_eq!(class_is_a_service("Terrain"), Some(false));
assert_eq!(class_is_a_service("Work-space"), None);
assert_eq!(class_is_a_service("CSG Dictionary Service"), None);
}
}