From 501933e3f99209d792ece7f4b8b6d7ff05c17b61 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Fri, 10 Mar 2023 11:07:03 +0100 Subject: [PATCH] Initial implementation of datatypes module, first datatype impl --- Cargo.lock | 7 + packages/lib-roblox/Cargo.toml | 4 +- packages/lib-roblox/src/datatypes/mod.rs | 107 +++++++++++ packages/lib-roblox/src/datatypes/vector3.rs | 182 +++++++++++++++++++ packages/lib-roblox/src/document/mod.rs | 2 +- packages/lib-roblox/src/lib.rs | 22 ++- 6 files changed, 317 insertions(+), 7 deletions(-) create mode 100644 packages/lib-roblox/src/datatypes/mod.rs create mode 100644 packages/lib-roblox/src/datatypes/vector3.rs diff --git a/Cargo.lock b/Cargo.lock index adbc934..cf9e2a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -534,6 +534,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "glam" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e4afd9ad95555081e109fe1d21f2a30c691b5f0919c67dfa690a2e1eb6bd51c" + [[package]] name = "h2" version = "0.3.16" @@ -855,6 +861,7 @@ dependencies = [ name = "lune-roblox" version = "0.5.5" dependencies = [ + "glam", "mlua", "rbx_binary", "rbx_dom_weak", diff --git a/packages/lib-roblox/Cargo.toml b/packages/lib-roblox/Cargo.toml index 5e2c6cc..1d7ea0b 100644 --- a/packages/lib-roblox/Cargo.toml +++ b/packages/lib-roblox/Cargo.toml @@ -17,6 +17,7 @@ path = "src/lib.rs" [dependencies] mlua.workspace = true +glam = "0.23" thiserror = "1.0" rbx_binary = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" } @@ -24,6 +25,3 @@ rbx_dom_weak = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18 rbx_reflection = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" } rbx_reflection_database = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" } rbx_xml = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" } - -# TODO: Split lune lib out into something like lune-core so -# that we can use filesystem and async apis in this crate diff --git a/packages/lib-roblox/src/datatypes/mod.rs b/packages/lib-roblox/src/datatypes/mod.rs new file mode 100644 index 0000000..ef162ba --- /dev/null +++ b/packages/lib-roblox/src/datatypes/mod.rs @@ -0,0 +1,107 @@ +use mlua::prelude::*; + +pub(crate) use rbx_dom_weak::types::{Variant as RbxVariant, VariantType as RbxVariantType}; + +// NOTE: We create a new inner module scope here to make imports of datatypes more ergonomic + +mod vector3; + +pub mod types { + pub use super::vector3::Vector3; +} + +// Trait definitions for conversion between rbx_dom_weak variant <-> datatype + +#[allow(dead_code)] +pub(crate) enum RbxConversionError { + FromRbxVariant { + from: &'static str, + to: &'static str, + detail: Option, + }, + ToRbxVariant { + to: &'static str, + from: &'static str, + detail: Option, + }, + DesiredTypeMismatch { + actual: &'static str, + detail: Option, + }, +} + +pub(crate) type RbxConversionResult = Result; + +pub(crate) trait ToRbxVariant { + fn to_rbx_variant( + &self, + desired_type: Option, + ) -> RbxConversionResult; +} + +pub(crate) trait FromRbxVariant: Sized { + fn from_rbx_variant(variant: &RbxVariant) -> RbxConversionResult; +} + +pub(crate) trait DatatypeTable { + fn make_dt_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()>; +} + +// NOTE: This implementation is .. not great, but it's the best we can +// do since we can't implement a trait like Display on a foreign type, +// and we are really only using it to make better error messages anyway + +trait RbxVariantDisplayName { + fn display_name(&self) -> &'static str; +} + +impl RbxVariantDisplayName for RbxVariantType { + fn display_name(&self) -> &'static str { + use RbxVariantType::*; + match self { + 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", + Int32 => "Int32", + Int64 => "Int64", + NumberRange => "NumberRange", + NumberSequence => "NumberSequence", + PhysicalProperties => "PhysicalProperties", + Ray => "Ray", + Rect => "Rect", + Ref => "Ref", + Region3 => "Region3", + Region3int16 => "Region3int16", + SharedString => "SharedString", + String => "String", + UDim => "UDim", + UDim2 => "UDim2", + Vector2 => "Vector2", + Vector2int16 => "Vector2int16", + Vector3 => "Vector3", + Vector3int16 => "Vector3int16", + OptionalCFrame => "OptionalCFrame", + _ => "?", + } + } +} + +impl RbxVariantDisplayName for RbxVariant { + fn display_name(&self) -> &'static str { + self.ty().display_name() + } +} + +// TODO: Implement tests for all datatypes in lua and run them here +// using the same mechanic we have to run tests in the main lib, these +// tests should also live next to other folders like fs, net, task, .. diff --git a/packages/lib-roblox/src/datatypes/vector3.rs b/packages/lib-roblox/src/datatypes/vector3.rs new file mode 100644 index 0000000..0917c8e --- /dev/null +++ b/packages/lib-roblox/src/datatypes/vector3.rs @@ -0,0 +1,182 @@ +use core::fmt; + +use glam::Vec3A; +use mlua::prelude::*; +use rbx_dom_weak::types::Vector3 as RbxVector3; + +use super::*; + +/** + An implementation of the [Vector3](https://create.roblox.com/docs/reference/engine/datatypes/Vector3) + Roblox datatype, backed by [`glam::Vec3A`]. + + This implements all documented properties & methods of the Vector3 + class as of March 2023, as well as the `new(x, y, z)` constructor. + + 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)] +pub struct Vector3(pub Vec3A); + +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 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: Vector3| { + Ok(this.0.angle_between(rhs.0)) + }); + methods.add_method("Cross", |_, this, rhs: Vector3| { + Ok(Vector3(this.0.cross(rhs.0))) + }); + methods.add_method("Dot", |_, this, rhs: Vector3| Ok(this.0.dot(rhs.0))); + methods.add_method("FuzzyEq", |_, this, (rhs, epsilon): (Vector3, 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): (Vector3, f32)| { + Ok(Vector3(this.0.lerp(rhs.0, alpha))) + }); + methods.add_method("Max", |_, this, rhs: Vector3| { + Ok(Vector3(this.0.max(rhs.0))) + }); + methods.add_method("Min", |_, this, rhs: Vector3| { + Ok(Vector3(this.0.min(rhs.0))) + }); + // Metamethods - normal + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(this.to_string())); + methods.add_meta_method(LuaMetaMethod::Eq, |_, this, rhs: LuaValue| { + if let LuaValue::UserData(ud) = rhs { + if let Ok(vec) = ud.borrow::() { + Ok(this.0 == vec.0) + } else { + Ok(false) + } + } else { + Ok(false) + } + }); + // Metamethods - math + methods.add_meta_method(LuaMetaMethod::Unm, |_, this, ()| Ok(Vector3(-this.0))); + methods.add_meta_method(LuaMetaMethod::Add, |_, this, rhs: Vector3| { + Ok(Vector3(this.0 + rhs.0)) + }); + methods.add_meta_method(LuaMetaMethod::Sub, |_, this, rhs: Vector3| { + Ok(Vector3(this.0 - rhs.0)) + }); + methods.add_meta_method(LuaMetaMethod::Mul, |_, this, rhs: LuaValue| { + match &rhs { + LuaValue::Number(n) => return Ok(Vector3(this.0 * Vec3A::splat(*n as f32))), + LuaValue::UserData(ud) => { + if let Ok(vec) = ud.borrow::() { + return Ok(Vector3(this.0 * vec.0)); + } + } + _ => {} + }; + Err(LuaError::FromLuaConversionError { + from: rhs.type_name(), + to: "Vector3", + message: Some(format!( + "Expected Vector3 or number, got {}", + rhs.type_name() + )), + }) + }); + methods.add_meta_method(LuaMetaMethod::Div, |_, this, rhs: LuaValue| { + match &rhs { + LuaValue::Number(n) => return Ok(Vector3(this.0 / Vec3A::splat(*n as f32))), + LuaValue::UserData(ud) => { + if let Ok(vec) = ud.borrow::() { + return Ok(Vector3(this.0 / vec.0)); + } + } + _ => {} + }; + Err(LuaError::FromLuaConversionError { + from: rhs.type_name(), + to: "Vector3", + message: Some(format!( + "Expected Vector3 or number, got {}", + rhs.type_name() + )), + }) + }); + } +} + +impl DatatypeTable for Vector3 { + fn make_dt_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { + // Constants + datatype_table.set("xAxis", Vector3(Vec3A::X))?; + datatype_table.set("yAxis", Vector3(Vec3A::Y))?; + datatype_table.set("zAxis", Vector3(Vec3A::Z))?; + datatype_table.set("zero", Vector3(Vec3A::ZERO))?; + datatype_table.set("one", Vector3(Vec3A::ONE))?; + // Constructors + datatype_table.set( + "new", + lua.create_function(|_, (x, y, z): (Option, Option, Option)| { + Ok(Vector3(Vec3A { + x: x.unwrap_or_default(), + y: y.unwrap_or_default(), + z: z.unwrap_or_default(), + })) + })?, + ) + // FUTURE: Implement FromNormalId and FromAxis constructors? + } +} + +impl FromRbxVariant for Vector3 { + fn from_rbx_variant(variant: &RbxVariant) -> RbxConversionResult { + if let RbxVariant::Vector3(v) = variant { + Ok(Vector3(Vec3A { + x: v.x, + y: v.y, + z: v.z, + })) + } else { + Err(RbxConversionError::FromRbxVariant { + from: variant.display_name(), + to: "Vector3", + detail: None, + }) + } + } +} + +impl ToRbxVariant for Vector3 { + fn to_rbx_variant( + &self, + desired_type: Option, + ) -> RbxConversionResult { + if matches!(desired_type, None | Some(RbxVariantType::Vector3)) { + Ok(RbxVariant::Vector3(RbxVector3 { + x: self.0.x, + y: self.0.y, + z: self.0.z, + })) + } else { + Err(RbxConversionError::DesiredTypeMismatch { + actual: RbxVariantType::Vector3.display_name(), + detail: None, + }) + } + } +} diff --git a/packages/lib-roblox/src/document/mod.rs b/packages/lib-roblox/src/document/mod.rs index 48bc8f6..162ec52 100644 --- a/packages/lib-roblox/src/document/mod.rs +++ b/packages/lib-roblox/src/document/mod.rs @@ -17,7 +17,7 @@ pub use kind::*; pub type DocumentResult = Result; /** - A wrapper for [`rbx_dom_weak::WeakDom`] that also takes care of + A container for [`rbx_dom_weak::WeakDom`] that also takes care of reading and writing different kinds and formats of roblox files. ```rust ignore diff --git a/packages/lib-roblox/src/lib.rs b/packages/lib-roblox/src/lib.rs index 44777ba..3ccd6e0 100644 --- a/packages/lib-roblox/src/lib.rs +++ b/packages/lib-roblox/src/lib.rs @@ -1,11 +1,27 @@ use mlua::prelude::*; -mod instance; - +pub mod datatypes; pub mod document; +pub mod instance; + +use datatypes::types::*; +use datatypes::DatatypeTable; + +fn make_dt(lua: &Lua, f: F) -> LuaResult +where + F: Fn(&Lua, &LuaTable) -> LuaResult<()>, +{ + let tab = lua.create_table()?; + f(lua, &tab)?; + tab.set_readonly(true); + Ok(tab) +} pub fn module(lua: &Lua) -> LuaResult { + let datatypes = vec![("Vector3", make_dt(lua, Vector3::make_dt_table)?)]; let exports = lua.create_table()?; - + for (name, tab) in datatypes { + exports.set(name, tab)?; + } Ok(exports) }