local HttpService = game:GetService("HttpService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Log = require(Packages.Log)
local Timer = require(Plugin.Timer)
local Types = require(Plugin.Types)
local decodeValue = require(Plugin.Reconciler.decodeValue)
local getProperty = require(Plugin.Reconciler.getProperty)
local function yieldIfNeeded(clock)
if os.clock() - clock > 1 / 20 then
task.wait()
return os.clock()
end
return clock
end
local function alphabeticalNext(t, state)
local key = nil
if state == nil then
local orderedIndex, i = table.create(5), 0
for k in t do
i += 1
orderedIndex[i] = k
end
table.sort(orderedIndex, function(a, b)
local nodeA, nodeB = t[a], t[b]
return (nodeA.name or "") < (nodeB.name or "")
end)
t.__orderedIndex = orderedIndex
key = orderedIndex[1]
else
for i, orderedState in t.__orderedIndex do
if orderedState == state then
key = t.__orderedIndex[i + 1]
break
end
end
end
if key then
return key, t[key]
end
t.__orderedIndex = nil
return
end
local function alphabeticalPairs(t)
return alphabeticalNext, t, nil
end
local Tree = {}
Tree.__index = Tree
function Tree.new()
local tree = {
idToNode = {},
ROOT = {
className = "DataModel",
name = "ROOT",
children = {},
},
}
tree.idToNode["ROOT"] = tree.ROOT
return setmetatable(tree, Tree)
end
function Tree:getCount()
local count = 0
self:forEach(function()
count += 1
end)
return count
end
function Tree:forEach(callback, node, depth)
depth = depth or 1
for _, child in alphabeticalPairs(if node then node.children else self.ROOT.children) do
callback(child, depth)
if type(child.children) == "table" then
self:forEach(callback, child, depth + 1)
end
end
end
function Tree:getNode(id, searchNode)
if self.idToNode[id] then
return self.idToNode[id]
end
local searchChildren = (searchNode or self.ROOT).children
for nodeId, node in searchChildren do
if nodeId == id then
self.idToNode[id] = node
return node
end
local descendant = self:getNode(id, node)
if descendant then
return descendant
end
end
return nil
end
function Tree:doesNodeExist(id)
return self.idToNode[id] ~= nil
end
function Tree:addNode(parent, props)
assert(props.id, "props must contain id")
parent = parent or "ROOT"
if self:doesNodeExist(props.id) then
local node = self:getNode(props.id)
for k, v in props do
node[k] = v
end
return node
end
local node = table.clone(props)
node.children = {}
node.parentId = parent
local parentNode = self:getNode(parent)
if not parentNode then
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
return
end
parentNode.children[node.id] = node
self.idToNode[node.id] = node
return node
end
function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, patch, instanceMap)
local clock = os.clock()
previousId = previousId or "ROOT"
for _, ancestorId in ancestryIds do
clock = yieldIfNeeded(clock)
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
if not value then
Log.warn("Failed to find ancestor object for " .. ancestorId)
continue
end
self:addNode(previousId, {
id = ancestorId,
className = value.ClassName,
name = value.Name,
instance = if typeof(value) == "Instance" then value else nil,
})
previousId = ancestorId
end
end
local PatchTree = {}
function PatchTree.build(patch, instanceMap, changeListHeaders)
Timer.start("PatchTree.build")
local clock = os.clock()
local tree = Tree.new()
local knownAncestors = {}
Timer.start("patch.updated")
for _, change in patch.updated do
clock = yieldIfNeeded(clock)
local instance = instanceMap.fromIds[change.id]
if not instance then
continue
end
local ancestryIds = {}
local parentObject = instance.Parent
local parentId = instanceMap.fromInstances[parentObject]
local previousId = nil
while parentObject do
if knownAncestors[parentId] then
previousId = parentId
break
end
table.insert(ancestryIds, 1, parentId)
knownAncestors[parentId] = true
parentObject = parentObject.Parent
parentId = instanceMap.fromInstances[parentObject]
end
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
local changeList, changeInfo = nil, nil
if next(change.changedProperties) or change.changedName then
changeList = {}
local changeIndex = 0
local function addProp(prop: string, current: any?, incoming: any?, metadata: any?)
changeIndex += 1
changeList[changeIndex] = { prop, current, incoming, metadata }
end
if change.changedName then
addProp("Name", instance.Name, change.changedName)
end
for prop, incoming in change.changedProperties do
local incomingSuccess, incomingValue = decodeValue(incoming, instanceMap)
local currentSuccess, currentValue = getProperty(instance, prop)
addProp(
prop,
if currentSuccess then currentValue else "[Error]",
if incomingSuccess then incomingValue else select(2, next(incoming))
)
end
changeInfo = {
edits = changeIndex,
}
table.sort(changeList, function(a, b)
return a[1] < b[1]
end)
table.insert(changeList, 1, changeListHeaders)
end
tree:addNode(instanceMap.fromInstances[instance.Parent], {
id = change.id,
patchType = "Edit",
className = instance.ClassName,
name = instance.Name,
instance = instance,
changeInfo = changeInfo,
changeList = changeList,
})
end
Timer.stop()
Timer.start("patch.removed")
for _, idOrInstance in patch.removed do
clock = yieldIfNeeded(clock)
local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance
if not instance then
continue
end
local ancestryIds = {}
local parentObject = instance.Parent
local parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
local previousId = nil
while parentObject do
if knownAncestors[parentId] then
previousId = parentId
break
end
instanceMap:insert(parentId, parentObject) table.insert(ancestryIds, 1, parentId)
knownAncestors[parentId] = true
parentObject = parentObject.Parent
parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
end
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
local nodeId = instanceMap.fromInstances[instance] or HttpService:GenerateGUID(false)
instanceMap:insert(nodeId, instance)
tree:addNode(instanceMap.fromInstances[instance.Parent], {
id = nodeId,
patchType = "Remove",
className = instance.ClassName,
name = instance.Name,
instance = instance,
})
end
Timer.stop()
Timer.start("patch.added")
for id, change in patch.added do
clock = yieldIfNeeded(clock)
local ancestryIds = {}
local parentId = change.Parent
local parentData = patch.added[parentId]
local parentObject = instanceMap.fromIds[parentId]
local previousId = nil
while parentId do
if knownAncestors[parentId] then
previousId = parentId
break
end
table.insert(ancestryIds, 1, parentId)
knownAncestors[parentId] = true
parentId = nil
if parentData then
parentId = parentData.Parent
parentData = patch.added[parentId]
parentObject = instanceMap.fromIds[parentId]
elseif parentObject then
parentObject = parentObject.Parent
parentId = instanceMap.fromInstances[parentObject]
parentData = patch.added[parentId]
end
end
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
local changeList, changeInfo = nil, nil
if next(change.Properties) then
changeList = {}
local changeIndex = 0
local function addProp(prop: string, incoming: any)
changeIndex += 1
changeList[changeIndex] = { prop, "N/A", incoming }
end
for prop, incoming in change.Properties do
local success, incomingValue = decodeValue(incoming, instanceMap)
addProp(prop, if success then incomingValue else select(2, next(incoming)))
end
changeInfo = {
edits = changeIndex,
}
table.sort(changeList, function(a, b)
return a[1] < b[1]
end)
table.insert(changeList, 1, changeListHeaders)
end
tree:addNode(change.Parent, {
id = change.Id,
patchType = "Add",
className = change.ClassName,
name = change.Name,
changeInfo = changeInfo,
changeList = changeList,
instance = instanceMap.fromIds[id],
})
end
Timer.stop()
Timer.stop()
return tree
end
function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
Timer.start("PatchTree.updateMetadata")
if tree then
tree = table.clone(tree)
else
tree = PatchTree.build(patch, instanceMap)
end
Timer.start("isWarning")
for _, failedChange in unappliedPatch.updated do
local node = tree:getNode(failedChange.id)
if not node then
continue
end
node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name)
if not node.changeList then
continue
end
local warnings = 0
for _, change in node.changeList do
local property = change[1]
local propertyFailedToApply = if property == "Name"
then failedChange.changedName ~= nil else failedChange.changedProperties[property] ~= nil
if not propertyFailedToApply then
continue
end
warnings += 1
if change[4] == nil then
change[4] = { isWarning = true }
else
change[4].isWarning = true
end
Log.trace(" Marked property as warning: {}.{}", node.name, property)
end
node.changeInfo = {
edits = (node.changeInfo.edits or (#node.changeList - 1)) - warnings,
failed = if warnings > 0 then warnings else nil,
}
end
for failedAdditionId in unappliedPatch.added do
local node = tree:getNode(failedAdditionId)
if not node then
continue
end
node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name)
if not node.changeList then
continue
end
for _, change in node.changeList do
if change[4] == nil then
change[4] = { isWarning = true }
else
change[4].isWarning = true
end
Log.trace(" Marked property as warning: {}.{}", node.name, change[1])
end
node.changeInfo = {
failed = node.changeInfo.edits or (#node.changeList - 1),
}
end
for _, failedRemovalIdOrInstance in unappliedPatch.removed do
local failedRemovalId = if Types.RbxId(failedRemovalIdOrInstance)
then failedRemovalIdOrInstance
else instanceMap.fromInstances[failedRemovalIdOrInstance]
if not failedRemovalId then
continue
end
local node = tree:getNode(failedRemovalId)
if not node then
continue
end
node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name)
end
Timer.stop()
Timer.start("instanceAncestry")
tree:forEach(function(node)
if node.instance then
if node.instance.Parent == nil and node.instance ~= game then
Log.trace("Removed instance from node: {} {}", node.id, node.name)
node.instance = nil
end
else
node.instance = instanceMap.fromIds[node.id]
if node.instance then
Log.trace("Added instance to node: {} {}", node.id, node.name)
end
end
end)
Timer.stop()
Timer.stop()
return tree
end
return PatchTree