diff --git a/packages/lib-roblox/src/datatypes/attributes.rs b/packages/lib-roblox/src/datatypes/attributes.rs index 42c55fd..a23d155 100644 --- a/packages/lib-roblox/src/datatypes/attributes.rs +++ b/packages/lib-roblox/src/datatypes/attributes.rs @@ -50,7 +50,7 @@ pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> { } else { Err(LuaError::RuntimeError(format!( "'{}' is not a valid attribute type", - value.ty().variant_name() + value.ty().variant_name().unwrap_or("???") ))) } } diff --git a/packages/lib-roblox/src/datatypes/conversion.rs b/packages/lib-roblox/src/datatypes/conversion.rs index 692fe4c..9e7271d 100644 --- a/packages/lib-roblox/src/datatypes/conversion.rs +++ b/packages/lib-roblox/src/datatypes/conversion.rs @@ -116,7 +116,7 @@ impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> { (LuaValue::UserData(u), d) => u.lua_to_dom_value(lua, Some(d)), (v, d) => Err(DomConversionError::ToDomValue { - to: d.variant_name(), + to: d.variant_name().unwrap_or("???"), from: v.type_name(), detail: None, }), @@ -154,6 +154,16 @@ macro_rules! dom_to_userdata { }; } +/** + 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>() { @@ -212,7 +222,7 @@ impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> { v => { Err(DomConversionError::FromDomValue { - from: v.variant_name(), + from: v.variant_name().unwrap_or("???"), to: "userdata", detail: Some("Type not supported".to_string()), }) @@ -282,7 +292,7 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> { ty => { return Err(DomConversionError::ToDomValue { - to: ty.variant_name(), + to: ty.variant_name().unwrap_or("???"), from: "userdata", detail: Some("Type not supported".to_string()), }) @@ -305,11 +315,11 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> { 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 Instance => dom::Ref), 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), diff --git a/packages/lib-roblox/src/datatypes/extension.rs b/packages/lib-roblox/src/datatypes/extension.rs index ed66a3a..64c460b 100644 --- a/packages/lib-roblox/src/datatypes/extension.rs +++ b/packages/lib-roblox/src/datatypes/extension.rs @@ -1,13 +1,17 @@ +use mlua::prelude::*; + +use crate::instance::Instance; + use super::*; pub(super) trait DomValueExt { - fn variant_name(&self) -> &'static str; + fn variant_name(&self) -> Option<&'static str>; } impl DomValueExt for DomType { - fn variant_name(&self) -> &'static str { + fn variant_name(&self) -> Option<&'static str> { use DomType::*; - match self { + Some(match self { Axes => "Axes", BinaryString => "BinaryString", Bool => "Bool", @@ -40,13 +44,54 @@ impl DomValueExt for DomType { Vector3 => "Vector3", Vector3int16 => "Vector3int16", OptionalCFrame => "OptionalCFrame", - _ => "?", - } + _ => return None, + }) } } impl DomValueExt for DomValue { - fn variant_name(&self) -> &'static str { + fn variant_name(&self) -> Option<&'static str> { self.ty().variant_name() } } + +pub trait RobloxUserdataTypenameExt { + fn roblox_type_name(&self) -> Option<&'static str>; +} + +impl<'lua> RobloxUserdataTypenameExt for LuaAnyUserData<'lua> { + #[rustfmt::skip] + fn roblox_type_name(&self) -> Option<&'static str> { + use super::types::*; + + Some(match self { + value if value.is::() => "Axes", + value if value.is::() => "BrickColor", + value if value.is::() => "CFrame", + value if value.is::() => "Color3", + value if value.is::() => "ColorSequence", + value if value.is::() => "ColorSequenceKeypoint", + value if value.is::() => "Enums", + value if value.is::() => "Enum", + value if value.is::() => "EnumItem", + value if value.is::() => "Faces", + value if value.is::() => "Font", + value if value.is::() => "Instance", + value if value.is::() => "NumberRange", + value if value.is::() => "NumberSequence", + value if value.is::() => "NumberSequenceKeypoint", + value if value.is::() => "PhysicalProperties", + value if value.is::() => "Ray", + value if value.is::() => "Rect", + value if value.is::() => "Region3", + value if value.is::() => "Region3int16", + value if value.is::() => "UDim", + value if value.is::() => "UDim2", + value if value.is::() => "Vector2", + value if value.is::() => "Vector2int16", + value if value.is::() => "Vector3", + value if value.is::() => "Vector3int16", + _ => return None, + }) + } +} diff --git a/packages/lib/src/builtins/task.rs b/packages/lib/src/builtins/task.rs index fe5c9ee..38e87c9 100644 --- a/packages/lib/src/builtins/task.rs +++ b/packages/lib/src/builtins/task.rs @@ -48,16 +48,11 @@ pub fn create(lua: &'static Lua) -> LuaResult> { .build_readonly()?, )? .into_function()?; - // We want the task scheduler to be transparent, - // but it does not return real lua threads, so - // we need to override some globals to fake it - let globals = lua.globals(); - globals.set("type", lua.create_function(proxy_type)?)?; - globals.set("typeof", lua.create_function(proxy_typeof)?)?; // Functions in the built-in coroutine library also need to be // replaced, these are a bit different than the ones above because // calling resume or the function that wrap returns must return // whatever lua value(s) that the thread or task yielded back + let globals = lua.globals(); let coroutine = globals.get::<_, LuaTable>("coroutine")?; coroutine.set("status", lua.create_function(coroutine_status)?)?; coroutine.set("resume", lua.create_function(coroutine_resume)?)?; @@ -98,30 +93,6 @@ fn task_delay( sched.schedule_blocking_after_seconds(secs, tof.into_thread(lua)?, args) } -/* - Type getter overrides for compat with task scheduler -*/ - -fn proxy_type<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult> { - if let LuaValue::UserData(u) = &value { - if u.is::() { - return lua.create_string("thread"); - } - } - lua.named_registry_value::<_, LuaFunction>("type")? - .call(value) -} - -fn proxy_typeof<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult> { - if let LuaValue::UserData(u) = &value { - if u.is::() { - return lua.create_string("thread"); - } - } - lua.named_registry_value::<_, LuaFunction>("typeof")? - .call(value) -} - /* Coroutine library overrides for compat with task scheduler */ diff --git a/packages/lib/src/builtins/top_level.rs b/packages/lib/src/builtins/top_level.rs index cb3fc97..2d22027 100644 --- a/packages/lib/src/builtins/top_level.rs +++ b/packages/lib/src/builtins/top_level.rs @@ -1,6 +1,12 @@ use mlua::prelude::*; -use crate::lua::stdio::formatting::{format_label, pretty_format_multi_value}; +#[cfg(feature = "roblox")] +use lune_roblox::datatypes::extension::RobloxUserdataTypenameExt; + +use crate::lua::{ + stdio::formatting::{format_label, pretty_format_multi_value}, + task::TaskReference, +}; // HACK: We need to preserve the default behavior of the // print and error functions, for pcall and such, which @@ -42,4 +48,30 @@ pub fn error(lua: &Lua, (arg, level): (LuaValue, Option)) -> LuaResult<()> Ok(()) } +pub fn proxy_type<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult> { + if let LuaValue::UserData(u) = &value { + if u.is::() { + return lua.create_string("thread"); + } + } + lua.named_registry_value::<_, LuaFunction>("type")? + .call(value) +} + +pub fn proxy_typeof<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult> { + if let LuaValue::UserData(u) = &value { + if u.is::() { + return lua.create_string("thread"); + } + #[cfg(feature = "roblox")] + { + if let Some(type_name) = u.roblox_type_name() { + return lua.create_string(type_name); + } + } + } + lua.named_registry_value::<_, LuaFunction>("typeof")? + .call(value) +} + // TODO: Add an override for tostring that formats errors in a nicer way diff --git a/packages/lib/src/importer/mod.rs b/packages/lib/src/importer/mod.rs index d4811b9..cb561b7 100644 --- a/packages/lib/src/importer/mod.rs +++ b/packages/lib/src/importer/mod.rs @@ -37,6 +37,8 @@ pub fn create(lua: &'static Lua, args: Vec) -> LuaResult<()> { ("print", lua.create_function(top_level::print)?), ("warn", lua.create_function(top_level::warn)?), ("error", lua.create_function(top_level::error)?), + ("type", lua.create_function(top_level::proxy_type)?), + ("typeof", lua.create_function(top_level::proxy_typeof)?), ]; // Set top-level globals diff --git a/packages/lib/src/tests.rs b/packages/lib/src/tests.rs index 9355b8d..b9b3cb7 100644 --- a/packages/lib/src/tests.rs +++ b/packages/lib/src/tests.rs @@ -147,4 +147,6 @@ create_tests! { roblox_instance_methods_is_a: "roblox/instance/methods/IsA", roblox_instance_methods_is_ancestor_of: "roblox/instance/methods/IsAncestorOf", roblox_instance_methods_is_descendant_of: "roblox/instance/methods/IsDescendantOf", + + roblox_misc_typeof: "roblox/misc/typeof", } diff --git a/tests/roblox/misc/typeof.luau b/tests/roblox/misc/typeof.luau new file mode 100644 index 0000000..fd354f4 --- /dev/null +++ b/tests/roblox/misc/typeof.luau @@ -0,0 +1,44 @@ +local roblox = require("@lune/roblox") :: any + +local TYPES_AND_VALUES = { + Axes = roblox.Axes.new(), + BrickColor = roblox.BrickColor.new("Really red"), + CFrame = roblox.CFrame.new(), + Color3 = roblox.Color3.new(0, 0, 0), + ColorSequence = roblox.ColorSequence.new(roblox.Color3.new(0, 0, 0)), + ColorSequenceKeypoint = roblox.ColorSequenceKeypoint.new(0, roblox.Color3.new(0, 0, 0)), + Enums = roblox.Enum, + Enum = roblox.Enum.KeyCode, + EnumItem = roblox.Enum.KeyCode.Unknown, + Faces = roblox.Faces.new(), + Font = roblox.Font.new("Gotham"), + NumberRange = roblox.NumberRange.new(0, 1), + NumberSequence = roblox.NumberSequence.new(0, 1), + NumberSequenceKeypoint = roblox.NumberSequenceKeypoint.new(0, 1), + PhysicalProperties = roblox.PhysicalProperties.new(1, 1, 1), + Ray = roblox.Ray.new(roblox.Vector3.zero, roblox.Vector3.one), + Rect = roblox.Rect.new(0, 0, 0, 0), + Region3 = roblox.Region3.new(roblox.Vector3.zero, roblox.Vector3.one), + Region3int16 = roblox.Region3int16.new( + roblox.Vector3int16.new(0, 0, 0), + roblox.Vector3int16.new(1, 1, 1) + ), + UDim = roblox.UDim.new(0, 0), + UDim2 = roblox.UDim2.new(0, 0, 0, 0), + Vector2 = roblox.Vector2.new(0, 0), + Vector2int16 = roblox.Vector2int16.new(0, 0), + Vector3 = roblox.Vector3.new(0, 0), + Vector3int16 = roblox.Vector3int16.new(0, 0), +} + +for name, value in TYPES_AND_VALUES :: { [string]: any } do + if typeof(value) ~= name then + error( + string.format( + "typeof() did not return correct value!\nExpected: %s\nActual: %s", + name, + typeof(value) + ) + ) + end +end