mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Start work on custom instance properties and methods in roblox builtin
This commit is contained in:
parent
02d812f103
commit
9fe3b02d71
6 changed files with 312 additions and 37 deletions
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
roblox::{
|
roblox::{
|
||||||
self,
|
self,
|
||||||
document::{Document, DocumentError, DocumentFormat, DocumentKind},
|
document::{Document, DocumentError, DocumentFormat, DocumentKind},
|
||||||
instance::Instance,
|
instance::{registry::InstanceRegistry, Instance},
|
||||||
reflection::Database as ReflectionDatabase,
|
reflection::Database as ReflectionDatabase,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -31,6 +31,8 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
|
||||||
.with_async_function("serializeModel", serialize_model)?
|
.with_async_function("serializeModel", serialize_model)?
|
||||||
.with_function("getAuthCookie", get_auth_cookie)?
|
.with_function("getAuthCookie", get_auth_cookie)?
|
||||||
.with_function("getReflectionDatabase", get_reflection_database)?
|
.with_function("getReflectionDatabase", get_reflection_database)?
|
||||||
|
.with_function("implementProperty", implement_property)?
|
||||||
|
.with_function("implementMethod", implement_method)?
|
||||||
.build_readonly()
|
.build_readonly()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,3 +107,43 @@ fn get_auth_cookie(_: &Lua, raw: Option<bool>) -> LuaResult<Option<String>> {
|
||||||
fn get_reflection_database(_: &Lua, _: ()) -> LuaResult<ReflectionDatabase> {
|
fn get_reflection_database(_: &Lua, _: ()) -> LuaResult<ReflectionDatabase> {
|
||||||
Ok(*REFLECTION_DATABASE.get_or_init(ReflectionDatabase::new))
|
Ok(*REFLECTION_DATABASE.get_or_init(ReflectionDatabase::new))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn implement_property(
|
||||||
|
lua: &Lua,
|
||||||
|
(class_name, property_name, property_getter, property_setter): (
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
LuaFunction,
|
||||||
|
Option<LuaFunction>,
|
||||||
|
),
|
||||||
|
) -> LuaResult<()> {
|
||||||
|
let property_setter = match property_setter {
|
||||||
|
Some(setter) => setter,
|
||||||
|
None => {
|
||||||
|
let property_name = property_name.clone();
|
||||||
|
lua.create_function(move |_, _: LuaMultiValue| {
|
||||||
|
Err::<(), _>(LuaError::runtime(format!(
|
||||||
|
"Property '{property_name}' is read-only"
|
||||||
|
)))
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// TODO: Wrap getter and setter functions in async compat layers,
|
||||||
|
// the roblox library does not know about the Lune runtime or the
|
||||||
|
// scheduler and users may want to call async functions, some of
|
||||||
|
// which are not obvious that they are async such as print, warn, ...
|
||||||
|
InstanceRegistry::insert_property_getter(lua, &class_name, &property_name, property_getter)
|
||||||
|
.into_lua_err()?;
|
||||||
|
InstanceRegistry::insert_property_setter(lua, &class_name, &property_name, property_setter)
|
||||||
|
.into_lua_err()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn implement_method(
|
||||||
|
lua: &Lua,
|
||||||
|
(class_name, method_name, method): (String, String, LuaFunction),
|
||||||
|
) -> LuaResult<()> {
|
||||||
|
// TODO: Same as above, wrap the provided method in an async compat layer
|
||||||
|
InstanceRegistry::insert_method(lua, &class_name, &method_name, method).into_lua_err()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ use crate::roblox::{
|
||||||
shared::instance::{class_is_a, find_property_info},
|
shared::instance::{class_is_a, find_property_info},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{data_model, Instance};
|
use super::{data_model, registry::InstanceRegistry, Instance};
|
||||||
|
|
||||||
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
||||||
m.add_meta_method(LuaMetaMethod::ToString, |lua, this, ()| {
|
m.add_meta_method(LuaMetaMethod::ToString, |lua, this, ()| {
|
||||||
|
@ -267,6 +267,10 @@ fn instance_property_get<'lua>(
|
||||||
}
|
}
|
||||||
} else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
|
} else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
|
||||||
Ok(LuaValue::UserData(lua.create_userdata(inst)?))
|
Ok(LuaValue::UserData(lua.create_userdata(inst)?))
|
||||||
|
} else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) {
|
||||||
|
getter.call(this.clone())
|
||||||
|
} else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) {
|
||||||
|
Ok(LuaValue::Function(method))
|
||||||
} else {
|
} else {
|
||||||
Err(LuaError::RuntimeError(format!(
|
Err(LuaError::RuntimeError(format!(
|
||||||
"{} is not a valid member of {}",
|
"{} is not a valid member of {}",
|
||||||
|
@ -317,40 +321,39 @@ fn instance_property_set<'lua>(
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let info = match find_property_info(&this.class_name, &prop_name) {
|
if let Some(info) = find_property_info(&this.class_name, &prop_name) {
|
||||||
Some(b) => b,
|
if let Some(enum_name) = info.enum_name {
|
||||||
None => {
|
match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) {
|
||||||
return Err(LuaError::RuntimeError(format!(
|
Ok(given_enum) if given_enum.parent.desc.name == enum_name => {
|
||||||
"{} is not a valid member of {}",
|
this.set_property(prop_name, DomValue::Enum((*given_enum).clone().into()));
|
||||||
prop_name, this
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok(given_enum) => Err(LuaError::RuntimeError(format!(
|
||||||
|
"Failed to set property '{}' - expected Enum.{}, got Enum.{}",
|
||||||
|
prop_name, enum_name, given_enum.parent.desc.name
|
||||||
|
))),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
} else if let Some(dom_type) = info.value_type {
|
||||||
|
match prop_value.lua_to_dom_value(lua, Some(dom_type)) {
|
||||||
|
Ok(dom_value) => {
|
||||||
|
this.set_property(prop_name, dom_value);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(LuaError::RuntimeError(format!(
|
||||||
|
"Failed to set property '{}' - malformed property info",
|
||||||
|
prop_name
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
};
|
} else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) {
|
||||||
|
setter.call((this.clone(), prop_value))
|
||||||
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()));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Ok(given_enum) => Err(LuaError::RuntimeError(format!(
|
|
||||||
"Failed to set property '{}' - expected Enum.{}, got Enum.{}",
|
|
||||||
prop_name, enum_name, given_enum.parent.desc.name
|
|
||||||
))),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
} else if let Some(dom_type) = info.value_type {
|
|
||||||
match prop_value.lua_to_dom_value(lua, Some(dom_type)) {
|
|
||||||
Ok(dom_value) => {
|
|
||||||
this.set_property(prop_name, dom_value);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Err(LuaError::RuntimeError(format!(
|
Err(LuaError::RuntimeError(format!(
|
||||||
"Failed to set property '{}' - malformed property info",
|
"{} is not a valid member of {}",
|
||||||
prop_name
|
prop_name, this
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,8 @@ use super::Instance;
|
||||||
|
|
||||||
pub const CLASS_NAME: &str = "DataModel";
|
pub const CLASS_NAME: &str = "DataModel";
|
||||||
|
|
||||||
pub fn add_fields<'lua, M: LuaUserDataFields<'lua, Instance>>(m: &mut M) {
|
pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) {
|
||||||
add_class_restricted_getter(m, CLASS_NAME, "Workspace", data_model_get_workspace);
|
add_class_restricted_getter(f, CLASS_NAME, "Workspace", data_model_get_workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
||||||
|
|
|
@ -23,6 +23,8 @@ pub(crate) mod data_model;
|
||||||
pub(crate) mod terrain;
|
pub(crate) mod terrain;
|
||||||
pub(crate) mod workspace;
|
pub(crate) mod workspace;
|
||||||
|
|
||||||
|
pub mod registry;
|
||||||
|
|
||||||
const PROPERTY_NAME_ATTRIBUTES: &str = "Attributes";
|
const PROPERTY_NAME_ATTRIBUTES: &str = "Attributes";
|
||||||
const PROPERTY_NAME_TAGS: &str = "Tags";
|
const PROPERTY_NAME_TAGS: &str = "Tags";
|
||||||
|
|
||||||
|
@ -735,6 +737,9 @@ impl LuaExportsTable<'_> for Instance {
|
||||||
and methods we support here - we should only implement methods that
|
and methods we support here - we should only implement methods that
|
||||||
are necessary for modifying the dom and / or having ergonomic access
|
are necessary for modifying the dom and / or having ergonomic access
|
||||||
to the dom, not try to replicate Roblox engine behavior of instances
|
to the dom, not try to replicate Roblox engine behavior of instances
|
||||||
|
|
||||||
|
If a user wants to replicate Roblox engine behavior, they can use the
|
||||||
|
instance registry, and register properties + methods from the lua side
|
||||||
*/
|
*/
|
||||||
impl LuaUserData for Instance {
|
impl LuaUserData for Instance {
|
||||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||||
|
|
225
src/roblox/instance/registry.rs
Normal file
225
src/roblox/instance/registry.rs
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
use std::{
|
||||||
|
borrow::Borrow,
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use mlua::{prelude::*, AppDataRef};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use super::Instance;
|
||||||
|
|
||||||
|
type InstanceRegistryMap = HashMap<String, HashMap<String, LuaRegistryKey>>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum InstanceRegistryError {
|
||||||
|
#[error("class name '{0}' is not valid")]
|
||||||
|
InvalidClassName(String),
|
||||||
|
#[error("class '{class_name}' already registered method '{method_name}'")]
|
||||||
|
MethodAlreadyExists {
|
||||||
|
class_name: String,
|
||||||
|
method_name: String,
|
||||||
|
},
|
||||||
|
#[error("class '{class_name}' already registered property '{property_name}'")]
|
||||||
|
PropertyAlreadyExists {
|
||||||
|
class_name: String,
|
||||||
|
property_name: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct InstanceRegistry {
|
||||||
|
getters: Arc<Mutex<InstanceRegistryMap>>,
|
||||||
|
setters: Arc<Mutex<InstanceRegistryMap>>,
|
||||||
|
methods: Arc<Mutex<InstanceRegistryMap>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstanceRegistry {
|
||||||
|
// NOTE: We lazily create the instance registry instead
|
||||||
|
// of always creating it together with the roblox builtin
|
||||||
|
// since it is less commonly used and it simplifies some app
|
||||||
|
// data borrowing relationship problems we'd otherwise have
|
||||||
|
fn get_or_create(lua: &Lua) -> AppDataRef<'_, Self> {
|
||||||
|
if lua.app_data_ref::<Self>().is_none() {
|
||||||
|
lua.set_app_data(Self {
|
||||||
|
getters: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
setters: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
methods: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
lua.app_data_ref::<Self>()
|
||||||
|
.expect("Missing InstanceRegistry in app data")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_method<'lua>(
|
||||||
|
lua: &'lua Lua,
|
||||||
|
class_name: &str,
|
||||||
|
method_name: &str,
|
||||||
|
method: LuaFunction<'lua>,
|
||||||
|
) -> Result<(), InstanceRegistryError> {
|
||||||
|
let registry = Self::get_or_create(lua);
|
||||||
|
|
||||||
|
let mut methods = registry
|
||||||
|
.methods
|
||||||
|
.lock()
|
||||||
|
.expect("Failed to lock instance registry methods");
|
||||||
|
|
||||||
|
let class_methods = methods.entry(class_name.to_string()).or_default();
|
||||||
|
if class_methods.contains_key(method_name) {
|
||||||
|
return Err(InstanceRegistryError::MethodAlreadyExists {
|
||||||
|
class_name: class_name.to_string(),
|
||||||
|
method_name: method_name.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = lua
|
||||||
|
.create_registry_value(method)
|
||||||
|
.expect("Failed to store method in lua registry");
|
||||||
|
class_methods.insert(method_name.to_string(), key);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_property_getter<'lua>(
|
||||||
|
lua: &'lua Lua,
|
||||||
|
class_name: &str,
|
||||||
|
property_name: &str,
|
||||||
|
property_getter: LuaFunction<'lua>,
|
||||||
|
) -> Result<(), InstanceRegistryError> {
|
||||||
|
let registry = Self::get_or_create(lua);
|
||||||
|
|
||||||
|
let mut getters = registry
|
||||||
|
.getters
|
||||||
|
.lock()
|
||||||
|
.expect("Failed to lock instance registry getters");
|
||||||
|
|
||||||
|
let class_getters = getters.entry(class_name.to_string()).or_default();
|
||||||
|
if class_getters.contains_key(property_name) {
|
||||||
|
return Err(InstanceRegistryError::PropertyAlreadyExists {
|
||||||
|
class_name: class_name.to_string(),
|
||||||
|
property_name: property_name.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = lua
|
||||||
|
.create_registry_value(property_getter)
|
||||||
|
.expect("Failed to store getter in lua registry");
|
||||||
|
class_getters.insert(property_name.to_string(), key);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_property_setter<'lua>(
|
||||||
|
lua: &'lua Lua,
|
||||||
|
class_name: &str,
|
||||||
|
property_name: &str,
|
||||||
|
property_setter: LuaFunction<'lua>,
|
||||||
|
) -> Result<(), InstanceRegistryError> {
|
||||||
|
let registry = Self::get_or_create(lua);
|
||||||
|
|
||||||
|
let mut setters = registry
|
||||||
|
.setters
|
||||||
|
.lock()
|
||||||
|
.expect("Failed to lock instance registry getters");
|
||||||
|
|
||||||
|
let class_setters = setters.entry(class_name.to_string()).or_default();
|
||||||
|
if class_setters.contains_key(property_name) {
|
||||||
|
return Err(InstanceRegistryError::PropertyAlreadyExists {
|
||||||
|
class_name: class_name.to_string(),
|
||||||
|
property_name: property_name.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = lua
|
||||||
|
.create_registry_value(property_setter)
|
||||||
|
.expect("Failed to store getter in lua registry");
|
||||||
|
class_setters.insert(property_name.to_string(), key);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_method<'lua>(
|
||||||
|
lua: &'lua Lua,
|
||||||
|
instance: &Instance,
|
||||||
|
method_name: &str,
|
||||||
|
) -> Option<LuaFunction<'lua>> {
|
||||||
|
let registry = Self::get_or_create(lua);
|
||||||
|
let methods = registry
|
||||||
|
.methods
|
||||||
|
.lock()
|
||||||
|
.expect("Failed to lock instance registry methods");
|
||||||
|
|
||||||
|
class_name_chain(&instance.class_name)
|
||||||
|
.iter()
|
||||||
|
.find_map(|&class_name| {
|
||||||
|
methods
|
||||||
|
.get(class_name)
|
||||||
|
.and_then(|class_methods| class_methods.get(method_name))
|
||||||
|
.map(|key| lua.registry_value::<LuaFunction>(key).unwrap())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_property_getter<'lua>(
|
||||||
|
lua: &'lua Lua,
|
||||||
|
instance: &Instance,
|
||||||
|
property_name: &str,
|
||||||
|
) -> Option<LuaFunction<'lua>> {
|
||||||
|
let registry = Self::get_or_create(lua);
|
||||||
|
let getters = registry
|
||||||
|
.getters
|
||||||
|
.lock()
|
||||||
|
.expect("Failed to lock instance registry getters");
|
||||||
|
|
||||||
|
class_name_chain(&instance.class_name)
|
||||||
|
.iter()
|
||||||
|
.find_map(|&class_name| {
|
||||||
|
getters
|
||||||
|
.get(class_name)
|
||||||
|
.and_then(|class_getters| class_getters.get(property_name))
|
||||||
|
.map(|key| lua.registry_value::<LuaFunction>(key).unwrap())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_property_setter<'lua>(
|
||||||
|
lua: &'lua Lua,
|
||||||
|
instance: &Instance,
|
||||||
|
property_name: &str,
|
||||||
|
) -> Option<LuaFunction<'lua>> {
|
||||||
|
let registry = Self::get_or_create(lua);
|
||||||
|
let setters = registry
|
||||||
|
.setters
|
||||||
|
.lock()
|
||||||
|
.expect("Failed to lock instance registry setters");
|
||||||
|
|
||||||
|
class_name_chain(&instance.class_name)
|
||||||
|
.iter()
|
||||||
|
.find_map(|&class_name| {
|
||||||
|
setters
|
||||||
|
.get(class_name)
|
||||||
|
.and_then(|class_setters| class_setters.get(property_name))
|
||||||
|
.map(|key| lua.registry_value::<LuaFunction>(key).unwrap())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn class_name_chain(class_name: &str) -> Vec<&str> {
|
||||||
|
let db = rbx_reflection_database::get();
|
||||||
|
|
||||||
|
let mut list = vec![class_name];
|
||||||
|
let mut current_name = class_name;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let class_descriptor = db
|
||||||
|
.classes
|
||||||
|
.get(current_name)
|
||||||
|
.expect("Got invalid class name");
|
||||||
|
if let Some(sup) = &class_descriptor.superclass {
|
||||||
|
current_name = sup.borrow();
|
||||||
|
list.push(current_name);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list
|
||||||
|
}
|
|
@ -8,9 +8,9 @@ use super::Instance;
|
||||||
|
|
||||||
pub const CLASS_NAME: &str = "Workspace";
|
pub const CLASS_NAME: &str = "Workspace";
|
||||||
|
|
||||||
pub fn add_fields<'lua, M: LuaUserDataFields<'lua, Instance>>(m: &mut M) {
|
pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) {
|
||||||
add_class_restricted_getter(m, CLASS_NAME, "Terrain", workspace_get_terrain);
|
add_class_restricted_getter(f, CLASS_NAME, "Terrain", workspace_get_terrain);
|
||||||
add_class_restricted_getter(m, CLASS_NAME, "CurrentCamera", workspace_get_camera);
|
add_class_restricted_getter(f, CLASS_NAME, "CurrentCamera", workspace_get_camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue