diff --git a/Cargo.lock b/Cargo.lock index 057b728..c483d2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1541,7 +1541,18 @@ dependencies = [ name = "lune-roblox" version = "0.1.0" dependencies = [ + "glam", + "lune-utils", "mlua", + "once_cell", + "rand", + "rbx_binary", + "rbx_cookie", + "rbx_dom_weak", + "rbx_reflection", + "rbx_reflection_database", + "rbx_xml", + "thiserror", ] [[package]] diff --git a/crates/lune-roblox/Cargo.toml b/crates/lune-roblox/Cargo.toml index 9a3c668..2f0ccdd 100644 --- a/crates/lune-roblox/Cargo.toml +++ b/crates/lune-roblox/Cargo.toml @@ -12,3 +12,18 @@ workspace = true [dependencies] mlua = { version = "0.9.7", features = ["luau"] } + +glam = "0.27" +rand = "0.8" +thiserror = "1.0" +once_cell = "1.17" + +rbx_cookie = { version = "0.1.4", default-features = false } + +rbx_binary = "0.7.3" +rbx_dom_weak = "2.6.0" +rbx_reflection = "4.4.0" +rbx_reflection_database = "0.2.9" +rbx_xml = "0.13.2" + +lune-utils = { version = "0.1.0", path = "../lune-utils" } diff --git a/crates/lune-roblox/src/datatypes/attributes.rs b/crates/lune-roblox/src/datatypes/attributes.rs new file mode 100644 index 0000000..d827801 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/attributes.rs @@ -0,0 +1,56 @@ +use mlua::prelude::*; + +use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType}; + +use super::extension::DomValueExt; + +pub fn ensure_valid_attribute_name(name: impl AsRef) -> LuaResult<()> { + let name = name.as_ref(); + if name.to_ascii_uppercase().starts_with("RBX") { + Err(LuaError::RuntimeError( + "Attribute names must not start with the prefix \"RBX\"".to_string(), + )) + } else if !name.chars().all(|c| c == '_' || c.is_alphanumeric()) { + Err(LuaError::RuntimeError( + "Attribute names must only use alphanumeric characters and underscore".to_string(), + )) + } else if name.len() > 100 { + Err(LuaError::RuntimeError( + "Attribute names must be 100 characters or less in length".to_string(), + )) + } else { + Ok(()) + } +} + +pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> { + let is_valid = matches!( + value.ty(), + DomType::Bool + | DomType::BrickColor + | DomType::CFrame + | DomType::Color3 + | DomType::ColorSequence + | DomType::Float32 + | DomType::Float64 + | DomType::Font + | DomType::Int32 + | DomType::Int64 + | DomType::NumberRange + | DomType::NumberSequence + | DomType::Rect + | DomType::String + | DomType::UDim + | DomType::UDim2 + | DomType::Vector2 + | DomType::Vector3 + ); + if is_valid { + Ok(()) + } else { + Err(LuaError::RuntimeError(format!( + "'{}' is not a valid attribute type", + value.ty().variant_name().unwrap_or("???") + ))) + } +} diff --git a/crates/lune-roblox/src/datatypes/conversion.rs b/crates/lune-roblox/src/datatypes/conversion.rs new file mode 100644 index 0000000..4f254ae --- /dev/null +++ b/crates/lune-roblox/src/datatypes/conversion.rs @@ -0,0 +1,340 @@ +use mlua::prelude::*; + +use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType}; + +use crate::{datatypes::extension::DomValueExt, instance::Instance}; + +use super::*; + +pub(crate) trait LuaToDomValue<'lua> { + /** + Converts a lua value into a weak dom value. + + If a `variant_type` is given the conversion will be more strict + and also more accurate, it should be given whenever possible. + */ + fn lua_to_dom_value( + &self, + lua: &'lua Lua, + variant_type: Option, + ) -> DomConversionResult; +} + +pub(crate) trait DomValueToLua<'lua>: Sized { + /** + Converts a weak dom value into a lua value. + */ + fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult; +} + +/* + + Blanket trait implementations for converting between LuaValue and rbx_dom Variant values + + These should be considered stable and done, already containing all of the known primitives + + See bottom of module for implementations between our custom datatypes and lua userdata + +*/ + +impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> { + fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult { + use rbx_dom_weak::types as dom; + + match LuaAnyUserData::dom_value_to_lua(lua, variant) { + Ok(value) => Ok(LuaValue::UserData(value)), + Err(e) => match variant { + DomValue::Bool(b) => Ok(LuaValue::Boolean(*b)), + DomValue::Int64(i) => Ok(LuaValue::Number(*i as f64)), + DomValue::Int32(i) => Ok(LuaValue::Number(*i as f64)), + DomValue::Float64(n) => Ok(LuaValue::Number(*n)), + DomValue::Float32(n) => Ok(LuaValue::Number(*n as f64)), + DomValue::String(s) => Ok(LuaValue::String(lua.create_string(s)?)), + DomValue::BinaryString(s) => Ok(LuaValue::String(lua.create_string(s)?)), + DomValue::Content(s) => Ok(LuaValue::String( + lua.create_string(AsRef::::as_ref(s))?, + )), + + // NOTE: Dom references may point to instances that + // no longer exist, so we handle that here instead of + // in the userdata conversion to be able to return nils + DomValue::Ref(value) => match Instance::new_opt(*value) { + Some(inst) => Ok(inst.into_lua(lua)?), + None => Ok(LuaValue::Nil), + }, + + // NOTE: Some values are either optional or default and we should handle + // that properly here since the userdata conversion above will always fail + DomValue::OptionalCFrame(None) => Ok(LuaValue::Nil), + DomValue::PhysicalProperties(dom::PhysicalProperties::Default) => Ok(LuaValue::Nil), + + _ => Err(e), + }, + } + } +} + +impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> { + fn lua_to_dom_value( + &self, + lua: &'lua Lua, + variant_type: Option, + ) -> DomConversionResult { + use rbx_dom_weak::types as dom; + + if let Some(variant_type) = variant_type { + match (self, variant_type) { + (LuaValue::Boolean(b), DomType::Bool) => Ok(DomValue::Bool(*b)), + + (LuaValue::Integer(i), DomType::Int64) => Ok(DomValue::Int64(*i as i64)), + (LuaValue::Integer(i), DomType::Int32) => Ok(DomValue::Int32(*i)), + (LuaValue::Integer(i), DomType::Float64) => Ok(DomValue::Float64(*i as f64)), + (LuaValue::Integer(i), DomType::Float32) => Ok(DomValue::Float32(*i as f32)), + + (LuaValue::Number(n), DomType::Int64) => Ok(DomValue::Int64(*n as i64)), + (LuaValue::Number(n), DomType::Int32) => Ok(DomValue::Int32(*n as i32)), + (LuaValue::Number(n), DomType::Float64) => Ok(DomValue::Float64(*n)), + (LuaValue::Number(n), DomType::Float32) => Ok(DomValue::Float32(*n as f32)), + + (LuaValue::String(s), DomType::String) => { + Ok(DomValue::String(s.to_str()?.to_string())) + } + (LuaValue::String(s), DomType::BinaryString) => { + Ok(DomValue::BinaryString(s.as_ref().into())) + } + (LuaValue::String(s), DomType::Content) => { + Ok(DomValue::Content(s.to_str()?.to_string().into())) + } + + // NOTE: Some values are either optional or default and we + // should handle that here before trying to convert as userdata + (LuaValue::Nil, DomType::OptionalCFrame) => Ok(DomValue::OptionalCFrame(None)), + (LuaValue::Nil, DomType::PhysicalProperties) => Ok(DomValue::PhysicalProperties( + dom::PhysicalProperties::Default, + )), + + (LuaValue::UserData(u), d) => u.lua_to_dom_value(lua, Some(d)), + + (v, d) => Err(DomConversionError::ToDomValue { + to: d.variant_name().unwrap_or("???"), + from: v.type_name(), + detail: None, + }), + } + } else { + match self { + LuaValue::Boolean(b) => Ok(DomValue::Bool(*b)), + LuaValue::Integer(i) => Ok(DomValue::Int32(*i)), + LuaValue::Number(n) => Ok(DomValue::Float64(*n)), + LuaValue::String(s) => Ok(DomValue::String(s.to_str()?.to_string())), + LuaValue::UserData(u) => u.lua_to_dom_value(lua, None), + v => Err(DomConversionError::ToDomValue { + to: "unknown", + from: v.type_name(), + detail: None, + }), + } + } + } +} + +/* + + Trait implementations for converting between all of + our custom datatypes and generic Lua userdata values + + NOTE: When adding a new datatype, make sure to add it below to _both_ + of the traits and not just one to allow for bidirectional conversion + +*/ + +macro_rules! dom_to_userdata { + ($lua:expr, $value:ident => $to_type:ty) => { + Ok($lua.create_userdata(Into::<$to_type>::into($value.clone()))?) + }; +} + +/** + Converts a generic lua userdata to an rbx-dom type. + + Since the type of the userdata needs to be specified + in an explicit manner, this macro syntax was chosen: + + ```rs + userdata_to_dom!(value_identifier as UserdataType => DomType) + ``` +*/ +macro_rules! userdata_to_dom { + ($userdata:ident as $from_type:ty => $to_type:ty) => { + match $userdata.borrow::<$from_type>() { + Ok(value) => Ok(From::<$to_type>::from(value.clone().into())), + Err(error) => match error { + LuaError::UserDataTypeMismatch => Err(DomConversionError::ToDomValue { + to: stringify!($to_type), + from: "userdata", + detail: Some("Type mismatch".to_string()), + }), + e => Err(DomConversionError::ToDomValue { + to: stringify!($to_type), + from: "userdata", + detail: Some(format!("Internal error: {e}")), + }), + }, + } + }; +} + +impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> { + #[rustfmt::skip] + fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult { + use super::types::*; + + use rbx_dom_weak::types as dom; + + match variant { + DomValue::Axes(value) => dom_to_userdata!(lua, value => Axes), + DomValue::BrickColor(value) => dom_to_userdata!(lua, value => BrickColor), + DomValue::CFrame(value) => dom_to_userdata!(lua, value => CFrame), + DomValue::Color3(value) => dom_to_userdata!(lua, value => Color3), + DomValue::Color3uint8(value) => dom_to_userdata!(lua, value => Color3), + DomValue::ColorSequence(value) => dom_to_userdata!(lua, value => ColorSequence), + DomValue::Faces(value) => dom_to_userdata!(lua, value => Faces), + DomValue::Font(value) => dom_to_userdata!(lua, value => Font), + DomValue::NumberRange(value) => dom_to_userdata!(lua, value => NumberRange), + DomValue::NumberSequence(value) => dom_to_userdata!(lua, value => NumberSequence), + DomValue::Ray(value) => dom_to_userdata!(lua, value => Ray), + DomValue::Rect(value) => dom_to_userdata!(lua, value => Rect), + DomValue::Region3(value) => dom_to_userdata!(lua, value => Region3), + DomValue::Region3int16(value) => dom_to_userdata!(lua, value => Region3int16), + DomValue::UDim(value) => dom_to_userdata!(lua, value => UDim), + DomValue::UDim2(value) => dom_to_userdata!(lua, value => UDim2), + DomValue::Vector2(value) => dom_to_userdata!(lua, value => Vector2), + DomValue::Vector2int16(value) => dom_to_userdata!(lua, value => Vector2int16), + DomValue::Vector3(value) => dom_to_userdata!(lua, value => Vector3), + DomValue::Vector3int16(value) => dom_to_userdata!(lua, value => Vector3int16), + + // NOTE: The none and default variants of these types are handled in + // DomValueToLua for the LuaValue type instead, allowing for nil/default + DomValue::OptionalCFrame(Some(value)) => dom_to_userdata!(lua, value => CFrame), + DomValue::PhysicalProperties(dom::PhysicalProperties::Custom(value)) => { + dom_to_userdata!(lua, value => PhysicalProperties) + }, + + v => { + Err(DomConversionError::FromDomValue { + from: v.variant_name().unwrap_or("???"), + to: "userdata", + detail: Some("Type not supported".to_string()), + }) + } + } + } +} + +impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> { + #[rustfmt::skip] + fn lua_to_dom_value( + &self, + _: &'lua Lua, + variant_type: Option, + ) -> DomConversionResult { + use super::types::*; + + use rbx_dom_weak::types as dom; + + if let Some(variant_type) = variant_type { + /* + Strict target type, use it to skip checking the actual + type of the userdata and try to just do a pure conversion + */ + match variant_type { + DomType::Axes => userdata_to_dom!(self as Axes => dom::Axes), + DomType::BrickColor => userdata_to_dom!(self as BrickColor => dom::BrickColor), + DomType::CFrame => userdata_to_dom!(self as CFrame => dom::CFrame), + DomType::Color3 => userdata_to_dom!(self as Color3 => dom::Color3), + DomType::Color3uint8 => userdata_to_dom!(self as Color3 => dom::Color3uint8), + DomType::ColorSequence => userdata_to_dom!(self as ColorSequence => dom::ColorSequence), + DomType::Enum => userdata_to_dom!(self as EnumItem => dom::Enum), + DomType::Faces => userdata_to_dom!(self as Faces => dom::Faces), + DomType::Font => userdata_to_dom!(self as Font => dom::Font), + DomType::NumberRange => userdata_to_dom!(self as NumberRange => dom::NumberRange), + DomType::NumberSequence => userdata_to_dom!(self as NumberSequence => dom::NumberSequence), + DomType::Ray => userdata_to_dom!(self as Ray => dom::Ray), + DomType::Rect => userdata_to_dom!(self as Rect => dom::Rect), + DomType::Ref => userdata_to_dom!(self as Instance => dom::Ref), + DomType::Region3 => userdata_to_dom!(self as Region3 => dom::Region3), + DomType::Region3int16 => userdata_to_dom!(self as Region3int16 => dom::Region3int16), + DomType::UDim => userdata_to_dom!(self as UDim => dom::UDim), + DomType::UDim2 => userdata_to_dom!(self as UDim2 => dom::UDim2), + DomType::Vector2 => userdata_to_dom!(self as Vector2 => dom::Vector2), + DomType::Vector2int16 => userdata_to_dom!(self as Vector2int16 => dom::Vector2int16), + DomType::Vector3 => userdata_to_dom!(self as Vector3 => dom::Vector3), + DomType::Vector3int16 => userdata_to_dom!(self as Vector3int16 => dom::Vector3int16), + + // NOTE: The none and default variants of these types are handled in + // LuaToDomValue for the LuaValue type instead, allowing for nil/default + DomType::OptionalCFrame => { + return match self.borrow::() { + Err(_) => unreachable!("Invalid use of conversion method, should be using LuaValue"), + Ok(value) => Ok(DomValue::OptionalCFrame(Some(dom::CFrame::from(*value)))), + } + } + DomType::PhysicalProperties => { + return match self.borrow::() { + Err(_) => unreachable!("Invalid use of conversion method, should be using LuaValue"), + Ok(value) => { + let props = dom::CustomPhysicalProperties::from(*value); + let custom = dom::PhysicalProperties::Custom(props); + Ok(DomValue::PhysicalProperties(custom)) + } + } + } + + ty => { + Err(DomConversionError::ToDomValue { + to: ty.variant_name().unwrap_or("???"), + from: "userdata", + detail: Some("Type not supported".to_string()), + }) + } + } + } else { + /* + Non-strict target type, here we need to do manual typechecks + on the userdata to see what we should be converting it into + + This is used for example for attributes, where the wanted + type is not known by the dom and instead determined by the user + */ + match self { + value if value.is::() => userdata_to_dom!(value as Axes => dom::Axes), + value if value.is::() => userdata_to_dom!(value as BrickColor => dom::BrickColor), + value if value.is::() => userdata_to_dom!(value as CFrame => dom::CFrame), + value if value.is::() => userdata_to_dom!(value as Color3 => dom::Color3), + value if value.is::() => userdata_to_dom!(value as ColorSequence => dom::ColorSequence), + value if value.is::() => userdata_to_dom!(value as EnumItem => dom::Enum), + value if value.is::() => userdata_to_dom!(value as Faces => dom::Faces), + value if value.is::() => userdata_to_dom!(value as Font => dom::Font), + value if value.is::() => userdata_to_dom!(value as Instance => dom::Ref), + value if value.is::() => userdata_to_dom!(value as NumberRange => dom::NumberRange), + value if value.is::() => userdata_to_dom!(value as NumberSequence => dom::NumberSequence), + value if value.is::() => userdata_to_dom!(value as Ray => dom::Ray), + value if value.is::() => userdata_to_dom!(value as Rect => dom::Rect), + value if value.is::() => userdata_to_dom!(value as Region3 => dom::Region3), + value if value.is::() => userdata_to_dom!(value as Region3int16 => dom::Region3int16), + value if value.is::() => userdata_to_dom!(value as UDim => dom::UDim), + value if value.is::() => userdata_to_dom!(value as UDim2 => dom::UDim2), + value if value.is::() => userdata_to_dom!(value as Vector2 => dom::Vector2), + value if value.is::() => userdata_to_dom!(value as Vector2int16 => dom::Vector2int16), + value if value.is::() => userdata_to_dom!(value as Vector3 => dom::Vector3), + value if value.is::() => userdata_to_dom!(value as Vector3int16 => dom::Vector3int16), + + _ => Err(DomConversionError::ToDomValue { + to: "unknown", + from: "userdata", + detail: Some("Type not supported".to_string()), + }) + } + } + } +} diff --git a/crates/lune-roblox/src/datatypes/extension.rs b/crates/lune-roblox/src/datatypes/extension.rs new file mode 100644 index 0000000..30df47c --- /dev/null +++ b/crates/lune-roblox/src/datatypes/extension.rs @@ -0,0 +1,58 @@ +use super::*; + +pub(crate) trait DomValueExt { + fn variant_name(&self) -> Option<&'static str>; +} + +impl DomValueExt for DomType { + fn variant_name(&self) -> Option<&'static str> { + use DomType::*; + Some(match self { + Attributes => "Attributes", + Axes => "Axes", + BinaryString => "BinaryString", + Bool => "Bool", + BrickColor => "BrickColor", + CFrame => "CFrame", + Color3 => "Color3", + Color3uint8 => "Color3uint8", + ColorSequence => "ColorSequence", + Content => "Content", + Enum => "Enum", + Faces => "Faces", + Float32 => "Float32", + Float64 => "Float64", + Font => "Font", + Int32 => "Int32", + Int64 => "Int64", + MaterialColors => "MaterialColors", + NumberRange => "NumberRange", + NumberSequence => "NumberSequence", + PhysicalProperties => "PhysicalProperties", + Ray => "Ray", + Rect => "Rect", + Ref => "Ref", + Region3 => "Region3", + Region3int16 => "Region3int16", + SharedString => "SharedString", + String => "String", + Tags => "Tags", + UDim => "UDim", + UDim2 => "UDim2", + UniqueId => "UniqueId", + Vector2 => "Vector2", + Vector2int16 => "Vector2int16", + Vector3 => "Vector3", + Vector3int16 => "Vector3int16", + OptionalCFrame => "OptionalCFrame", + SecurityCapabilities => "SecurityCapabilities", + _ => return None, + }) + } +} + +impl DomValueExt for DomValue { + fn variant_name(&self) -> Option<&'static str> { + self.ty().variant_name() + } +} diff --git a/crates/lune-roblox/src/datatypes/mod.rs b/crates/lune-roblox/src/datatypes/mod.rs new file mode 100644 index 0000000..4da4715 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/mod.rs @@ -0,0 +1,13 @@ +pub(crate) use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType}; + +pub mod attributes; +pub mod conversion; +pub mod extension; +pub mod result; +pub mod types; + +mod util; + +use result::*; + +pub use crate::shared::userdata::*; diff --git a/crates/lune-roblox/src/datatypes/result.rs b/crates/lune-roblox/src/datatypes/result.rs new file mode 100644 index 0000000..3ceb41e --- /dev/null +++ b/crates/lune-roblox/src/datatypes/result.rs @@ -0,0 +1,75 @@ +use core::fmt; + +use std::error::Error; +use std::io::Error as IoError; + +use mlua::Error as LuaError; + +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub(crate) enum DomConversionError { + LuaError(LuaError), + External { + message: String, + }, + FromDomValue { + from: &'static str, + to: &'static str, + detail: Option, + }, + ToDomValue { + to: &'static str, + from: &'static str, + detail: Option, + }, +} + +impl fmt::Display for DomConversionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::LuaError(error) => error.to_string(), + Self::External { message } => message.to_string(), + Self::FromDomValue { from, to, detail } | Self::ToDomValue { from, to, detail } => { + match detail { + Some(d) => format!("Failed to convert from '{from}' into '{to}' - {d}"), + None => format!("Failed to convert from '{from}' into '{to}'",), + } + } + } + ) + } +} + +impl Error for DomConversionError {} + +impl From for LuaError { + fn from(value: DomConversionError) -> Self { + use DomConversionError as E; + match value { + E::LuaError(e) => e, + E::External { message } => LuaError::external(message), + E::FromDomValue { .. } | E::ToDomValue { .. } => { + LuaError::RuntimeError(value.to_string()) + } + } + } +} + +impl From for DomConversionError { + fn from(value: LuaError) -> Self { + Self::LuaError(value) + } +} + +impl From for DomConversionError { + fn from(value: IoError) -> Self { + DomConversionError::External { + message: value.to_string(), + } + } +} + +pub(crate) type DomConversionResult = Result; diff --git a/crates/lune-roblox/src/datatypes/types/axes.rs b/crates/lune-roblox/src/datatypes/types/axes.rs new file mode 100644 index 0000000..670b1f0 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/axes.rs @@ -0,0 +1,127 @@ +use core::fmt; + +use mlua::prelude::*; +use rbx_dom_weak::types::Axes as DomAxes; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, EnumItem}; + +/** + An implementation of the [Axes](https://create.roblox.com/docs/reference/engine/datatypes/Axes) Roblox datatype. + + This implements all documented properties, methods & constructors of the Axes class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Axes { + pub(crate) x: bool, + pub(crate) y: bool, + pub(crate) z: bool, +} + +impl LuaExportsTable<'_> for Axes { + const EXPORT_NAME: &'static str = "Axes"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let axes_new = |_, args: LuaMultiValue| { + let mut x = false; + let mut y = false; + let mut z = false; + + let mut check = |e: &EnumItem| { + if e.parent.desc.name == "Axis" { + match &e.name { + name if name == "X" => x = true, + name if name == "Y" => y = true, + name if name == "Z" => z = true, + _ => {} + } + } else if e.parent.desc.name == "NormalId" { + match &e.name { + name if name == "Left" || name == "Right" => x = true, + name if name == "Top" || name == "Bottom" => y = true, + name if name == "Front" || name == "Back" => z = true, + _ => {} + } + } + }; + + for (index, arg) in args.into_iter().enumerate() { + if let LuaValue::UserData(u) = arg { + if let Ok(e) = u.borrow::() { + check(&e); + } else { + return Err(LuaError::RuntimeError(format!( + "Expected argument #{} to be an EnumItem, got userdata", + index + ))); + } + } else { + return Err(LuaError::RuntimeError(format!( + "Expected argument #{} to be an EnumItem, got {}", + index, + arg.type_name() + ))); + } + } + + Ok(Axes { x, y, z }) + }; + + TableBuilder::new(lua)? + .with_function("new", axes_new)? + .build_readonly() + } +} + +impl LuaUserData for Axes { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("X", |_, this| Ok(this.x)); + fields.add_field_method_get("Y", |_, this| Ok(this.y)); + fields.add_field_method_get("Z", |_, this| Ok(this.z)); + fields.add_field_method_get("Left", |_, this| Ok(this.x)); + fields.add_field_method_get("Right", |_, this| Ok(this.x)); + fields.add_field_method_get("Top", |_, this| Ok(this.y)); + fields.add_field_method_get("Bottom", |_, this| Ok(this.y)); + fields.add_field_method_get("Front", |_, this| Ok(this.z)); + fields.add_field_method_get("Back", |_, this| Ok(this.z)); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for Axes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let write = make_list_writer(); + write(f, self.x, "X")?; + write(f, self.y, "Y")?; + write(f, self.z, "Z")?; + Ok(()) + } +} + +impl From for Axes { + fn from(v: DomAxes) -> Self { + let bits = v.bits(); + Self { + x: (bits & 1) == 1, + y: ((bits >> 1) & 1) == 1, + z: ((bits >> 2) & 1) == 1, + } + } +} + +impl From for DomAxes { + fn from(v: Axes) -> Self { + let mut bits = 0; + bits += v.x as u8; + bits += (v.y as u8) << 1; + bits += (v.z as u8) << 2; + DomAxes::from_bits(bits).expect("Invalid bits") + } +} diff --git a/crates/lune-roblox/src/datatypes/types/brick_color.rs b/crates/lune-roblox/src/datatypes/types/brick_color.rs new file mode 100644 index 0000000..307402e --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/brick_color.rs @@ -0,0 +1,441 @@ +use core::fmt; + +use mlua::prelude::*; +use rand::seq::SliceRandom; +use rbx_dom_weak::types::BrickColor as DomBrickColor; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, Color3}; + +/** + An implementation of the [BrickColor](https://create.roblox.com/docs/reference/engine/datatypes/BrickColor) Roblox datatype. + + This implements all documented properties, methods & constructors of the BrickColor class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct BrickColor { + // Unfortunately we can't use DomBrickColor as the backing type here + // because it does not expose any way of getting the actual rgb colors :-( + pub(crate) number: u16, + pub(crate) name: &'static str, + pub(crate) rgb: (u8, u8, u8), +} + +impl LuaExportsTable<'_> for BrickColor { + const EXPORT_NAME: &'static str = "BrickColor"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + type ArgsNumber = u16; + type ArgsName = String; + type ArgsRgb = (u8, u8, u8); + type ArgsColor3<'lua> = LuaUserDataRef<'lua, Color3>; + + let brick_color_new = |lua, args: LuaMultiValue| { + if let Ok(number) = ArgsNumber::from_lua_multi(args.clone(), lua) { + Ok(color_from_number(number)) + } else if let Ok(name) = ArgsName::from_lua_multi(args.clone(), lua) { + Ok(color_from_name(name)) + } else if let Ok((r, g, b)) = ArgsRgb::from_lua_multi(args.clone(), lua) { + Ok(color_from_rgb(r, g, b)) + } else if let Ok(color) = ArgsColor3::from_lua_multi(args.clone(), lua) { + Ok(Self::from(*color)) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + let brick_color_palette = |_, index: u16| { + if index == 0 { + Err(LuaError::RuntimeError("Invalid index".to_string())) + } else if let Some(number) = BRICK_COLOR_PALETTE.get((index - 1) as usize) { + Ok(color_from_number(*number)) + } else { + Err(LuaError::RuntimeError("Invalid index".to_string())) + } + }; + + let brick_color_random = |_, ()| { + let number = BRICK_COLOR_PALETTE.choose(&mut rand::thread_rng()); + Ok(color_from_number(*number.unwrap())) + }; + + let mut builder = TableBuilder::new(lua)? + .with_function("new", brick_color_new)? + .with_function("palette", brick_color_palette)? + .with_function("random", brick_color_random)?; + + for (name, number) in BRICK_COLOR_CONSTRUCTORS { + let f = |_, ()| Ok(color_from_number(*number)); + builder = builder.with_function(*name, f)?; + } + + builder.build_readonly() + } +} + +impl LuaUserData for BrickColor { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Number", |_, this| Ok(this.number)); + fields.add_field_method_get("Name", |_, this| Ok(this.name)); + fields.add_field_method_get("R", |_, this| Ok(this.rgb.0 as f32 / 255f32)); + fields.add_field_method_get("G", |_, this| Ok(this.rgb.1 as f32 / 255f32)); + fields.add_field_method_get("B", |_, this| Ok(this.rgb.2 as f32 / 255f32)); + fields.add_field_method_get("r", |_, this| Ok(this.rgb.0 as f32 / 255f32)); + fields.add_field_method_get("g", |_, this| Ok(this.rgb.1 as f32 / 255f32)); + fields.add_field_method_get("b", |_, this| Ok(this.rgb.2 as f32 / 255f32)); + fields.add_field_method_get("Color", |_, this| Ok(Color3::from(*this))); + } + + 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 Default for BrickColor { + fn default() -> Self { + color_from_number(BRICK_COLOR_DEFAULT) + } +} + +impl fmt::Display for BrickColor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +impl From for BrickColor { + fn from(value: Color3) -> Self { + let r = value.r.clamp(u8::MIN as f32, u8::MAX as f32) as u8; + let g = value.g.clamp(u8::MIN as f32, u8::MAX as f32) as u8; + let b = value.b.clamp(u8::MIN as f32, u8::MAX as f32) as u8; + color_from_rgb(r, g, b) + } +} + +impl From for Color3 { + fn from(value: BrickColor) -> Self { + Self { + r: (value.rgb.0 as f32) / 255.0, + g: (value.rgb.1 as f32) / 255.0, + b: (value.rgb.2 as f32) / 255.0, + } + } +} + +impl From for BrickColor { + fn from(v: DomBrickColor) -> Self { + color_from_name(v.to_string()) + } +} + +impl From for DomBrickColor { + fn from(v: BrickColor) -> Self { + DomBrickColor::from_number(v.number).unwrap_or(DomBrickColor::MediumStoneGrey) + } +} + +/* + + NOTE: The brick color definitions below are generated using + the brick_color script in the scripts dir next to src, which can + be ran using `cargo run packages/lib-roblox/scripts/brick_color` + +*/ + +type BrickColorDef = &'static (u16, &'static str, (u8, u8, u8)); + +impl From for BrickColor { + fn from(value: BrickColorDef) -> Self { + Self { + number: value.0, + name: value.1, + rgb: value.2, + } + } +} + +const BRICK_COLOR_DEFAULT_VALUE: BrickColorDef = + &BRICK_COLOR_VALUES[(BRICK_COLOR_DEFAULT - 1) as usize]; + +fn color_from_number(index: u16) -> BrickColor { + BRICK_COLOR_VALUES + .iter() + .find(|color| color.0 == index) + .unwrap_or(BRICK_COLOR_DEFAULT_VALUE) + .into() +} + +fn color_from_name(name: impl AsRef) -> BrickColor { + let name = name.as_ref(); + BRICK_COLOR_VALUES + .iter() + .find(|color| color.1 == name) + .unwrap_or(BRICK_COLOR_DEFAULT_VALUE) + .into() +} + +fn color_from_rgb(r: u8, g: u8, b: u8) -> BrickColor { + let r = r as i16; + let g = g as i16; + let b = b as i16; + BRICK_COLOR_VALUES + .iter() + .fold( + (None, u16::MAX), + |(closest_color, closest_distance), color| { + let cr = color.2 .0 as i16; + let cg = color.2 .1 as i16; + let cb = color.2 .2 as i16; + let distance = ((r - cr) + (g - cg) + (b - cb)).unsigned_abs(); + if distance < closest_distance { + (Some(color), distance) + } else { + (closest_color, closest_distance) + } + }, + ) + .0 + .unwrap_or(BRICK_COLOR_DEFAULT_VALUE) + .into() +} + +const BRICK_COLOR_DEFAULT: u16 = 194; + +const BRICK_COLOR_VALUES: &[(u16, &str, (u8, u8, u8))] = &[ + (1, "White", (242, 243, 243)), + (2, "Grey", (161, 165, 162)), + (3, "Light yellow", (249, 233, 153)), + (5, "Brick yellow", (215, 197, 154)), + (6, "Light green (Mint)", (194, 218, 184)), + (9, "Light reddish violet", (232, 186, 200)), + (11, "Pastel Blue", (128, 187, 219)), + (12, "Light orange brown", (203, 132, 66)), + (18, "Nougat", (204, 142, 105)), + (21, "Bright red", (196, 40, 28)), + (22, "Med. reddish violet", (196, 112, 160)), + (23, "Bright blue", (13, 105, 172)), + (24, "Bright yellow", (245, 205, 48)), + (25, "Earth orange", (98, 71, 50)), + (26, "Black", (27, 42, 53)), + (27, "Dark grey", (109, 110, 108)), + (28, "Dark green", (40, 127, 71)), + (29, "Medium green", (161, 196, 140)), + (36, "Lig. Yellowich orange", (243, 207, 155)), + (37, "Bright green", (75, 151, 75)), + (38, "Dark orange", (160, 95, 53)), + (39, "Light bluish violet", (193, 202, 222)), + (40, "Transparent", (236, 236, 236)), + (41, "Tr. Red", (205, 84, 75)), + (42, "Tr. Lg blue", (193, 223, 240)), + (43, "Tr. Blue", (123, 182, 232)), + (44, "Tr. Yellow", (247, 241, 141)), + (45, "Light blue", (180, 210, 228)), + (47, "Tr. Flu. Reddish orange", (217, 133, 108)), + (48, "Tr. Green", (132, 182, 141)), + (49, "Tr. Flu. Green", (248, 241, 132)), + (50, "Phosph. White", (236, 232, 222)), + (100, "Light red", (238, 196, 182)), + (101, "Medium red", (218, 134, 122)), + (102, "Medium blue", (110, 153, 202)), + (103, "Light grey", (199, 193, 183)), + (104, "Bright violet", (107, 50, 124)), + (105, "Br. yellowish orange", (226, 155, 64)), + (106, "Bright orange", (218, 133, 65)), + (107, "Bright bluish green", (0, 143, 156)), + (108, "Earth yellow", (104, 92, 67)), + (110, "Bright bluish violet", (67, 84, 147)), + (111, "Tr. Brown", (191, 183, 177)), + (112, "Medium bluish violet", (104, 116, 172)), + (113, "Tr. Medi. reddish violet", (229, 173, 200)), + (115, "Med. yellowish green", (199, 210, 60)), + (116, "Med. bluish green", (85, 165, 175)), + (118, "Light bluish green", (183, 215, 213)), + (119, "Br. yellowish green", (164, 189, 71)), + (120, "Lig. yellowish green", (217, 228, 167)), + (121, "Med. yellowish orange", (231, 172, 88)), + (123, "Br. reddish orange", (211, 111, 76)), + (124, "Bright reddish violet", (146, 57, 120)), + (125, "Light orange", (234, 184, 146)), + (126, "Tr. Bright bluish violet", (165, 165, 203)), + (127, "Gold", (220, 188, 129)), + (128, "Dark nougat", (174, 122, 89)), + (131, "Silver", (156, 163, 168)), + (133, "Neon orange", (213, 115, 61)), + (134, "Neon green", (216, 221, 86)), + (135, "Sand blue", (116, 134, 157)), + (136, "Sand violet", (135, 124, 144)), + (137, "Medium orange", (224, 152, 100)), + (138, "Sand yellow", (149, 138, 115)), + (140, "Earth blue", (32, 58, 86)), + (141, "Earth green", (39, 70, 45)), + (143, "Tr. Flu. Blue", (207, 226, 247)), + (145, "Sand blue metallic", (121, 136, 161)), + (146, "Sand violet metallic", (149, 142, 163)), + (147, "Sand yellow metallic", (147, 135, 103)), + (148, "Dark grey metallic", (87, 88, 87)), + (149, "Black metallic", (22, 29, 50)), + (150, "Light grey metallic", (171, 173, 172)), + (151, "Sand green", (120, 144, 130)), + (153, "Sand red", (149, 121, 119)), + (154, "Dark red", (123, 46, 47)), + (157, "Tr. Flu. Yellow", (255, 246, 123)), + (158, "Tr. Flu. Red", (225, 164, 194)), + (168, "Gun metallic", (117, 108, 98)), + (176, "Red flip/flop", (151, 105, 91)), + (178, "Yellow flip/flop", (180, 132, 85)), + (179, "Silver flip/flop", (137, 135, 136)), + (180, "Curry", (215, 169, 75)), + (190, "Fire Yellow", (249, 214, 46)), + (191, "Flame yellowish orange", (232, 171, 45)), + (192, "Reddish brown", (105, 64, 40)), + (193, "Flame reddish orange", (207, 96, 36)), + (194, "Medium stone grey", (163, 162, 165)), + (195, "Royal blue", (70, 103, 164)), + (196, "Dark Royal blue", (35, 71, 139)), + (198, "Bright reddish lilac", (142, 66, 133)), + (199, "Dark stone grey", (99, 95, 98)), + (200, "Lemon metalic", (130, 138, 93)), + (208, "Light stone grey", (229, 228, 223)), + (209, "Dark Curry", (176, 142, 68)), + (210, "Faded green", (112, 149, 120)), + (211, "Turquoise", (121, 181, 181)), + (212, "Light Royal blue", (159, 195, 233)), + (213, "Medium Royal blue", (108, 129, 183)), + (216, "Rust", (144, 76, 42)), + (217, "Brown", (124, 92, 70)), + (218, "Reddish lilac", (150, 112, 159)), + (219, "Lilac", (107, 98, 155)), + (220, "Light lilac", (167, 169, 206)), + (221, "Bright purple", (205, 98, 152)), + (222, "Light purple", (228, 173, 200)), + (223, "Light pink", (220, 144, 149)), + (224, "Light brick yellow", (240, 213, 160)), + (225, "Warm yellowish orange", (235, 184, 127)), + (226, "Cool yellow", (253, 234, 141)), + (232, "Dove blue", (125, 187, 221)), + (268, "Medium lilac", (52, 43, 117)), + (301, "Slime green", (80, 109, 84)), + (302, "Smoky grey", (91, 93, 105)), + (303, "Dark blue", (0, 16, 176)), + (304, "Parsley green", (44, 101, 29)), + (305, "Steel blue", (82, 124, 174)), + (306, "Storm blue", (51, 88, 130)), + (307, "Lapis", (16, 42, 220)), + (308, "Dark indigo", (61, 21, 133)), + (309, "Sea green", (52, 142, 64)), + (310, "Shamrock", (91, 154, 76)), + (311, "Fossil", (159, 161, 172)), + (312, "Mulberry", (89, 34, 89)), + (313, "Forest green", (31, 128, 29)), + (314, "Cadet blue", (159, 173, 192)), + (315, "Electric blue", (9, 137, 207)), + (316, "Eggplant", (123, 0, 123)), + (317, "Moss", (124, 156, 107)), + (318, "Artichoke", (138, 171, 133)), + (319, "Sage green", (185, 196, 177)), + (320, "Ghost grey", (202, 203, 209)), + (321, "Lilac", (167, 94, 155)), + (322, "Plum", (123, 47, 123)), + (323, "Olivine", (148, 190, 129)), + (324, "Laurel green", (168, 189, 153)), + (325, "Quill grey", (223, 223, 222)), + (327, "Crimson", (151, 0, 0)), + (328, "Mint", (177, 229, 166)), + (329, "Baby blue", (152, 194, 219)), + (330, "Carnation pink", (255, 152, 220)), + (331, "Persimmon", (255, 89, 89)), + (332, "Maroon", (117, 0, 0)), + (333, "Gold", (239, 184, 56)), + (334, "Daisy orange", (248, 217, 109)), + (335, "Pearl", (231, 231, 236)), + (336, "Fog", (199, 212, 228)), + (337, "Salmon", (255, 148, 148)), + (338, "Terra Cotta", (190, 104, 98)), + (339, "Cocoa", (86, 36, 36)), + (340, "Wheat", (241, 231, 199)), + (341, "Buttermilk", (254, 243, 187)), + (342, "Mauve", (224, 178, 208)), + (343, "Sunrise", (212, 144, 189)), + (344, "Tawny", (150, 85, 85)), + (345, "Rust", (143, 76, 42)), + (346, "Cashmere", (211, 190, 150)), + (347, "Khaki", (226, 220, 188)), + (348, "Lily white", (237, 234, 234)), + (349, "Seashell", (233, 218, 218)), + (350, "Burgundy", (136, 62, 62)), + (351, "Cork", (188, 155, 93)), + (352, "Burlap", (199, 172, 120)), + (353, "Beige", (202, 191, 163)), + (354, "Oyster", (187, 179, 178)), + (355, "Pine Cone", (108, 88, 75)), + (356, "Fawn brown", (160, 132, 79)), + (357, "Hurricane grey", (149, 137, 136)), + (358, "Cloudy grey", (171, 168, 158)), + (359, "Linen", (175, 148, 131)), + (360, "Copper", (150, 103, 102)), + (361, "Dirt brown", (86, 66, 54)), + (362, "Bronze", (126, 104, 63)), + (363, "Flint", (105, 102, 92)), + (364, "Dark taupe", (90, 76, 66)), + (365, "Burnt Sienna", (106, 57, 9)), + (1001, "Institutional white", (248, 248, 248)), + (1002, "Mid gray", (205, 205, 205)), + (1003, "Really black", (17, 17, 17)), + (1004, "Really red", (255, 0, 0)), + (1005, "Deep orange", (255, 176, 0)), + (1006, "Alder", (180, 128, 255)), + (1007, "Dusty Rose", (163, 75, 75)), + (1008, "Olive", (193, 190, 66)), + (1009, "New Yeller", (255, 255, 0)), + (1010, "Really blue", (0, 0, 255)), + (1011, "Navy blue", (0, 32, 96)), + (1012, "Deep blue", (33, 84, 185)), + (1013, "Cyan", (4, 175, 236)), + (1014, "CGA brown", (170, 85, 0)), + (1015, "Magenta", (170, 0, 170)), + (1016, "Pink", (255, 102, 204)), + (1017, "Deep orange", (255, 175, 0)), + (1018, "Teal", (18, 238, 212)), + (1019, "Toothpaste", (0, 255, 255)), + (1020, "Lime green", (0, 255, 0)), + (1021, "Camo", (58, 125, 21)), + (1022, "Grime", (127, 142, 100)), + (1023, "Lavender", (140, 91, 159)), + (1024, "Pastel light blue", (175, 221, 255)), + (1025, "Pastel orange", (255, 201, 201)), + (1026, "Pastel violet", (177, 167, 255)), + (1027, "Pastel blue-green", (159, 243, 233)), + (1028, "Pastel green", (204, 255, 204)), + (1029, "Pastel yellow", (255, 255, 204)), + (1030, "Pastel brown", (255, 204, 153)), + (1031, "Royal purple", (98, 37, 209)), + (1032, "Hot pink", (255, 0, 191)), +]; + +const BRICK_COLOR_PALETTE: &[u16] = &[ + 141, 301, 107, 26, 1012, 303, 1011, 304, 28, 1018, 302, 305, 306, 307, 308, 1021, 309, 310, + 1019, 135, 102, 23, 1010, 312, 313, 37, 1022, 1020, 1027, 311, 315, 1023, 1031, 316, 151, 317, + 318, 319, 1024, 314, 1013, 1006, 321, 322, 104, 1008, 119, 323, 324, 325, 320, 11, 1026, 1016, + 1032, 1015, 327, 1005, 1009, 29, 328, 1028, 208, 45, 329, 330, 331, 1004, 21, 332, 333, 24, + 334, 226, 1029, 335, 336, 342, 343, 338, 1007, 339, 133, 106, 340, 341, 1001, 1, 9, 1025, 337, + 344, 345, 1014, 105, 346, 347, 348, 349, 1030, 125, 101, 350, 192, 351, 352, 353, 354, 1002, 5, + 18, 217, 355, 356, 153, 357, 358, 359, 360, 38, 361, 362, 199, 194, 363, 364, 365, 1003, +]; + +const BRICK_COLOR_CONSTRUCTORS: &[(&str, u16)] = &[ + ("Yellow", 24), + ("White", 1), + ("Black", 26), + ("Green", 28), + ("Red", 21), + ("DarkGray", 199), + ("Blue", 23), + ("Gray", 194), +]; diff --git a/crates/lune-roblox/src/datatypes/types/cframe.rs b/crates/lune-roblox/src/datatypes/types/cframe.rs new file mode 100644 index 0000000..443e8c1 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/cframe.rs @@ -0,0 +1,473 @@ +use core::fmt; +use std::ops; + +use glam::{EulerRot, Mat3, Mat4, Quat, Vec3}; +use mlua::{prelude::*, Variadic}; +use rbx_dom_weak::types::{CFrame as DomCFrame, Matrix3 as DomMatrix3, Vector3 as DomVector3}; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, Vector3}; + +/** + An implementation of the [CFrame](https://create.roblox.com/docs/reference/engine/datatypes/CFrame) + Roblox datatype, backed by [`glam::Mat4`]. + + This implements all documented properties, methods & + constructors of the CFrame class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct CFrame(pub Mat4); + +impl CFrame { + pub const IDENTITY: Self = Self(Mat4::IDENTITY); + + fn position(&self) -> Vec3 { + self.0.w_axis.truncate() + } + + fn orientation(&self) -> Mat3 { + Mat3::from_cols( + self.0.x_axis.truncate(), + self.0.y_axis.truncate(), + self.0.z_axis.truncate(), + ) + } + + fn inverse(&self) -> Self { + Self(self.0.inverse()) + } +} + +impl LuaExportsTable<'_> for CFrame { + const EXPORT_NAME: &'static str = "CFrame"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let cframe_angles = |_, (rx, ry, rz): (f32, f32, f32)| { + Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz))) + }; + + let cframe_from_axis_angle = + |_, (v, r): (LuaUserDataRef, f32)| Ok(CFrame(Mat4::from_axis_angle(v.0, r))); + + let cframe_from_euler_angles_xyz = |_, (rx, ry, rz): (f32, f32, f32)| { + Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz))) + }; + + let cframe_from_euler_angles_yxz = |_, (rx, ry, rz): (f32, f32, f32)| { + Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz))) + }; + + let cframe_from_matrix = |_, + (pos, rx, ry, rz): ( + LuaUserDataRef, + LuaUserDataRef, + LuaUserDataRef, + Option>, + )| { + Ok(CFrame(Mat4::from_cols( + rx.0.extend(0.0), + ry.0.extend(0.0), + rz.map(|r| r.0) + .unwrap_or_else(|| rx.0.cross(ry.0).normalize()) + .extend(0.0), + pos.0.extend(1.0), + ))) + }; + + let cframe_from_orientation = |_, (rx, ry, rz): (f32, f32, f32)| { + Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz))) + }; + + let cframe_look_at = |_, + (from, to, up): ( + LuaUserDataRef, + LuaUserDataRef, + Option>, + )| { + Ok(CFrame(look_at( + from.0, + to.0, + up.as_deref().unwrap_or(&Vector3(Vec3::Y)).0, + ))) + }; + + // Dynamic args constructor + type ArgsPos<'lua> = LuaUserDataRef<'lua, Vector3>; + type ArgsLook<'lua> = ( + LuaUserDataRef<'lua, Vector3>, + LuaUserDataRef<'lua, Vector3>, + Option>, + ); + + type ArgsPosXYZ = (f32, f32, f32); + type ArgsPosXYZQuat = (f32, f32, f32, f32, f32, f32, f32); + type ArgsMatrix = (f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32); + + let cframe_new = |lua, args: LuaMultiValue| match args.len() { + 0 => Ok(CFrame(Mat4::IDENTITY)), + + 1 => match ArgsPos::from_lua_multi(args, lua) { + Ok(pos) => Ok(CFrame(Mat4::from_translation(pos.0))), + Err(err) => Err(err), + }, + + 3 => { + if let Ok((from, to, up)) = ArgsLook::from_lua_multi(args.clone(), lua) { + Ok(CFrame(look_at( + from.0, + to.0, + up.as_deref().unwrap_or(&Vector3(Vec3::Y)).0, + ))) + } else if let Ok((x, y, z)) = ArgsPosXYZ::from_lua_multi(args, lua) { + Ok(CFrame(Mat4::from_translation(Vec3::new(x, y, z)))) + } else { + // TODO: Make this error message better + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + } + + 7 => match ArgsPosXYZQuat::from_lua_multi(args, lua) { + Ok((x, y, z, qx, qy, qz, qw)) => Ok(CFrame(Mat4::from_rotation_translation( + Quat::from_array([qx, qy, qz, qw]), + Vec3::new(x, y, z), + ))), + Err(err) => Err(err), + }, + + 12 => match ArgsMatrix::from_lua_multi(args, lua) { + Ok((x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22)) => { + Ok(CFrame(Mat4::from_cols_array_2d(&[ + [r00, r10, r20, 0.0], + [r01, r11, r21, 0.0], + [r02, r12, r22, 0.0], + [x, y, z, 1.0], + ]))) + } + Err(err) => Err(err), + }, + + _ => Err(LuaError::RuntimeError(format!( + "Invalid number of arguments: expected 0, 1, 3, 7, or 12, got {}", + args.len() + ))), + }; + + TableBuilder::new(lua)? + .with_function("Angles", cframe_angles)? + .with_value("identity", CFrame(Mat4::IDENTITY))? + .with_function("fromAxisAngle", cframe_from_axis_angle)? + .with_function("fromEulerAnglesXYZ", cframe_from_euler_angles_xyz)? + .with_function("fromEulerAnglesYXZ", cframe_from_euler_angles_yxz)? + .with_function("fromMatrix", cframe_from_matrix)? + .with_function("fromOrientation", cframe_from_orientation)? + .with_function("lookAt", cframe_look_at)? + .with_function("new", cframe_new)? + .build_readonly() + } +} + +impl LuaUserData for CFrame { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Position", |_, this| Ok(Vector3(this.position()))); + fields.add_field_method_get("Rotation", |_, this| { + Ok(CFrame(Mat4::from_cols( + this.0.x_axis, + this.0.y_axis, + this.0.z_axis, + Vec3::ZERO.extend(1.0), + ))) + }); + fields.add_field_method_get("X", |_, this| Ok(this.position().x)); + fields.add_field_method_get("Y", |_, this| Ok(this.position().y)); + fields.add_field_method_get("Z", |_, this| Ok(this.position().z)); + fields.add_field_method_get("XVector", |_, this| Ok(Vector3(this.orientation().x_axis))); + fields.add_field_method_get("YVector", |_, this| Ok(Vector3(this.orientation().y_axis))); + fields.add_field_method_get("ZVector", |_, this| Ok(Vector3(this.orientation().z_axis))); + fields.add_field_method_get("RightVector", |_, this| { + Ok(Vector3(this.orientation().x_axis)) + }); + fields.add_field_method_get("UpVector", |_, this| Ok(Vector3(this.orientation().y_axis))); + fields.add_field_method_get("LookVector", |_, this| { + Ok(Vector3(-this.orientation().z_axis)) + }); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Methods + methods.add_method("Inverse", |_, this, ()| Ok(this.inverse())); + methods.add_method( + "Lerp", + |_, this, (goal, alpha): (LuaUserDataRef, f32)| { + let quat_this = Quat::from_mat4(&this.0); + let quat_goal = Quat::from_mat4(&goal.0); + let translation = this + .0 + .w_axis + .truncate() + .lerp(goal.0.w_axis.truncate(), alpha); + let rotation = quat_this.slerp(quat_goal, alpha); + Ok(CFrame(Mat4::from_rotation_translation( + rotation, + translation, + ))) + }, + ); + methods.add_method("Orthonormalize", |_, this, ()| { + let rotation = Quat::from_mat4(&this.0); + let translation = this.0.w_axis.truncate(); + Ok(CFrame(Mat4::from_rotation_translation( + rotation.normalize(), + translation, + ))) + }); + methods.add_method( + "ToWorldSpace", + |_, this, rhs: Variadic>| { + Ok(Variadic::from_iter(rhs.into_iter().map(|cf| *this * *cf))) + }, + ); + methods.add_method( + "ToObjectSpace", + |_, this, rhs: Variadic>| { + let inverse = this.inverse(); + Ok(Variadic::from_iter(rhs.into_iter().map(|cf| inverse * *cf))) + }, + ); + methods.add_method( + "PointToWorldSpace", + |_, this, rhs: Variadic>| { + Ok(Variadic::from_iter(rhs.into_iter().map(|v3| *this * *v3))) + }, + ); + methods.add_method( + "PointToObjectSpace", + |_, this, rhs: Variadic>| { + let inverse = this.inverse(); + Ok(Variadic::from_iter(rhs.into_iter().map(|v3| inverse * *v3))) + }, + ); + methods.add_method( + "VectorToWorldSpace", + |_, this, rhs: Variadic>| { + let result = *this - Vector3(this.position()); + Ok(Variadic::from_iter(rhs.into_iter().map(|v3| result * *v3))) + }, + ); + methods.add_method( + "VectorToObjectSpace", + |_, this, rhs: Variadic>| { + let inverse = this.inverse(); + let result = inverse - Vector3(inverse.position()); + + Ok(Variadic::from_iter(rhs.into_iter().map(|v3| result * *v3))) + }, + ); + #[rustfmt::skip] + methods.add_method("GetComponents", |_, this, ()| { + let pos = this.position(); + let transposed = this.orientation().transpose(); + Ok(( + pos.x, pos.y, pos.z, + transposed.x_axis.x, transposed.x_axis.y, transposed.x_axis.z, + transposed.y_axis.x, transposed.y_axis.y, transposed.y_axis.z, + transposed.z_axis.x, transposed.z_axis.y, transposed.z_axis.z, + )) + }); + methods.add_method("ToEulerAnglesXYZ", |_, this, ()| { + Ok(Quat::from_mat4(&this.0).to_euler(EulerRot::XYZ)) + }); + methods.add_method("ToEulerAnglesYXZ", |_, this, ()| { + let (ry, rx, rz) = Quat::from_mat4(&this.0).to_euler(EulerRot::YXZ); + Ok((rx, ry, rz)) + }); + methods.add_method("ToOrientation", |_, this, ()| { + let (ry, rx, rz) = Quat::from_mat4(&this.0).to_euler(EulerRot::YXZ); + Ok((rx, ry, rz)) + }); + methods.add_method("ToAxisAngle", |_, this, ()| { + let (axis, angle) = Quat::from_mat4(&this.0).to_axis_angle(); + Ok((Vector3(axis), angle)) + }); + // Metamethods + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + methods.add_meta_method(LuaMetaMethod::Mul, |lua, this, rhs: LuaValue| { + if let LuaValue::UserData(ud) = &rhs { + if let Ok(cf) = ud.borrow::() { + return lua.create_userdata(*this * *cf); + } else if let Ok(vec) = ud.borrow::() { + return lua.create_userdata(*this * *vec); + } + }; + Err(LuaError::FromLuaConversionError { + from: rhs.type_name(), + to: "userdata", + message: Some(format!( + "Expected CFrame or Vector3, got {}", + rhs.type_name() + )), + }) + }); + methods.add_meta_method( + LuaMetaMethod::Add, + |_, this, vec: LuaUserDataRef| Ok(*this + *vec), + ); + methods.add_meta_method( + LuaMetaMethod::Sub, + |_, this, vec: LuaUserDataRef| Ok(*this - *vec), + ); + } +} + +impl fmt::Display for CFrame { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let pos = self.position(); + let transposed = self.orientation().transpose(); + write!( + f, + "{}, {}, {}, {}", + Vector3(pos), + Vector3(transposed.x_axis), + Vector3(transposed.y_axis), + Vector3(transposed.z_axis) + ) + } +} + +impl ops::Mul for CFrame { + type Output = Self; + fn mul(self, rhs: Self) -> Self::Output { + CFrame(self.0 * rhs.0) + } +} + +impl ops::Mul for CFrame { + type Output = Vector3; + fn mul(self, rhs: Vector3) -> Self::Output { + Vector3(self.0.project_point3(rhs.0)) + } +} + +impl ops::Add for CFrame { + type Output = Self; + fn add(self, rhs: Vector3) -> Self::Output { + CFrame(Mat4::from_cols( + self.0.x_axis, + self.0.y_axis, + self.0.z_axis, + self.0.w_axis + rhs.0.extend(0.0), + )) + } +} + +impl ops::Sub for CFrame { + type Output = Self; + fn sub(self, rhs: Vector3) -> Self::Output { + CFrame(Mat4::from_cols( + self.0.x_axis, + self.0.y_axis, + self.0.z_axis, + self.0.w_axis - rhs.0.extend(0.0), + )) + } +} + +impl From for CFrame { + fn from(v: DomCFrame) -> Self { + let transposed = v.orientation.transpose(); + CFrame(Mat4::from_cols( + Vector3::from(transposed.x).0.extend(0.0), + Vector3::from(transposed.y).0.extend(0.0), + Vector3::from(transposed.z).0.extend(0.0), + Vector3::from(v.position).0.extend(1.0), + )) + } +} + +impl From for DomCFrame { + fn from(v: CFrame) -> Self { + let transposed = v.orientation().transpose(); + DomCFrame { + position: DomVector3::from(Vector3(v.position())), + orientation: DomMatrix3::new( + DomVector3::from(Vector3(transposed.x_axis)), + DomVector3::from(Vector3(transposed.y_axis)), + DomVector3::from(Vector3(transposed.z_axis)), + ), + } + } +} + +/** + Creates a matrix at the position `from`, looking towards `to`. + + [`glam`] does provide functions such as [`look_at_lh`], [`look_at_rh`] and more but + they all create view matrices for camera transforms which is not what we want here. +*/ +fn look_at(from: Vec3, to: Vec3, up: Vec3) -> Mat4 { + let dir = (to - from).normalize(); + let xaxis = up.cross(dir).normalize(); + let yaxis = dir.cross(xaxis).normalize(); + + Mat4::from_cols( + Vec3::new(xaxis.x, yaxis.x, dir.x).extend(0.0), + Vec3::new(xaxis.y, yaxis.y, dir.y).extend(0.0), + Vec3::new(xaxis.z, yaxis.z, dir.z).extend(0.0), + from.extend(1.0), + ) +} + +#[cfg(test)] +mod cframe_test { + use glam::{Mat4, Vec3}; + use rbx_dom_weak::types::{CFrame as DomCFrame, Matrix3 as DomMatrix3, Vector3 as DomVector3}; + + use super::CFrame; + + #[test] + fn dom_cframe_from_cframe() { + let dom_cframe = DomCFrame::new( + DomVector3::new(1.0, 2.0, 3.0), + DomMatrix3::new( + DomVector3::new(1.0, 2.0, 3.0), + DomVector3::new(1.0, 2.0, 3.0), + DomVector3::new(1.0, 2.0, 3.0), + ), + ); + + let cframe = CFrame(Mat4::from_cols( + Vec3::new(1.0, 1.0, 1.0).extend(0.0), + Vec3::new(2.0, 2.0, 2.0).extend(0.0), + Vec3::new(3.0, 3.0, 3.0).extend(0.0), + Vec3::new(1.0, 2.0, 3.0).extend(1.0), + )); + + assert_eq!(CFrame::from(dom_cframe), cframe) + } + + #[test] + fn cframe_from_dom_cframe() { + let cframe = CFrame(Mat4::from_cols( + Vec3::new(1.0, 2.0, 3.0).extend(0.0), + Vec3::new(1.0, 2.0, 3.0).extend(0.0), + Vec3::new(1.0, 2.0, 3.0).extend(0.0), + Vec3::new(1.0, 2.0, 3.0).extend(1.0), + )); + + let dom_cframe = DomCFrame::new( + DomVector3::new(1.0, 2.0, 3.0), + DomMatrix3::new( + DomVector3::new(1.0, 1.0, 1.0), + DomVector3::new(2.0, 2.0, 2.0), + DomVector3::new(3.0, 3.0, 3.0), + ), + ); + + assert_eq!(DomCFrame::from(cframe), dom_cframe) + } +} diff --git a/crates/lune-roblox/src/datatypes/types/color3.rs b/crates/lune-roblox/src/datatypes/types/color3.rs new file mode 100644 index 0000000..4c635d0 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/color3.rs @@ -0,0 +1,316 @@ +#![allow(clippy::many_single_char_names)] + +use core::fmt; +use std::ops; + +use glam::Vec3; +use mlua::prelude::*; +use rbx_dom_weak::types::{Color3 as DomColor3, Color3uint8 as DomColor3uint8}; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::super::*; + +/** + An implementation of the [Color3](https://create.roblox.com/docs/reference/engine/datatypes/Color3) Roblox datatype. + + This implements all documented properties, methods & constructors of the Color3 class as of March 2023. + + It also implements math operations for addition, subtraction, multiplication, and division, + all of which are suspiciously missing from the Roblox implementation of the Color3 datatype. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Color3 { + pub(crate) r: f32, + pub(crate) g: f32, + pub(crate) b: f32, +} + +impl LuaExportsTable<'_> for Color3 { + const EXPORT_NAME: &'static str = "Color3"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let color3_from_rgb = |_, (r, g, b): (Option, Option, Option)| { + Ok(Color3 { + r: (r.unwrap_or_default() as f32) / 255f32, + g: (g.unwrap_or_default() as f32) / 255f32, + b: (b.unwrap_or_default() as f32) / 255f32, + }) + }; + + let color3_from_hsv = |_, (h, s, v): (f32, f32, f32)| { + // https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c + let i = (h * 6.0).floor(); + let f = h * 6.0 - i; + let p = v * (1.0 - s); + let q = v * (1.0 - f * s); + let t = v * (1.0 - (1.0 - f) * s); + + let (r, g, b) = match (i % 6.0) as u8 { + 0 => (v, t, p), + 1 => (q, v, p), + 2 => (p, v, t), + 3 => (p, q, v), + 4 => (t, p, v), + 5 => (v, p, q), + _ => unreachable!(), + }; + + Ok(Color3 { r, g, b }) + }; + + let color3_from_hex = |_, hex: String| { + let trimmed = hex.trim_start_matches('#').to_ascii_uppercase(); + let chars = if trimmed.len() == 3 { + ( + u8::from_str_radix(&trimmed[..1].repeat(2), 16), + u8::from_str_radix(&trimmed[1..2].repeat(2), 16), + u8::from_str_radix(&trimmed[2..3].repeat(2), 16), + ) + } else if trimmed.len() == 6 { + ( + u8::from_str_radix(&trimmed[..2], 16), + u8::from_str_radix(&trimmed[2..4], 16), + u8::from_str_radix(&trimmed[4..6], 16), + ) + } else { + return Err(LuaError::RuntimeError(format!( + "Hex color string must be 3 or 6 characters long, got {} character{}", + trimmed.len(), + if trimmed.len() == 1 { "" } else { "s" } + ))); + }; + match chars { + (Ok(r), Ok(g), Ok(b)) => Ok(Color3 { + r: (r as f32) / 255f32, + g: (g as f32) / 255f32, + b: (b as f32) / 255f32, + }), + _ => Err(LuaError::RuntimeError(format!( + "Hex color string '{}' contains invalid character", + trimmed + ))), + } + }; + + let color3_new = |_, (r, g, b): (Option, Option, Option)| { + Ok(Color3 { + r: r.unwrap_or_default(), + g: g.unwrap_or_default(), + b: b.unwrap_or_default(), + }) + }; + + TableBuilder::new(lua)? + .with_function("fromRGB", color3_from_rgb)? + .with_function("fromHSV", color3_from_hsv)? + .with_function("fromHex", color3_from_hex)? + .with_function("new", color3_new)? + .build_readonly() + } +} + +impl<'lua> FromLua<'lua> for Color3 { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + if let LuaValue::UserData(ud) = value { + Ok(*ud.borrow::()?) + } else { + Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "Color3", + message: None, + }) + } + } +} + +impl LuaUserData for Color3 { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("R", |_, this| Ok(this.r)); + fields.add_field_method_get("G", |_, this| Ok(this.g)); + fields.add_field_method_get("B", |_, this| Ok(this.b)); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Methods + methods.add_method( + "Lerp", + |_, this, (rhs, alpha): (LuaUserDataRef, f32)| { + let v3_this = Vec3::new(this.r, this.g, this.b); + let v3_rhs = Vec3::new(rhs.r, rhs.g, rhs.b); + let v3 = v3_this.lerp(v3_rhs, alpha); + Ok(Color3 { + r: v3.x, + g: v3.y, + b: v3.z, + }) + }, + ); + methods.add_method("ToHSV", |_, this, ()| { + // https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c + let (r, g, b) = (this.r, this.g, this.b); + let min = r.min(g).min(b); + let max = r.max(g).max(b); + let diff = max - min; + + let hue = (match max { + max if max == min => 0.0, + max if max == r => (g - b) / diff + (if g < b { 6.0 } else { 0.0 }), + max if max == g => (b - r) / diff + 2.0, + max if max == b => (r - g) / diff + 4.0, + _ => unreachable!(), + }) / 6.0; + + let sat = if max == 0.0 { + 0.0 + } else { + (diff / max).clamp(0.0, 1.0) + }; + + Ok((hue, sat, max)) + }); + methods.add_method("ToHex", |_, this, ()| { + Ok(format!( + "{:02X}{:02X}{:02X}", + (this.r * 255.0).clamp(u8::MIN as f32, u8::MAX as f32) as u8, + (this.g * 255.0).clamp(u8::MIN as f32, u8::MAX as f32) as u8, + (this.b * 255.0).clamp(u8::MIN as f32, u8::MAX as f32) as u8, + )) + }); + // Metamethods + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + methods.add_meta_method(LuaMetaMethod::Unm, userdata_impl_unm); + methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add); + methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub); + methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_f32); + methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_f32); + } +} + +impl Default for Color3 { + fn default() -> Self { + Self { + r: 0f32, + g: 0f32, + b: 0f32, + } + } +} + +impl fmt::Display for Color3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}, {}", self.r, self.g, self.b) + } +} + +impl ops::Neg for Color3 { + type Output = Self; + fn neg(self) -> Self::Output { + Color3 { + r: -self.r, + g: -self.g, + b: -self.b, + } + } +} + +impl ops::Add for Color3 { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + Color3 { + r: self.r + rhs.r, + g: self.g + rhs.g, + b: self.b + rhs.b, + } + } +} + +impl ops::Sub for Color3 { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + Color3 { + r: self.r - rhs.r, + g: self.g - rhs.g, + b: self.b - rhs.b, + } + } +} + +impl ops::Mul for Color3 { + type Output = Color3; + fn mul(self, rhs: Self) -> Self::Output { + Color3 { + r: self.r * rhs.r, + g: self.g * rhs.g, + b: self.b * rhs.b, + } + } +} + +impl ops::Mul for Color3 { + type Output = Color3; + fn mul(self, rhs: f32) -> Self::Output { + Color3 { + r: self.r * rhs, + g: self.g * rhs, + b: self.b * rhs, + } + } +} + +impl ops::Div for Color3 { + type Output = Color3; + fn div(self, rhs: Self) -> Self::Output { + Color3 { + r: self.r / rhs.r, + g: self.g / rhs.g, + b: self.b / rhs.b, + } + } +} + +impl ops::Div for Color3 { + type Output = Color3; + fn div(self, rhs: f32) -> Self::Output { + Color3 { + r: self.r / rhs, + g: self.g / rhs, + b: self.b / rhs, + } + } +} + +impl From for Color3 { + fn from(v: DomColor3) -> Self { + Self { + r: v.r, + g: v.g, + b: v.b, + } + } +} + +impl From for DomColor3 { + fn from(v: Color3) -> Self { + Self { + r: v.r, + g: v.g, + b: v.b, + } + } +} + +impl From for Color3 { + fn from(v: DomColor3uint8) -> Self { + Color3::from(DomColor3::from(v)) + } +} + +impl From for DomColor3uint8 { + fn from(v: Color3) -> Self { + DomColor3uint8::from(DomColor3::from(v)) + } +} diff --git a/crates/lune-roblox/src/datatypes/types/color_sequence.rs b/crates/lune-roblox/src/datatypes/types/color_sequence.rs new file mode 100644 index 0000000..9be331f --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/color_sequence.rs @@ -0,0 +1,125 @@ +use core::fmt; + +use mlua::prelude::*; +use rbx_dom_weak::types::{ + ColorSequence as DomColorSequence, ColorSequenceKeypoint as DomColorSequenceKeypoint, +}; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, Color3, ColorSequenceKeypoint}; + +/** + An implementation of the [ColorSequence](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequence) Roblox datatype. + + This implements all documented properties, methods & constructors of the ColorSequence class as of March 2023. +*/ +#[derive(Debug, Clone, PartialEq)] +pub struct ColorSequence { + pub(crate) keypoints: Vec, +} + +impl LuaExportsTable<'_> for ColorSequence { + const EXPORT_NAME: &'static str = "ColorSequence"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + type ArgsColor<'lua> = LuaUserDataRef<'lua, Color3>; + type ArgsColors<'lua> = (LuaUserDataRef<'lua, Color3>, LuaUserDataRef<'lua, Color3>); + type ArgsKeypoints<'lua> = Vec>; + + let color_sequence_new = |lua, args: LuaMultiValue| { + if let Ok(color) = ArgsColor::from_lua_multi(args.clone(), lua) { + Ok(ColorSequence { + keypoints: vec![ + ColorSequenceKeypoint { + time: 0.0, + color: *color, + }, + ColorSequenceKeypoint { + time: 1.0, + color: *color, + }, + ], + }) + } else if let Ok((c0, c1)) = ArgsColors::from_lua_multi(args.clone(), lua) { + Ok(ColorSequence { + keypoints: vec![ + ColorSequenceKeypoint { + time: 0.0, + color: *c0, + }, + ColorSequenceKeypoint { + time: 1.0, + color: *c1, + }, + ], + }) + } else if let Ok(keypoints) = ArgsKeypoints::from_lua_multi(args, lua) { + Ok(ColorSequence { + keypoints: keypoints.iter().map(|k| **k).collect(), + }) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + TableBuilder::new(lua)? + .with_function("new", color_sequence_new)? + .build_readonly() + } +} + +impl LuaUserData for ColorSequence { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Keypoints", |_, this| Ok(this.keypoints.clone())); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for ColorSequence { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (index, keypoint) in self.keypoints.iter().enumerate() { + if index < self.keypoints.len() - 1 { + write!(f, "{}, ", keypoint)?; + } else { + write!(f, "{}", keypoint)?; + } + } + Ok(()) + } +} + +impl From for ColorSequence { + fn from(v: DomColorSequence) -> Self { + Self { + keypoints: v + .keypoints + .iter() + .cloned() + .map(ColorSequenceKeypoint::from) + .collect(), + } + } +} + +impl From for DomColorSequence { + fn from(v: ColorSequence) -> Self { + Self { + keypoints: v + .keypoints + .iter() + .cloned() + .map(DomColorSequenceKeypoint::from) + .collect(), + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/color_sequence_keypoint.rs b/crates/lune-roblox/src/datatypes/types/color_sequence_keypoint.rs new file mode 100644 index 0000000..00bd15d --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/color_sequence_keypoint.rs @@ -0,0 +1,74 @@ +use core::fmt; + +use mlua::prelude::*; +use rbx_dom_weak::types::ColorSequenceKeypoint as DomColorSequenceKeypoint; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, Color3}; + +/** + An implementation of the [ColorSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequenceKeypoint) Roblox datatype. + + This implements all documented properties, methods & constructors of the ColorSequenceKeypoint class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ColorSequenceKeypoint { + pub(crate) time: f32, + pub(crate) color: Color3, +} + +impl LuaExportsTable<'_> for ColorSequenceKeypoint { + const EXPORT_NAME: &'static str = "ColorSequenceKeypoint"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let color_sequence_keypoint_new = |_, (time, color): (f32, LuaUserDataRef)| { + Ok(ColorSequenceKeypoint { + time, + color: *color, + }) + }; + + TableBuilder::new(lua)? + .with_function("new", color_sequence_keypoint_new)? + .build_readonly() + } +} + +impl LuaUserData for ColorSequenceKeypoint { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Time", |_, this| Ok(this.time)); + fields.add_field_method_get("Value", |_, this| Ok(this.color)); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for ColorSequenceKeypoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} > {}", self.time, self.color) + } +} + +impl From for ColorSequenceKeypoint { + fn from(v: DomColorSequenceKeypoint) -> Self { + Self { + time: v.time, + color: v.color.into(), + } + } +} + +impl From for DomColorSequenceKeypoint { + fn from(v: ColorSequenceKeypoint) -> Self { + Self { + time: v.time, + color: v.color.into(), + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/enum.rs b/crates/lune-roblox/src/datatypes/types/enum.rs new file mode 100644 index 0000000..4682f23 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/enum.rs @@ -0,0 +1,71 @@ +use core::fmt; + +use mlua::prelude::*; +use rbx_reflection::EnumDescriptor; + +use super::{super::*, EnumItem}; + +/** + An implementation of the [Enum](https://create.roblox.com/docs/reference/engine/datatypes/Enum) Roblox datatype. + + This implements all documented properties, methods & constructors of the Enum class as of March 2023. +*/ +#[derive(Debug, Clone)] +pub struct Enum { + pub(crate) desc: &'static EnumDescriptor<'static>, +} + +impl Enum { + pub(crate) fn from_name(name: impl AsRef) -> Option { + let db = rbx_reflection_database::get(); + db.enums.get(name.as_ref()).map(Enum::from) + } +} + +impl LuaUserData for Enum { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Methods + methods.add_method("GetEnumItems", |_, this, ()| { + Ok(this + .desc + .items + .iter() + .map(|(name, value)| EnumItem { + parent: this.clone(), + name: name.to_string(), + value: *value, + }) + .collect::>()) + }); + methods.add_meta_method(LuaMetaMethod::Index, |_, this, name: String| { + match EnumItem::from_enum_and_name(this, &name) { + Some(item) => Ok(item), + None => Err(LuaError::RuntimeError(format!( + "The enum item '{}' does not exist for enum '{}'", + name, this.desc.name + ))), + } + }); + // Metamethods + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for Enum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Enum.{}", self.desc.name) + } +} + +impl PartialEq for Enum { + fn eq(&self, other: &Self) -> bool { + self.desc.name == other.desc.name + } +} + +impl From<&'static EnumDescriptor<'static>> for Enum { + fn from(value: &'static EnumDescriptor<'static>) -> Self { + Self { desc: value } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/enum_item.rs b/crates/lune-roblox/src/datatypes/types/enum_item.rs new file mode 100644 index 0000000..8a3633c --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/enum_item.rs @@ -0,0 +1,107 @@ +use core::fmt; + +use mlua::prelude::*; +use rbx_dom_weak::types::Enum as DomEnum; + +use super::{super::*, Enum}; + +/** + An implementation of the [EnumItem](https://create.roblox.com/docs/reference/engine/datatypes/EnumItem) Roblox datatype. + + This implements all documented properties, methods & constructors of the EnumItem class as of March 2023. +*/ +#[derive(Debug, Clone)] +pub struct EnumItem { + pub(crate) parent: Enum, + pub(crate) name: String, + pub(crate) value: u32, +} + +impl EnumItem { + pub(crate) fn from_enum_and_name(parent: &Enum, name: impl AsRef) -> Option { + let enum_name = name.as_ref(); + parent.desc.items.iter().find_map(|(name, v)| { + if *name == enum_name { + Some(Self { + parent: parent.clone(), + name: enum_name.to_string(), + value: *v, + }) + } else { + None + } + }) + } + + pub(crate) fn from_enum_and_value(parent: &Enum, value: u32) -> Option { + parent.desc.items.iter().find_map(|(name, v)| { + if *v == value { + Some(Self { + parent: parent.clone(), + name: name.to_string(), + value, + }) + } else { + None + } + }) + } + + pub(crate) fn from_enum_name_and_name( + enum_name: impl AsRef, + name: impl AsRef, + ) -> Option { + let parent = Enum::from_name(enum_name)?; + Self::from_enum_and_name(&parent, name) + } + + pub(crate) fn from_enum_name_and_value(enum_name: impl AsRef, value: u32) -> Option { + let parent = Enum::from_name(enum_name)?; + Self::from_enum_and_value(&parent, value) + } +} + +impl LuaUserData for EnumItem { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Name", |_, this| Ok(this.name.clone())); + fields.add_field_method_get("Value", |_, this| Ok(this.value)); + fields.add_field_method_get("EnumType", |_, this| Ok(this.parent.clone())); + } + + 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<'lua> FromLua<'lua> for EnumItem { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + if let LuaValue::UserData(ud) = value { + Ok(ud.borrow::()?.to_owned()) + } else { + Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "EnumItem", + message: None, + }) + } + } +} + +impl fmt::Display for EnumItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}", self.parent, self.name) + } +} + +impl PartialEq for EnumItem { + fn eq(&self, other: &Self) -> bool { + self.parent == other.parent && self.value == other.value + } +} + +impl From for DomEnum { + fn from(v: EnumItem) -> Self { + DomEnum::from_u32(v.value) + } +} diff --git a/crates/lune-roblox/src/datatypes/types/enums.rs b/crates/lune-roblox/src/datatypes/types/enums.rs new file mode 100644 index 0000000..5ce78d7 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/enums.rs @@ -0,0 +1,42 @@ +use core::fmt; + +use mlua::prelude::*; + +use super::{super::*, Enum}; + +/** + An implementation of the [Enums](https://create.roblox.com/docs/reference/engine/datatypes/Enums) Roblox datatype. + + This implements all documented properties, methods & constructors of the Enums class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Enums; + +impl LuaUserData for Enums { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Methods + methods.add_method("GetEnums", |_, _, ()| { + let db = rbx_reflection_database::get(); + Ok(db.enums.values().map(Enum::from).collect::>()) + }); + methods.add_meta_method( + LuaMetaMethod::Index, + |_, _, name: String| match Enum::from_name(&name) { + Some(e) => Ok(e), + None => Err(LuaError::RuntimeError(format!( + "The enum '{}' does not exist", + name + ))), + }, + ); + // Metamethods + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for Enums { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Enum") + } +} diff --git a/crates/lune-roblox/src/datatypes/types/faces.rs b/crates/lune-roblox/src/datatypes/types/faces.rs new file mode 100644 index 0000000..705964a --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/faces.rs @@ -0,0 +1,142 @@ +use core::fmt; + +use mlua::prelude::*; +use rbx_dom_weak::types::Faces as DomFaces; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, EnumItem}; + +/** + An implementation of the [Faces](https://create.roblox.com/docs/reference/engine/datatypes/Faces) Roblox datatype. + + This implements all documented properties, methods & constructors of the Faces class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Faces { + pub(crate) right: bool, + pub(crate) top: bool, + pub(crate) back: bool, + pub(crate) left: bool, + pub(crate) bottom: bool, + pub(crate) front: bool, +} + +impl LuaExportsTable<'_> for Faces { + const EXPORT_NAME: &'static str = "Faces"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let faces_new = |_, args: LuaMultiValue| { + let mut right = false; + let mut top = false; + let mut back = false; + let mut left = false; + let mut bottom = false; + let mut front = false; + + let mut check = |e: &EnumItem| { + if e.parent.desc.name == "NormalId" { + match &e.name { + name if name == "Right" => right = true, + name if name == "Top" => top = true, + name if name == "Back" => back = true, + name if name == "Left" => left = true, + name if name == "Bottom" => bottom = true, + name if name == "Front" => front = true, + _ => {} + } + } + }; + + for (index, arg) in args.into_iter().enumerate() { + if let LuaValue::UserData(u) = arg { + if let Ok(e) = u.borrow::() { + check(&e); + } else { + return Err(LuaError::RuntimeError(format!( + "Expected argument #{} to be an EnumItem, got userdata", + index + ))); + } + } else { + return Err(LuaError::RuntimeError(format!( + "Expected argument #{} to be an EnumItem, got {}", + index, + arg.type_name() + ))); + } + } + + Ok(Faces { + right, + top, + back, + left, + bottom, + front, + }) + }; + + TableBuilder::new(lua)? + .with_function("new", faces_new)? + .build_readonly() + } +} + +impl LuaUserData for Faces { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Right", |_, this| Ok(this.right)); + fields.add_field_method_get("Top", |_, this| Ok(this.top)); + fields.add_field_method_get("Back", |_, this| Ok(this.back)); + fields.add_field_method_get("Left", |_, this| Ok(this.left)); + fields.add_field_method_get("Bottom", |_, this| Ok(this.bottom)); + fields.add_field_method_get("Front", |_, this| Ok(this.front)); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for Faces { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let write = make_list_writer(); + write(f, self.right, "Right")?; + write(f, self.top, "Top")?; + write(f, self.back, "Back")?; + write(f, self.left, "Left")?; + write(f, self.bottom, "Bottom")?; + write(f, self.front, "Front")?; + Ok(()) + } +} + +impl From for Faces { + fn from(v: DomFaces) -> Self { + let bits = v.bits(); + Self { + right: (bits & 1) == 1, + top: ((bits >> 1) & 1) == 1, + back: ((bits >> 2) & 1) == 1, + left: ((bits >> 3) & 1) == 1, + bottom: ((bits >> 4) & 1) == 1, + front: ((bits >> 5) & 1) == 1, + } + } +} + +impl From for DomFaces { + fn from(v: Faces) -> Self { + let mut bits = 0; + bits += v.right as u8; + bits += (v.top as u8) << 1; + bits += (v.back as u8) << 2; + bits += (v.left as u8) << 3; + bits += (v.bottom as u8) << 4; + bits += (v.front as u8) << 5; + DomFaces::from_bits(bits).expect("Invalid bits") + } +} diff --git a/crates/lune-roblox/src/datatypes/types/font.rs b/crates/lune-roblox/src/datatypes/types/font.rs new file mode 100644 index 0000000..5750145 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/font.rs @@ -0,0 +1,469 @@ +use core::fmt; +use std::str::FromStr; + +use mlua::prelude::*; +use rbx_dom_weak::types::{ + Font as DomFont, FontStyle as DomFontStyle, FontWeight as DomFontWeight, +}; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, EnumItem}; + +/** + An implementation of the [Font](https://create.roblox.com/docs/reference/engine/datatypes/Font) Roblox datatype. + + This implements all documented properties, methods & constructors of the Font class as of March 2023. +*/ +#[derive(Debug, Clone, PartialEq)] +pub struct Font { + pub(crate) family: String, + pub(crate) weight: FontWeight, + pub(crate) style: FontStyle, + pub(crate) cached_id: Option, +} + +impl Font { + pub(crate) fn from_enum_item(material_enum_item: &EnumItem) -> Option { + FONT_ENUM_MAP + .iter() + .find(|props| props.0 == material_enum_item.name && props.1.is_some()) + .map(|props| props.1.as_ref().unwrap()) + .map(|props| Font { + family: props.0.to_string(), + weight: props.1, + style: props.2, + cached_id: None, + }) + } +} + +impl LuaExportsTable<'_> for Font { + const EXPORT_NAME: &'static str = "Font"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let font_from_enum = |_, value: LuaUserDataRef| { + if value.parent.desc.name == "Font" { + match Font::from_enum_item(&value) { + Some(props) => Ok(props), + None => Err(LuaError::RuntimeError(format!( + "Found unknown Font '{}'", + value.name + ))), + } + } else { + Err(LuaError::RuntimeError(format!( + "Expected argument #1 to be a Font, got {}", + value.parent.desc.name + ))) + } + }; + + let font_from_name = + |_, (file, weight, style): (String, Option, Option)| { + Ok(Font { + family: format!("rbxasset://fonts/families/{}.json", file), + weight: weight.unwrap_or_default(), + style: style.unwrap_or_default(), + cached_id: None, + }) + }; + + let font_from_id = + |_, (id, weight, style): (i32, Option, Option)| { + Ok(Font { + family: format!("rbxassetid://{}", id), + weight: weight.unwrap_or_default(), + style: style.unwrap_or_default(), + cached_id: None, + }) + }; + + let font_new = + |_, (family, weight, style): (String, Option, Option)| { + Ok(Font { + family, + weight: weight.unwrap_or_default(), + style: style.unwrap_or_default(), + cached_id: None, + }) + }; + + TableBuilder::new(lua)? + .with_function("fromEnum", font_from_enum)? + .with_function("fromName", font_from_name)? + .with_function("fromId", font_from_id)? + .with_function("new", font_new)? + .build_readonly() + } +} + +impl LuaUserData for Font { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + // Getters + fields.add_field_method_get("Family", |_, this| Ok(this.family.clone())); + fields.add_field_method_get("Weight", |_, this| Ok(this.weight)); + fields.add_field_method_get("Style", |_, this| Ok(this.style)); + fields.add_field_method_get("Bold", |_, this| Ok(this.weight.as_u16() >= 600)); + // Setters + fields.add_field_method_set("Weight", |_, this, value: FontWeight| { + this.weight = value; + Ok(()) + }); + fields.add_field_method_set("Style", |_, this, value: FontStyle| { + this.style = value; + Ok(()) + }); + fields.add_field_method_set("Bold", |_, this, value: bool| { + if value { + this.weight = FontWeight::Bold; + } else { + this.weight = FontWeight::Regular; + } + Ok(()) + }); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for Font { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}, {}", self.family, self.weight, self.style) + } +} + +impl From for Font { + fn from(v: DomFont) -> Self { + Self { + family: v.family, + weight: v.weight.into(), + style: v.style.into(), + cached_id: v.cached_face_id, + } + } +} + +impl From for DomFont { + fn from(v: Font) -> Self { + DomFont { + family: v.family, + weight: v.weight.into(), + style: v.style.into(), + cached_face_id: v.cached_id, + } + } +} + +impl From for FontWeight { + fn from(v: DomFontWeight) -> Self { + FontWeight::from_u16(v.as_u16()).expect("Missing font weight") + } +} + +impl From for DomFontWeight { + fn from(v: FontWeight) -> Self { + DomFontWeight::from_u16(v.as_u16()).expect("Missing rbx font weight") + } +} + +impl From for FontStyle { + fn from(v: DomFontStyle) -> Self { + FontStyle::from_u8(v.as_u8()).expect("Missing font weight") + } +} + +impl From for DomFontStyle { + fn from(v: FontStyle) -> Self { + DomFontStyle::from_u8(v.as_u8()).expect("Missing rbx font weight") + } +} + +/* + + NOTE: The font code below is all generated using the + font_enum_map script in the scripts dir next to src, + which can be ran in the Roblox Studio command bar + +*/ + +type FontData = (&'static str, FontWeight, FontStyle); + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum FontWeight { + Thin, + ExtraLight, + Light, + Regular, + Medium, + SemiBold, + Bold, + ExtraBold, + Heavy, +} + +impl FontWeight { + pub(crate) fn as_u16(&self) -> u16 { + match self { + Self::Thin => 100, + Self::ExtraLight => 200, + Self::Light => 300, + Self::Regular => 400, + Self::Medium => 500, + Self::SemiBold => 600, + Self::Bold => 700, + Self::ExtraBold => 800, + Self::Heavy => 900, + } + } + + pub(crate) fn from_u16(n: u16) -> Option { + match n { + 100 => Some(Self::Thin), + 200 => Some(Self::ExtraLight), + 300 => Some(Self::Light), + 400 => Some(Self::Regular), + 500 => Some(Self::Medium), + 600 => Some(Self::SemiBold), + 700 => Some(Self::Bold), + 800 => Some(Self::ExtraBold), + 900 => Some(Self::Heavy), + _ => None, + } + } +} + +impl Default for FontWeight { + fn default() -> Self { + Self::Regular + } +} + +impl std::str::FromStr for FontWeight { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s { + "Thin" => Ok(Self::Thin), + "ExtraLight" => Ok(Self::ExtraLight), + "Light" => Ok(Self::Light), + "Regular" => Ok(Self::Regular), + "Medium" => Ok(Self::Medium), + "SemiBold" => Ok(Self::SemiBold), + "Bold" => Ok(Self::Bold), + "ExtraBold" => Ok(Self::ExtraBold), + "Heavy" => Ok(Self::Heavy), + _ => Err("Unknown FontWeight"), + } + } +} + +impl std::fmt::Display for FontWeight { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Thin => "Thin", + Self::ExtraLight => "ExtraLight", + Self::Light => "Light", + Self::Regular => "Regular", + Self::Medium => "Medium", + Self::SemiBold => "SemiBold", + Self::Bold => "Bold", + Self::ExtraBold => "ExtraBold", + Self::Heavy => "Heavy", + } + ) + } +} + +impl<'lua> FromLua<'lua> for FontWeight { + fn from_lua(lua_value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + let mut message = None; + if let LuaValue::UserData(ud) = &lua_value { + let value = ud.borrow::()?; + if value.parent.desc.name == "FontWeight" { + if let Ok(value) = FontWeight::from_str(&value.name) { + return Ok(value); + } + message = Some(format!( + "Found unknown Enum.FontWeight value '{}'", + value.name + )); + } else { + message = Some(format!( + "Expected Enum.FontWeight, got Enum.{}", + value.parent.desc.name + )); + } + } + Err(LuaError::FromLuaConversionError { + from: lua_value.type_name(), + to: "Enum.FontWeight", + message, + }) + } +} + +impl<'lua> IntoLua<'lua> for FontWeight { + fn into_lua(self, lua: &'lua Lua) -> LuaResult> { + match EnumItem::from_enum_name_and_name("FontWeight", self.to_string()) { + Some(enum_item) => Ok(LuaValue::UserData(lua.create_userdata(enum_item)?)), + None => Err(LuaError::ToLuaConversionError { + from: "FontWeight", + to: "EnumItem", + message: Some(format!("Found unknown Enum.FontWeight value '{}'", self)), + }), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum FontStyle { + Normal, + Italic, +} + +impl FontStyle { + pub(crate) fn as_u8(&self) -> u8 { + match self { + Self::Normal => 0, + Self::Italic => 1, + } + } + + pub(crate) fn from_u8(n: u8) -> Option { + match n { + 0 => Some(Self::Normal), + 1 => Some(Self::Italic), + _ => None, + } + } +} + +impl Default for FontStyle { + fn default() -> Self { + Self::Normal + } +} + +impl std::str::FromStr for FontStyle { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s { + "Normal" => Ok(Self::Normal), + "Italic" => Ok(Self::Italic), + _ => Err("Unknown FontStyle"), + } + } +} + +impl std::fmt::Display for FontStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Normal => "Normal", + Self::Italic => "Italic", + } + ) + } +} + +impl<'lua> FromLua<'lua> for FontStyle { + fn from_lua(lua_value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + let mut message = None; + if let LuaValue::UserData(ud) = &lua_value { + let value = ud.borrow::()?; + if value.parent.desc.name == "FontStyle" { + if let Ok(value) = FontStyle::from_str(&value.name) { + return Ok(value); + } + message = Some(format!( + "Found unknown Enum.FontStyle value '{}'", + value.name + )); + } else { + message = Some(format!( + "Expected Enum.FontStyle, got Enum.{}", + value.parent.desc.name + )); + } + } + Err(LuaError::FromLuaConversionError { + from: lua_value.type_name(), + to: "Enum.FontStyle", + message, + }) + } +} + +impl<'lua> IntoLua<'lua> for FontStyle { + fn into_lua(self, lua: &'lua Lua) -> LuaResult> { + match EnumItem::from_enum_name_and_name("FontStyle", self.to_string()) { + Some(enum_item) => Ok(LuaValue::UserData(lua.create_userdata(enum_item)?)), + None => Err(LuaError::ToLuaConversionError { + from: "FontStyle", + to: "EnumItem", + message: Some(format!("Found unknown Enum.FontStyle value '{}'", self)), + }), + } + } +} + +#[rustfmt::skip] +const FONT_ENUM_MAP: &[(&str, Option)] = &[ + ("Legacy", Some(("rbxasset://fonts/families/LegacyArial.json", FontWeight::Regular, FontStyle::Normal))), + ("Arial", Some(("rbxasset://fonts/families/Arial.json", FontWeight::Regular, FontStyle::Normal))), + ("ArialBold", Some(("rbxasset://fonts/families/Arial.json", FontWeight::Bold, FontStyle::Normal))), + ("SourceSans", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::Regular, FontStyle::Normal))), + ("SourceSansBold", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::Bold, FontStyle::Normal))), + ("SourceSansSemibold", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::SemiBold, FontStyle::Normal))), + ("SourceSansLight", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::Light, FontStyle::Normal))), + ("SourceSansItalic", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::Regular, FontStyle::Italic))), + ("Bodoni", Some(("rbxasset://fonts/families/AccanthisADFStd.json", FontWeight::Regular, FontStyle::Normal))), + ("Garamond", Some(("rbxasset://fonts/families/Guru.json", FontWeight::Regular, FontStyle::Normal))), + ("Cartoon", Some(("rbxasset://fonts/families/ComicNeueAngular.json", FontWeight::Regular, FontStyle::Normal))), + ("Code", Some(("rbxasset://fonts/families/Inconsolata.json", FontWeight::Regular, FontStyle::Normal))), + ("Highway", Some(("rbxasset://fonts/families/HighwayGothic.json", FontWeight::Regular, FontStyle::Normal))), + ("SciFi", Some(("rbxasset://fonts/families/Zekton.json", FontWeight::Regular, FontStyle::Normal))), + ("Arcade", Some(("rbxasset://fonts/families/PressStart2P.json", FontWeight::Regular, FontStyle::Normal))), + ("Fantasy", Some(("rbxasset://fonts/families/Balthazar.json", FontWeight::Regular, FontStyle::Normal))), + ("Antique", Some(("rbxasset://fonts/families/RomanAntique.json", FontWeight::Regular, FontStyle::Normal))), + ("Gotham", Some(("rbxasset://fonts/families/GothamSSm.json", FontWeight::Regular, FontStyle::Normal))), + ("GothamMedium", Some(("rbxasset://fonts/families/GothamSSm.json", FontWeight::Medium, FontStyle::Normal))), + ("GothamBold", Some(("rbxasset://fonts/families/GothamSSm.json", FontWeight::Bold, FontStyle::Normal))), + ("GothamBlack", Some(("rbxasset://fonts/families/GothamSSm.json", FontWeight::Heavy, FontStyle::Normal))), + ("AmaticSC", Some(("rbxasset://fonts/families/AmaticSC.json", FontWeight::Regular, FontStyle::Normal))), + ("Bangers", Some(("rbxasset://fonts/families/Bangers.json", FontWeight::Regular, FontStyle::Normal))), + ("Creepster", Some(("rbxasset://fonts/families/Creepster.json", FontWeight::Regular, FontStyle::Normal))), + ("DenkOne", Some(("rbxasset://fonts/families/DenkOne.json", FontWeight::Regular, FontStyle::Normal))), + ("Fondamento", Some(("rbxasset://fonts/families/Fondamento.json", FontWeight::Regular, FontStyle::Normal))), + ("FredokaOne", Some(("rbxasset://fonts/families/FredokaOne.json", FontWeight::Regular, FontStyle::Normal))), + ("GrenzeGotisch", Some(("rbxasset://fonts/families/GrenzeGotisch.json", FontWeight::Regular, FontStyle::Normal))), + ("IndieFlower", Some(("rbxasset://fonts/families/IndieFlower.json", FontWeight::Regular, FontStyle::Normal))), + ("JosefinSans", Some(("rbxasset://fonts/families/JosefinSans.json", FontWeight::Regular, FontStyle::Normal))), + ("Jura", Some(("rbxasset://fonts/families/Jura.json", FontWeight::Regular, FontStyle::Normal))), + ("Kalam", Some(("rbxasset://fonts/families/Kalam.json", FontWeight::Regular, FontStyle::Normal))), + ("LuckiestGuy", Some(("rbxasset://fonts/families/LuckiestGuy.json", FontWeight::Regular, FontStyle::Normal))), + ("Merriweather", Some(("rbxasset://fonts/families/Merriweather.json", FontWeight::Regular, FontStyle::Normal))), + ("Michroma", Some(("rbxasset://fonts/families/Michroma.json", FontWeight::Regular, FontStyle::Normal))), + ("Nunito", Some(("rbxasset://fonts/families/Nunito.json", FontWeight::Regular, FontStyle::Normal))), + ("Oswald", Some(("rbxasset://fonts/families/Oswald.json", FontWeight::Regular, FontStyle::Normal))), + ("PatrickHand", Some(("rbxasset://fonts/families/PatrickHand.json", FontWeight::Regular, FontStyle::Normal))), + ("PermanentMarker", Some(("rbxasset://fonts/families/PermanentMarker.json", FontWeight::Regular, FontStyle::Normal))), + ("Roboto", Some(("rbxasset://fonts/families/Roboto.json", FontWeight::Regular, FontStyle::Normal))), + ("RobotoCondensed", Some(("rbxasset://fonts/families/RobotoCondensed.json", FontWeight::Regular, FontStyle::Normal))), + ("RobotoMono", Some(("rbxasset://fonts/families/RobotoMono.json", FontWeight::Regular, FontStyle::Normal))), + ("Sarpanch", Some(("rbxasset://fonts/families/Sarpanch.json", FontWeight::Regular, FontStyle::Normal))), + ("SpecialElite", Some(("rbxasset://fonts/families/SpecialElite.json", FontWeight::Regular, FontStyle::Normal))), + ("TitilliumWeb", Some(("rbxasset://fonts/families/TitilliumWeb.json", FontWeight::Regular, FontStyle::Normal))), + ("Ubuntu", Some(("rbxasset://fonts/families/Ubuntu.json", FontWeight::Regular, FontStyle::Normal))), + ("Unknown", None), +]; diff --git a/crates/lune-roblox/src/datatypes/types/mod.rs b/crates/lune-roblox/src/datatypes/types/mod.rs new file mode 100644 index 0000000..394b210 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/mod.rs @@ -0,0 +1,51 @@ +mod axes; +mod brick_color; +mod cframe; +mod color3; +mod color_sequence; +mod color_sequence_keypoint; +mod r#enum; +mod r#enum_item; +mod r#enums; +mod faces; +mod font; +mod number_range; +mod number_sequence; +mod number_sequence_keypoint; +mod physical_properties; +mod ray; +mod rect; +mod region3; +mod region3int16; +mod udim; +mod udim2; +mod vector2; +mod vector2int16; +mod vector3; +mod vector3int16; + +pub use axes::Axes; +pub use brick_color::BrickColor; +pub use cframe::CFrame; +pub use color3::Color3; +pub use color_sequence::ColorSequence; +pub use color_sequence_keypoint::ColorSequenceKeypoint; +pub use faces::Faces; +pub use font::Font; +pub use number_range::NumberRange; +pub use number_sequence::NumberSequence; +pub use number_sequence_keypoint::NumberSequenceKeypoint; +pub use physical_properties::PhysicalProperties; +pub use r#enum::Enum; +pub use r#enum_item::EnumItem; +pub use r#enums::Enums; +pub use ray::Ray; +pub use rect::Rect; +pub use region3::Region3; +pub use region3int16::Region3int16; +pub use udim::UDim; +pub use udim2::UDim2; +pub use vector2::Vector2; +pub use vector2int16::Vector2int16; +pub use vector3::Vector3; +pub use vector3int16::Vector3int16; diff --git a/crates/lune-roblox/src/datatypes/types/number_range.rs b/crates/lune-roblox/src/datatypes/types/number_range.rs new file mode 100644 index 0000000..60cf0d7 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/number_range.rs @@ -0,0 +1,77 @@ +use core::fmt; + +use mlua::prelude::*; +use rbx_dom_weak::types::NumberRange as DomNumberRange; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::super::*; + +/** + An implementation of the [NumberRange](https://create.roblox.com/docs/reference/engine/datatypes/NumberRange) Roblox datatype. + + This implements all documented properties, methods & constructors of the NumberRange class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct NumberRange { + pub(crate) min: f32, + pub(crate) max: f32, +} + +impl LuaExportsTable<'_> for NumberRange { + const EXPORT_NAME: &'static str = "NumberRange"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let number_range_new = |_, (min, max): (f32, Option)| { + Ok(match max { + Some(max) => NumberRange { + min: min.min(max), + max: min.max(max), + }, + None => NumberRange { min, max: min }, + }) + }; + + TableBuilder::new(lua)? + .with_function("new", number_range_new)? + .build_readonly() + } +} + +impl LuaUserData for NumberRange { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Min", |_, this| Ok(this.min)); + fields.add_field_method_get("Max", |_, this| Ok(this.max)); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for NumberRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}", self.min, self.max) + } +} + +impl From for NumberRange { + fn from(v: DomNumberRange) -> Self { + Self { + min: v.min, + max: v.max, + } + } +} + +impl From for DomNumberRange { + fn from(v: NumberRange) -> Self { + Self { + min: v.min, + max: v.max, + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/number_sequence.rs b/crates/lune-roblox/src/datatypes/types/number_sequence.rs new file mode 100644 index 0000000..aa27ff6 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/number_sequence.rs @@ -0,0 +1,129 @@ +use core::fmt; + +use mlua::prelude::*; +use rbx_dom_weak::types::{ + NumberSequence as DomNumberSequence, NumberSequenceKeypoint as DomNumberSequenceKeypoint, +}; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, NumberSequenceKeypoint}; + +/** + An implementation of the [NumberSequence](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequence) Roblox datatype. + + This implements all documented properties, methods & constructors of the NumberSequence class as of March 2023. +*/ +#[derive(Debug, Clone, PartialEq)] +pub struct NumberSequence { + pub(crate) keypoints: Vec, +} + +impl LuaExportsTable<'_> for NumberSequence { + const EXPORT_NAME: &'static str = "NumberSequence"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + type ArgsColor = f32; + type ArgsColors = (f32, f32); + type ArgsKeypoints<'lua> = Vec>; + + let number_sequence_new = |lua, args: LuaMultiValue| { + if let Ok(value) = ArgsColor::from_lua_multi(args.clone(), lua) { + Ok(NumberSequence { + keypoints: vec![ + NumberSequenceKeypoint { + time: 0.0, + value, + envelope: 0.0, + }, + NumberSequenceKeypoint { + time: 1.0, + value, + envelope: 0.0, + }, + ], + }) + } else if let Ok((v0, v1)) = ArgsColors::from_lua_multi(args.clone(), lua) { + Ok(NumberSequence { + keypoints: vec![ + NumberSequenceKeypoint { + time: 0.0, + value: v0, + envelope: 0.0, + }, + NumberSequenceKeypoint { + time: 1.0, + value: v1, + envelope: 0.0, + }, + ], + }) + } else if let Ok(keypoints) = ArgsKeypoints::from_lua_multi(args, lua) { + Ok(NumberSequence { + keypoints: keypoints.iter().map(|k| **k).collect(), + }) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + TableBuilder::new(lua)? + .with_function("new", number_sequence_new)? + .build_readonly() + } +} + +impl LuaUserData for NumberSequence { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Keypoints", |_, this| Ok(this.keypoints.clone())); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for NumberSequence { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (index, keypoint) in self.keypoints.iter().enumerate() { + if index < self.keypoints.len() - 1 { + write!(f, "{}, ", keypoint)?; + } else { + write!(f, "{}", keypoint)?; + } + } + Ok(()) + } +} + +impl From for NumberSequence { + fn from(v: DomNumberSequence) -> Self { + Self { + keypoints: v + .keypoints + .iter() + .cloned() + .map(NumberSequenceKeypoint::from) + .collect(), + } + } +} + +impl From for DomNumberSequence { + fn from(v: NumberSequence) -> Self { + Self { + keypoints: v + .keypoints + .iter() + .cloned() + .map(DomNumberSequenceKeypoint::from) + .collect(), + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/number_sequence_keypoint.rs b/crates/lune-roblox/src/datatypes/types/number_sequence_keypoint.rs new file mode 100644 index 0000000..45190eb --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/number_sequence_keypoint.rs @@ -0,0 +1,79 @@ +use core::fmt; + +use mlua::prelude::*; +use rbx_dom_weak::types::NumberSequenceKeypoint as DomNumberSequenceKeypoint; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::super::*; + +/** + An implementation of the [NumberSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequenceKeypoint) Roblox datatype. + + This implements all documented properties, methods & constructors of the NumberSequenceKeypoint class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct NumberSequenceKeypoint { + pub(crate) time: f32, + pub(crate) value: f32, + pub(crate) envelope: f32, +} + +impl LuaExportsTable<'_> for NumberSequenceKeypoint { + const EXPORT_NAME: &'static str = "NumberSequenceKeypoint"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let number_sequence_keypoint_new = |_, (time, value, envelope): (f32, f32, Option)| { + Ok(NumberSequenceKeypoint { + time, + value, + envelope: envelope.unwrap_or_default(), + }) + }; + + TableBuilder::new(lua)? + .with_function("new", number_sequence_keypoint_new)? + .build_readonly() + } +} + +impl LuaUserData for NumberSequenceKeypoint { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Time", |_, this| Ok(this.time)); + fields.add_field_method_get("Value", |_, this| Ok(this.value)); + fields.add_field_method_get("Envelope", |_, this| Ok(this.envelope)); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for NumberSequenceKeypoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} > {}", self.time, self.value) + } +} + +impl From for NumberSequenceKeypoint { + fn from(v: DomNumberSequenceKeypoint) -> Self { + Self { + time: v.time, + value: v.value, + envelope: v.envelope, + } + } +} + +impl From for DomNumberSequenceKeypoint { + fn from(v: NumberSequenceKeypoint) -> Self { + Self { + time: v.time, + value: v.value, + envelope: v.envelope, + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/physical_properties.rs b/crates/lune-roblox/src/datatypes/types/physical_properties.rs new file mode 100644 index 0000000..d2a0de6 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/physical_properties.rs @@ -0,0 +1,188 @@ +use core::fmt; + +use mlua::prelude::*; +use rbx_dom_weak::types::CustomPhysicalProperties as DomCustomPhysicalProperties; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, EnumItem}; + +/** + An implementation of the [PhysicalProperties](https://create.roblox.com/docs/reference/engine/datatypes/PhysicalProperties) Roblox datatype. + + This implements all documented properties, methods & constructors of the PhysicalProperties class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct PhysicalProperties { + pub(crate) density: f32, + pub(crate) friction: f32, + pub(crate) friction_weight: f32, + pub(crate) elasticity: f32, + pub(crate) elasticity_weight: f32, +} + +impl PhysicalProperties { + pub(crate) fn from_material(material_enum_item: &EnumItem) -> Option { + MATERIAL_ENUM_MAP + .iter() + .find(|props| props.0 == material_enum_item.name) + .map(|props| PhysicalProperties { + density: props.1, + friction: props.2, + elasticity: props.3, + friction_weight: props.4, + elasticity_weight: props.5, + }) + } +} + +impl LuaExportsTable<'_> for PhysicalProperties { + const EXPORT_NAME: &'static str = "PhysicalProperties"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + type ArgsMaterial<'lua> = LuaUserDataRef<'lua, EnumItem>; + type ArgsNumbers = (f32, f32, f32, Option, Option); + + let physical_properties_new = |lua, args: LuaMultiValue| { + if let Ok(value) = ArgsMaterial::from_lua_multi(args.clone(), lua) { + if value.parent.desc.name == "Material" { + match PhysicalProperties::from_material(&value) { + Some(props) => Ok(props), + None => Err(LuaError::RuntimeError(format!( + "Found unknown Material '{}'", + value.name + ))), + } + } else { + Err(LuaError::RuntimeError(format!( + "Expected argument #1 to be a Material, got {}", + value.parent.desc.name + ))) + } + } else if let Ok((density, friction, elasticity, friction_weight, elasticity_weight)) = + ArgsNumbers::from_lua_multi(args, lua) + { + Ok(PhysicalProperties { + density, + friction, + friction_weight: friction_weight.unwrap_or(1.0), + elasticity, + elasticity_weight: elasticity_weight.unwrap_or(1.0), + }) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + TableBuilder::new(lua)? + .with_function("new", physical_properties_new)? + .build_readonly() + } +} + +impl LuaUserData for PhysicalProperties { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Density", |_, this| Ok(this.density)); + fields.add_field_method_get("Friction", |_, this| Ok(this.friction)); + fields.add_field_method_get("FrictionWeight", |_, this| Ok(this.friction_weight)); + fields.add_field_method_get("Elasticity", |_, this| Ok(this.elasticity)); + fields.add_field_method_get("ElasticityWeight", |_, this| Ok(this.elasticity_weight)); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for PhysicalProperties { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}, {}, {}, {}, {}", + self.density, + self.friction, + self.elasticity, + self.friction_weight, + self.elasticity_weight + ) + } +} + +impl From for PhysicalProperties { + fn from(v: DomCustomPhysicalProperties) -> Self { + Self { + density: v.density, + friction: v.friction, + friction_weight: v.friction_weight, + elasticity: v.elasticity, + elasticity_weight: v.elasticity_weight, + } + } +} + +impl From for DomCustomPhysicalProperties { + fn from(v: PhysicalProperties) -> Self { + DomCustomPhysicalProperties { + density: v.density, + friction: v.friction, + friction_weight: v.friction_weight, + elasticity: v.elasticity, + elasticity_weight: v.elasticity_weight, + } + } +} + +/* + + NOTE: The material definitions below are generated using the + physical_properties_enum_map script in the scripts dir next + to src, which can be ran in the Roblox Studio command bar + +*/ + +#[rustfmt::skip] +const MATERIAL_ENUM_MAP: &[(&str, f32, f32, f32, f32, f32)] = &[ + ("Plastic", 0.70, 0.30, 0.50, 1.00, 1.00), + ("Wood", 0.35, 0.48, 0.20, 1.00, 1.00), + ("Slate", 2.69, 0.40, 0.20, 1.00, 1.00), + ("Concrete", 2.40, 0.70, 0.20, 0.30, 1.00), + ("CorrodedMetal", 7.85, 0.70, 0.20, 1.00, 1.00), + ("DiamondPlate", 7.85, 0.35, 0.25, 1.00, 1.00), + ("Foil", 2.70, 0.40, 0.25, 1.00, 1.00), + ("Grass", 0.90, 0.40, 0.10, 1.00, 1.50), + ("Ice", 0.92, 0.02, 0.15, 3.00, 1.00), + ("Marble", 2.56, 0.20, 0.17, 1.00, 1.00), + ("Granite", 2.69, 0.40, 0.20, 1.00, 1.00), + ("Brick", 1.92, 0.80, 0.15, 0.30, 1.00), + ("Pebble", 2.40, 0.40, 0.17, 1.00, 1.50), + ("Sand", 1.60, 0.50, 0.05, 5.00, 2.50), + ("Fabric", 0.70, 0.35, 0.05, 1.00, 1.00), + ("SmoothPlastic", 0.70, 0.20, 0.50, 1.00, 1.00), + ("Metal", 7.85, 0.40, 0.25, 1.00, 1.00), + ("WoodPlanks", 0.35, 0.48, 0.20, 1.00, 1.00), + ("Cobblestone", 2.69, 0.50, 0.17, 1.00, 1.00), + ("Air", 0.01, 0.01, 0.01, 1.00, 1.00), + ("Water", 1.00, 0.00, 0.01, 1.00, 1.00), + ("Rock", 2.69, 0.50, 0.17, 1.00, 1.00), + ("Glacier", 0.92, 0.05, 0.15, 2.00, 1.00), + ("Snow", 0.90, 0.30, 0.03, 3.00, 4.00), + ("Sandstone", 2.69, 0.50, 0.15, 5.00, 1.00), + ("Mud", 0.90, 0.30, 0.07, 3.00, 4.00), + ("Basalt", 2.69, 0.70, 0.15, 0.30, 1.00), + ("Ground", 0.90, 0.45, 0.10, 1.00, 1.00), + ("CrackedLava", 2.69, 0.65, 0.15, 1.00, 1.00), + ("Neon", 0.70, 0.30, 0.20, 1.00, 1.00), + ("Glass", 2.40, 0.25, 0.20, 1.00, 1.00), + ("Asphalt", 2.36, 0.80, 0.20, 0.30, 1.00), + ("LeafyGrass", 0.90, 0.40, 0.10, 2.00, 2.00), + ("Salt", 2.16, 0.50, 0.05, 1.00, 1.00), + ("Limestone", 2.69, 0.50, 0.15, 1.00, 1.00), + ("Pavement", 2.69, 0.50, 0.17, 0.30, 1.00), + ("ForceField", 2.40, 0.25, 0.20, 1.00, 1.00), +]; diff --git a/crates/lune-roblox/src/datatypes/types/ray.rs b/crates/lune-roblox/src/datatypes/types/ray.rs new file mode 100644 index 0000000..23a44d7 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/ray.rs @@ -0,0 +1,102 @@ +use core::fmt; + +use glam::Vec3; +use mlua::prelude::*; +use rbx_dom_weak::types::Ray as DomRay; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, Vector3}; + +/** + An implementation of the [Ray](https://create.roblox.com/docs/reference/engine/datatypes/Ray) + Roblox datatype, backed by [`glam::Vec3`]. + + This implements all documented properties, methods & constructors of the Ray class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Ray { + pub(crate) origin: Vec3, + pub(crate) direction: Vec3, +} + +impl Ray { + fn closest_point(&self, point: Vec3) -> Vec3 { + let norm = self.direction.normalize(); + let lhs = point - self.origin; + + let dot_product = lhs.dot(norm).max(0.0); + self.origin + norm * dot_product + } +} + +impl LuaExportsTable<'_> for Ray { + const EXPORT_NAME: &'static str = "Ray"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let ray_new = + |_, (origin, direction): (LuaUserDataRef, LuaUserDataRef)| { + Ok(Ray { + origin: origin.0, + direction: direction.0, + }) + }; + + TableBuilder::new(lua)? + .with_function("new", ray_new)? + .build_readonly() + } +} + +impl LuaUserData for Ray { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Origin", |_, this| Ok(Vector3(this.origin))); + fields.add_field_method_get("Direction", |_, this| Ok(Vector3(this.direction))); + fields.add_field_method_get("Unit", |_, this| { + Ok(Ray { + origin: this.origin, + direction: this.direction.normalize(), + }) + }); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Methods + methods.add_method("ClosestPoint", |_, this, to: LuaUserDataRef| { + Ok(Vector3(this.closest_point(to.0))) + }); + methods.add_method("Distance", |_, this, to: LuaUserDataRef| { + let closest = this.closest_point(to.0); + Ok((closest - to.0).length()) + }); + // Metamethods + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for Ray { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}", Vector3(self.origin), Vector3(self.direction)) + } +} + +impl From for Ray { + fn from(v: DomRay) -> Self { + Ray { + origin: Vector3::from(v.origin).0, + direction: Vector3::from(v.direction).0, + } + } +} + +impl From for DomRay { + fn from(v: Ray) -> Self { + DomRay { + origin: Vector3(v.origin).into(), + direction: Vector3(v.direction).into(), + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/rect.rs b/crates/lune-roblox/src/datatypes/types/rect.rs new file mode 100644 index 0000000..0fa805c --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/rect.rs @@ -0,0 +1,129 @@ +use core::fmt; +use std::ops; + +use glam::Vec2; +use mlua::prelude::*; +use rbx_dom_weak::types::Rect as DomRect; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, Vector2}; + +/** + An implementation of the [Rect](https://create.roblox.com/docs/reference/engine/datatypes/Rect) + Roblox datatype, backed by [`glam::Vec2`]. + + This implements all documented properties, methods & constructors of the Rect class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Rect { + pub(crate) min: Vec2, + pub(crate) max: Vec2, +} + +impl Rect { + fn new(lhs: Vec2, rhs: Vec2) -> Self { + Self { + min: lhs.min(rhs), + max: lhs.max(rhs), + } + } +} + +impl LuaExportsTable<'_> for Rect { + const EXPORT_NAME: &'static str = "Rect"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + type ArgsVector2s<'lua> = ( + Option>, + Option>, + ); + type ArgsNums = (Option, Option, Option, Option); + + let rect_new = |lua, args: LuaMultiValue| { + if let Ok((min, max)) = ArgsVector2s::from_lua_multi(args.clone(), lua) { + Ok(Rect::new( + min.map(|m| *m).unwrap_or_default().0, + max.map(|m| *m).unwrap_or_default().0, + )) + } else if let Ok((x0, y0, x1, y1)) = ArgsNums::from_lua_multi(args, lua) { + let min = Vec2::new(x0.unwrap_or_default(), y0.unwrap_or_default()); + let max = Vec2::new(x1.unwrap_or_default(), y1.unwrap_or_default()); + Ok(Rect::new(min, max)) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + TableBuilder::new(lua)? + .with_function("new", rect_new)? + .build_readonly() + } +} + +impl LuaUserData for Rect { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Min", |_, this| Ok(Vector2(this.min))); + fields.add_field_method_get("Max", |_, this| Ok(Vector2(this.max))); + fields.add_field_method_get("Width", |_, this| Ok(this.max.x - this.min.x)); + fields.add_field_method_get("Height", |_, this| Ok(this.max.y - this.min.y)); + } + + 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_meta_method(LuaMetaMethod::Unm, userdata_impl_unm); + methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add); + methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub); + } +} + +impl fmt::Display for Rect { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}", self.min, self.max) + } +} + +impl ops::Neg for Rect { + type Output = Self; + fn neg(self) -> Self::Output { + Rect::new(-self.min, -self.max) + } +} + +impl ops::Add for Rect { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + Rect::new(self.min + rhs.min, self.max + rhs.max) + } +} + +impl ops::Sub for Rect { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + Rect::new(self.min - rhs.min, self.max - rhs.max) + } +} + +impl From for Rect { + fn from(v: DomRect) -> Self { + Rect { + min: Vec2::new(v.min.x, v.min.y), + max: Vec2::new(v.max.x, v.max.y), + } + } +} + +impl From for DomRect { + fn from(v: Rect) -> Self { + DomRect { + min: Vector2(v.min).into(), + max: Vector2(v.max).into(), + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/region3.rs b/crates/lune-roblox/src/datatypes/types/region3.rs new file mode 100644 index 0000000..cb44df7 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/region3.rs @@ -0,0 +1,86 @@ +use core::fmt; + +use glam::{Mat4, Vec3}; +use mlua::prelude::*; +use rbx_dom_weak::types::Region3 as DomRegion3; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, CFrame, Vector3}; + +/** + An implementation of the [Region3](https://create.roblox.com/docs/reference/engine/datatypes/Region3) + Roblox datatype, backed by [`glam::Vec3`]. + + This implements all documented properties, methods & constructors of the Region3 class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Region3 { + pub(crate) min: Vec3, + pub(crate) max: Vec3, +} + +impl LuaExportsTable<'_> for Region3 { + const EXPORT_NAME: &'static str = "Region3"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let region3_new = |_, (min, max): (LuaUserDataRef, LuaUserDataRef)| { + Ok(Region3 { + min: min.0, + max: max.0, + }) + }; + + TableBuilder::new(lua)? + .with_function("new", region3_new)? + .build_readonly() + } +} + +impl LuaUserData for Region3 { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("CFrame", |_, this| { + Ok(CFrame(Mat4::from_translation(this.min.lerp(this.max, 0.5)))) + }); + fields.add_field_method_get("Size", |_, this| Ok(Vector3(this.max - this.min))); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Methods + methods.add_method("ExpandToGrid", |_, this, resolution: f32| { + Ok(Region3 { + min: (this.min / resolution).floor() * resolution, + max: (this.max / resolution).ceil() * resolution, + }) + }); + // Metamethods + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for Region3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}", Vector3(self.min), Vector3(self.max)) + } +} + +impl From for Region3 { + fn from(v: DomRegion3) -> Self { + Region3 { + min: Vector3::from(v.min).0, + max: Vector3::from(v.max).0, + } + } +} + +impl From for DomRegion3 { + fn from(v: Region3) -> Self { + DomRegion3 { + min: Vector3(v.min).into(), + max: Vector3(v.max).into(), + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/region3int16.rs b/crates/lune-roblox/src/datatypes/types/region3int16.rs new file mode 100644 index 0000000..61075b0 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/region3int16.rs @@ -0,0 +1,77 @@ +use core::fmt; + +use glam::IVec3; +use mlua::prelude::*; +use rbx_dom_weak::types::Region3int16 as DomRegion3int16; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, Vector3int16}; + +/** + An implementation of the [Region3int16](https://create.roblox.com/docs/reference/engine/datatypes/Region3int16) + Roblox datatype, backed by [`glam::IVec3`]. + + This implements all documented properties, methods & constructors of the Region3int16 class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Region3int16 { + pub(crate) min: IVec3, + pub(crate) max: IVec3, +} + +impl LuaExportsTable<'_> for Region3int16 { + const EXPORT_NAME: &'static str = "Region3int16"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let region3int16_new = + |_, (min, max): (LuaUserDataRef, LuaUserDataRef)| { + Ok(Region3int16 { + min: min.0, + max: max.0, + }) + }; + + TableBuilder::new(lua)? + .with_function("new", region3int16_new)? + .build_readonly() + } +} + +impl LuaUserData for Region3int16 { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Min", |_, this| Ok(Vector3int16(this.min))); + fields.add_field_method_get("Max", |_, this| Ok(Vector3int16(this.max))); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl fmt::Display for Region3int16 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}", Vector3int16(self.min), Vector3int16(self.max)) + } +} + +impl From for Region3int16 { + fn from(v: DomRegion3int16) -> Self { + Region3int16 { + min: Vector3int16::from(v.min).0, + max: Vector3int16::from(v.max).0, + } + } +} + +impl From for DomRegion3int16 { + fn from(v: Region3int16) -> Self { + DomRegion3int16 { + min: Vector3int16(v.min).into(), + max: Vector3int16(v.max).into(), + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/udim.rs b/crates/lune-roblox/src/datatypes/types/udim.rs new file mode 100644 index 0000000..8a48bb0 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/udim.rs @@ -0,0 +1,123 @@ +use core::fmt; +use std::ops; + +use mlua::prelude::*; +use rbx_dom_weak::types::UDim as DomUDim; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::super::*; + +/** + An implementation of the [UDim](https://create.roblox.com/docs/reference/engine/datatypes/UDim) Roblox datatype. + + This implements all documented properties, methods & constructors of the UDim class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct UDim { + pub(crate) scale: f32, + pub(crate) offset: i32, +} + +impl UDim { + pub(super) fn new(scale: f32, offset: i32) -> Self { + Self { scale, offset } + } +} + +impl LuaExportsTable<'_> for UDim { + const EXPORT_NAME: &'static str = "UDim"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let udim_new = |_, (scale, offset): (Option, Option)| { + Ok(UDim { + scale: scale.unwrap_or_default(), + offset: offset.unwrap_or_default(), + }) + }; + + TableBuilder::new(lua)? + .with_function("new", udim_new)? + .build_readonly() + } +} + +impl LuaUserData for UDim { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Scale", |_, this| Ok(this.scale)); + fields.add_field_method_get("Offset", |_, this| Ok(this.offset)); + } + + 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_meta_method(LuaMetaMethod::Unm, userdata_impl_unm); + methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add); + methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub); + } +} + +impl Default for UDim { + fn default() -> Self { + Self { + scale: 0f32, + offset: 0, + } + } +} + +impl fmt::Display for UDim { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}", self.scale, self.offset) + } +} + +impl ops::Neg for UDim { + type Output = Self; + fn neg(self) -> Self::Output { + UDim { + scale: -self.scale, + offset: -self.offset, + } + } +} + +impl ops::Add for UDim { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + UDim { + scale: self.scale + rhs.scale, + offset: self.offset + rhs.offset, + } + } +} + +impl ops::Sub for UDim { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + UDim { + scale: self.scale - rhs.scale, + offset: self.offset - rhs.offset, + } + } +} + +impl From for UDim { + fn from(v: DomUDim) -> Self { + UDim { + scale: v.scale, + offset: v.offset, + } + } +} + +impl From for DomUDim { + fn from(v: UDim) -> Self { + DomUDim { + scale: v.scale, + offset: v.offset, + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/udim2.rs b/crates/lune-roblox/src/datatypes/types/udim2.rs new file mode 100644 index 0000000..b3baa3a --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/udim2.rs @@ -0,0 +1,170 @@ +use core::fmt; +use std::ops; + +use glam::Vec2; +use mlua::prelude::*; +use rbx_dom_weak::types::UDim2 as DomUDim2; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::{super::*, UDim}; + +/** + An implementation of the [UDim2](https://create.roblox.com/docs/reference/engine/datatypes/UDim2) Roblox datatype. + + This implements all documented properties, methods & constructors of the UDim2 class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct UDim2 { + pub(crate) x: UDim, + pub(crate) y: UDim, +} + +impl LuaExportsTable<'_> for UDim2 { + const EXPORT_NAME: &'static str = "UDim2"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let udim2_from_offset = |_, (x, y): (Option, Option)| { + Ok(UDim2 { + x: UDim::new(0f32, x.unwrap_or_default()), + y: UDim::new(0f32, y.unwrap_or_default()), + }) + }; + + let udim2_from_scale = |_, (x, y): (Option, Option)| { + Ok(UDim2 { + x: UDim::new(x.unwrap_or_default(), 0), + y: UDim::new(y.unwrap_or_default(), 0), + }) + }; + + type ArgsUDims<'lua> = ( + Option>, + Option>, + ); + type ArgsNums = (Option, Option, Option, Option); + let udim2_new = |lua, args: LuaMultiValue| { + if let Ok((x, y)) = ArgsUDims::from_lua_multi(args.clone(), lua) { + Ok(UDim2 { + x: x.map(|x| *x).unwrap_or_default(), + y: y.map(|y| *y).unwrap_or_default(), + }) + } else if let Ok((sx, ox, sy, oy)) = ArgsNums::from_lua_multi(args, lua) { + Ok(UDim2 { + x: UDim::new(sx.unwrap_or_default(), ox.unwrap_or_default()), + y: UDim::new(sy.unwrap_or_default(), oy.unwrap_or_default()), + }) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + TableBuilder::new(lua)? + .with_function("fromOffset", udim2_from_offset)? + .with_function("fromScale", udim2_from_scale)? + .with_function("new", udim2_new)? + .build_readonly() + } +} + +impl LuaUserData for UDim2 { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("X", |_, this| Ok(this.x)); + fields.add_field_method_get("Y", |_, this| Ok(this.y)); + fields.add_field_method_get("Width", |_, this| Ok(this.x)); + fields.add_field_method_get("Height", |_, this| Ok(this.y)); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Methods + methods.add_method( + "Lerp", + |_, this, (goal, alpha): (LuaUserDataRef, f32)| { + let this_x = Vec2::new(this.x.scale, this.x.offset as f32); + let goal_x = Vec2::new(goal.x.scale, goal.x.offset as f32); + + let this_y = Vec2::new(this.y.scale, this.y.offset as f32); + let goal_y = Vec2::new(goal.y.scale, goal.y.offset as f32); + + let x = this_x.lerp(goal_x, alpha); + let y = this_y.lerp(goal_y, alpha); + + Ok(UDim2 { + x: UDim { + scale: x.x, + offset: x.y.clamp(i32::MIN as f32, i32::MAX as f32).round() as i32, + }, + y: UDim { + scale: y.x, + offset: y.y.clamp(i32::MIN as f32, i32::MAX as f32).round() as i32, + }, + }) + }, + ); + // Metamethods + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + methods.add_meta_method(LuaMetaMethod::Unm, userdata_impl_unm); + methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add); + methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub); + } +} + +impl fmt::Display for UDim2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}", self.x, self.y) + } +} + +impl ops::Neg for UDim2 { + type Output = Self; + fn neg(self) -> Self::Output { + UDim2 { + x: -self.x, + y: -self.y, + } + } +} + +impl ops::Add for UDim2 { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + UDim2 { + x: self.x + rhs.x, + y: self.y + rhs.y, + } + } +} + +impl ops::Sub for UDim2 { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + UDim2 { + x: self.x - rhs.x, + y: self.y - rhs.y, + } + } +} + +impl From for UDim2 { + fn from(v: DomUDim2) -> Self { + UDim2 { + x: v.x.into(), + y: v.y.into(), + } + } +} + +impl From for DomUDim2 { + fn from(v: UDim2) -> Self { + DomUDim2 { + x: v.x.into(), + y: v.y.into(), + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/vector2.rs b/crates/lune-roblox/src/datatypes/types/vector2.rs new file mode 100644 index 0000000..e0c352e --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/vector2.rs @@ -0,0 +1,151 @@ +use core::fmt; +use std::ops; + +use glam::{Vec2, Vec3}; +use mlua::prelude::*; +use rbx_dom_weak::types::Vector2 as DomVector2; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::super::*; + +/** + An implementation of the [Vector2](https://create.roblox.com/docs/reference/engine/datatypes/Vector2) + Roblox datatype, backed by [`glam::Vec2`]. + + This implements all documented properties, methods & + constructors of the Vector2 class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Vector2(pub Vec2); + +impl LuaExportsTable<'_> for Vector2 { + const EXPORT_NAME: &'static str = "Vector2"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let vector2_new = |_, (x, y): (Option, Option)| { + Ok(Vector2(Vec2 { + x: x.unwrap_or_default(), + y: y.unwrap_or_default(), + })) + }; + + TableBuilder::new(lua)? + .with_value("xAxis", Vector2(Vec2::X))? + .with_value("yAxis", Vector2(Vec2::Y))? + .with_value("zero", Vector2(Vec2::ZERO))? + .with_value("one", Vector2(Vec2::ONE))? + .with_function("new", vector2_new)? + .build_readonly() + } +} + +impl LuaUserData for Vector2 { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Magnitude", |_, this| Ok(this.0.length())); + fields.add_field_method_get("Unit", |_, this| Ok(Vector2(this.0.normalize()))); + fields.add_field_method_get("X", |_, this| Ok(this.0.x)); + fields.add_field_method_get("Y", |_, this| Ok(this.0.y)); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Methods + methods.add_method("Cross", |_, this, rhs: LuaUserDataRef| { + let this_v3 = Vec3::new(this.0.x, this.0.y, 0f32); + let rhs_v3 = Vec3::new(rhs.0.x, rhs.0.y, 0f32); + Ok(this_v3.cross(rhs_v3).z) + }); + methods.add_method("Dot", |_, this, rhs: LuaUserDataRef| { + Ok(this.0.dot(rhs.0)) + }); + methods.add_method( + "Lerp", + |_, this, (rhs, alpha): (LuaUserDataRef, f32)| { + Ok(Vector2(this.0.lerp(rhs.0, alpha))) + }, + ); + methods.add_method("Max", |_, this, rhs: LuaUserDataRef| { + Ok(Vector2(this.0.max(rhs.0))) + }); + methods.add_method("Min", |_, this, rhs: LuaUserDataRef| { + Ok(Vector2(this.0.min(rhs.0))) + }); + // Metamethods + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + methods.add_meta_method(LuaMetaMethod::Unm, userdata_impl_unm); + methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add); + methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub); + methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_f32); + methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_f32); + } +} + +impl fmt::Display for Vector2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}", self.0.x, self.0.y) + } +} + +impl ops::Neg for Vector2 { + type Output = Self; + fn neg(self) -> Self::Output { + Vector2(-self.0) + } +} + +impl ops::Add for Vector2 { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + Vector2(self.0 + rhs.0) + } +} + +impl ops::Sub for Vector2 { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + Vector2(self.0 - rhs.0) + } +} + +impl ops::Mul for Vector2 { + type Output = Vector2; + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0 * rhs.0) + } +} + +impl ops::Mul for Vector2 { + type Output = Vector2; + fn mul(self, rhs: f32) -> Self::Output { + Self(self.0 * rhs) + } +} + +impl ops::Div for Vector2 { + type Output = Vector2; + fn div(self, rhs: Self) -> Self::Output { + Self(self.0 / rhs.0) + } +} + +impl ops::Div for Vector2 { + type Output = Vector2; + fn div(self, rhs: f32) -> Self::Output { + Self(self.0 / rhs) + } +} + +impl From for Vector2 { + fn from(v: DomVector2) -> Self { + Vector2(Vec2 { x: v.x, y: v.y }) + } +} + +impl From for DomVector2 { + fn from(v: Vector2) -> Self { + DomVector2 { x: v.0.x, y: v.0.y } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/vector2int16.rs b/crates/lune-roblox/src/datatypes/types/vector2int16.rs new file mode 100644 index 0000000..31931a0 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/vector2int16.rs @@ -0,0 +1,129 @@ +use core::fmt; +use std::ops; + +use glam::IVec2; +use mlua::prelude::*; +use rbx_dom_weak::types::Vector2int16 as DomVector2int16; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::super::*; + +/** + An implementation of the [Vector2int16](https://create.roblox.com/docs/reference/engine/datatypes/Vector2int16) + Roblox datatype, backed by [`glam::IVec2`]. + + This implements all documented properties, methods & + constructors of the Vector2int16 class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Vector2int16(pub IVec2); + +impl LuaExportsTable<'_> for Vector2int16 { + const EXPORT_NAME: &'static str = "Vector2int16"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let vector2int16_new = |_, (x, y): (Option, Option)| { + Ok(Vector2int16(IVec2 { + x: x.unwrap_or_default() as i32, + y: y.unwrap_or_default() as i32, + })) + }; + + TableBuilder::new(lua)? + .with_function("new", vector2int16_new)? + .build_readonly() + } +} + +impl LuaUserData for Vector2int16 { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("X", |_, this| Ok(this.0.x)); + fields.add_field_method_get("Y", |_, this| Ok(this.0.y)); + } + + 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_meta_method(LuaMetaMethod::Unm, userdata_impl_unm); + methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add); + methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub); + methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_i32); + methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_i32); + } +} + +impl fmt::Display for Vector2int16 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}", self.0.x, self.0.y) + } +} + +impl ops::Neg for Vector2int16 { + type Output = Self; + fn neg(self) -> Self::Output { + Vector2int16(-self.0) + } +} + +impl ops::Add for Vector2int16 { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + Vector2int16(self.0 + rhs.0) + } +} + +impl ops::Sub for Vector2int16 { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + Vector2int16(self.0 - rhs.0) + } +} + +impl ops::Mul for Vector2int16 { + type Output = Vector2int16; + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0 * rhs.0) + } +} + +impl ops::Mul for Vector2int16 { + type Output = Vector2int16; + fn mul(self, rhs: i32) -> Self::Output { + Self(self.0 * rhs) + } +} + +impl ops::Div for Vector2int16 { + type Output = Vector2int16; + fn div(self, rhs: Self) -> Self::Output { + Self(self.0 / rhs.0) + } +} + +impl ops::Div for Vector2int16 { + type Output = Vector2int16; + fn div(self, rhs: i32) -> Self::Output { + Self(self.0 / rhs) + } +} + +impl From for Vector2int16 { + fn from(v: DomVector2int16) -> Self { + Vector2int16(IVec2 { + x: v.x.clamp(i16::MIN, i16::MAX) as i32, + y: v.y.clamp(i16::MIN, i16::MAX) as i32, + }) + } +} + +impl From for DomVector2int16 { + fn from(v: Vector2int16) -> Self { + DomVector2int16 { + x: v.0.x.clamp(i16::MIN as i32, i16::MAX as i32) as i16, + y: v.0.y.clamp(i16::MIN as i32, i16::MAX as i32) as i16, + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/vector3.rs b/crates/lune-roblox/src/datatypes/types/vector3.rs new file mode 100644 index 0000000..6708b8c --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/vector3.rs @@ -0,0 +1,222 @@ +use core::fmt; +use std::ops; + +use glam::Vec3; +use mlua::prelude::*; +use rbx_dom_weak::types::Vector3 as DomVector3; + +use lune_utils::TableBuilder; + +use crate::{datatypes::util::round_float_decimal, exports::LuaExportsTable}; + +use super::{super::*, EnumItem}; + +/** + An implementation of the [Vector3](https://create.roblox.com/docs/reference/engine/datatypes/Vector3) + Roblox datatype, backed by [`glam::Vec3`]. + + This implements all documented properties, methods & + constructors of the Vector3 class as of March 2023. + + Note that this does not use native Luau vectors to simplify implementation + and instead allow us to implement all abovementioned APIs accurately. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Vector3(pub Vec3); + +impl LuaExportsTable<'_> for Vector3 { + const EXPORT_NAME: &'static str = "Vector3"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let vector3_from_axis = |_, normal_id: LuaUserDataRef| { + if normal_id.parent.desc.name == "Axis" { + Ok(match normal_id.name.as_str() { + "X" => Vector3(Vec3::X), + "Y" => Vector3(Vec3::Y), + "Z" => Vector3(Vec3::Z), + name => { + return Err(LuaError::RuntimeError(format!( + "Axis '{}' is not known", + name + ))) + } + }) + } else { + Err(LuaError::RuntimeError(format!( + "EnumItem must be a Axis, got {}", + normal_id.parent.desc.name + ))) + } + }; + + let vector3_from_normal_id = |_, normal_id: LuaUserDataRef| { + if normal_id.parent.desc.name == "NormalId" { + Ok(match normal_id.name.as_str() { + "Left" => Vector3(Vec3::X), + "Top" => Vector3(Vec3::Y), + "Front" => Vector3(-Vec3::Z), + "Right" => Vector3(-Vec3::X), + "Bottom" => Vector3(-Vec3::Y), + "Back" => Vector3(Vec3::Z), + name => { + return Err(LuaError::RuntimeError(format!( + "NormalId '{}' is not known", + name + ))) + } + }) + } else { + Err(LuaError::RuntimeError(format!( + "EnumItem must be a NormalId, got {}", + normal_id.parent.desc.name + ))) + } + }; + + let vector3_new = |_, (x, y, z): (Option, Option, Option)| { + Ok(Vector3(Vec3 { + x: x.unwrap_or_default(), + y: y.unwrap_or_default(), + z: z.unwrap_or_default(), + })) + }; + + TableBuilder::new(lua)? + .with_value("xAxis", Vector3(Vec3::X))? + .with_value("yAxis", Vector3(Vec3::Y))? + .with_value("zAxis", Vector3(Vec3::Z))? + .with_value("zero", Vector3(Vec3::ZERO))? + .with_value("one", Vector3(Vec3::ONE))? + .with_function("fromAxis", vector3_from_axis)? + .with_function("fromNormalId", vector3_from_normal_id)? + .with_function("new", vector3_new)? + .build_readonly() + } +} + +impl LuaUserData for Vector3 { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Magnitude", |_, this| Ok(this.0.length())); + fields.add_field_method_get("Unit", |_, this| Ok(Vector3(this.0.normalize()))); + fields.add_field_method_get("X", |_, this| Ok(this.0.x)); + fields.add_field_method_get("Y", |_, this| Ok(this.0.y)); + fields.add_field_method_get("Z", |_, this| Ok(this.0.z)); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + // Methods + methods.add_method("Angle", |_, this, rhs: LuaUserDataRef| { + Ok(this.0.angle_between(rhs.0)) + }); + methods.add_method("Cross", |_, this, rhs: LuaUserDataRef| { + Ok(Vector3(this.0.cross(rhs.0))) + }); + methods.add_method("Dot", |_, this, rhs: LuaUserDataRef| { + Ok(this.0.dot(rhs.0)) + }); + methods.add_method( + "FuzzyEq", + |_, this, (rhs, epsilon): (LuaUserDataRef, f32)| { + let eq_x = (rhs.0.x - this.0.x).abs() <= epsilon; + let eq_y = (rhs.0.y - this.0.y).abs() <= epsilon; + let eq_z = (rhs.0.z - this.0.z).abs() <= epsilon; + Ok(eq_x && eq_y && eq_z) + }, + ); + methods.add_method( + "Lerp", + |_, this, (rhs, alpha): (LuaUserDataRef, f32)| { + Ok(Vector3(this.0.lerp(rhs.0, alpha))) + }, + ); + methods.add_method("Max", |_, this, rhs: LuaUserDataRef| { + Ok(Vector3(this.0.max(rhs.0))) + }); + methods.add_method("Min", |_, this, rhs: LuaUserDataRef| { + Ok(Vector3(this.0.min(rhs.0))) + }); + // Metamethods + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + methods.add_meta_method(LuaMetaMethod::Unm, userdata_impl_unm); + methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add); + methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub); + methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_f32); + methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_f32); + } +} + +impl fmt::Display for Vector3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}, {}", self.0.x, self.0.y, self.0.z) + } +} + +impl ops::Neg for Vector3 { + type Output = Self; + fn neg(self) -> Self::Output { + Vector3(-self.0) + } +} + +impl ops::Add for Vector3 { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + Vector3(self.0 + rhs.0) + } +} + +impl ops::Sub for Vector3 { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + Vector3(self.0 - rhs.0) + } +} + +impl ops::Mul for Vector3 { + type Output = Vector3; + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0 * rhs.0) + } +} + +impl ops::Mul for Vector3 { + type Output = Vector3; + fn mul(self, rhs: f32) -> Self::Output { + Self(self.0 * rhs) + } +} + +impl ops::Div for Vector3 { + type Output = Vector3; + fn div(self, rhs: Self) -> Self::Output { + Self(self.0 / rhs.0) + } +} + +impl ops::Div for Vector3 { + type Output = Vector3; + fn div(self, rhs: f32) -> Self::Output { + Self(self.0 / rhs) + } +} + +impl From for Vector3 { + fn from(v: DomVector3) -> Self { + Vector3(Vec3 { + x: v.x, + y: v.y, + z: v.z, + }) + } +} + +impl From for DomVector3 { + fn from(v: Vector3) -> Self { + DomVector3 { + x: round_float_decimal(v.0.x), + y: round_float_decimal(v.0.y), + z: round_float_decimal(v.0.z), + } + } +} diff --git a/crates/lune-roblox/src/datatypes/types/vector3int16.rs b/crates/lune-roblox/src/datatypes/types/vector3int16.rs new file mode 100644 index 0000000..b8f4f31 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/types/vector3int16.rs @@ -0,0 +1,133 @@ +use core::fmt; +use std::ops; + +use glam::IVec3; +use mlua::prelude::*; +use rbx_dom_weak::types::Vector3int16 as DomVector3int16; + +use lune_utils::TableBuilder; + +use crate::exports::LuaExportsTable; + +use super::super::*; + +/** + An implementation of the [Vector3int16](https://create.roblox.com/docs/reference/engine/datatypes/Vector3int16) + Roblox datatype, backed by [`glam::IVec3`]. + + This implements all documented properties, methods & + constructors of the Vector3int16 class as of March 2023. +*/ +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Vector3int16(pub IVec3); + +impl LuaExportsTable<'_> for Vector3int16 { + const EXPORT_NAME: &'static str = "Vector3int16"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let vector3int16_new = |_, (x, y, z): (Option, Option, Option)| { + Ok(Vector3int16(IVec3 { + x: x.unwrap_or_default() as i32, + y: y.unwrap_or_default() as i32, + z: z.unwrap_or_default() as i32, + })) + }; + + TableBuilder::new(lua)? + .with_function("new", vector3int16_new)? + .build_readonly() + } +} + +impl LuaUserData for Vector3int16 { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("X", |_, this| Ok(this.0.x)); + fields.add_field_method_get("Y", |_, this| Ok(this.0.y)); + fields.add_field_method_get("Z", |_, this| Ok(this.0.z)); + } + + 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_meta_method(LuaMetaMethod::Unm, userdata_impl_unm); + methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add); + methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub); + methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_i32); + methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_i32); + } +} + +impl fmt::Display for Vector3int16 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}, {}", self.0.x, self.0.y) + } +} + +impl ops::Neg for Vector3int16 { + type Output = Self; + fn neg(self) -> Self::Output { + Vector3int16(-self.0) + } +} + +impl ops::Add for Vector3int16 { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + Vector3int16(self.0 + rhs.0) + } +} + +impl ops::Sub for Vector3int16 { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + Vector3int16(self.0 - rhs.0) + } +} + +impl ops::Mul for Vector3int16 { + type Output = Vector3int16; + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0 * rhs.0) + } +} + +impl ops::Mul for Vector3int16 { + type Output = Vector3int16; + fn mul(self, rhs: i32) -> Self::Output { + Self(self.0 * rhs) + } +} + +impl ops::Div for Vector3int16 { + type Output = Vector3int16; + fn div(self, rhs: Self) -> Self::Output { + Self(self.0 / rhs.0) + } +} + +impl ops::Div for Vector3int16 { + type Output = Vector3int16; + fn div(self, rhs: i32) -> Self::Output { + Self(self.0 / rhs) + } +} + +impl From for Vector3int16 { + fn from(v: DomVector3int16) -> Self { + Vector3int16(IVec3 { + x: v.x.clamp(i16::MIN, i16::MAX) as i32, + y: v.y.clamp(i16::MIN, i16::MAX) as i32, + z: v.z.clamp(i16::MIN, i16::MAX) as i32, + }) + } +} + +impl From for DomVector3int16 { + fn from(v: Vector3int16) -> Self { + DomVector3int16 { + x: v.0.x.clamp(i16::MIN as i32, i16::MAX as i32) as i16, + y: v.0.y.clamp(i16::MIN as i32, i16::MAX as i32) as i16, + z: v.0.z.clamp(i16::MIN as i32, i16::MAX as i32) as i16, + } + } +} diff --git a/crates/lune-roblox/src/datatypes/util.rs b/crates/lune-roblox/src/datatypes/util.rs new file mode 100644 index 0000000..7855507 --- /dev/null +++ b/crates/lune-roblox/src/datatypes/util.rs @@ -0,0 +1,16 @@ +// HACK: We round to the nearest Very Small Decimal +// to reduce writing out floating point accumulation +// errors to files (mostly relevant for xml formats) +const ROUNDING: usize = 65_536; // 2 ^ 16 + +pub fn round_float_decimal(value: f32) -> f32 { + let place = ROUNDING as f32; + + // Round only the fractional part, we do not want to + // lose any float precision in case a user for some + // reason has very very large float numbers in files + let whole = value.trunc(); + let fract = (value.fract() * place).round() / place; + + whole + fract +} diff --git a/crates/lune-roblox/src/document/error.rs b/crates/lune-roblox/src/document/error.rs new file mode 100644 index 0000000..265bf45 --- /dev/null +++ b/crates/lune-roblox/src/document/error.rs @@ -0,0 +1,28 @@ +use mlua::prelude::*; +use thiserror::Error; + +#[derive(Debug, Clone, Error)] +pub enum DocumentError { + #[error("Unknown document kind")] + UnknownKind, + #[error("Unknown document format")] + UnknownFormat, + #[error("Failed to read document from buffer - {0}")] + ReadError(String), + #[error("Failed to write document to buffer - {0}")] + WriteError(String), + #[error("Failed to convert into a DataModel - the given document is not a place")] + IntoDataModelInvalidArgs, + #[error("Failed to convert into array of Instances - the given document is a model")] + IntoInstanceArrayInvalidArgs, + #[error("Failed to convert into a place - the given instance is not a DataModel")] + FromDataModelInvalidArgs, + #[error("Failed to convert into a model - a given instance is a DataModel")] + FromInstanceArrayInvalidArgs, +} + +impl From for LuaError { + fn from(value: DocumentError) -> Self { + Self::RuntimeError(value.to_string()) + } +} diff --git a/crates/lune-roblox/src/document/format.rs b/crates/lune-roblox/src/document/format.rs new file mode 100644 index 0000000..8ef6e8a --- /dev/null +++ b/crates/lune-roblox/src/document/format.rs @@ -0,0 +1,202 @@ +// Original implementation from Remodel: +// https://github.com/rojo-rbx/remodel/blob/master/src/sniff_type.rs + +use std::path::Path; + +/** + A document format specifier. + + Valid variants are the following: + + - `Binary` + - `Xml` + + Other variants are only to be used for logic internal to this crate. +*/ +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum DocumentFormat { + Binary, + Xml, +} + +impl DocumentFormat { + /** + Try to convert a file extension into a valid document format specifier. + + Returns `None` if the file extension is not a canonical roblox file format extension. + */ + pub fn from_extension(extension: impl AsRef) -> Option { + match extension.as_ref() { + "rbxl" | "rbxm" => Some(Self::Binary), + "rbxlx" | "rbxmx" => Some(Self::Xml), + _ => None, + } + } + + /** + Try to convert a file path into a valid document format specifier. + + Returns `None` if the file extension of the path + is not a canonical roblox file format extension. + */ + pub fn from_path(path: impl AsRef) -> Option { + match path + .as_ref() + .extension() + .map(|ext| ext.to_string_lossy()) + .as_deref() + { + Some("rbxl") | Some("rbxm") => Some(Self::Binary), + Some("rbxlx") | Some("rbxmx") => Some(Self::Xml), + _ => None, + } + } + + /** + Try to detect a document format specifier from file contents. + + Returns `None` if the file contents do not seem to be from a valid roblox file. + */ + pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Option { + let header = bytes.as_ref().get(0..8)?; + + if header.starts_with(b" Some(Self::Binary), + b' ' | b'>' => Some(Self::Xml), + _ => None, + } + } else { + None + } + } +} + +impl Default for DocumentFormat { + fn default() -> Self { + Self::Binary + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + #[test] + fn from_extension_binary() { + assert_eq!( + DocumentFormat::from_extension("rbxl"), + Some(DocumentFormat::Binary) + ); + + assert_eq!( + DocumentFormat::from_extension("rbxm"), + Some(DocumentFormat::Binary) + ); + } + + #[test] + fn from_extension_xml() { + assert_eq!( + DocumentFormat::from_extension("rbxlx"), + Some(DocumentFormat::Xml) + ); + + assert_eq!( + DocumentFormat::from_extension("rbxmx"), + Some(DocumentFormat::Xml) + ); + } + + #[test] + fn from_extension_invalid() { + assert_eq!(DocumentFormat::from_extension("csv"), None); + assert_eq!(DocumentFormat::from_extension("json"), None); + assert_eq!(DocumentFormat::from_extension("rbx"), None); + assert_eq!(DocumentFormat::from_extension("rbxn"), None); + assert_eq!(DocumentFormat::from_extension("xlx"), None); + assert_eq!(DocumentFormat::from_extension("xmx"), None); + } + + #[test] + fn from_path_binary() { + assert_eq!( + DocumentFormat::from_path(PathBuf::from("model.rbxl")), + Some(DocumentFormat::Binary) + ); + + assert_eq!( + DocumentFormat::from_path(PathBuf::from("model.rbxm")), + Some(DocumentFormat::Binary) + ); + } + + #[test] + fn from_path_xml() { + assert_eq!( + DocumentFormat::from_path(PathBuf::from("place.rbxlx")), + Some(DocumentFormat::Xml) + ); + + assert_eq!( + DocumentFormat::from_path(PathBuf::from("place.rbxmx")), + Some(DocumentFormat::Xml) + ); + } + + #[test] + fn from_path_invalid() { + assert_eq!( + DocumentFormat::from_path(PathBuf::from("data-file.csv")), + None + ); + assert_eq!( + DocumentFormat::from_path(PathBuf::from("nested/path/file.json")), + None + ); + assert_eq!( + DocumentFormat::from_path(PathBuf::from(".no-name-strange-rbx")), + None + ); + assert_eq!( + DocumentFormat::from_path(PathBuf::from("file_without_extension")), + None + ); + } + + #[test] + fn from_bytes_binary() { + assert_eq!( + DocumentFormat::from_bytes(b""), + Some(DocumentFormat::Xml) + ); + + assert_eq!( + DocumentFormat::from_bytes(b""), + Some(DocumentFormat::Xml) + ); + } + + #[test] + fn from_bytes_invalid() { + assert_eq!(DocumentFormat::from_bytes(b""), None); + assert_eq!(DocumentFormat::from_bytes(b" roblox"), None); + assert_eq!(DocumentFormat::from_bytes(b") -> Option { + match extension.as_ref() { + "rbxl" | "rbxlx" => Some(Self::Place), + "rbxm" | "rbxmx" => Some(Self::Model), + _ => None, + } + } + + /** + Try to convert a file path into a valid document kind specifier. + + Returns `None` if the file extension of the path + is not a canonical roblox file format extension. + */ + pub fn from_path(path: impl AsRef) -> Option { + match path + .as_ref() + .extension() + .map(|ext| ext.to_string_lossy()) + .as_deref() + { + Some("rbxl") | Some("rbxlx") => Some(Self::Place), + Some("rbxm") | Some("rbxmx") => Some(Self::Model), + _ => None, + } + } + + /** + Try to detect a document kind specifier from a weak dom. + + Returns `None` if the given dom is empty and as such can not have its kind inferred. + */ + pub fn from_weak_dom(dom: &WeakDom) -> Option { + let mut has_top_level_child = false; + let mut has_top_level_service = false; + for child_ref in dom.root().children() { + if let Some(child_inst) = dom.get_by_ref(*child_ref) { + has_top_level_child = true; + if class_is_a_service(&child_inst.class).unwrap_or(false) { + has_top_level_service = true; + break; + } + } + } + if has_top_level_service { + Some(Self::Place) + } else if has_top_level_child { + Some(Self::Model) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use rbx_dom_weak::InstanceBuilder; + + use super::*; + + #[test] + fn from_extension_place() { + assert_eq!( + DocumentKind::from_extension("rbxl"), + Some(DocumentKind::Place) + ); + + assert_eq!( + DocumentKind::from_extension("rbxlx"), + Some(DocumentKind::Place) + ); + } + + #[test] + fn from_extension_model() { + assert_eq!( + DocumentKind::from_extension("rbxm"), + Some(DocumentKind::Model) + ); + + assert_eq!( + DocumentKind::from_extension("rbxmx"), + Some(DocumentKind::Model) + ); + } + + #[test] + fn from_extension_invalid() { + assert_eq!(DocumentKind::from_extension("csv"), None); + assert_eq!(DocumentKind::from_extension("json"), None); + assert_eq!(DocumentKind::from_extension("rbx"), None); + assert_eq!(DocumentKind::from_extension("rbxn"), None); + assert_eq!(DocumentKind::from_extension("xlx"), None); + assert_eq!(DocumentKind::from_extension("xmx"), None); + } + + #[test] + fn from_path_place() { + assert_eq!( + DocumentKind::from_path(PathBuf::from("place.rbxl")), + Some(DocumentKind::Place) + ); + + assert_eq!( + DocumentKind::from_path(PathBuf::from("place.rbxlx")), + Some(DocumentKind::Place) + ); + } + + #[test] + fn from_path_model() { + assert_eq!( + DocumentKind::from_path(PathBuf::from("model.rbxm")), + Some(DocumentKind::Model) + ); + + assert_eq!( + DocumentKind::from_path(PathBuf::from("model.rbxmx")), + Some(DocumentKind::Model) + ); + } + + #[test] + fn from_path_invalid() { + assert_eq!( + DocumentKind::from_path(PathBuf::from("data-file.csv")), + None + ); + assert_eq!( + DocumentKind::from_path(PathBuf::from("nested/path/file.json")), + None + ); + assert_eq!( + DocumentKind::from_path(PathBuf::from(".no-name-strange-rbx")), + None + ); + assert_eq!( + DocumentKind::from_path(PathBuf::from("file_without_extension")), + None + ); + } + + #[test] + fn from_weak_dom() { + let empty = WeakDom::new(InstanceBuilder::new("Instance")); + assert_eq!(DocumentKind::from_weak_dom(&empty), None); + + let with_services = WeakDom::new( + InstanceBuilder::new("Instance") + .with_child(InstanceBuilder::new("Workspace")) + .with_child(InstanceBuilder::new("ReplicatedStorage")), + ); + assert_eq!( + DocumentKind::from_weak_dom(&with_services), + Some(DocumentKind::Place) + ); + + let with_children = WeakDom::new( + InstanceBuilder::new("Instance") + .with_child(InstanceBuilder::new("Model")) + .with_child(InstanceBuilder::new("Part")), + ); + assert_eq!( + DocumentKind::from_weak_dom(&with_children), + Some(DocumentKind::Model) + ); + + let with_mixed = WeakDom::new( + InstanceBuilder::new("Instance") + .with_child(InstanceBuilder::new("Workspace")) + .with_child(InstanceBuilder::new("Part")), + ); + assert_eq!( + DocumentKind::from_weak_dom(&with_mixed), + Some(DocumentKind::Place) + ); + } +} diff --git a/crates/lune-roblox/src/document/mod.rs b/crates/lune-roblox/src/document/mod.rs new file mode 100644 index 0000000..e0f6bb7 --- /dev/null +++ b/crates/lune-roblox/src/document/mod.rs @@ -0,0 +1,290 @@ +use rbx_dom_weak::{types::Ref as DomRef, InstanceBuilder as DomInstanceBuilder, WeakDom}; +use rbx_xml::{ + DecodeOptions as XmlDecodeOptions, DecodePropertyBehavior as XmlDecodePropertyBehavior, + EncodeOptions as XmlEncodeOptions, EncodePropertyBehavior as XmlEncodePropertyBehavior, +}; + +mod error; +mod format; +mod kind; +mod postprocessing; + +pub use error::*; +pub use format::*; +pub use kind::*; + +use postprocessing::*; + +use crate::instance::{data_model, Instance}; + +pub type DocumentResult = Result; + +/** + A container for [`rbx_dom_weak::WeakDom`] that also takes care of + reading and writing different kinds and formats of roblox files. + + --- + + ### Code Sample #1 + + ```rust ignore + // Reading a document from a file + + let file_path = PathBuf::from("place-file.rbxl"); + let file_contents = std::fs::read(&file_path)?; + + let document = Document::from_bytes_auto(file_contents)?; + + // Writing a document to a file + + let file_path = PathBuf::from("place-file") + .with_extension(document.extension()?); + + std::fs::write(&file_path, document.to_bytes()?)?; + ``` + + --- + + ### Code Sample #2 + + ```rust ignore + // Converting a Document to a DataModel or model child instances + let data_model = document.into_data_model_instance()?; + + let model_children = document.into_instance_array()?; + + // Converting a DataModel or model child instances into a Document + let place_doc = Document::from_data_model_instance(data_model)?; + + let model_doc = Document::from_instance_array(model_children)?; + ``` +*/ +#[derive(Debug)] +pub struct Document { + kind: DocumentKind, + format: DocumentFormat, + dom: WeakDom, +} + +impl Document { + /** + Gets the canonical file extension for a given kind and + format of document, which will follow this chart: + + | Kind | Format | Extension | + |:------|:-------|:----------| + | Place | Binary | `rbxl` | + | Place | Xml | `rbxlx` | + | Model | Binary | `rbxm` | + | Model | Xml | `rbxmx` | + */ + #[rustfmt::skip] + pub fn canonical_extension(kind: DocumentKind, format: DocumentFormat) -> &'static str { + match (kind, format) { + (DocumentKind::Place, DocumentFormat::Binary) => "rbxl", + (DocumentKind::Place, DocumentFormat::Xml) => "rbxlx", + (DocumentKind::Model, DocumentFormat::Binary) => "rbxm", + (DocumentKind::Model, DocumentFormat::Xml) => "rbxmx", + } + } + + fn from_bytes_inner(bytes: impl AsRef<[u8]>) -> DocumentResult<(DocumentFormat, WeakDom)> { + let bytes = bytes.as_ref(); + let format = DocumentFormat::from_bytes(bytes).ok_or(DocumentError::UnknownFormat)?; + let dom = match format { + DocumentFormat::Binary => rbx_binary::from_reader(bytes) + .map_err(|err| DocumentError::ReadError(err.to_string())), + DocumentFormat::Xml => { + let xml_options = XmlDecodeOptions::new() + .property_behavior(XmlDecodePropertyBehavior::ReadUnknown); + rbx_xml::from_reader(bytes, xml_options) + .map_err(|err| DocumentError::ReadError(err.to_string())) + } + }?; + Ok((format, dom)) + } + + /** + Decodes and creates a new document from a byte buffer. + + This will automatically handle and detect if the document should be decoded + using a roblox binary or roblox xml format, and if it is a model or place file. + + Note that detection of model vs place file is heavily dependent on the structure + of the file, and a model file with services in it will detect as a place file, so + if possible using [`Document::from_bytes`] with an explicit kind should be preferred. + */ + pub fn from_bytes_auto(bytes: impl AsRef<[u8]>) -> DocumentResult { + let (format, dom) = Self::from_bytes_inner(bytes)?; + let kind = DocumentKind::from_weak_dom(&dom).ok_or(DocumentError::UnknownKind)?; + Ok(Self { kind, format, dom }) + } + + /** + Decodes and creates a new document from a byte buffer. + + This will automatically handle and detect if the document + should be decoded using a roblox binary or roblox xml format. + */ + pub fn from_bytes(bytes: impl AsRef<[u8]>, kind: DocumentKind) -> DocumentResult { + let (format, dom) = Self::from_bytes_inner(bytes)?; + Ok(Self { kind, format, dom }) + } + + /** + Encodes the document as a vector of bytes, to + be written to a file or sent over the network. + + This will use the same format that the document was created + with, meaning if the document is a binary document the output + will be binary, and vice versa for xml and other future formats. + */ + pub fn to_bytes(&self) -> DocumentResult> { + self.to_bytes_with_format(self.format) + } + + /** + Encodes the document as a vector of bytes, to + be written to a file or sent over the network. + */ + pub fn to_bytes_with_format(&self, format: DocumentFormat) -> DocumentResult> { + let mut bytes = Vec::new(); + match format { + DocumentFormat::Binary => { + rbx_binary::to_writer(&mut bytes, &self.dom, self.dom.root().children()) + .map_err(|err| DocumentError::WriteError(err.to_string())) + } + DocumentFormat::Xml => { + let xml_options = XmlEncodeOptions::new() + .property_behavior(XmlEncodePropertyBehavior::WriteUnknown); + rbx_xml::to_writer( + &mut bytes, + &self.dom, + self.dom.root().children(), + xml_options, + ) + .map_err(|err| DocumentError::WriteError(err.to_string())) + } + }?; + Ok(bytes) + } + + /** + Gets the kind this document was created with. + */ + pub fn kind(&self) -> DocumentKind { + self.kind + } + + /** + Gets the format this document was created with. + */ + pub fn format(&self) -> DocumentFormat { + self.format + } + + /** + Gets the file extension for this document. + */ + pub fn extension(&self) -> &'static str { + Self::canonical_extension(self.kind, self.format) + } + + /** + Creates a DataModel instance out of this place document. + + Will error if the document is not a place. + */ + pub fn into_data_model_instance(mut self) -> DocumentResult { + if self.kind != DocumentKind::Place { + return Err(DocumentError::IntoDataModelInvalidArgs); + } + + let dom_root = self.dom.root_ref(); + + let data_model_ref = self + .dom + .insert(dom_root, DomInstanceBuilder::new(data_model::CLASS_NAME)); + let data_model_child_refs = self.dom.root().children().to_vec(); + + for child_ref in data_model_child_refs { + if child_ref != data_model_ref { + self.dom.transfer_within(child_ref, data_model_ref); + } + } + + Ok(Instance::from_external_dom(&mut self.dom, data_model_ref)) + } + + /** + Creates an array of instances out of this model document. + + Will error if the document is not a model. + */ + pub fn into_instance_array(mut self) -> DocumentResult> { + if self.kind != DocumentKind::Model { + return Err(DocumentError::IntoInstanceArrayInvalidArgs); + } + + let dom_child_refs = self.dom.root().children().to_vec(); + + let root_child_instances = dom_child_refs + .into_iter() + .map(|child_ref| Instance::from_external_dom(&mut self.dom, child_ref)) + .collect(); + + Ok(root_child_instances) + } + + /** + Creates a place document out of a DataModel instance. + + Will error if the instance is not a DataModel. + */ + pub fn from_data_model_instance(i: Instance) -> DocumentResult { + if i.get_class_name() != data_model::CLASS_NAME { + return Err(DocumentError::FromDataModelInvalidArgs); + } + + let mut dom = WeakDom::new(DomInstanceBuilder::new("ROOT")); + let children: Vec = i + .get_children() + .iter() + .map(|instance| instance.dom_ref) + .collect(); + + Instance::clone_multiple_into_external_dom(&children, &mut dom); + postprocess_dom_for_place(&mut dom); + + Ok(Self { + kind: DocumentKind::Place, + format: DocumentFormat::default(), + dom, + }) + } + + /** + Creates a model document out of an array of instances. + + Will error if any of the instances is a DataModel. + */ + pub fn from_instance_array(v: Vec) -> DocumentResult { + for i in &v { + if i.get_class_name() == data_model::CLASS_NAME { + return Err(DocumentError::FromInstanceArrayInvalidArgs); + } + } + + let mut dom = WeakDom::new(DomInstanceBuilder::new("ROOT")); + let instances: Vec = v.iter().map(|instance| instance.dom_ref).collect(); + + Instance::clone_multiple_into_external_dom(&instances, &mut dom); + postprocess_dom_for_model(&mut dom); + + Ok(Self { + kind: DocumentKind::Model, + format: DocumentFormat::default(), + dom, + }) + } +} diff --git a/crates/lune-roblox/src/document/postprocessing.rs b/crates/lune-roblox/src/document/postprocessing.rs new file mode 100644 index 0000000..69481f5 --- /dev/null +++ b/crates/lune-roblox/src/document/postprocessing.rs @@ -0,0 +1,47 @@ +use rbx_dom_weak::{ + types::{Ref as DomRef, VariantType as DomType}, + Instance as DomInstance, WeakDom, +}; + +use crate::shared::instance::class_is_a; + +pub fn postprocess_dom_for_place(_dom: &mut WeakDom) { + // Nothing here yet +} + +pub fn postprocess_dom_for_model(dom: &mut WeakDom) { + let root_ref = dom.root_ref(); + recurse_instances(dom, root_ref, &|inst| { + // Get rid of some unique ids - roblox does not + // save these in model files, and we shouldn't either + remove_matching_prop(inst, DomType::UniqueId, "UniqueId"); + remove_matching_prop(inst, DomType::UniqueId, "HistoryId"); + // Similar story with ScriptGuid - this is used + // in the studio-only cloud script drafts feature + if class_is_a(&inst.class, "LuaSourceContainer").unwrap_or(false) { + inst.properties.remove("ScriptGuid"); + } + }); +} + +fn recurse_instances(dom: &mut WeakDom, dom_ref: DomRef, f: &F) +where + F: Fn(&mut DomInstance) + 'static, +{ + let child_refs = match dom.get_by_ref_mut(dom_ref) { + Some(inst) => { + f(inst); + inst.children().to_vec() + } + None => Vec::new(), + }; + for child_ref in child_refs { + recurse_instances(dom, child_ref, f); + } +} + +fn remove_matching_prop(inst: &mut DomInstance, ty: DomType, name: &'static str) { + if inst.properties.get(name).map_or(false, |u| u.ty() == ty) { + inst.properties.remove(name); + } +} diff --git a/crates/lune-roblox/src/exports.rs b/crates/lune-roblox/src/exports.rs new file mode 100644 index 0000000..2a8f834 --- /dev/null +++ b/crates/lune-roblox/src/exports.rs @@ -0,0 +1,68 @@ +use mlua::prelude::*; + +/** + Trait for any item that should be exported as part of the `roblox` built-in library. + + This may be an enum or a struct that should export constants and/or constructs. + + ### Example usage + + ```rs + use mlua::prelude::*; + + struct MyType(usize); + + impl MyType { + pub fn new(n: usize) -> Self { + Self(n) + } + } + + impl LuaExportsTable<'_> for MyType { + const EXPORT_NAME: &'static str = "MyType"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let my_type_new = |lua, n: Option| { + Self::new(n.unwrap_or_default()) + }; + + TableBuilder::new(lua)? + .with_function("new", my_type_new)? + .build_readonly() + } + } + + impl LuaUserData for MyType { + // ... + } + ``` +*/ +pub trait LuaExportsTable<'lua> { + const EXPORT_NAME: &'static str; + + fn create_exports_table(lua: &'lua Lua) -> LuaResult>; +} + +/** + Exports a single item that implements the [`LuaExportsTable`] trait. + + Returns the name of the export, as well as the export table. + + ### Example usage + + ```rs + let lua: mlua::Lua::new(); + + let (name1, table1) = export::(lua)?; + let (name2, table2) = export::(lua)?; + ``` +*/ +pub fn export<'lua, T>(lua: &'lua Lua) -> LuaResult<(&'static str, LuaValue<'lua>)> +where + T: LuaExportsTable<'lua>, +{ + Ok(( + T::EXPORT_NAME, + ::create_exports_table(lua)?.into_lua(lua)?, + )) +} diff --git a/crates/lune-roblox/src/instance/base.rs b/crates/lune-roblox/src/instance/base.rs new file mode 100644 index 0000000..dc326df --- /dev/null +++ b/crates/lune-roblox/src/instance/base.rs @@ -0,0 +1,362 @@ +use mlua::prelude::*; + +use rbx_dom_weak::{ + types::{Variant as DomValue, VariantType as DomType}, + Instance as DomInstance, +}; + +use crate::{ + datatypes::{ + attributes::{ensure_valid_attribute_name, ensure_valid_attribute_value}, + conversion::{DomValueToLua, LuaToDomValue}, + types::EnumItem, + userdata_impl_eq, userdata_impl_to_string, + }, + shared::instance::{class_is_a, find_property_info}, +}; + +use super::{data_model, registry::InstanceRegistry, Instance}; + +pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) { + m.add_meta_method(LuaMetaMethod::ToString, |lua, this, ()| { + ensure_not_destroyed(this)?; + userdata_impl_to_string(lua, this, ()) + }); + m.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + m.add_meta_method(LuaMetaMethod::Index, instance_property_get); + m.add_meta_method_mut(LuaMetaMethod::NewIndex, instance_property_set); + m.add_method("Clone", |lua, this, ()| { + ensure_not_destroyed(this)?; + this.clone_instance().into_lua(lua) + }); + m.add_method_mut("Destroy", |_, this, ()| { + this.destroy(); + Ok(()) + }); + m.add_method_mut("ClearAllChildren", |_, this, ()| { + this.clear_all_children(); + Ok(()) + }); + m.add_method("GetChildren", |lua, this, ()| { + ensure_not_destroyed(this)?; + this.get_children().into_lua(lua) + }); + m.add_method("GetDescendants", |lua, this, ()| { + ensure_not_destroyed(this)?; + this.get_descendants().into_lua(lua) + }); + m.add_method("GetFullName", |lua, this, ()| { + ensure_not_destroyed(this)?; + this.get_full_name().into_lua(lua) + }); + m.add_method("GetDebugId", |lua, this, ()| { + this.dom_ref.to_string().into_lua(lua) + }); + m.add_method("FindFirstAncestor", |lua, this, name: String| { + ensure_not_destroyed(this)?; + this.find_ancestor(|child| child.name == name).into_lua(lua) + }); + m.add_method( + "FindFirstAncestorOfClass", + |lua, this, class_name: String| { + ensure_not_destroyed(this)?; + this.find_ancestor(|child| child.class == class_name) + .into_lua(lua) + }, + ); + m.add_method( + "FindFirstAncestorWhichIsA", + |lua, this, class_name: String| { + ensure_not_destroyed(this)?; + this.find_ancestor(|child| class_is_a(&child.class, &class_name).unwrap_or(false)) + .into_lua(lua) + }, + ); + m.add_method( + "FindFirstChild", + |lua, this, (name, recursive): (String, Option)| { + ensure_not_destroyed(this)?; + let predicate = |child: &DomInstance| child.name == name; + if matches!(recursive, Some(true)) { + this.find_descendant(predicate).into_lua(lua) + } else { + this.find_child(predicate).into_lua(lua) + } + }, + ); + m.add_method( + "FindFirstChildOfClass", + |lua, this, (class_name, recursive): (String, Option)| { + ensure_not_destroyed(this)?; + let predicate = |child: &DomInstance| child.class == class_name; + if matches!(recursive, Some(true)) { + this.find_descendant(predicate).into_lua(lua) + } else { + this.find_child(predicate).into_lua(lua) + } + }, + ); + m.add_method( + "FindFirstChildWhichIsA", + |lua, this, (class_name, recursive): (String, Option)| { + ensure_not_destroyed(this)?; + let predicate = + |child: &DomInstance| class_is_a(&child.class, &class_name).unwrap_or(false); + if matches!(recursive, Some(true)) { + this.find_descendant(predicate).into_lua(lua) + } else { + this.find_child(predicate).into_lua(lua) + } + }, + ); + m.add_method("IsA", |_, this, class_name: String| { + ensure_not_destroyed(this)?; + Ok(class_is_a(&this.class_name, class_name).unwrap_or(false)) + }); + m.add_method( + "IsAncestorOf", + |_, this, instance: LuaUserDataRef| { + ensure_not_destroyed(this)?; + Ok(instance + .find_ancestor(|ancestor| ancestor.referent() == this.dom_ref) + .is_some()) + }, + ); + m.add_method( + "IsDescendantOf", + |_, this, instance: LuaUserDataRef| { + ensure_not_destroyed(this)?; + Ok(this + .find_ancestor(|ancestor| ancestor.referent() == instance.dom_ref) + .is_some()) + }, + ); + m.add_method("GetAttribute", |lua, this, name: String| { + ensure_not_destroyed(this)?; + match this.get_attribute(name) { + Some(attribute) => Ok(LuaValue::dom_value_to_lua(lua, &attribute)?), + None => Ok(LuaValue::Nil), + } + }); + m.add_method("GetAttributes", |lua, this, ()| { + ensure_not_destroyed(this)?; + let attributes = this.get_attributes(); + let tab = lua.create_table_with_capacity(0, attributes.len())?; + for (key, value) in attributes.into_iter() { + tab.set(key, LuaValue::dom_value_to_lua(lua, &value)?)?; + } + Ok(tab) + }); + m.add_method( + "SetAttribute", + |lua, this, (attribute_name, lua_value): (String, LuaValue)| { + ensure_not_destroyed(this)?; + ensure_valid_attribute_name(&attribute_name)?; + match lua_value.lua_to_dom_value(lua, None) { + Ok(dom_value) => { + ensure_valid_attribute_value(&dom_value)?; + this.set_attribute(attribute_name, dom_value); + Ok(()) + } + Err(e) => Err(e.into()), + } + }, + ); + m.add_method("GetTags", |_, this, ()| { + ensure_not_destroyed(this)?; + Ok(this.get_tags()) + }); + m.add_method("HasTag", |_, this, tag: String| { + ensure_not_destroyed(this)?; + Ok(this.has_tag(tag)) + }); + m.add_method("AddTag", |_, this, tag: String| { + ensure_not_destroyed(this)?; + this.add_tag(tag); + Ok(()) + }); + m.add_method("RemoveTag", |_, this, tag: String| { + ensure_not_destroyed(this)?; + this.remove_tag(tag); + Ok(()) + }); +} + +fn ensure_not_destroyed(inst: &Instance) -> LuaResult<()> { + if inst.is_destroyed() { + Err(LuaError::RuntimeError( + "Instance has been destroyed".to_string(), + )) + } else { + Ok(()) + } +} + +/* + Gets a property value for an instance. + + Getting a value does the following: + + 1. Check if it is a special property like "ClassName", "Name" or "Parent" + 2. Check if a property exists for the wanted name + 2a. Get an existing instance property OR + 2b. Get a property from a known default value + 3. Get a current child of the instance + 4. No valid property or instance found, throw error +*/ +fn instance_property_get<'lua>( + lua: &'lua Lua, + this: &Instance, + prop_name: String, +) -> LuaResult> { + ensure_not_destroyed(this)?; + + match prop_name.as_str() { + "ClassName" => return this.get_class_name().into_lua(lua), + "Name" => { + return this.get_name().into_lua(lua); + } + "Parent" => { + return this.get_parent().into_lua(lua); + } + _ => {} + } + + if let Some(info) = find_property_info(&this.class_name, &prop_name) { + if let Some(prop) = this.get_property(&prop_name) { + if let DomValue::Enum(enum_value) = prop { + let enum_name = info.enum_name.ok_or_else(|| { + LuaError::RuntimeError(format!( + "Failed to get property '{}' - encountered unknown enum", + prop_name + )) + })?; + EnumItem::from_enum_name_and_value(&enum_name, enum_value.to_u32()) + .ok_or_else(|| { + LuaError::RuntimeError(format!( + "Failed to get property '{}' - Enum.{} does not contain numeric value {}", + prop_name, enum_name, enum_value.to_u32() + )) + })? + .into_lua(lua) + } else { + Ok(LuaValue::dom_value_to_lua(lua, &prop)?) + } + } else if let (Some(enum_name), Some(enum_value)) = (info.enum_name, info.enum_default) { + EnumItem::from_enum_name_and_value(&enum_name, enum_value) + .ok_or_else(|| { + LuaError::RuntimeError(format!( + "Failed to get property '{}' - Enum.{} does not contain numeric value {}", + prop_name, enum_name, enum_value + )) + })? + .into_lua(lua) + } else if let Some(prop_default) = info.value_default { + Ok(LuaValue::dom_value_to_lua(lua, prop_default)?) + } else if info.value_type.is_some() { + if info.value_type == Some(DomType::Ref) { + Ok(LuaValue::Nil) + } else { + Err(LuaError::RuntimeError(format!( + "Failed to get property '{}' - missing default value", + prop_name + ))) + } + } else { + Err(LuaError::RuntimeError(format!( + "Failed to get property '{}' - malformed property info", + prop_name + ))) + } + } else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) { + Ok(LuaValue::UserData(lua.create_userdata(inst)?)) + } else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) { + getter.call(this.clone()) + } else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) { + Ok(LuaValue::Function(method)) + } else { + Err(LuaError::RuntimeError(format!( + "{} is not a valid member of {}", + prop_name, this + ))) + } +} + +/* + Sets a property value for an instance. + + Setting a value does the following: + + 1. Check if it is a special property like "ClassName", "Name" or "Parent" + 2. Check if a property exists for the wanted name + 2a. Set a strict enum from a given EnumItem OR + 2b. Set a normal property from a given value +*/ +fn instance_property_set<'lua>( + lua: &'lua Lua, + this: &mut Instance, + (prop_name, prop_value): (String, LuaValue<'lua>), +) -> LuaResult<()> { + ensure_not_destroyed(this)?; + + match prop_name.as_str() { + "ClassName" => { + return Err(LuaError::RuntimeError( + "Failed to set ClassName - property is read-only".to_string(), + )); + } + "Name" => { + let name = String::from_lua(prop_value, lua)?; + this.set_name(name); + return Ok(()); + } + "Parent" => { + if this.get_class_name() == data_model::CLASS_NAME { + return Err(LuaError::RuntimeError( + "Failed to set Parent - DataModel can not be reparented".to_string(), + )); + } + type Parent<'lua> = Option>; + let parent = Parent::from_lua(prop_value, lua)?; + this.set_parent(parent.map(|p| p.clone())); + return Ok(()); + } + _ => {} + } + + if let Some(info) = find_property_info(&this.class_name, &prop_name) { + if let Some(enum_name) = info.enum_name { + match LuaUserDataRef::::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 { + 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)) + } else { + Err(LuaError::RuntimeError(format!( + "{} is not a valid member of {}", + prop_name, this + ))) + } +} diff --git a/crates/lune-roblox/src/instance/data_model.rs b/crates/lune-roblox/src/instance/data_model.rs new file mode 100644 index 0000000..0d6503d --- /dev/null +++ b/crates/lune-roblox/src/instance/data_model.rs @@ -0,0 +1,79 @@ +use mlua::prelude::*; + +use crate::shared::{ + classes::{ + add_class_restricted_getter, add_class_restricted_method, + get_or_create_property_ref_instance, + }, + instance::class_is_a_service, +}; + +use super::Instance; + +pub const CLASS_NAME: &str = "DataModel"; + +pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) { + add_class_restricted_getter(f, CLASS_NAME, "Workspace", data_model_get_workspace); +} + +pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) { + add_class_restricted_method(m, CLASS_NAME, "GetService", data_model_get_service); + add_class_restricted_method(m, CLASS_NAME, "FindService", data_model_find_service); +} + +/** + Get the workspace parented under this datamodel, or create it if it doesn't exist. + + ### See Also + * [`Terrain`](https://create.roblox.com/docs/reference/engine/classes/Workspace#Terrain) + on the Roblox Developer Hub +*/ +fn data_model_get_workspace(_: &Lua, this: &Instance) -> LuaResult { + get_or_create_property_ref_instance(this, "Workspace", "Workspace") +} + +/** + Gets or creates a service for this DataModel. + + ### See Also + * [`GetService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#GetService) + on the Roblox Developer Hub +*/ +fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> LuaResult { + if matches!(class_is_a_service(&service_name), None | Some(false)) { + Err(LuaError::RuntimeError(format!( + "'{}' is not a valid service name", + service_name + ))) + } else if let Some(service) = this.find_child(|child| child.class == service_name) { + Ok(service) + } else { + let service = Instance::new_orphaned(service_name); + service.set_parent(Some(this.clone())); + Ok(service) + } +} + +/** + Gets a service for this DataModel, if it exists. + + ### See Also + * [`FindService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#FindService) + on the Roblox Developer Hub +*/ +fn data_model_find_service( + _: &Lua, + this: &Instance, + service_name: String, +) -> LuaResult> { + if matches!(class_is_a_service(&service_name), None | Some(false)) { + Err(LuaError::RuntimeError(format!( + "'{}' is not a valid service name", + service_name + ))) + } else if let Some(service) = this.find_child(|child| child.class == service_name) { + Ok(Some(service)) + } else { + Ok(None) + } +} diff --git a/crates/lune-roblox/src/instance/mod.rs b/crates/lune-roblox/src/instance/mod.rs new file mode 100644 index 0000000..26040cd --- /dev/null +++ b/crates/lune-roblox/src/instance/mod.rs @@ -0,0 +1,788 @@ +use std::{ + collections::{BTreeMap, VecDeque}, + fmt, + hash::{Hash, Hasher}, + sync::Mutex, +}; + +use mlua::prelude::*; +use once_cell::sync::Lazy; +use rbx_dom_weak::{ + types::{Attributes as DomAttributes, Ref as DomRef, Variant as DomValue}, + Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom, +}; + +use lune_utils::TableBuilder; + +use crate::{ + exports::LuaExportsTable, + shared::instance::{class_exists, class_is_a}, +}; + +pub(crate) mod base; +pub(crate) mod data_model; +pub(crate) mod terrain; +pub(crate) mod workspace; + +pub mod registry; + +const PROPERTY_NAME_ATTRIBUTES: &str = "Attributes"; +const PROPERTY_NAME_TAGS: &str = "Tags"; + +static INTERNAL_DOM: Lazy> = + Lazy::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT")))); + +#[derive(Debug, Clone)] +pub struct Instance { + pub(crate) dom_ref: DomRef, + pub(crate) class_name: String, +} + +impl Instance { + /** + Creates a new `Instance` from an existing dom object ref. + + Panics if the instance does not exist in the internal dom, + or if the given dom object ref points to the dom root. + + **WARNING:** Creating a new instance requires locking the internal dom, + any existing lock must first be released to prevent any deadlocking. + */ + pub(crate) fn new(dom_ref: DomRef) -> Self { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + let instance = dom + .get_by_ref(dom_ref) + .expect("Failed to find instance in document"); + + if instance.referent() == dom.root_ref() { + panic!("Instances can not be created from dom roots") + } + + Self { + dom_ref, + class_name: instance.class.clone(), + } + } + + /** + Creates a new `Instance` from a dom object ref, if the instance exists. + + Panics if the given dom object ref points to the dom root. + + **WARNING:** Creating a new instance requires locking the internal dom, + any existing lock must first be released to prevent any deadlocking. + */ + pub(crate) fn new_opt(dom_ref: DomRef) -> Option { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + if let Some(instance) = dom.get_by_ref(dom_ref) { + if instance.referent() == dom.root_ref() { + panic!("Instances can not be created from dom roots") + } + + Some(Self { + dom_ref, + class_name: instance.class.clone(), + }) + } else { + None + } + } + + /** + Creates a new orphaned `Instance` with a given class name. + + An orphaned instance is an instance at the root of a weak dom. + + **WARNING:** Creating a new instance requires locking the internal dom, + any existing lock must first be released to prevent any deadlocking. + */ + pub(crate) fn new_orphaned(class_name: impl AsRef) -> Self { + let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + let class_name = class_name.as_ref(); + + let instance = DomInstanceBuilder::new(class_name.to_string()); + + let dom_root = dom.root_ref(); + let dom_ref = dom.insert(dom_root, instance); + + Self { + dom_ref, + class_name: class_name.to_string(), + } + } + + /** + Creates a new orphaned `Instance` by transferring + it from an external weak dom to the internal one. + + An orphaned instance is an instance at the root of a weak dom. + + Panics if the given dom ref is the root dom ref of the external weak dom. + */ + pub fn from_external_dom(external_dom: &mut WeakDom, external_dom_ref: DomRef) -> Self { + let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + let dom_root = dom.root_ref(); + + external_dom.transfer(external_dom_ref, &mut dom, dom_root); + + drop(dom); // Self::new needs mutex handle, drop it first + Self::new(external_dom_ref) + } + + /** + Clones an instance to an external weak dom. + + This will place the instance as a child of the + root of the weak dom, and return its referent. + */ + pub fn clone_into_external_dom(self, external_dom: &mut WeakDom) -> DomRef { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + let cloned = dom.clone_into_external(self.dom_ref, external_dom); + external_dom.transfer_within(cloned, external_dom.root_ref()); + + cloned + } + + pub fn clone_multiple_into_external_dom( + referents: &[DomRef], + external_dom: &mut WeakDom, + ) -> Vec { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + let cloned = dom.clone_multiple_into_external(referents, external_dom); + + for referent in cloned.iter() { + external_dom.transfer_within(*referent, external_dom.root_ref()); + } + + cloned + } + + /** + Clones the instance and all of its descendants, and orphans it. + + To then save the new instance it must be re-parented, + which matches the exact behavior of Roblox's instances. + + ### See Also + * [`Clone`](https://create.roblox.com/docs/reference/engine/classes/Instance#Clone) + on the Roblox Developer Hub + */ + pub fn clone_instance(&self) -> Instance { + let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + let new_ref = dom.clone_within(self.dom_ref); + drop(dom); // Self::new needs mutex handle, drop it first + + let new_inst = Self::new(new_ref); + new_inst.set_parent(None); + new_inst + } + + /** + Destroys the instance, removing it completely + from the weak dom with no way of recovering it. + + All member methods will throw errors when called from lua and panic + when called from rust after the instance has been destroyed. + + Returns `true` if destroyed successfully, `false` if already destroyed. + + ### See Also + * [`Destroy`](https://create.roblox.com/docs/reference/engine/classes/Instance#Destroy) + on the Roblox Developer Hub + */ + pub fn destroy(&mut self) -> bool { + if self.is_destroyed() { + false + } else { + let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + dom.destroy(self.dom_ref); + true + } + } + + fn is_destroyed(&self) -> bool { + // NOTE: This property can not be cached since instance references + // other than this one may have destroyed this one, and we don't + // keep track of all current instance reference structs + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + dom.get_by_ref(self.dom_ref).is_none() + } + + /** + Destroys all child instances. + + ### See Also + * [`Instance::Destroy`] for more info about what happens when an instance gets destroyed + * [`ClearAllChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClearAllChildren) + on the Roblox Developer Hub + */ + pub fn clear_all_children(&mut self) { + let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + let instance = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document"); + + let child_refs = instance.children().to_vec(); + for child_ref in child_refs { + dom.destroy(child_ref); + } + } + + /** + Checks if the instance matches or inherits a given class name. + + ### See Also + * [`IsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA) + on the Roblox Developer Hub + */ + pub fn is_a(&self, class_name: impl AsRef) -> bool { + class_is_a(&self.class_name, class_name).unwrap_or(false) + } + + /** + Gets the class name of the instance. + + This will return the correct class name even if the instance has been destroyed. + + ### See Also + * [`ClassName`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClassName) + on the Roblox Developer Hub + */ + pub fn get_class_name(&self) -> &str { + self.class_name.as_str() + } + + /** + Gets the name of the instance, if it exists. + + ### See Also + * [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name) + on the Roblox Developer Hub + */ + pub fn get_name(&self) -> String { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + dom.get_by_ref(self.dom_ref) + .expect("Failed to find instance in document") + .name + .clone() + } + + /** + Sets the name of the instance, if it exists. + + ### See Also + * [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name) + on the Roblox Developer Hub + */ + pub fn set_name(&self, name: impl Into) { + let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + dom.get_by_ref_mut(self.dom_ref) + .expect("Failed to find instance in document") + .name = name.into() + } + + /** + Gets the parent of the instance, if it exists. + + ### See Also + * [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent) + on the Roblox Developer Hub + */ + pub fn get_parent(&self) -> Option { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + let parent_ref = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document") + .parent(); + + if parent_ref == dom.root_ref() { + None + } else { + drop(dom); // Self::new needs mutex handle, drop it first + Some(Self::new(parent_ref)) + } + } + + /** + Sets the parent of the instance, if it exists. + + If the provided parent is [`None`] the instance will become orphaned. + + An orphaned instance is an instance at the root of a weak dom. + + ### See Also + * [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent) + on the Roblox Developer Hub + */ + pub fn set_parent(&self, parent: Option) { + let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + let parent_ref = parent + .map(|parent| parent.dom_ref) + .unwrap_or_else(|| dom.root_ref()); + + dom.transfer_within(self.dom_ref, parent_ref); + } + + /** + Gets a property for the instance, if it exists. + */ + pub fn get_property(&self, name: impl AsRef) -> Option { + INTERNAL_DOM + .lock() + .expect("Failed to lock document") + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document") + .properties + .get(name.as_ref()) + .cloned() + } + + /** + Sets a property for the instance. + + Note that setting a property here will not fail even if the + property does not actually exist for the instance class. + */ + pub fn set_property(&self, name: impl AsRef, value: DomValue) { + INTERNAL_DOM + .lock() + .expect("Failed to lock document") + .get_by_ref_mut(self.dom_ref) + .expect("Failed to find instance in document") + .properties + .insert(name.as_ref().to_string(), value); + } + + /** + Gets an attribute for the instance, if it exists. + + ### See Also + * [`GetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttribute) + on the Roblox Developer Hub + */ + pub fn get_attribute(&self, name: impl AsRef) -> Option { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + let inst = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document"); + if let Some(DomValue::Attributes(attributes)) = + inst.properties.get(PROPERTY_NAME_ATTRIBUTES) + { + attributes.get(name.as_ref()).cloned() + } else { + None + } + } + + /** + Gets all known attributes for the instance. + + ### See Also + * [`GetAttributes`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttributes) + on the Roblox Developer Hub + */ + pub fn get_attributes(&self) -> BTreeMap { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + let inst = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document"); + if let Some(DomValue::Attributes(attributes)) = + inst.properties.get(PROPERTY_NAME_ATTRIBUTES) + { + attributes.clone().into_iter().collect() + } else { + BTreeMap::new() + } + } + + /** + Sets an attribute for the instance. + + ### See Also + * [`SetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#SetAttribute) + on the Roblox Developer Hub + */ + pub fn set_attribute(&self, name: impl AsRef, value: DomValue) { + let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + let inst = dom + .get_by_ref_mut(self.dom_ref) + .expect("Failed to find instance in document"); + // NOTE: Attributes do not support integers, only floats + let value = match value { + DomValue::Int32(i) => DomValue::Float32(i as f32), + DomValue::Int64(i) => DomValue::Float64(i as f64), + value => value, + }; + if let Some(DomValue::Attributes(attributes)) = + inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES) + { + attributes.insert(name.as_ref().to_string(), value); + } else { + let mut attributes = DomAttributes::new(); + attributes.insert(name.as_ref().to_string(), value); + inst.properties.insert( + PROPERTY_NAME_ATTRIBUTES.to_string(), + DomValue::Attributes(attributes), + ); + } + } + + /** + Adds a tag to the instance. + + ### See Also + * [`AddTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#AddTag) + on the Roblox Developer Hub + */ + pub fn add_tag(&self, name: impl AsRef) { + let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + let inst = dom + .get_by_ref_mut(self.dom_ref) + .expect("Failed to find instance in document"); + if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) { + tags.push(name.as_ref()); + } else { + inst.properties.insert( + PROPERTY_NAME_TAGS.to_string(), + DomValue::Tags(vec![name.as_ref().to_string()].into()), + ); + } + } + + /** + Gets all current tags for the instance. + + ### See Also + * [`GetTags`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#GetTags) + on the Roblox Developer Hub + */ + pub fn get_tags(&self) -> Vec { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + let inst = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document"); + if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) { + tags.iter().map(ToString::to_string).collect() + } else { + Vec::new() + } + } + + /** + Checks if the instance has a specific tag. + + ### See Also + * [`HasTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#HasTag) + on the Roblox Developer Hub + */ + pub fn has_tag(&self, name: impl AsRef) -> bool { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + let inst = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document"); + if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) { + let name = name.as_ref(); + tags.iter().any(|tag| tag == name) + } else { + false + } + } + + /** + Removes a tag from the instance. + + ### See Also + * [`RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#RemoveTag) + on the Roblox Developer Hub + */ + pub fn remove_tag(&self, name: impl AsRef) { + let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + let inst = dom + .get_by_ref_mut(self.dom_ref) + .expect("Failed to find instance in document"); + if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) { + let name = name.as_ref(); + let mut new_tags = tags.iter().map(ToString::to_string).collect::>(); + new_tags.retain(|tag| tag != name); + inst.properties.insert( + PROPERTY_NAME_TAGS.to_string(), + DomValue::Tags(new_tags.into()), + ); + } + } + + /** + Gets all of the current children of this `Instance`. + + Note that this is a somewhat expensive operation and that other + operations using weak dom referents should be preferred if possible. + + ### See Also + * [`GetChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetChildren) + on the Roblox Developer Hub + */ + pub fn get_children(&self) -> Vec { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + let children = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document") + .children() + .to_vec(); + + drop(dom); // Self::new needs mutex handle, drop it first + children.into_iter().map(Self::new).collect() + } + + /** + Gets all of the current descendants of this `Instance` using a breadth-first search. + + Note that this is a somewhat expensive operation and that other + operations using weak dom referents should be preferred if possible. + + ### See Also + * [`GetDescendants`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetDescendants) + on the Roblox Developer Hub + */ + pub fn get_descendants(&self) -> Vec { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + let mut descendants = Vec::new(); + let mut queue = VecDeque::from_iter( + dom.get_by_ref(self.dom_ref) + .expect("Failed to find instance in document") + .children(), + ); + + while let Some(queue_ref) = queue.pop_front() { + descendants.push(*queue_ref); + let queue_inst = dom.get_by_ref(*queue_ref).unwrap(); + for queue_ref_inner in queue_inst.children().iter().rev() { + queue.push_back(queue_ref_inner); + } + } + + drop(dom); // Self::new needs mutex handle, drop it first + descendants.into_iter().map(Self::new).collect() + } + + /** + Gets the "full name" of this instance. + + This will be a path composed of instance names from the top-level + ancestor of this instance down to itself, in the following format: + + `Ancestor.Child.Descendant.Instance` + + ### See Also + * [`GetFullName`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetFullName) + on the Roblox Developer Hub + */ + pub fn get_full_name(&self) -> String { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + let dom_root = dom.root_ref(); + + let mut parts = Vec::new(); + let mut instance_ref = self.dom_ref; + + while let Some(instance) = dom.get_by_ref(instance_ref) { + if instance_ref != dom_root && instance.class != data_model::CLASS_NAME { + instance_ref = instance.parent(); + parts.push(instance.name.clone()); + } else { + break; + } + } + + parts.reverse(); + parts.join(".") + } + + /** + Finds a child of the instance using the given predicate callback. + + ### See Also + * [`FindFirstChild`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChild) on the Roblox Developer Hub + * [`FindFirstChildOfClass`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChildOfClass) on the Roblox Developer Hub + * [`FindFirstChildWhichIsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChildWhichIsA) on the Roblox Developer Hub + */ + pub fn find_child(&self, predicate: F) -> Option + where + F: Fn(&DomInstance) -> bool, + { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + let children = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document") + .children() + .to_vec(); + + let found_ref = children.into_iter().find(|child_ref| { + if let Some(child_inst) = dom.get_by_ref(*child_ref) { + predicate(child_inst) + } else { + false + } + }); + + drop(dom); // Self::new needs mutex handle, drop it first + found_ref.map(Self::new) + } + + /** + Finds an ancestor of the instance using the given predicate callback. + + ### See Also + * [`FindFirstAncestor`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestor) on the Roblox Developer Hub + * [`FindFirstAncestorOfClass`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestorOfClass) on the Roblox Developer Hub + * [`FindFirstAncestorWhichIsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestorWhichIsA) on the Roblox Developer Hub + */ + pub fn find_ancestor(&self, predicate: F) -> Option + where + F: Fn(&DomInstance) -> bool, + { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + let mut ancestor_ref = dom + .get_by_ref(self.dom_ref) + .expect("Failed to find instance in document") + .parent(); + + while let Some(ancestor) = dom.get_by_ref(ancestor_ref) { + if predicate(ancestor) { + drop(dom); // Self::new needs mutex handle, drop it first + return Some(Self::new(ancestor_ref)); + } else { + ancestor_ref = ancestor.parent(); + } + } + + None + } + + /** + Finds a descendant of the instance using the given + predicate callback and a breadth-first search. + + ### See Also + * [`FindFirstDescendant`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstDescendant) + on the Roblox Developer Hub + */ + pub fn find_descendant(&self, predicate: F) -> Option + where + F: Fn(&DomInstance) -> bool, + { + let dom = INTERNAL_DOM.lock().expect("Failed to lock document"); + + let mut queue = VecDeque::from_iter( + dom.get_by_ref(self.dom_ref) + .expect("Failed to find instance in document") + .children(), + ); + + while let Some(queue_item) = queue + .pop_front() + .and_then(|queue_ref| dom.get_by_ref(*queue_ref)) + { + if predicate(queue_item) { + let queue_ref = queue_item.referent(); + drop(dom); // Self::new needs mutex handle, drop it first + return Some(Self::new(queue_ref)); + } else { + queue.extend(queue_item.children()) + } + } + + None + } +} + +impl LuaExportsTable<'_> for Instance { + const EXPORT_NAME: &'static str = "Instance"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let instance_new = |lua, class_name: String| { + if class_exists(&class_name) { + Instance::new_orphaned(class_name).into_lua(lua) + } else { + Err(LuaError::RuntimeError(format!( + "Failed to create Instance - '{}' is not a valid class name", + class_name + ))) + } + }; + + TableBuilder::new(lua)? + .with_function("new", instance_new)? + .build_readonly() + } +} + +/* + Here we add inheritance-like behavior for instances by creating + fields that are restricted to specific classnames / base classes + + Note that we should try to be conservative with how many classes + and methods we support here - we should only implement methods that + are necessary for modifying the dom and / or having ergonomic access + 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 { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + data_model::add_fields(fields); + workspace::add_fields(fields); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + base::add_methods(methods); + data_model::add_methods(methods); + terrain::add_methods(methods); + } +} + +impl Hash for Instance { + fn hash(&self, state: &mut H) { + self.dom_ref.hash(state) + } +} + +impl fmt::Display for Instance { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + if self.is_destroyed() { + "<>".to_string() + } else { + self.get_name() + } + ) + } +} + +impl PartialEq for Instance { + fn eq(&self, other: &Self) -> bool { + self.dom_ref == other.dom_ref + } +} + +impl From for DomRef { + fn from(value: Instance) -> Self { + value.dom_ref + } +} diff --git a/crates/lune-roblox/src/instance/registry.rs b/crates/lune-roblox/src/instance/registry.rs new file mode 100644 index 0000000..1dfd6f0 --- /dev/null +++ b/crates/lune-roblox/src/instance/registry.rs @@ -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>; + +#[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>, + setters: Arc>, + methods: Arc>, +} + +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::().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::() + .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> { + 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::(key).unwrap()) + }) + } + + pub fn find_property_getter<'lua>( + lua: &'lua Lua, + instance: &Instance, + property_name: &str, + ) -> Option> { + 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::(key).unwrap()) + }) + } + + pub fn find_property_setter<'lua>( + lua: &'lua Lua, + instance: &Instance, + property_name: &str, + ) -> Option> { + 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::(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 +} diff --git a/crates/lune-roblox/src/instance/terrain.rs b/crates/lune-roblox/src/instance/terrain.rs new file mode 100644 index 0000000..256a6c9 --- /dev/null +++ b/crates/lune-roblox/src/instance/terrain.rs @@ -0,0 +1,94 @@ +use mlua::prelude::*; +use rbx_dom_weak::types::{MaterialColors, TerrainMaterials, Variant}; + +use crate::{ + datatypes::types::{Color3, EnumItem}, + shared::classes::{add_class_restricted_method, add_class_restricted_method_mut}, +}; + +use super::Instance; + +pub const CLASS_NAME: &str = "Terrain"; + +pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M) { + add_class_restricted_method( + methods, + CLASS_NAME, + "GetMaterialColor", + terrain_get_material_color, + ); + + add_class_restricted_method_mut( + methods, + CLASS_NAME, + "SetMaterialColor", + terrain_set_material_color, + ) +} + +fn get_or_create_material_colors(instance: &Instance) -> MaterialColors { + if let Some(Variant::MaterialColors(material_colors)) = instance.get_property("MaterialColors") + { + material_colors + } else { + MaterialColors::default() + } +} + +/** + Returns the color of the given terrain material. + + ### See Also + * [`GetMaterialColor`](https://create.roblox.com/docs/reference/engine/classes/Terrain#GetMaterialColor) + on the Roblox Developer Hub +*/ +fn terrain_get_material_color(_: &Lua, this: &Instance, material: EnumItem) -> LuaResult { + let material_colors = get_or_create_material_colors(this); + + if &material.parent.desc.name != "Material" { + return Err(LuaError::RuntimeError(format!( + "Expected Enum.Material, got Enum.{}", + &material.parent.desc.name + ))); + } + + let terrain_material = material + .name + .parse::() + .map_err(|err| LuaError::RuntimeError(err.to_string()))?; + + Ok(material_colors.get_color(terrain_material).into()) +} + +/** + Sets the color of the given terrain material. + + ### See Also + * [`SetMaterialColor`](https://create.roblox.com/docs/reference/engine/classes/Terrain#SetMaterialColor) + on the Roblox Developer Hub +*/ +fn terrain_set_material_color( + _: &Lua, + this: &mut Instance, + args: (EnumItem, Color3), +) -> LuaResult<()> { + let mut material_colors = get_or_create_material_colors(this); + let material = args.0; + let color = args.1; + + if &material.parent.desc.name != "Material" { + return Err(LuaError::RuntimeError(format!( + "Expected Enum.Material, got Enum.{}", + &material.parent.desc.name + ))); + } + + let terrain_material = material + .name + .parse::() + .map_err(|err| LuaError::RuntimeError(err.to_string()))?; + + material_colors.set_color(terrain_material, color.into()); + this.set_property("MaterialColors", Variant::MaterialColors(material_colors)); + Ok(()) +} diff --git a/crates/lune-roblox/src/instance/workspace.rs b/crates/lune-roblox/src/instance/workspace.rs new file mode 100644 index 0000000..70f1b88 --- /dev/null +++ b/crates/lune-roblox/src/instance/workspace.rs @@ -0,0 +1,34 @@ +use mlua::prelude::*; + +use crate::shared::classes::{add_class_restricted_getter, get_or_create_property_ref_instance}; + +use super::Instance; + +pub const CLASS_NAME: &str = "Workspace"; + +pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) { + add_class_restricted_getter(f, CLASS_NAME, "Terrain", workspace_get_terrain); + add_class_restricted_getter(f, CLASS_NAME, "CurrentCamera", workspace_get_camera); +} + +/** + Get the terrain parented under this workspace, or create it if it doesn't exist. + + ### See Also + * [`Terrain`](https://create.roblox.com/docs/reference/engine/classes/Workspace#Terrain) + on the Roblox Developer Hub +*/ +fn workspace_get_terrain(_: &Lua, this: &Instance) -> LuaResult { + get_or_create_property_ref_instance(this, "Terrain", "Terrain") +} + +/** + Get the camera parented under this workspace, or create it if it doesn't exist. + + ### See Also + * [`CurrentCamera`](https://create.roblox.com/docs/reference/engine/classes/Workspace#CurrentCamera) + on the Roblox Developer Hub +*/ +fn workspace_get_camera(_: &Lua, this: &Instance) -> LuaResult { + get_or_create_property_ref_instance(this, "CurrentCamera", "Camera") +} diff --git a/crates/lune-roblox/src/lib.rs b/crates/lune-roblox/src/lib.rs index 2e802e7..878a2f2 100644 --- a/crates/lune-roblox/src/lib.rs +++ b/crates/lune-roblox/src/lib.rs @@ -1 +1,71 @@ #![allow(clippy::cargo_common_metadata)] + +use mlua::prelude::*; + +use lune_utils::TableBuilder; + +pub mod datatypes; +pub mod document; +pub mod instance; +pub mod reflection; + +pub(crate) mod exports; +pub(crate) mod shared; + +use exports::export; + +fn create_all_exports(lua: &Lua) -> LuaResult> { + use datatypes::types::*; + use instance::Instance; + Ok(vec![ + // Datatypes + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + // Classes + export::(lua)?, + // Singletons + ("Enum", Enums.into_lua(lua)?), + ]) +} + +/** + Creates a table containing all the Roblox datatypes, classes, and singletons. + + Note that this is not guaranteed to contain any value unless indexed directly, + it may be optimized to use lazy initialization in the future. + + # Errors + + Errors when out of memory or when a value cannot be created. +*/ +pub fn module(lua: &Lua) -> LuaResult { + // FUTURE: We can probably create these lazily as users + // index the main exports (this return value) table and + // save some memory and startup time. The full exports + // table is quite big and probably won't get any smaller + // since we impl all roblox constructors for each datatype. + let exports = create_all_exports(lua)?; + TableBuilder::new(lua)? + .with_values(exports)? + .build_readonly() +} diff --git a/crates/lune-roblox/src/reflection/class.rs b/crates/lune-roblox/src/reflection/class.rs new file mode 100644 index 0000000..da61595 --- /dev/null +++ b/crates/lune-roblox/src/reflection/class.rs @@ -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::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 { + let sup = self.0.superclass.as_ref()?; + Some(sup.to_string()) + } + + /** + Get all known properties for this class. + */ + pub fn get_properties(&self) -> HashMap { + 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 { + 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::>() + } +} + +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) -> Option { + 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, value: u32) -> LuaResult { + 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 + )) + }) +} diff --git a/crates/lune-roblox/src/reflection/enums.rs b/crates/lune-roblox/src/reflection/enums.rs new file mode 100644 index 0000000..f0054f3 --- /dev/null +++ b/crates/lune-roblox/src/reflection/enums.rs @@ -0,0 +1,67 @@ +use std::{collections::HashMap, fmt}; + +use mlua::prelude::*; + +use rbx_reflection::EnumDescriptor; + +use crate::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 { + 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) + } +} diff --git a/crates/lune-roblox/src/reflection/mod.rs b/crates/lune-roblox/src/reflection/mod.rs new file mode 100644 index 0000000..29ef54b --- /dev/null +++ b/crates/lune-roblox/src/reflection/mod.rs @@ -0,0 +1,144 @@ +use std::fmt; + +use mlua::prelude::*; + +use rbx_reflection::ReflectionDatabase; + +use crate::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::default() + } + + /** + 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 { + 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 { + 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) -> Option { + 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) -> Option { + 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) -> Option { + 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) -> Option { + 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 Default for Database { + fn default() -> Self { + Self(rbx_reflection_database::get()) + } +} + +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") + } +} diff --git a/crates/lune-roblox/src/reflection/property.rs b/crates/lune-roblox/src/reflection/property.rs new file mode 100644 index 0000000..16415c1 --- /dev/null +++ b/crates/lune-roblox/src/reflection/property.rs @@ -0,0 +1,95 @@ +use std::fmt; + +use mlua::prelude::*; + +use rbx_reflection::{ClassDescriptor, PropertyDescriptor}; + +use super::utils::*; +use crate::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::>() + } +} + +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 + ) + } +} diff --git a/crates/lune-roblox/src/reflection/utils.rs b/crates/lune-roblox/src/reflection/utils.rs new file mode 100644 index 0000000..7445fb3 --- /dev/null +++ b/crates/lune-roblox/src/reflection/utils.rs @@ -0,0 +1,56 @@ +use rbx_reflection::{ClassTag, DataType, PropertyTag, Scriptability}; + +use crate::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"), + } +} diff --git a/crates/lune-roblox/src/shared/classes.rs b/crates/lune-roblox/src/shared/classes.rs new file mode 100644 index 0000000..cc47d46 --- /dev/null +++ b/crates/lune-roblox/src/shared/classes.rs @@ -0,0 +1,133 @@ +use mlua::prelude::*; + +use rbx_dom_weak::types::Variant as DomValue; + +use crate::instance::Instance; + +use super::instance::class_is_a; + +pub(crate) fn add_class_restricted_getter<'lua, F: LuaUserDataFields<'lua, Instance>, R, G>( + fields: &mut F, + class_name: &'static str, + field_name: &'static str, + field_getter: G, +) where + R: IntoLua<'lua>, + G: 'static + Fn(&'lua Lua, &Instance) -> LuaResult, +{ + fields.add_field_method_get(field_name, move |lua, this| { + if class_is_a(this.get_class_name(), class_name).unwrap_or(false) { + field_getter(lua, this) + } else { + Err(LuaError::RuntimeError(format!( + "{} is not a valid member of {}", + field_name, class_name + ))) + } + }); +} + +#[allow(dead_code)] +pub(crate) fn add_class_restricted_setter<'lua, F: LuaUserDataFields<'lua, Instance>, A, G>( + fields: &mut F, + class_name: &'static str, + field_name: &'static str, + field_getter: G, +) where + A: FromLua<'lua>, + G: 'static + Fn(&'lua Lua, &Instance, A) -> LuaResult<()>, +{ + fields.add_field_method_set(field_name, move |lua, this, value| { + if class_is_a(this.get_class_name(), class_name).unwrap_or(false) { + field_getter(lua, this, value) + } else { + Err(LuaError::RuntimeError(format!( + "{} is not a valid member of {}", + field_name, class_name + ))) + } + }); +} + +pub(crate) fn add_class_restricted_method<'lua, M: LuaUserDataMethods<'lua, Instance>, A, R, F>( + methods: &mut M, + class_name: &'static str, + method_name: &'static str, + method: F, +) where + A: FromLuaMulti<'lua>, + R: IntoLuaMulti<'lua>, + F: 'static + Fn(&'lua Lua, &Instance, A) -> LuaResult, +{ + methods.add_method(method_name, move |lua, this, args| { + if class_is_a(this.get_class_name(), class_name).unwrap_or(false) { + method(lua, this, args) + } else { + Err(LuaError::RuntimeError(format!( + "{} is not a valid member of {}", + method_name, class_name + ))) + } + }); +} + +pub(crate) fn add_class_restricted_method_mut< + 'lua, + M: LuaUserDataMethods<'lua, Instance>, + A, + R, + F, +>( + methods: &mut M, + class_name: &'static str, + method_name: &'static str, + method: F, +) where + A: FromLuaMulti<'lua>, + R: IntoLuaMulti<'lua>, + F: 'static + Fn(&'lua Lua, &mut Instance, A) -> LuaResult, +{ + methods.add_method_mut(method_name, move |lua, this, args| { + if class_is_a(this.get_class_name(), class_name).unwrap_or(false) { + method(lua, this, args) + } else { + Err(LuaError::RuntimeError(format!( + "{} is not a valid member of {}", + method_name, class_name + ))) + } + }); +} + +/** + Gets or creates the instance child with the given reference prop name and class name. + + Note that the class name here must be an exact match, it is not checked using IsA. + + The instance may be in one of several states but this function will guarantee that the + property reference is correct and that the instance exists after it has been called: + + 1. Instance exists as property ref - just return it + 2. Instance exists under workspace but not as a property ref - save it and return it + 3. Instance does not exist - create it, save it, then return it +*/ +pub(crate) fn get_or_create_property_ref_instance( + this: &Instance, + prop_name: &'static str, + class_name: &'static str, +) -> LuaResult { + if let Some(DomValue::Ref(inst_ref)) = this.get_property(prop_name) { + if let Some(inst) = Instance::new_opt(inst_ref) { + return Ok(inst); + } + } + if let Some(inst) = this.find_child(|child| child.class == class_name) { + this.set_property(prop_name, DomValue::Ref(inst.dom_ref)); + Ok(inst) + } else { + let inst = Instance::new_orphaned(class_name); + inst.set_parent(Some(this.clone())); + this.set_property(prop_name, DomValue::Ref(inst.dom_ref)); + Ok(inst) + } +} diff --git a/crates/lune-roblox/src/shared/instance.rs b/crates/lune-roblox/src/shared/instance.rs new file mode 100644 index 0000000..a685ffb --- /dev/null +++ b/crates/lune-roblox/src/shared/instance.rs @@ -0,0 +1,209 @@ +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>, + pub enum_default: Option, + pub value_type: Option, + 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, + property_name: impl AsRef, +) -> Option { + 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) -> 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, class_name: impl AsRef) -> Option { + 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) -> Option { + 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); + } +} diff --git a/crates/lune-roblox/src/shared/mod.rs b/crates/lune-roblox/src/shared/mod.rs new file mode 100644 index 0000000..b57e1c4 --- /dev/null +++ b/crates/lune-roblox/src/shared/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod classes; +pub(crate) mod instance; +pub(crate) mod userdata; diff --git a/crates/lune-roblox/src/shared/userdata.rs b/crates/lune-roblox/src/shared/userdata.rs new file mode 100644 index 0000000..e43fb9a --- /dev/null +++ b/crates/lune-roblox/src/shared/userdata.rs @@ -0,0 +1,165 @@ +use std::{any::type_name, cell::RefCell, fmt, ops}; + +use mlua::prelude::*; + +// Utility functions + +type ListWriter = dyn Fn(&mut fmt::Formatter<'_>, bool, &str) -> fmt::Result; +pub fn make_list_writer() -> Box { + let first = RefCell::new(true); + Box::new(move |f, flag, literal| { + if flag { + if first.take() { + write!(f, "{}", literal)?; + } else { + write!(f, ", {}", literal)?; + } + } + Ok::<_, fmt::Error>(()) + }) +} + +// Userdata metamethod implementations + +pub fn userdata_impl_to_string(_: &Lua, datatype: &D, _: ()) -> LuaResult +where + D: LuaUserData + ToString + 'static, +{ + Ok(datatype.to_string()) +} + +pub fn userdata_impl_eq(_: &Lua, datatype: &D, value: LuaValue) -> LuaResult +where + D: LuaUserData + PartialEq + 'static, +{ + if let LuaValue::UserData(ud) = value { + if let Ok(value_as_datatype) = ud.borrow::() { + Ok(*datatype == *value_as_datatype) + } else { + Ok(false) + } + } else { + Ok(false) + } +} + +pub fn userdata_impl_unm(_: &Lua, datatype: &D, _: ()) -> LuaResult +where + D: LuaUserData + ops::Neg + Copy, +{ + Ok(-*datatype) +} + +pub fn userdata_impl_add(_: &Lua, datatype: &D, value: LuaUserDataRef) -> LuaResult +where + D: LuaUserData + ops::Add + Copy, +{ + Ok(*datatype + *value) +} + +pub fn userdata_impl_sub(_: &Lua, datatype: &D, value: LuaUserDataRef) -> LuaResult +where + D: LuaUserData + ops::Sub + Copy, +{ + Ok(*datatype - *value) +} + +pub fn userdata_impl_mul_f32(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult +where + D: LuaUserData + ops::Mul + ops::Mul + Copy + 'static, +{ + match &rhs { + LuaValue::Number(n) => return Ok(*datatype * *n as f32), + LuaValue::Integer(i) => return Ok(*datatype * *i as f32), + LuaValue::UserData(ud) => { + if let Ok(vec) = ud.borrow::() { + return Ok(*datatype * *vec); + } + } + _ => {} + }; + Err(LuaError::FromLuaConversionError { + from: rhs.type_name(), + to: type_name::(), + message: Some(format!( + "Expected {} or number, got {}", + type_name::(), + rhs.type_name() + )), + }) +} + +pub fn userdata_impl_mul_i32(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult +where + D: LuaUserData + ops::Mul + ops::Mul + Copy + 'static, +{ + match &rhs { + LuaValue::Number(n) => return Ok(*datatype * *n as i32), + LuaValue::Integer(i) => return Ok(*datatype * *i), + LuaValue::UserData(ud) => { + if let Ok(vec) = ud.borrow::() { + return Ok(*datatype * *vec); + } + } + _ => {} + }; + Err(LuaError::FromLuaConversionError { + from: rhs.type_name(), + to: type_name::(), + message: Some(format!( + "Expected {} or number, got {}", + type_name::(), + rhs.type_name() + )), + }) +} + +pub fn userdata_impl_div_f32(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult +where + D: LuaUserData + ops::Div + ops::Div + Copy + 'static, +{ + match &rhs { + LuaValue::Number(n) => return Ok(*datatype / *n as f32), + LuaValue::Integer(i) => return Ok(*datatype / *i as f32), + LuaValue::UserData(ud) => { + if let Ok(vec) = ud.borrow::() { + return Ok(*datatype / *vec); + } + } + _ => {} + }; + Err(LuaError::FromLuaConversionError { + from: rhs.type_name(), + to: type_name::(), + message: Some(format!( + "Expected {} or number, got {}", + type_name::(), + rhs.type_name() + )), + }) +} + +pub fn userdata_impl_div_i32(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult +where + D: LuaUserData + ops::Div + ops::Div + Copy + 'static, +{ + match &rhs { + LuaValue::Number(n) => return Ok(*datatype / *n as i32), + LuaValue::Integer(i) => return Ok(*datatype / *i), + LuaValue::UserData(ud) => { + if let Ok(vec) = ud.borrow::() { + return Ok(*datatype / *vec); + } + } + _ => {} + }; + Err(LuaError::FromLuaConversionError { + from: rhs.type_name(), + to: type_name::(), + message: Some(format!( + "Expected {} or number, got {}", + type_name::(), + rhs.type_name() + )), + }) +}