mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Implement reflection database accessible from lua in roblox builtin
This commit is contained in:
parent
49ae85af03
commit
8853aad620
14 changed files with 842 additions and 12 deletions
|
@ -1,3 +1,135 @@
|
|||
export type DatabaseScriptability = "None" | "Custom" | "Read" | "ReadWrite" | "Write"
|
||||
|
||||
export type DatabasePropertyTag =
|
||||
"Deprecated"
|
||||
| "Hidden"
|
||||
| "NotBrowsable"
|
||||
| "NotReplicated"
|
||||
| "NotScriptable"
|
||||
| "ReadOnly"
|
||||
| "WriteOnly"
|
||||
|
||||
export type DatabaseClassTag =
|
||||
"Deprecated"
|
||||
| "NotBrowsable"
|
||||
| "NotCreatable"
|
||||
| "NotReplicated"
|
||||
| "PlayerReplicated"
|
||||
| "Service"
|
||||
| "Settings"
|
||||
| "UserSettings"
|
||||
|
||||
export type DatabaseProperty = {
|
||||
--[=[
|
||||
The name of the property.
|
||||
]=]
|
||||
Name: string,
|
||||
--[=[
|
||||
The datatype of the property.
|
||||
|
||||
For normal datatypes this will be a string such as `string`, `Color3`, ...
|
||||
|
||||
For enums this will be a string formatted as `Enum.EnumName`.
|
||||
]=]
|
||||
Datatype: string,
|
||||
--[=[
|
||||
The scriptability of this property, meaning if it can be written / read at runtime.
|
||||
|
||||
All properties are writable and readable in Lune even if scriptability is not.
|
||||
]=]
|
||||
Scriptability: DatabaseScriptability,
|
||||
--[=[
|
||||
Tags describing the property.
|
||||
|
||||
These include information such as if the property can be replicated to players
|
||||
at runtime, if the property should be hidden in Roblox Studio, and more.
|
||||
]=]
|
||||
Tags: { DatabasePropertyTag },
|
||||
}
|
||||
|
||||
export type DatabaseClass = {
|
||||
--[=[
|
||||
The name of the class.
|
||||
]=]
|
||||
Name: string,
|
||||
--[=[
|
||||
The superclass (parent class) of this class.
|
||||
|
||||
May be nil if no parent class exists.
|
||||
]=]
|
||||
Superclass: string?,
|
||||
--[=[
|
||||
Known properties for this class.
|
||||
]=]
|
||||
Properties: { [string]: DatabaseProperty },
|
||||
--[=[
|
||||
Default values for properties of this class.
|
||||
|
||||
Note that these default properties use Lune's built-in datatype
|
||||
userdatas, and that if there is a new datatype that Lune does
|
||||
not yet know about, it may be missing from this table.
|
||||
]=]
|
||||
DefaultProperties: { [string]: any },
|
||||
--[=[
|
||||
Tags describing the class.
|
||||
|
||||
These include information such as if the class can be replicated
|
||||
to players at runtime, and top-level class categories.
|
||||
]=]
|
||||
Tags: { DatabaseClassTag },
|
||||
}
|
||||
|
||||
export type DatabaseEnum = {
|
||||
--[=[
|
||||
The name of this enum, for example `PartType` or `UserInputState`.
|
||||
]=]
|
||||
Name: string,
|
||||
--[=[
|
||||
Members of this enum.
|
||||
|
||||
Note that this is a direct map of name -> enum values,
|
||||
and does not actually use the EnumItem datatype itself.
|
||||
]=]
|
||||
Items: { [string]: number },
|
||||
}
|
||||
|
||||
export type Database = {
|
||||
--[=[
|
||||
The current version of the reflection database.
|
||||
|
||||
This will follow the format `x.y.z.w`, which most commonly looks something like `0.567.0.123456789`
|
||||
]=]
|
||||
Version: string,
|
||||
--[=[
|
||||
Retrieves a list of all currently known class names.
|
||||
]=]
|
||||
GetClassNames: (self: Database) -> { string },
|
||||
--[=[
|
||||
Retrieves a list of all currently known enum names.
|
||||
]=]
|
||||
GetEnumNames: (self: Database) -> { string },
|
||||
--[=[
|
||||
Gets a class with the exact given name, if one exists.
|
||||
]=]
|
||||
GetClass: (self: Database, name: string) -> DatabaseClass?,
|
||||
--[=[
|
||||
Gets an enum with the exact given name, if one exists.
|
||||
]=]
|
||||
GetEnum: (self: Database, name: string) -> DatabaseEnum?,
|
||||
--[=[
|
||||
Finds a class with the given name.
|
||||
|
||||
This will use case-insensitive matching and ignore leading and trailing whitespace.
|
||||
]=]
|
||||
FindClass: (self: Database, name: string) -> DatabaseClass?,
|
||||
--[=[
|
||||
Finds an enum with the given name.
|
||||
|
||||
This will use case-insensitive matching and ignore leading and trailing whitespace.
|
||||
]=]
|
||||
FindEnum: (self: Database, name: string) -> DatabaseEnum?,
|
||||
}
|
||||
|
||||
type InstanceProperties = {
|
||||
Parent: Instance?,
|
||||
ClassName: string,
|
||||
|
@ -219,6 +351,39 @@ return {
|
|||
getAuthCookie = function(raw: boolean?): string?
|
||||
return nil :: any
|
||||
end,
|
||||
--[=[
|
||||
@within Roblox
|
||||
@must_use
|
||||
|
||||
Gets the bundled reflection database.
|
||||
|
||||
This database contains information about Roblox enums, classes, and their properties.
|
||||
|
||||
### Example usage
|
||||
|
||||
```lua
|
||||
local roblox = require("@lune/roblox")
|
||||
|
||||
local db = roblox.getReflectionDatabase()
|
||||
|
||||
print("There are", #db:GetClassNames(), "classes in the reflection database")
|
||||
|
||||
print("All base instance properties:")
|
||||
|
||||
local class = db:GetClass("Instance")
|
||||
for name, prop in class.Properties do
|
||||
print(string.format(
|
||||
"- %s with datatype %s and default value %s",
|
||||
prop.Name,
|
||||
prop.Datatype,
|
||||
tostring(class.DefaultProperties[prop.Name])
|
||||
))
|
||||
end
|
||||
```
|
||||
]=]
|
||||
getReflectionDatabase = function(): Database
|
||||
return nil :: any
|
||||
end,
|
||||
-- TODO: Make typedefs for all of the datatypes as well...
|
||||
Instance = (nil :: any) :: {
|
||||
new: ((className: "DataModel") -> DataModel) & ((className: string) -> Instance),
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
use mlua::prelude::*;
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
use crate::roblox::{
|
||||
self,
|
||||
document::{Document, DocumentError, DocumentFormat, DocumentKind},
|
||||
instance::Instance,
|
||||
reflection::Database as ReflectionDatabase,
|
||||
};
|
||||
|
||||
use tokio::task;
|
||||
|
||||
use crate::lune::lua::table::TableBuilder;
|
||||
|
||||
static REFLECTION_DATABASE: OnceCell<ReflectionDatabase> = OnceCell::new();
|
||||
|
||||
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
|
||||
let mut roblox_constants = Vec::new();
|
||||
let roblox_module = roblox::module(lua)?;
|
||||
|
@ -21,7 +26,8 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
|
|||
.with_async_function("deserializeModel", deserialize_model)?
|
||||
.with_async_function("serializePlace", serialize_place)?
|
||||
.with_async_function("serializeModel", serialize_model)?
|
||||
.with_async_function("getAuthCookie", get_auth_cookie)?
|
||||
.with_function("getAuthCookie", get_auth_cookie)?
|
||||
.with_function("getReflectionDatabase", get_reflection_database)?
|
||||
.build_readonly()
|
||||
}
|
||||
|
||||
|
@ -85,14 +91,14 @@ async fn serialize_model<'lua>(
|
|||
lua.create_string(bytes)
|
||||
}
|
||||
|
||||
async fn get_auth_cookie(_: &Lua, raw: Option<bool>) -> LuaResult<Option<String>> {
|
||||
task::spawn_blocking(move || {
|
||||
fn get_auth_cookie(_: &Lua, raw: Option<bool>) -> LuaResult<Option<String>> {
|
||||
if matches!(raw, Some(true)) {
|
||||
Ok(rbx_cookie::get_value())
|
||||
} else {
|
||||
Ok(rbx_cookie::get())
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(LuaError::external)?
|
||||
}
|
||||
|
||||
fn get_reflection_database(_: &Lua, _: ()) -> LuaResult<ReflectionDatabase> {
|
||||
Ok(*REFLECTION_DATABASE.get_or_init(ReflectionDatabase::new))
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::roblox::instance::Instance;
|
|||
|
||||
use super::*;
|
||||
|
||||
pub(super) trait DomValueExt {
|
||||
pub(crate) trait DomValueExt {
|
||||
fn variant_name(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ impl DomValueExt for DomType {
|
|||
fn variant_name(&self) -> Option<&'static str> {
|
||||
use DomType::*;
|
||||
Some(match self {
|
||||
Attributes => "Attributes",
|
||||
Axes => "Axes",
|
||||
BinaryString => "BinaryString",
|
||||
Bool => "Bool",
|
||||
|
@ -25,6 +26,7 @@ impl DomValueExt for DomType {
|
|||
Faces => "Faces",
|
||||
Float32 => "Float32",
|
||||
Float64 => "Float64",
|
||||
Font => "Font",
|
||||
Int32 => "Int32",
|
||||
Int64 => "Int64",
|
||||
NumberRange => "NumberRange",
|
||||
|
@ -37,8 +39,10 @@ impl DomValueExt for DomType {
|
|||
Region3int16 => "Region3int16",
|
||||
SharedString => "SharedString",
|
||||
String => "String",
|
||||
Tags => "Tags",
|
||||
UDim => "UDim",
|
||||
UDim2 => "UDim2",
|
||||
UniqueId => "UniqueId",
|
||||
Vector2 => "Vector2",
|
||||
Vector2int16 => "Vector2int16",
|
||||
Vector3 => "Vector3",
|
||||
|
|
|
@ -5,6 +5,7 @@ use crate::roblox::instance::Instance;
|
|||
pub mod datatypes;
|
||||
pub mod document;
|
||||
pub mod instance;
|
||||
pub mod reflection;
|
||||
|
||||
pub(crate) mod shared;
|
||||
|
||||
|
|
148
src/roblox/reflection/class.rs
Normal file
148
src/roblox/reflection/class.rs
Normal file
|
@ -0,0 +1,148 @@
|
|||
use core::fmt;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use rbx_dom_weak::types::Variant as DomVariant;
|
||||
use rbx_reflection::{ClassDescriptor, DataType};
|
||||
|
||||
use super::{property::DatabaseProperty, utils::*};
|
||||
use crate::roblox::datatypes::{
|
||||
conversion::DomValueToLua, types::EnumItem, userdata_impl_eq, userdata_impl_to_string,
|
||||
};
|
||||
|
||||
type DbClass = &'static ClassDescriptor<'static>;
|
||||
|
||||
/**
|
||||
A wrapper for [`rbx_reflection::ClassDescriptor`] that
|
||||
also provides access to the class descriptor from lua.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DatabaseClass(DbClass);
|
||||
|
||||
impl DatabaseClass {
|
||||
pub(crate) fn new(inner: DbClass) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the name of this class.
|
||||
*/
|
||||
pub fn get_name(&self) -> String {
|
||||
self.0.name.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Get the superclass (parent class) of this class.
|
||||
|
||||
May be `None` if no parent class exists.
|
||||
*/
|
||||
pub fn get_superclass(&self) -> Option<String> {
|
||||
let sup = self.0.superclass.as_ref()?;
|
||||
Some(sup.to_string())
|
||||
}
|
||||
|
||||
/**
|
||||
Get all known properties for this class.
|
||||
*/
|
||||
pub fn get_properties(&self) -> HashMap<String, DatabaseProperty> {
|
||||
self.0
|
||||
.properties
|
||||
.iter()
|
||||
.map(|(name, prop)| (name.to_string(), DatabaseProperty::new(self.0, prop)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Get all default values for properties of this class.
|
||||
*/
|
||||
pub fn get_defaults(&self) -> HashMap<String, DomVariant> {
|
||||
self.0
|
||||
.default_properties
|
||||
.iter()
|
||||
.map(|(name, prop)| (name.to_string(), prop.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Get all tags describing the class.
|
||||
|
||||
These include information such as if the class can be replicated
|
||||
to players at runtime, and top-level class categories.
|
||||
*/
|
||||
pub fn get_tags_str(&self) -> Vec<&'static str> {
|
||||
self.0.tags.iter().map(class_tag_to_str).collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for DatabaseClass {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Name", |_, this| Ok(this.get_name()));
|
||||
fields.add_field_method_get("Superclass", |_, this| Ok(this.get_superclass()));
|
||||
fields.add_field_method_get("Properties", |_, this| Ok(this.get_properties()));
|
||||
fields.add_field_method_get("DefaultProperties", |lua, this| {
|
||||
let defaults = this.get_defaults();
|
||||
let mut map = HashMap::with_capacity(defaults.len());
|
||||
for (name, prop) in defaults {
|
||||
let value = if let DomVariant::Enum(enum_value) = prop {
|
||||
make_enum_value(this.0, &name, enum_value.to_u32())
|
||||
.and_then(|e| e.into_lua(lua))
|
||||
} else {
|
||||
LuaValue::dom_value_to_lua(lua, &prop).into_lua_err()
|
||||
};
|
||||
if let Ok(value) = value {
|
||||
map.insert(name, value);
|
||||
}
|
||||
}
|
||||
Ok(map)
|
||||
});
|
||||
fields.add_field_method_get("Tags", |_, this| Ok(this.get_tags_str()));
|
||||
}
|
||||
|
||||
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 PartialEq for DatabaseClass {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.name == other.0.name
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DatabaseClass {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ReflectionDatabaseClass({})", self.0.name)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_enum_name(inner: DbClass, name: impl AsRef<str>) -> Option<String> {
|
||||
inner.properties.iter().find_map(|(prop_name, prop_info)| {
|
||||
if prop_name == name.as_ref() {
|
||||
if let DataType::Enum(enum_name) = &prop_info.data_type {
|
||||
Some(enum_name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn make_enum_value(inner: DbClass, name: impl AsRef<str>, value: u32) -> LuaResult<EnumItem> {
|
||||
let name = name.as_ref();
|
||||
let enum_name = find_enum_name(inner, name).ok_or_else(|| {
|
||||
LuaError::RuntimeError(format!(
|
||||
"Failed to get default property '{}' - No enum descriptor was found",
|
||||
name
|
||||
))
|
||||
})?;
|
||||
EnumItem::from_enum_name_and_value(&enum_name, value).ok_or_else(|| {
|
||||
LuaError::RuntimeError(format!(
|
||||
"Failed to get default property '{}' - Enum.{} does not contain numeric value {}",
|
||||
name, enum_name, value
|
||||
))
|
||||
})
|
||||
}
|
67
src/roblox/reflection/enums.rs
Normal file
67
src/roblox/reflection/enums.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use std::{collections::HashMap, fmt};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use rbx_reflection::EnumDescriptor;
|
||||
|
||||
use crate::roblox::datatypes::{userdata_impl_eq, userdata_impl_to_string};
|
||||
|
||||
type DbEnum = &'static EnumDescriptor<'static>;
|
||||
|
||||
/**
|
||||
A wrapper for [`rbx_reflection::EnumDescriptor`] that
|
||||
also provides access to the class descriptor from lua.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DatabaseEnum(DbEnum);
|
||||
|
||||
impl DatabaseEnum {
|
||||
pub(crate) fn new(inner: DbEnum) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the name of this enum.
|
||||
*/
|
||||
pub fn get_name(&self) -> String {
|
||||
self.0.name.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Get all known members of this enum.
|
||||
|
||||
Note that this is a direct map of name -> enum values,
|
||||
and does not actually use the EnumItem datatype itself.
|
||||
*/
|
||||
pub fn get_items(&self) -> HashMap<String, u32> {
|
||||
self.0
|
||||
.items
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), *v))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for DatabaseEnum {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Name", |_, this| Ok(this.get_name()));
|
||||
fields.add_field_method_get("Items", |_, this| Ok(this.get_items()));
|
||||
}
|
||||
|
||||
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 PartialEq for DatabaseEnum {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.name == other.0.name
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DatabaseEnum {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ReflectionDatabaseEnum({})", self.0.name)
|
||||
}
|
||||
}
|
138
src/roblox/reflection/mod.rs
Normal file
138
src/roblox/reflection/mod.rs
Normal file
|
@ -0,0 +1,138 @@
|
|||
use std::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use rbx_reflection::ReflectionDatabase;
|
||||
|
||||
use crate::roblox::datatypes::userdata_impl_eq;
|
||||
|
||||
mod class;
|
||||
mod enums;
|
||||
mod property;
|
||||
mod utils;
|
||||
|
||||
pub use class::DatabaseClass;
|
||||
pub use enums::DatabaseEnum;
|
||||
pub use property::DatabaseProperty;
|
||||
|
||||
use super::datatypes::userdata_impl_to_string;
|
||||
|
||||
type Db = &'static ReflectionDatabase<'static>;
|
||||
|
||||
/**
|
||||
A wrapper for [`rbx_reflection::ReflectionDatabase`] that
|
||||
also provides access to the reflection database from lua.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Database(Db);
|
||||
|
||||
impl Database {
|
||||
/**
|
||||
Creates a new database struct, referencing the bundled reflection database.
|
||||
*/
|
||||
pub fn new() -> Self {
|
||||
Self(rbx_reflection_database::get())
|
||||
}
|
||||
|
||||
/**
|
||||
Get the version string of the database.
|
||||
|
||||
This will follow the format `x.y.z.w`, which most
|
||||
commonly looks something like `0.567.0.123456789`.
|
||||
*/
|
||||
pub fn get_version(&self) -> String {
|
||||
let [x, y, z, w] = self.0.version;
|
||||
format!("{x}.{y}.{z}.{w}")
|
||||
}
|
||||
|
||||
/**
|
||||
Retrieves a list of all currently known enum names.
|
||||
*/
|
||||
pub fn get_enum_names(&self) -> Vec<String> {
|
||||
self.0.enums.keys().map(|e| e.to_string()).collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Retrieves a list of all currently known class names.
|
||||
*/
|
||||
pub fn get_class_names(&self) -> Vec<String> {
|
||||
self.0.classes.keys().map(|e| e.to_string()).collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Gets an enum with the exact given name, if one exists.
|
||||
*/
|
||||
pub fn get_enum(&self, name: impl AsRef<str>) -> Option<DatabaseEnum> {
|
||||
let e = self.0.enums.get(name.as_ref())?;
|
||||
Some(DatabaseEnum::new(e))
|
||||
}
|
||||
|
||||
/**
|
||||
Gets a class with the exact given name, if one exists.
|
||||
*/
|
||||
pub fn get_class(&self, name: impl AsRef<str>) -> Option<DatabaseClass> {
|
||||
let c = self.0.classes.get(name.as_ref())?;
|
||||
Some(DatabaseClass::new(c))
|
||||
}
|
||||
|
||||
/**
|
||||
Finds an enum with the given name.
|
||||
|
||||
This will use case-insensitive matching and ignore leading and trailing whitespace.
|
||||
*/
|
||||
pub fn find_enum(&self, name: impl AsRef<str>) -> Option<DatabaseEnum> {
|
||||
let name = name.as_ref().trim().to_lowercase();
|
||||
let (ename, _) = self
|
||||
.0
|
||||
.enums
|
||||
.iter()
|
||||
.find(|(ename, _)| ename.trim().to_lowercase() == name)?;
|
||||
self.get_enum(ename)
|
||||
}
|
||||
|
||||
/**
|
||||
Finds a class with the given name.
|
||||
|
||||
This will use case-insensitive matching and ignore leading and trailing whitespace.
|
||||
*/
|
||||
pub fn find_class(&self, name: impl AsRef<str>) -> Option<DatabaseClass> {
|
||||
let name = name.as_ref().trim().to_lowercase();
|
||||
let (cname, _) = self
|
||||
.0
|
||||
.classes
|
||||
.iter()
|
||||
.find(|(cname, _)| cname.trim().to_lowercase() == name)?;
|
||||
self.get_class(cname)
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Database {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Version", |_, this| Ok(this.get_version()))
|
||||
}
|
||||
|
||||
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);
|
||||
methods.add_method("GetEnumNames", |_, this, _: ()| Ok(this.get_enum_names()));
|
||||
methods.add_method("GetClassNames", |_, this, _: ()| Ok(this.get_class_names()));
|
||||
methods.add_method("GetEnum", |_, this, name: String| Ok(this.get_enum(name)));
|
||||
methods.add_method("GetClass", |_, this, name: String| Ok(this.get_class(name)));
|
||||
methods.add_method("FindEnum", |_, this, name: String| Ok(this.find_enum(name)));
|
||||
methods.add_method("FindClass", |_, this, name: String| {
|
||||
Ok(this.find_class(name))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Database {
|
||||
fn eq(&self, _other: &Self) -> bool {
|
||||
true // All database userdatas refer to the same underlying rbx-dom database
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Database {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ReflectionDatabase")
|
||||
}
|
||||
}
|
95
src/roblox/reflection/property.rs
Normal file
95
src/roblox/reflection/property.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
use std::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use rbx_reflection::{ClassDescriptor, PropertyDescriptor};
|
||||
|
||||
use super::utils::*;
|
||||
use crate::roblox::datatypes::{userdata_impl_eq, userdata_impl_to_string};
|
||||
|
||||
type DbClass = &'static ClassDescriptor<'static>;
|
||||
type DbProp = &'static PropertyDescriptor<'static>;
|
||||
|
||||
/**
|
||||
A wrapper for [`rbx_reflection::PropertyDescriptor`] that
|
||||
also provides access to the property descriptor from lua.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DatabaseProperty(DbClass, DbProp);
|
||||
|
||||
impl DatabaseProperty {
|
||||
pub(crate) fn new(inner: DbClass, inner_prop: DbProp) -> Self {
|
||||
Self(inner, inner_prop)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the name of this property.
|
||||
*/
|
||||
pub fn get_name(&self) -> String {
|
||||
self.1.name.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Get the datatype name of the property.
|
||||
|
||||
For normal datatypes this will be a string such as `string`, `Color3`, ...
|
||||
|
||||
For enums this will be a string formatted as `Enum.EnumName`.
|
||||
*/
|
||||
pub fn get_datatype_name(&self) -> String {
|
||||
data_type_to_str(self.1.data_type.clone())
|
||||
}
|
||||
|
||||
/**
|
||||
Get the scriptability of this property, meaning if it can be written / read at runtime.
|
||||
|
||||
All properties are writable and readable in Lune even if scriptability is not.
|
||||
*/
|
||||
pub fn get_scriptability_str(&self) -> &'static str {
|
||||
scriptability_to_str(&self.1.scriptability)
|
||||
}
|
||||
|
||||
/**
|
||||
Get all known tags describing the property.
|
||||
|
||||
These include information such as if the property can be replicated to players
|
||||
at runtime, if the property should be hidden in Roblox Studio, and more.
|
||||
*/
|
||||
pub fn get_tags_str(&self) -> Vec<&'static str> {
|
||||
self.1
|
||||
.tags
|
||||
.iter()
|
||||
.map(property_tag_to_str)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for DatabaseProperty {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Name", |_, this| Ok(this.get_name()));
|
||||
fields.add_field_method_get("Datatype", |_, this| Ok(this.get_datatype_name()));
|
||||
fields.add_field_method_get("Scriptability", |_, this| Ok(this.get_scriptability_str()));
|
||||
fields.add_field_method_get("Tags", |_, this| Ok(this.get_tags_str()));
|
||||
}
|
||||
|
||||
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 PartialEq for DatabaseProperty {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.name == other.0.name && self.1.name == other.1.name
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DatabaseProperty {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"ReflectionDatabaseProperty({} > {})",
|
||||
self.0.name, self.1.name
|
||||
)
|
||||
}
|
||||
}
|
56
src/roblox/reflection/utils.rs
Normal file
56
src/roblox/reflection/utils.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use rbx_reflection::{ClassTag, DataType, PropertyTag, Scriptability};
|
||||
|
||||
use crate::roblox::datatypes::extension::DomValueExt;
|
||||
|
||||
pub fn data_type_to_str(data_type: DataType) -> String {
|
||||
match data_type {
|
||||
DataType::Enum(e) => format!("Enum.{e}"),
|
||||
DataType::Value(v) => v
|
||||
.variant_name()
|
||||
.expect("Encountered unknown data type variant")
|
||||
.to_string(),
|
||||
_ => panic!("Encountered unknown data type"),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
NOTE: Remember to add any new strings here to typedefs too!
|
||||
*/
|
||||
|
||||
pub fn scriptability_to_str(scriptability: &Scriptability) -> &'static str {
|
||||
match scriptability {
|
||||
Scriptability::None => "None",
|
||||
Scriptability::Custom => "Custom",
|
||||
Scriptability::Read => "Read",
|
||||
Scriptability::ReadWrite => "ReadWrite",
|
||||
Scriptability::Write => "Write",
|
||||
_ => panic!("Encountered unknown scriptability"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn property_tag_to_str(tag: &PropertyTag) -> &'static str {
|
||||
match tag {
|
||||
PropertyTag::Deprecated => "Deprecated",
|
||||
PropertyTag::Hidden => "Hidden",
|
||||
PropertyTag::NotBrowsable => "NotBrowsable",
|
||||
PropertyTag::NotReplicated => "NotReplicated",
|
||||
PropertyTag::NotScriptable => "NotScriptable",
|
||||
PropertyTag::ReadOnly => "ReadOnly",
|
||||
PropertyTag::WriteOnly => "WriteOnly",
|
||||
_ => panic!("Encountered unknown property tag"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn class_tag_to_str(tag: &ClassTag) -> &'static str {
|
||||
match tag {
|
||||
ClassTag::Deprecated => "Deprecated",
|
||||
ClassTag::NotBrowsable => "NotBrowsable",
|
||||
ClassTag::NotCreatable => "NotCreatable",
|
||||
ClassTag::NotReplicated => "NotReplicated",
|
||||
ClassTag::PlayerReplicated => "PlayerReplicated",
|
||||
ClassTag::Service => "Service",
|
||||
ClassTag::Settings => "Settings",
|
||||
ClassTag::UserSettings => "UserSettings",
|
||||
_ => panic!("Encountered unknown class tag"),
|
||||
}
|
||||
}
|
|
@ -158,4 +158,9 @@ create_tests! {
|
|||
roblox_instance_methods_is_descendant_of: "roblox/instance/methods/IsDescendantOf",
|
||||
|
||||
roblox_misc_typeof: "roblox/misc/typeof",
|
||||
|
||||
roblox_reflection_class: "roblox/reflection/class",
|
||||
roblox_reflection_database: "roblox/reflection/database",
|
||||
roblox_reflection_enums: "roblox/reflection/enums",
|
||||
roblox_reflection_property: "roblox/reflection/property",
|
||||
}
|
||||
|
|
41
tests/roblox/reflection/class.luau
Normal file
41
tests/roblox/reflection/class.luau
Normal file
|
@ -0,0 +1,41 @@
|
|||
local roblox = require("@lune/roblox")
|
||||
|
||||
local db = roblox.getReflectionDatabase()
|
||||
|
||||
-- Make sure database classes exist + fields / properties are correct types
|
||||
|
||||
for _, className in db:GetClassNames() do
|
||||
local class = db:GetClass(className)
|
||||
assert(class ~= nil, "Missing " .. className .. " class in database")
|
||||
assert(type(class.Name) == "string", "Name property must be a string")
|
||||
assert(
|
||||
class.Superclass == nil or type(class.Superclass) == "string",
|
||||
"Superclass property must be nil or a string"
|
||||
)
|
||||
assert(type(class.Properties) == "table", "Properties property must be a table")
|
||||
assert(type(class.DefaultProperties) == "table", "DefaultProperties property must be a table")
|
||||
assert(type(class.Tags) == "table", "Tags property must be a table")
|
||||
end
|
||||
|
||||
-- Any property present in default properties must also
|
||||
-- be in properties *or* the properties of a superclass
|
||||
|
||||
for _, className in db:GetClassNames() do
|
||||
local class = db:GetClass(className)
|
||||
assert(class ~= nil)
|
||||
for name, value in class.DefaultProperties do
|
||||
local found = false
|
||||
local current = class
|
||||
while current ~= nil do
|
||||
if current.Properties[name] ~= nil then
|
||||
found = true
|
||||
break
|
||||
elseif current.Superclass ~= nil then
|
||||
current = db:GetClass(current.Superclass)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
assert(found, "Missing default property " .. name .. " in properties table")
|
||||
end
|
||||
end
|
55
tests/roblox/reflection/database.luau
Normal file
55
tests/roblox/reflection/database.luau
Normal file
|
@ -0,0 +1,55 @@
|
|||
local roblox = require("@lune/roblox")
|
||||
|
||||
local db = roblox.getReflectionDatabase()
|
||||
local db2 = roblox.getReflectionDatabase()
|
||||
|
||||
-- Subsequent calls to getReflectionDatabase should return the same database
|
||||
assert(db == db2, "Database should always compare as equal to other database")
|
||||
|
||||
-- Database should not be empty
|
||||
assert(#db:GetClassNames() > 0, "Database should not be empty (no class names)")
|
||||
assert(#db:GetEnumNames() > 0, "Database should not be empty (no enum names)")
|
||||
|
||||
-- Make sure our database finds classes correctly
|
||||
|
||||
local class = db:GetClass("Instance")
|
||||
assert(class ~= nil, "Missing Instance class in database")
|
||||
local prop = class.Properties.Parent
|
||||
assert(prop ~= nil, "Missing Parent property on Instance class in database")
|
||||
|
||||
local class2 = db:FindClass(" instance ")
|
||||
assert(class2 ~= nil, "Missing Instance class in database (2)")
|
||||
local prop2 = class2.Properties.Parent
|
||||
assert(prop2 ~= nil, "Missing Parent property on Instance class in database (2)")
|
||||
|
||||
assert(class == class2, "Class userdatas from the database should compare as equal")
|
||||
assert(prop == prop2, "Property userdatas from the database should compare as equal")
|
||||
|
||||
assert(db:GetClass("PVInstance") ~= nil, "Missing PVInstance class in database")
|
||||
assert(db:GetClass("BasePart") ~= nil, "Missing BasePart class in database")
|
||||
assert(db:GetClass("Part") ~= nil, "Missing Part class in database")
|
||||
|
||||
-- Make sure our database finds enums correctly
|
||||
|
||||
local enum = db:GetEnum("PartType")
|
||||
assert(enum ~= nil, "Missing PartType enum in database")
|
||||
|
||||
local enum2 = db:FindEnum(" parttype ")
|
||||
assert(enum2 ~= nil, "Missing PartType enum in database (2)")
|
||||
|
||||
assert(enum == enum2, "Enum userdatas from the database should compare as equal")
|
||||
|
||||
assert(db:GetEnum("UserInputType") ~= nil, "Missing UserInputType enum in database")
|
||||
assert(db:GetEnum("NormalId") ~= nil, "Missing NormalId enum in database")
|
||||
assert(db:GetEnum("Font") ~= nil, "Missing Font enum in database")
|
||||
|
||||
-- All the class and enum names gotten from the database should be accessible
|
||||
|
||||
for _, className in db:GetClassNames() do
|
||||
assert(db:GetClass(className) ~= nil, "Missing " .. className .. " class in database (3)")
|
||||
assert(db:FindClass(className) ~= nil, "Missing " .. className .. " class in database (4)")
|
||||
end
|
||||
for _, enumName in db:GetEnumNames() do
|
||||
assert(db:GetEnum(enumName) ~= nil, "Missing " .. enumName .. " enum in database (3)")
|
||||
assert(db:FindEnum(enumName) ~= nil, "Missing " .. enumName .. " enum in database (4)")
|
||||
end
|
32
tests/roblox/reflection/enums.luau
Normal file
32
tests/roblox/reflection/enums.luau
Normal file
|
@ -0,0 +1,32 @@
|
|||
local roblox = require("@lune/roblox")
|
||||
|
||||
local db = roblox.getReflectionDatabase()
|
||||
|
||||
-- Make sure database enums exist + fields / properties are correct types
|
||||
|
||||
for _, enumName in db:GetEnumNames() do
|
||||
local enum = db:GetEnum(enumName)
|
||||
assert(enum ~= nil, "Missing " .. enumName .. " enum in database")
|
||||
assert(type(enum.Name) == "string", "Name property must be a string")
|
||||
assert(type(enum.Items) == "table", "Items property must be a table")
|
||||
end
|
||||
|
||||
-- Enum items should be a non-empty map of string -> positive integer values
|
||||
|
||||
for _, enumName in db:GetEnumNames() do
|
||||
local enum = db:GetEnum(enumName)
|
||||
assert(enum ~= nil)
|
||||
local empty = true
|
||||
for name, value in enum.Items do
|
||||
assert(
|
||||
type(name) == "string" and #name > 0,
|
||||
"Enum items map must only contain non-empty string keys"
|
||||
)
|
||||
assert(
|
||||
type(value) == "number" and value >= 0 and math.floor(value) == value,
|
||||
"Enum items map must only contain positive integer values"
|
||||
)
|
||||
empty = false
|
||||
end
|
||||
assert(not empty, "Enum items map must not be empty")
|
||||
end
|
17
tests/roblox/reflection/property.luau
Normal file
17
tests/roblox/reflection/property.luau
Normal file
|
@ -0,0 +1,17 @@
|
|||
local roblox = require("@lune/roblox")
|
||||
|
||||
local db = roblox.getReflectionDatabase()
|
||||
|
||||
-- Make sure database class properties exist + their fields / properties are correct types
|
||||
|
||||
for _, className in db:GetClassNames() do
|
||||
local class = db:GetClass(className)
|
||||
assert(class ~= nil)
|
||||
|
||||
for name, prop in class.Properties do
|
||||
assert(type(prop.Name) == "string", "Name property must be a string")
|
||||
assert(type(prop.Datatype) == "string", "Datatype property must be a string")
|
||||
assert(type(prop.Scriptability) == "string", "Scriptability property must be a string")
|
||||
assert(type(prop.Tags) == "table", "Tags property must be a table")
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue