mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 21:10:36 +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 = {
|
type InstanceProperties = {
|
||||||
Parent: Instance?,
|
Parent: Instance?,
|
||||||
ClassName: string,
|
ClassName: string,
|
||||||
|
@ -219,6 +351,39 @@ return {
|
||||||
getAuthCookie = function(raw: boolean?): string?
|
getAuthCookie = function(raw: boolean?): string?
|
||||||
return nil :: any
|
return nil :: any
|
||||||
end,
|
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...
|
-- TODO: Make typedefs for all of the datatypes as well...
|
||||||
Instance = (nil :: any) :: {
|
Instance = (nil :: any) :: {
|
||||||
new: ((className: "DataModel") -> DataModel) & ((className: string) -> Instance),
|
new: ((className: "DataModel") -> DataModel) & ((className: string) -> Instance),
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
use crate::roblox::{
|
use crate::roblox::{
|
||||||
self,
|
self,
|
||||||
document::{Document, DocumentError, DocumentFormat, DocumentKind},
|
document::{Document, DocumentError, DocumentFormat, DocumentKind},
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
|
reflection::Database as ReflectionDatabase,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
|
|
||||||
use crate::lune::lua::table::TableBuilder;
|
use crate::lune::lua::table::TableBuilder;
|
||||||
|
|
||||||
|
static REFLECTION_DATABASE: OnceCell<ReflectionDatabase> = OnceCell::new();
|
||||||
|
|
||||||
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
|
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
|
||||||
let mut roblox_constants = Vec::new();
|
let mut roblox_constants = Vec::new();
|
||||||
let roblox_module = roblox::module(lua)?;
|
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("deserializeModel", deserialize_model)?
|
||||||
.with_async_function("serializePlace", serialize_place)?
|
.with_async_function("serializePlace", serialize_place)?
|
||||||
.with_async_function("serializeModel", serialize_model)?
|
.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()
|
.build_readonly()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,14 +91,14 @@ async fn serialize_model<'lua>(
|
||||||
lua.create_string(bytes)
|
lua.create_string(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_auth_cookie(_: &Lua, raw: Option<bool>) -> LuaResult<Option<String>> {
|
fn get_auth_cookie(_: &Lua, raw: Option<bool>) -> LuaResult<Option<String>> {
|
||||||
task::spawn_blocking(move || {
|
if matches!(raw, Some(true)) {
|
||||||
if matches!(raw, Some(true)) {
|
Ok(rbx_cookie::get_value())
|
||||||
Ok(rbx_cookie::get_value())
|
} else {
|
||||||
} else {
|
Ok(rbx_cookie::get())
|
||||||
Ok(rbx_cookie::get())
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.await
|
fn get_reflection_database(_: &Lua, _: ()) -> LuaResult<ReflectionDatabase> {
|
||||||
.map_err(LuaError::external)?
|
Ok(*REFLECTION_DATABASE.get_or_init(ReflectionDatabase::new))
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::roblox::instance::Instance;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub(super) trait DomValueExt {
|
pub(crate) trait DomValueExt {
|
||||||
fn variant_name(&self) -> Option<&'static str>;
|
fn variant_name(&self) -> Option<&'static str>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ impl DomValueExt for DomType {
|
||||||
fn variant_name(&self) -> Option<&'static str> {
|
fn variant_name(&self) -> Option<&'static str> {
|
||||||
use DomType::*;
|
use DomType::*;
|
||||||
Some(match self {
|
Some(match self {
|
||||||
|
Attributes => "Attributes",
|
||||||
Axes => "Axes",
|
Axes => "Axes",
|
||||||
BinaryString => "BinaryString",
|
BinaryString => "BinaryString",
|
||||||
Bool => "Bool",
|
Bool => "Bool",
|
||||||
|
@ -25,6 +26,7 @@ impl DomValueExt for DomType {
|
||||||
Faces => "Faces",
|
Faces => "Faces",
|
||||||
Float32 => "Float32",
|
Float32 => "Float32",
|
||||||
Float64 => "Float64",
|
Float64 => "Float64",
|
||||||
|
Font => "Font",
|
||||||
Int32 => "Int32",
|
Int32 => "Int32",
|
||||||
Int64 => "Int64",
|
Int64 => "Int64",
|
||||||
NumberRange => "NumberRange",
|
NumberRange => "NumberRange",
|
||||||
|
@ -37,8 +39,10 @@ impl DomValueExt for DomType {
|
||||||
Region3int16 => "Region3int16",
|
Region3int16 => "Region3int16",
|
||||||
SharedString => "SharedString",
|
SharedString => "SharedString",
|
||||||
String => "String",
|
String => "String",
|
||||||
|
Tags => "Tags",
|
||||||
UDim => "UDim",
|
UDim => "UDim",
|
||||||
UDim2 => "UDim2",
|
UDim2 => "UDim2",
|
||||||
|
UniqueId => "UniqueId",
|
||||||
Vector2 => "Vector2",
|
Vector2 => "Vector2",
|
||||||
Vector2int16 => "Vector2int16",
|
Vector2int16 => "Vector2int16",
|
||||||
Vector3 => "Vector3",
|
Vector3 => "Vector3",
|
||||||
|
|
|
@ -5,6 +5,7 @@ use crate::roblox::instance::Instance;
|
||||||
pub mod datatypes;
|
pub mod datatypes;
|
||||||
pub mod document;
|
pub mod document;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
|
pub mod reflection;
|
||||||
|
|
||||||
pub(crate) mod shared;
|
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_instance_methods_is_descendant_of: "roblox/instance/methods/IsDescendantOf",
|
||||||
|
|
||||||
roblox_misc_typeof: "roblox/misc/typeof",
|
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