Fix access of special instance link properties on some classes

This commit is contained in:
Filip Tibell 2023-03-27 15:59:06 +02:00
parent 9b568aa8ec
commit 03680eccc6
No known key found for this signature in database
8 changed files with 192 additions and 24 deletions

View file

@ -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. <br />
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

View file

@ -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<Instance> {
get_or_create_property_ref_instance(this, "Workspace", "Workspace")
}
/**
Gets or creates a service for this DataModel.

View file

@ -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<RwLock<WeakDom>> =
#[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<str>) -> Self {
pub(crate) fn new_orphaned(class_name: impl AsRef<str>) -> 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()?;

View file

@ -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<Instance> {
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<Instance> {
get_or_create_property_ref_instance(this, "CurrentCamera", "Camera")
}

View file

@ -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<R>,
{
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<Instance> {
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)
}
}

View file

@ -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",

View file

@ -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()

View file

@ -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"))