diff --git a/CHANGELOG.md b/CHANGELOG.md index d7d290c..ebda1b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,21 +12,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Functions such as `print`, `warn`, ... now respect `__tostring` metamethods +- Functions such as `print`, `warn`, ... now respect `__tostring` metamethods. ### Fixed -- Fixed issues with CFrame math operations +- Fixed access of roblox instance properties such as `Workspace.Terrain`, `game.Workspace` that are actually links to child instances.
+ These properties are always guaranteed to exist, and they are not always properly set, meaning they must be found through an internal lookup. +- Fixed issues with CFrame math operations. ## `0.6.4` - March 26th, 2023 ### Fixed -- Fixed instances with attributes not saving if they contain integer attributes -- Fixed attributes not being set properly if the instance has an empty attributes property -- Fixed error messages for reading & writing roblox files not containing the full error message -- Fixed crash when trying to access an instance reference property that points to a destroyed instance -- Fixed crash when trying to save instances that contain unsupported attribute types +- Fixed instances with attributes not saving if they contain integer attributes. +- Fixed attributes not being set properly if the instance has an empty attributes property. +- Fixed error messages for reading & writing roblox files not containing the full error message. +- Fixed crash when trying to access an instance reference property that points to a destroyed instance. +- Fixed crash when trying to save instances that contain unsupported attribute types. ## `0.6.3` - March 26th, 2023 @@ -37,9 +39,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fixed accessing a destroyed instance printing an error message even if placed inside a pcall +- Fixed accessing a destroyed instance printing an error message even if placed inside a pcall. - Fixed cloned instances not having correct instance reference properties set (`ObjectValue.Value`, `Motor6D.Part0`, ...) -- Fixed `Instance::GetDescendants` returning the same thing as `Instance::GetChildren` (oops) +- Fixed `Instance::GetDescendants` returning the same thing as `Instance::GetChildren`. ## `0.6.2` - March 25th, 2023 @@ -47,22 +49,22 @@ This release adds some new features and fixes for the `roblox` built-in. ### Added -- Added `GetAttribute`, `GetAttributes` and `SetAttribute` methods for instances -- Added support for getting & setting properties that are instance references +- Added `GetAttribute`, `GetAttributes` and `SetAttribute` methods for instances. +- Added support for getting & setting properties that are instance references. ### Changed -- Improved handling of optional property types such as optional cframes & default physical properties +- Improved handling of optional property types such as optional cframes & default physical properties. ### Fixed -- Fixed handling of instance properties that are serialized as binary strings +- Fixed handling of instance properties that are serialized as binary strings. ## `0.6.1` - March 22nd, 2023 ### Fixed -- Fixed `writePlaceFile` and `writeModelFile` in the new `roblox` built-in making mysterious "ROOT" instances +- Fixed `writePlaceFile` and `writeModelFile` in the new `roblox` built-in making mysterious "ROOT" instances. ## `0.6.0` - March 22nd, 2023 diff --git a/packages/lib-roblox/src/instance/data_model.rs b/packages/lib-roblox/src/instance/data_model.rs index 672d48f..040b455 100644 --- a/packages/lib-roblox/src/instance/data_model.rs +++ b/packages/lib-roblox/src/instance/data_model.rs @@ -1,16 +1,37 @@ use mlua::prelude::*; -use crate::shared::{classes::add_class_restricted_method, instance::class_is_a_service}; +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, M: LuaUserDataFields<'lua, Instance>>(m: &mut M) { + add_class_restricted_getter(m, 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. diff --git a/packages/lib-roblox/src/instance/mod.rs b/packages/lib-roblox/src/instance/mod.rs index dbd58d1..ab96b99 100644 --- a/packages/lib-roblox/src/instance/mod.rs +++ b/packages/lib-roblox/src/instance/mod.rs @@ -25,6 +25,7 @@ use crate::{ pub(crate) mod collection_service; pub(crate) mod data_model; +pub(crate) mod workspace; const PROPERTY_NAME_ATTRIBUTES: &str = "Attributes"; const PROPERTY_NAME_TAGS: &str = "Tags"; @@ -34,8 +35,8 @@ static INTERNAL_DOM: Lazy> = #[derive(Debug, Clone)] pub struct Instance { - dom_ref: DomRef, - class_name: String, + pub(crate) dom_ref: DomRef, + pub(crate) class_name: String, } impl Instance { @@ -45,7 +46,7 @@ impl Instance { Panics if the instance does not exist in the internal dom, or if the given dom object ref points to the dom root. */ - fn new(dom_ref: DomRef) -> Self { + pub(crate) fn new(dom_ref: DomRef) -> Self { let dom = INTERNAL_DOM .try_read() .expect("Failed to get read access to document"); @@ -93,7 +94,7 @@ impl Instance { An orphaned instance is an instance at the root of a weak dom. */ - fn new_orphaned(class_name: impl AsRef) -> Self { + pub(crate) fn new_orphaned(class_name: impl AsRef) -> Self { let mut dom = INTERNAL_DOM .try_write() .expect("Failed to get write access to document"); @@ -842,6 +843,13 @@ impl Instance { } impl LuaUserData for Instance { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + // Here we add inheritance-like behavior for instances by creating + // fields that are restricted to specific classnames / base classes + data_model::add_fields(fields); + workspace::add_fields(fields); + } + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_meta_method(LuaMetaMethod::ToString, |lua, this, ()| { this.ensure_not_destroyed()?; diff --git a/packages/lib-roblox/src/instance/workspace.rs b/packages/lib-roblox/src/instance/workspace.rs new file mode 100644 index 0000000..f1ae4b8 --- /dev/null +++ b/packages/lib-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, M: LuaUserDataFields<'lua, Instance>>(m: &mut M) { + add_class_restricted_getter(m, CLASS_NAME, "Terrain", workspace_get_terrain); + add_class_restricted_getter(m, 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/packages/lib-roblox/src/shared/classes.rs b/packages/lib-roblox/src/shared/classes.rs index bb0fb98..9a2a683 100644 --- a/packages/lib-roblox/src/shared/classes.rs +++ b/packages/lib-roblox/src/shared/classes.rs @@ -1,9 +1,54 @@ 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: ToLua<'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, @@ -54,3 +99,36 @@ pub(crate) fn add_class_restricted_method_mut< } }); } + +/** + 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/packages/lib/src/tests.rs b/packages/lib/src/tests.rs index 760ae49..3a24bc3 100644 --- a/packages/lib/src/tests.rs +++ b/packages/lib/src/tests.rs @@ -123,11 +123,13 @@ create_tests! { roblox_files_write_place: "roblox/files/writePlaceFile", roblox_instance_attributes: "roblox/instance/attributes", - roblox_instance_datamodel: "roblox/instance/datamodel", roblox_instance_new: "roblox/instance/new", roblox_instance_properties: "roblox/instance/properties", roblox_instance_tags: "roblox/instance/tags", + roblox_instance_classes_data_model: "roblox/instance/classes/DataModel", + roblox_instance_classes_workspace: "roblox/instance/classes/Workspace", + roblox_instance_methods_clear_all_children: "roblox/instance/methods/ClearAllChildren", roblox_instance_methods_clone: "roblox/instance/methods/Clone", roblox_instance_methods_destroy: "roblox/instance/methods/Destroy", diff --git a/tests/roblox/instance/datamodel.luau b/tests/roblox/instance/classes/DataModel.luau similarity index 51% rename from tests/roblox/instance/datamodel.luau rename to tests/roblox/instance/classes/DataModel.luau index f75e95b..5567c54 100644 --- a/tests/roblox/instance/datamodel.luau +++ b/tests/roblox/instance/classes/DataModel.luau @@ -3,20 +3,26 @@ local Instance = roblox.Instance local game = Instance.new("DataModel") -assert(game:FindService("Workspace") == nil) -assert(game:GetService("Workspace") ~= nil) -assert(game:FindService("Workspace") ~= nil) +-- Workspace should always exist as a "Workspace" property, or be created when accessed + +assert(game.Workspace ~= nil) +assert(game.Workspace:IsA("Workspace")) +assert(game.Workspace == game:FindFirstChildOfClass("Workspace")) + +-- GetService and FindService should work, GetService should create services that don't exist assert(game:FindService("CSGDictionaryService") == nil) assert(game:GetService("CSGDictionaryService") ~= nil) assert(game:FindService("CSGDictionaryService") ~= nil) +-- Service names should be strict and not allow weird characters or substrings + assert(not pcall(function() game:GetService("wrorokspacey") end)) assert(not pcall(function() - game:GetService("work-space") + game:GetService("Work-space") end)) assert(not pcall(function() diff --git a/tests/roblox/instance/classes/Workspace.luau b/tests/roblox/instance/classes/Workspace.luau new file mode 100644 index 0000000..523a1fa --- /dev/null +++ b/tests/roblox/instance/classes/Workspace.luau @@ -0,0 +1,17 @@ +local roblox = require("@lune/roblox") :: any +local Instance = roblox.Instance + +local game = Instance.new("DataModel") +local workspace = game:GetService("Workspace") + +-- Terrain should always exist as a "Terrain" property, or be created when accessed + +assert(workspace.Terrain ~= nil) +assert(workspace.Terrain:IsA("Terrain")) +assert(workspace.Terrain == workspace:FindFirstChildOfClass("Terrain")) + +-- Camera should always exist as a "CurrentCamera" property, or be created when accessed + +assert(workspace.CurrentCamera ~= nil) +assert(workspace.CurrentCamera:IsA("Camera")) +assert(workspace.CurrentCamera == workspace:FindFirstChildOfClass("Camera"))