merge impl-v2: plugin

This commit is contained in:
Lucien Greathouse
2018-06-10 22:53:22 -07:00
parent 7d7f671920
commit e30545c132
13 changed files with 243 additions and 859 deletions

View File

@@ -1,246 +0,0 @@
local RouteMap = require(script.Parent.RouteMap)
local function classEqual(a, b)
assert(typeof(a) == "string")
assert(typeof(b) == "string")
if a == "*" or b == "*" then
return true
end
return a == b
end
local function applyProperties(target, properties)
assert(typeof(target) == "Instance")
assert(typeof(properties) == "table")
for key, property in pairs(properties) do
-- TODO: Transform property value based on property.Type
-- Right now, we assume that 'value' is primitive!
target[key] = property.Value
end
end
--[[
Attempt to parent `rbx` to `parent`, doing nothing if:
* parent is already `parent`
* Changing parent threw an error
]]
local function reparent(rbx, parent)
assert(typeof(rbx) == "Instance")
assert(typeof(parent) == "Instance")
if rbx.Parent == parent then
return
end
-- Setting `Parent` can fail if:
-- * The object has been destroyed
-- * The object is a service and cannot be reparented
pcall(function()
rbx.Parent = parent
end)
end
--[[
Attempts to match up Roblox instances and object specifiers for
reconciliation.
An object is considered a match if they have the same Name and ClassName.
primaryChildren and secondaryChildren can each be either a list of Roblox
instances or object specifiers. Since they share a common shape, switching
the two around isn't problematic!
visited is expected to be an empty table initially. It will be filled with
the set of children that have been visited so far.
]]
local function findNextChildPair(primaryChildren, secondaryChildren, visited)
for _, primaryChild in ipairs(primaryChildren) do
if not visited[primaryChild] then
visited[primaryChild] = true
for _, secondaryChild in ipairs(secondaryChildren) do
if classEqual(primaryChild.ClassName, secondaryChild.ClassName) and primaryChild.Name == secondaryChild.Name then
visited[secondaryChild] = true
return primaryChild, secondaryChild
end
end
return primaryChild, nil
end
end
return nil, nil
end
local Reconciler = {}
Reconciler.__index = Reconciler
function Reconciler.new()
local reconciler = {
_routeMap = RouteMap.new(),
}
setmetatable(reconciler, Reconciler)
return reconciler
end
--[[
A semi-smart algorithm that attempts to apply the given item's children to
an existing Roblox object.
]]
function Reconciler:_reconcileChildren(rbx, item)
local visited = {}
local rbxChildren = rbx:GetChildren()
-- Reconcile any children that were added or updated
while true do
local itemChild, rbxChild = findNextChildPair(item.Children, rbxChildren, visited)
if itemChild == nil then
break
end
local newRbxChild = self:reconcile(rbxChild, itemChild)
if newRbxChild ~= nil then
newRbxChild.Parent = rbx
end
end
-- Reconcile any children that were deleted
while true do
local rbxChild, itemChild = findNextChildPair(rbxChildren, item.Children, visited)
if rbxChild == nil then
break
end
local newRbxChild = self:reconcile(rbxChild, itemChild)
if newRbxChild ~= nil then
newRbxChild.Parent = rbx
end
end
end
--[[
Construct a new Roblox object from the given item.
]]
function Reconciler:_reify(item)
local className = item.ClassName
-- "*" represents a match of any class. It reifies as a folder!
if className == "*" then
className = "Folder"
end
local rbx = Instance.new(className)
rbx.Name = item.Name
applyProperties(rbx, item.Properties)
for _, child in ipairs(item.Children) do
reparent(self:_reify(child), rbx)
end
if item.Route ~= nil then
self._routeMap:insert(item.Route, rbx)
end
return rbx
end
--[[
Clears any state that the Reconciler has, stopping it completely.
]]
function Reconciler:destruct()
self._routeMap:destruct()
end
--[[
Apply the changes represented by the given item to a Roblox object that's a
child of the given instance.
]]
function Reconciler:reconcile(rbx, item)
-- Item was deleted
if item == nil then
if rbx ~= nil then
self._routeMap:removeByRbx(rbx)
rbx:Destroy()
end
return nil
end
-- Item was created!
if rbx == nil then
return self:_reify(item)
end
-- Item changed type!
if not classEqual(rbx.ClassName, item.ClassName) then
self._routeMap:removeByRbx(rbx)
rbx:Destroy()
return self:_reify(item)
end
-- It's possible that the instance we're associating with this item hasn't
-- been inserted into the RouteMap yet.
if item.Route ~= nil then
self._routeMap:insert(item.Route, rbx)
end
applyProperties(rbx, item.Properties)
self:_reconcileChildren(rbx, item)
return rbx
end
function Reconciler:reconcileRoute(rbxRoute, item, fileRoute)
local parent
local rbx = game
for i = 1, #rbxRoute do
local piece = rbxRoute[i]
local child = rbx:FindFirstChild(piece)
-- We should get services instead of making folders here.
if rbx == game and child == nil then
local success
success, child = pcall(game.GetService, game, piece)
-- That isn't a valid service!
if not success then
child = nil
end
end
-- We don't want to create a folder if we're reaching our target item!
if child == nil and i ~= #rbxRoute then
child = Instance.new("Folder")
child.Parent = rbx
child.Name = piece
end
parent = rbx
rbx = child
end
-- Let's check the route map!
if rbx == nil then
rbx = self._routeMap:get(fileRoute)
end
rbx = self:reconcile(rbx, item)
reparent(rbx, parent)
end
return Reconciler