Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Update reconciler / reify to use CreateMeshPartAsync to create mesh parts #534

Closed
31 changes: 18 additions & 13 deletions plugin/src/ChangeBatcher/encodePatchUpdate.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,33 @@ return function(instance, instanceId, properties)
local update = {
id = instanceId,
changedProperties = {},
requiresRecreate = false
}

for propertyName in pairs(properties) do
if propertyName == "Name" then
update.changedName = instance.Name
else
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change, whilst structural (it now encodes the Name field as well as setting the changedName property) has not changed the intended logic and is more concise.

local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)

if not descriptor then
Log.debug("Could not sync back property {:?}.{}", instance, propertyName)
continue
end
elseif propertyName == "MeshId" then
update.requiresRecreate = true
elseif propertyName == "ClassName" then
update.requiresRecreate, update.changedClassName = true, instance.ClassName
end

local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)

local encodeSuccess, encodeResult = encodeProperty(instance, propertyName, descriptor)
if not descriptor then
Log.debug("Could not sync back property {:?}.{}", instance, propertyName)
continue
end

if not encodeSuccess then
Log.debug("Could not sync back property {:?}.{}: {}", instance, propertyName, encodeResult)
continue
end
local encodeSuccess, encodeResult = encodeProperty(instance, propertyName, descriptor)

update.changedProperties[propertyName] = encodeResult
if not encodeSuccess then
Log.debug("Could not sync back property {:?}.{}: {}", instance, propertyName, encodeResult)
continue
end

update.changedProperties[propertyName] = encodeResult
end

if next(update.changedProperties) == nil and update.changedName == nil then
Expand Down
12 changes: 12 additions & 0 deletions plugin/src/ChangeBatcher/encodePatchUpdate.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ return function()
expect(update.changedName).to.equal("We'reGettingToTheCoolPart")
end)

it("should recreate instance in the update when the instance's ClassName changes", function()
local part = Instance.new("Part")
local properties = {
ClassName = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should set this value to a real ClassName so that future validation in the patch update function won't choke when it sees a bool instead.

}

local update = encodePatchUpdate(part, "PART", properties)

expect(update.changedProperties.ClassName).to.be.ok()
expect(update.requiresRecreate).to.equal(true)
end)

it("should correctly encode property values", function()
local part = Instance.new("Part")
local properties = {
Expand Down
10 changes: 5 additions & 5 deletions plugin/src/Reconciler/applyPatch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ local function applyPatch(instanceMap, patch)
}
local partiallyApplied = false

-- If the instance's className changed, we have a bumpy ride ahead while
-- we recreate this instance and move all of its children into the new
-- version atomically...ish.
if update.changedClassName ~= nil then
-- If the instance's className changed, or there is custom recreate logic,
-- we have a bumpy ride ahead while we recreate this instance and move all
-- of its children into the new version atomically...ish.
if update.requiresRecreate then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are comments in this block of code about its caveats, notably not preserving existing properties if we end up needing to recreate an instance. It's probably not the end of the world, but some of the comments I left about this mostly occurring during Folder<->ModuleScript transitions are probably not accurate anymore.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having requiresRecreate as a property here, we should check the conditions that would cause us to recreate an instance directly. For now, that's just changedClassName ~= nil or changedProperties["MeshId"] ~= nil.

-- If the instance's name also changed, we'll do it here, since this
-- branch will skip the rest of the loop iteration.
local newName = update.changedName or instance.Name
Expand All @@ -121,7 +121,7 @@ local function applyPatch(instanceMap, patch)
local mockVirtualInstance = {
Id = update.id,
Name = newName,
ClassName = update.changedClassName,
ClassName = update.changedClassName or instance.ClassName,
Properties = newProperties,
Children = {},
}
Expand Down
31 changes: 30 additions & 1 deletion plugin/src/Reconciler/applyPatch.spec.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
return function()
local applyPatch = require(script.Parent.applyPatch)

local InsertService = game:GetService("InsertService")

local InstanceMap = require(script.Parent.Parent.InstanceMap)
local PatchSet = require(script.Parent.Parent.PatchSet)

Expand Down Expand Up @@ -156,7 +158,33 @@ return function()
expect(value.Value).to.equal("WORLD")
end)

