diff --git a/packages/lib-roblox/src/datatypes/conversion.rs b/packages/lib-roblox/src/datatypes/conversion.rs index 8285529..ac62908 100644 --- a/packages/lib-roblox/src/datatypes/conversion.rs +++ b/packages/lib-roblox/src/datatypes/conversion.rs @@ -113,23 +113,28 @@ impl<'lua> RbxVariantToLua<'lua> for LuaAnyUserData<'lua> { fn rbx_variant_to_lua(lua: &'lua Lua, variant: &RbxVariant) -> DatatypeConversionResult { use super::types::*; use RbxVariant as Rbx; + use rbx_dom_weak::types as rbx; - // NOTE: Enum is intentionally left out here, it has a custom - // conversion going from instance property > datatype instead, - // check `EnumItem::from_instance_property` for specifics + /* + NOTES: + + 1. Enum is intentionally left out here, it has a custom + conversion going from instance property > datatype instead, + check `EnumItem::from_instance_property` for specifics + + 2. PhysicalProperties can only be converted if they are custom + physical properties, since default physical properties values + depend on what other related properties an instance might have + + */ Ok(match variant.clone() { // Not yet implemented datatypes // Rbx::Font(_) => todo!(), - // Rbx::PhysicalProperties(_) => todo!(), Rbx::Axes(value) => lua.create_userdata(Axes::from(value))?, Rbx::Faces(value) => lua.create_userdata(Faces::from(value))?, Rbx::CFrame(value) => lua.create_userdata(CFrame::from(value))?, - Rbx::OptionalCFrame(value) => match value { - Some(value) => lua.create_userdata(CFrame::from(value))?, - None => lua.create_userdata(CFrame::IDENTITY)? - }, Rbx::BrickColor(value) => lua.create_userdata(BrickColor::from(value))?, Rbx::Color3(value) => lua.create_userdata(Color3::from(value))?, @@ -152,6 +157,15 @@ impl<'lua> RbxVariantToLua<'lua> for LuaAnyUserData<'lua> { Rbx::Vector3(value) => lua.create_userdata(Vector3::from(value))?, Rbx::Vector3int16(value) => lua.create_userdata(Vector3int16::from(value))?, + Rbx::OptionalCFrame(value) => match value { + Some(value) => lua.create_userdata(CFrame::from(value))?, + None => lua.create_userdata(CFrame::IDENTITY)? + }, + + Rbx::PhysicalProperties(rbx::PhysicalProperties::Custom(value)) => { + lua.create_userdata(PhysicalProperties::from(value))? + }, + v => { return Err(DatatypeConversionError::FromRbxVariant { from: v.variant_name(), @@ -177,11 +191,7 @@ impl<'lua> LuaToRbxVariant<'lua> for LuaAnyUserData<'lua> { RbxVariantType::Axes => convert::, RbxVariantType::Faces => convert::, - RbxVariantType::CFrame => convert::, - RbxVariantType::OptionalCFrame => return match self.borrow::() { - Ok(value) => Ok(RbxVariant::OptionalCFrame(Some(rbx::CFrame::from(*value)))), - Err(e) => Err(lua_userdata_error_to_conversion_error(variant_type, e)), - }, + RbxVariantType::CFrame => convert::, RbxVariantType::BrickColor => convert::, RbxVariantType::Color3 => convert::, @@ -206,6 +216,20 @@ impl<'lua> LuaToRbxVariant<'lua> for LuaAnyUserData<'lua> { RbxVariantType::Vector3 => convert::, RbxVariantType::Vector3int16 => convert::, + RbxVariantType::OptionalCFrame => return match self.borrow::() { + Ok(value) => Ok(RbxVariant::OptionalCFrame(Some(rbx::CFrame::from(*value)))), + Err(e) => Err(lua_userdata_error_to_conversion_error(variant_type, e)), + }, + + RbxVariantType::PhysicalProperties => return match self.borrow::() { + Ok(value) => { + let props = rbx::CustomPhysicalProperties::from(*value); + let custom = rbx::PhysicalProperties::Custom(props); + Ok(RbxVariant::PhysicalProperties(custom)) + }, + Err(e) => Err(lua_userdata_error_to_conversion_error(variant_type, e)), + }, + _ => return Err(DatatypeConversionError::ToRbxVariant { to: variant_type.variant_name(), from: "userdata", @@ -213,7 +237,7 @@ impl<'lua> LuaToRbxVariant<'lua> for LuaAnyUserData<'lua> { }), }; - f(self, variant_type) + f(self, variant_type) } } diff --git a/packages/lib-roblox/src/datatypes/types/mod.rs b/packages/lib-roblox/src/datatypes/types/mod.rs index 09fe054..bc2ebce 100644 --- a/packages/lib-roblox/src/datatypes/types/mod.rs +++ b/packages/lib-roblox/src/datatypes/types/mod.rs @@ -11,6 +11,7 @@ mod faces; mod number_range; mod number_sequence; mod number_sequence_keypoint; +mod physical_properties; mod ray; mod rect; mod region3; @@ -32,6 +33,7 @@ pub use faces::Faces; 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; @@ -99,6 +101,7 @@ mod tests { number_range: "datatypes/NumberRange", number_sequence: "datatypes/NumberSequence", number_sequence_keypoint: "datatypes/NumberSequenceKeypoint", + physical_properties: "datatypes/PhysicalProperties", ray: "datatypes/Ray", rect: "datatypes/Rect", udim: "datatypes/UDim", diff --git a/packages/lib-roblox/src/datatypes/types/physical_properties.rs b/packages/lib-roblox/src/datatypes/types/physical_properties.rs new file mode 100644 index 0000000..466d01c --- /dev/null +++ b/packages/lib-roblox/src/datatypes/types/physical_properties.rs @@ -0,0 +1,183 @@ +use core::fmt; + +use mlua::prelude::*; +use rbx_dom_weak::types::CustomPhysicalProperties as RbxCustomPhysicalProperties; + +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, + }) + } + + pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { + type ArgsMaterial = EnumItem; + type ArgsNumbers = (f32, f32, f32, Option, Option); + datatype_table.set( + "new", + lua.create_function(|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(), + )) + } + })?, + ) + } +} + +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: RbxCustomPhysicalProperties) -> Self { + Self { + density: v.density, + friction: v.friction, + friction_weight: v.friction_weight, + elasticity: v.elasticity, + elasticity_weight: v.elasticity_weight, + } + } +} + +impl From for RbxCustomPhysicalProperties { + fn from(v: PhysicalProperties) -> Self { + RbxCustomPhysicalProperties { + 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/packages/lib-roblox/src/lib.rs b/packages/lib-roblox/src/lib.rs index 2d66e1e..99201ab 100644 --- a/packages/lib-roblox/src/lib.rs +++ b/packages/lib-roblox/src/lib.rs @@ -29,6 +29,7 @@ fn make_all_datatypes(lua: &Lua) -> LuaResult> { ("NumberRange", make_dt(lua, NumberRange::make_table)?), ("NumberSequence", make_dt(lua, NumberSequence::make_table)?), ("NumberSequenceKeypoint", make_dt(lua, NumberSequenceKeypoint::make_table)?), + ("PhysicalProperties", make_dt(lua, PhysicalProperties::make_table)?), ("Ray", make_dt(lua, Ray::make_table)?), ("Rect", make_dt(lua, Rect::make_table)?), ("UDim", make_dt(lua, UDim::make_table)?), diff --git a/tests/roblox/datatypes/PhysicalProperties.luau b/tests/roblox/datatypes/PhysicalProperties.luau new file mode 100644 index 0000000..edf9722 --- /dev/null +++ b/tests/roblox/datatypes/PhysicalProperties.luau @@ -0,0 +1,65 @@ +-- HACK: Make luau happy, with the mlua rust +-- crate all globals are also present in _G +local PhysicalProperties = _G.PhysicalProperties +local Enum = _G.Enum + +-- Constructors & properties + +PhysicalProperties.new(Enum.Material.SmoothPlastic) +PhysicalProperties.new(0, 0, 0) +PhysicalProperties.new(0, 0, 0, 0, 0) + +assert(not pcall(function() + return PhysicalProperties.new() +end)) +assert(not pcall(function() + return PhysicalProperties.new(false) +end)) +assert(not pcall(function() + return PhysicalProperties.new({}) +end)) +assert(not pcall(function() + return PhysicalProperties.new(newproxy(true)) +end)) +assert(not pcall(function() + return PhysicalProperties.new(Enum.Axis.X) +end)) + +assert(PhysicalProperties.new(1, 2, 3).FrictionWeight == 1) +assert(PhysicalProperties.new(1, 2, 3).ElasticityWeight == 1) + +assert(PhysicalProperties.new(1, 2, 3, 4, 5).Density == 1) +assert(PhysicalProperties.new(1, 2, 3, 4, 5).Friction == 2) +assert(PhysicalProperties.new(1, 2, 3, 4, 5).Elasticity == 3) +assert(PhysicalProperties.new(1, 2, 3, 4, 5).FrictionWeight == 4) +assert(PhysicalProperties.new(1, 2, 3, 4, 5).ElasticityWeight == 5) + +local function fuzzyEq(n0: number, n1: number) + return math.abs(n1 - n0) <= 0.0001 +end + +local plastic = PhysicalProperties.new(Enum.Material.Plastic) +assert(fuzzyEq(plastic.Density, 0.7)) +assert(fuzzyEq(plastic.Friction, 0.3)) +assert(fuzzyEq(plastic.Elasticity, 0.5)) + +local splastic = PhysicalProperties.new(Enum.Material.SmoothPlastic) +assert(fuzzyEq(splastic.Density, 0.7)) +assert(fuzzyEq(splastic.Friction, 0.2)) +assert(fuzzyEq(splastic.Elasticity, 0.5)) + +local sand = PhysicalProperties.new(Enum.Material.Sand) +assert(fuzzyEq(sand.Density, 1.6)) +assert(fuzzyEq(sand.Friction, 0.5)) +assert(fuzzyEq(sand.Elasticity, 0.05)) +assert(fuzzyEq(sand.FrictionWeight, 5)) +assert(fuzzyEq(sand.ElasticityWeight, 2.5)) + +-- Ops + +assert(not pcall(function() + return PhysicalProperties.new() + PhysicalProperties.new() +end)) +assert(not pcall(function() + return PhysicalProperties.new() / PhysicalProperties.new() +end))