--[[ Methods to operate on either a patch created by the hydrate method, or a patch returned from the API. ]] local Packages = script.Parent.Parent.Packages local t = require(Packages.t) local Types = require(script.Parent.Types) local function deepEqual(a: any, b: any): boolean local typeA = typeof(a) if typeA ~= typeof(b) then return false end if typeof(a) == "table" then local checkedKeys = {} for key, value in a do checkedKeys[key] = true if deepEqual(value, b[key]) == false then return false end end for key, value in b do if checkedKeys[key] then continue end if deepEqual(value, a[key]) == false then return false end end return true end if a == b then return true end return false end local PatchSet = {} PatchSet.validate = t.interface({ removed = t.array(t.union(Types.RbxId, t.Instance)), added = t.map(Types.RbxId, Types.ApiInstance), updated = t.array(Types.ApiInstanceUpdate), }) --[[ Create a new, empty PatchSet. ]] function PatchSet.newEmpty() return { removed = {}, added = {}, updated = {}, } end --[[ Tells whether the given PatchSet is empty. ]] function PatchSet.isEmpty(patchSet) return next(patchSet.removed) == nil and next(patchSet.added) == nil and next(patchSet.updated) == nil end --[[ Tells whether the given PatchSet has any remove operations. ]] function PatchSet.hasRemoves(patchSet) return next(patchSet.removed) ~= nil end --[[ Tells whether the given PatchSet has any add operations. ]] function PatchSet.hasAdditions(patchSet) return next(patchSet.added) ~= nil end --[[ Tells whether the given PatchSet has any update operations. ]] function PatchSet.hasUpdates(patchSet) return next(patchSet.updated) ~= nil end --[[ Tells whether the given PatchSet contains changes to the given instance id ]] function PatchSet.containsId(patchSet, instanceMap, id) if patchSet.added[id] ~= nil then return true end for _, idOrInstance in patchSet.removed do local removedId = if Types.RbxId(idOrInstance) then idOrInstance else instanceMap.fromInstances[idOrInstance] if removedId == id then return true end end for _, update in patchSet.updated do if update.id == id then return true end end return false end --[[ Tells whether the given PatchSet contains changes to the given instance. If the given InstanceMap does not contain the instance, this function always returns false. ]] function PatchSet.containsInstance(patchSet, instanceMap, instance) local id = instanceMap.fromInstances[instance] if id == nil then return false end return PatchSet.containsId(patchSet, instanceMap, id) end --[[ Tells whether the given PatchSet contains changes to nothing but the given instance id ]] function PatchSet.containsOnlyId(patchSet, instanceMap, id) if not PatchSet.containsId(patchSet, instanceMap, id) then -- Patch doesn't contain the id at all return false end for addedId in patchSet.added do if addedId ~= id then return false end end for _, idOrInstance in patchSet.removed do local removedId = if Types.RbxId(idOrInstance) then idOrInstance else instanceMap.fromInstances[idOrInstance] if removedId ~= id then return false end end for _, update in patchSet.updated do if update.id ~= id then return false end end return true end --[[ Tells whether the given PatchSet contains changes to nothing but the given instance. If the given InstanceMap does not contain the instance, this function always returns false. ]] function PatchSet.containsOnlyInstance(patchSet, instanceMap, instance) local id = instanceMap.fromInstances[instance] if id == nil then return false end return PatchSet.containsOnlyId(patchSet, instanceMap, id) end --[[ Returns the update to the given instance id, or nil if there aren't any ]] function PatchSet.getUpdateForId(patchSet, id) for _, update in patchSet.updated do if update.id == id then return update end end return nil end --[[ Returns the update to the given instance, or nil if there aren't any. If the given InstanceMap does not contain the instance, this function always returns nil. ]] function PatchSet.getUpdateForInstance(patchSet, instanceMap, instance) local id = instanceMap.fromInstances[instance] if id == nil then return nil end return PatchSet.getUpdateForId(patchSet, id) end --[[ Tells whether the given PatchSets are equal. ]] function PatchSet.isEqual(patchA, patchB) return deepEqual(patchA, patchB) end --[[ Count the number of changes in the given PatchSet. ]] function PatchSet.countChanges(patch) local count = 0 for _ in patch.added do -- Adding an instance is 1 change count += 1 end for _ in patch.removed do -- Removing an instance is 1 change count += 1 end for _, update in patch.updated do -- Updating an instance is 1 change per property updated for _ in update.changedProperties do count += 1 end if update.changedName ~= nil then count += 1 end if update.changedClassName ~= nil then count += 1 end end return count end --[[ Count the number of instances affected by the given PatchSet. ]] function PatchSet.countInstances(patch) local count = 0 -- Added instances for _ in patch.added do count += 1 end -- Removed instances for _ in patch.removed do count += 1 end -- Updated instances for _ in patch.updated do count += 1 end return count end --[[ Merge multiple PatchSet objects into the given PatchSet. ]] function PatchSet.assign(target, ...) for i = 1, select("#", ...) do local sourcePatch = select(i, ...) for _, removed in ipairs(sourcePatch.removed) do table.insert(target.removed, removed) end for id, added in pairs(sourcePatch.added) do target.added[id] = added end for _, update in ipairs(sourcePatch.updated) do table.insert(target.updated, update) end end return target end --[[ Create a list of human-readable statements summarizing the contents of this patch, intended to be displayed to users. ]] function PatchSet.humanSummary(instanceMap, patchSet) local statements = {} for _, idOrInstance in ipairs(patchSet.removed) do local instance, id if Types.RbxId(idOrInstance) then id = idOrInstance instance = instanceMap.fromIds[id] else instance = idOrInstance id = instanceMap.fromInstances[instance] end if instance ~= nil then table.insert(statements, string.format("- Delete instance %s", instance:GetFullName())) else table.insert(statements, string.format("- Delete instance with ID %s", id)) end end local additionsMentioned = {} local function addAllDescendents(virtualInstance) additionsMentioned[virtualInstance.Id] = true for _, childId in ipairs(virtualInstance.Children) do addAllDescendents(patchSet.added[childId]) end end for id, addition in pairs(patchSet.added) do if additionsMentioned[id] then continue end local virtualInstance = addition while true do if virtualInstance.Parent == nil then break end local virtualParent = patchSet.added[virtualInstance.Parent] if virtualParent == nil then break end virtualInstance = virtualParent end local parentDisplayName = "nil (how strange!)" if virtualInstance.Parent ~= nil then local parent = instanceMap.fromIds[virtualInstance.Parent] if parent ~= nil then parentDisplayName = parent:GetFullName() end end table.insert(statements, string.format( "- Add instance %q (ClassName %q) to %s", virtualInstance.Name, virtualInstance.ClassName, parentDisplayName)) end for _, update in ipairs(patchSet.updated) do local updatedProperties = {} if update.changedMetadata ~= nil then table.insert(updatedProperties, "Rojo's Metadata") end if update.changedName ~= nil then table.insert(updatedProperties, "Name") end if update.changedClassName ~= nil then table.insert(updatedProperties, "ClassName") end for name in pairs(update.changedProperties) do table.insert(updatedProperties, name) end local instance = instanceMap.fromIds[update.id] local displayName if instance ~= nil then displayName = instance:GetFullName() else displayName = "[unknown instance]" end table.insert(statements, string.format( "- Update properties on %s: %s", displayName, table.concat(updatedProperties, ","))) end return table.concat(statements, "\n") end return PatchSet