it("should recreate instances when changedClassName is set, preserving children", function()
it("should recreate instances for MeshId updates", function()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great test!

local mesh = InsertService:CreateMeshPartAsync("rbxassetid://3229650568", Enum.CollisionFidelity.Default, Enum.RenderFidelity.Automatic)

local instanceMap = InstanceMap.new()
instanceMap:insert("VALUE", mesh)

local patch = PatchSet.newEmpty()
table.insert(patch.updated, {
id = "VALUE",
changedProperties = {
MeshId = {
Content = "rbxassetid://4868357305",
},
},
requiresRecreate = true
})

local unapplied = applyPatch(instanceMap, patch)
local newInstance = instanceMap.fromIds["VALUE"]

expect(mesh.Parent).never.to.be.ok()
expect(mesh).never.to.equal(newInstance)
expect(newInstance).to.be.ok()
expect(newInstance.MeshId).to.equal("rbxassetid://4868357305")
end)

it("should recreate instances when requiresRecreate is set, preserving children", function()
local root = Instance.new("Folder")
root.Name = "Initial Root Name"

Expand All @@ -173,6 +201,7 @@ return function()
id = "ROOT",
changedName = "Updated Root Name",
changedClassName = "StringValue",
requiresRecreate = true,
changedProperties = {
Value = {
String = "I am Root",
Expand Down
10 changes: 8 additions & 2 deletions plugin/src/Reconciler/diff.lua
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,10 @@ local function diff(instanceMap, virtualInstances, rootId)
end

local changedClassName = nil
local requiresRecreate = false
if virtualInstance.ClassName ~= instance.ClassName then
changedClassName = virtualInstance.ClassName
requiresRecreate = true
end

local changedName = nil
Expand All @@ -154,7 +156,7 @@ local function diff(instanceMap, virtualInstances, rootId)
local ok, decodedValue = decodeValue(virtualValue, instanceMap)

if ok then
if not trueEquals(existingValue, decodedValue) then
if not trueEquals(existingValue, decodedValue) or requiresRecreate then
Log.debug(
"{}.{} changed from '{}' to '{}'",
instance:GetFullName(),
Expand All @@ -163,6 +165,9 @@ local function diff(instanceMap, virtualInstances, rootId)
decodedValue
)
changedProperties[propertyName] = virtualValue
if propertyName == "MeshId" then
requiresRecreate = true
end
end
else
local propertyType = next(virtualValue)
Expand All @@ -186,10 +191,11 @@ local function diff(instanceMap, virtualInstances, rootId)
end
end

if changedName ~= nil or changedClassName ~= nil or not isEmpty(changedProperties) then
if changedName ~= nil or requiresRecreate or not isEmpty(changedProperties) then
table.insert(patch.updated, {
id = id,
changedName = changedName,
requiresRecreate = requiresRecreate,
changedClassName = changedClassName,
changedProperties = changedProperties,
changedMetadata = nil,
Expand Down
18 changes: 17 additions & 1 deletion plugin/src/Reconciler/reify.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"Reifies" a virtual DOM, constructing a real DOM with the same shape.
]]

local InsertService = game:GetService('InsertService')

local invariant = require(script.Parent.Parent.invariant)
local PatchSet = require(script.Parent.Parent.PatchSet)
local setProperty = require(script.Parent.setProperty)
Expand Down Expand Up @@ -40,6 +42,20 @@ local function addAllToPatch(patchSet, virtualInstances, id)
end
end

local function createInstance(instanceMap, className, properties)
-- Custom logic for MeshParts
-- see: https://github.com/rojo-rbx/rojo/issues/402
if className == "MeshPart" then
local okMeshId, meshId = decodeValue(properties.MeshId, instanceMap)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to handle okMeshId, because the second return value will not be a mesh ID if this failed to decode.


-- It is okay to provide default properties here, because
-- they will be overriden on the update cycle
Comment on lines +51 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-- It is okay to provide default properties here, because
-- they will be overriden on the update cycle
-- It is okay to provide default properties here, because
-- they will be set when properties are applied in reify.

return InsertService:CreateMeshPartAsync(meshId, Enum.CollisionFidelity.Default, Enum.RenderFidelity.Automatic)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the first time we'll be introducing an async function into Rojo's reconciler bits. This is a possible source of bugs that we should watch out for in the future.

else
return Instance.new(className)
end
end

--[[
Inner function that defines the core routine.
]]
Expand All @@ -53,7 +69,7 @@ function reifyInner(instanceMap, virtualInstances, id, parentInstance, unapplied
-- Instance.new can fail if we're passing in something that can't be
-- created, like a service, something enabled with a feature flag, or
-- something that requires higher security than we have.
local ok, instance = pcall(Instance.new, virtualInstance.ClassName)
local ok, instance = pcall(createInstance, instanceMap, virtualInstance.ClassName, virtualInstance.Properties)

if not ok then
addAllToPatch(unappliedPatch, virtualInstances, id)
Expand Down
1 change: 1 addition & 0 deletions plugin/src/Types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ local ApiInstanceUpdate = t.interface({
id = RbxId,
changedName = t.optional(t.string),
changedClassName = t.optional(t.string),
requiresRecreate = t.optional(t.boolean),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should update this interface at all. The server generates most of the patches that the Rojo plugin handles and it does not populate requiresRecreate. We can calculate this when we go to apply a patch instead.

changedProperties = t.map(t.string, ApiValue),
changedMetadata = t.optional(ApiInstanceMetadata),
})
Expand Down
12 changes: 12 additions & 0 deletions test-projects/mesh_parts/default.project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "mesh_parts",
"tree": {
"$className": "DataModel",

"Workspace": {
"TwoParts": {
"$path": "src"
}
}
}
}
Binary file added test-projects/mesh_parts/src/MeshPart.rbxm
Binary file not shown.
Loading