mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
Hydrate by best match
This commit is contained in:
parent
988efb45b1
commit
485eb333dd
7 changed files with 475 additions and 112 deletions
|
|
@ -4,6 +4,16 @@
|
|||
]]
|
||||
|
||||
local invariant = require(script.Parent.Parent.invariant)
|
||||
local countMatchingProperties = require(script.Parent.countMatchingProperties)
|
||||
|
||||
-- When several existing children share a Name and ClassName we disambiguate
|
||||
-- them by scoring how well each one's properties match the virtual instance.
|
||||
-- That scoring is far more expensive than a Name/ClassName check, so we only do
|
||||
-- it for reasonably-sized groups. Larger groups (e.g. a folder with thousands of
|
||||
-- identically-named parts) fall back to the original order-based matching, which
|
||||
-- bounds the added work to roughly MAX_CANDIDATES_TO_SCORE^2 property reads per
|
||||
-- group regardless of how large the group is.
|
||||
local MAX_CANDIDATES_TO_SCORE = 32
|
||||
|
||||
local function hydrate(instanceMap, virtualInstances, rootId, rootInstance)
|
||||
local virtualInstance = virtualInstances[rootId]
|
||||
|
|
@ -16,34 +26,106 @@ local function hydrate(instanceMap, virtualInstances, rootId, rootInstance)
|
|||
|
||||
local existingChildren = rootInstance:GetChildren()
|
||||
|
||||
-- For each existing child, we'll track whether it's been paired with an
|
||||
-- instance that the Rojo server knows about.
|
||||
local isExistingChildVisited = {}
|
||||
for i = 1, #existingChildren do
|
||||
isExistingChildVisited[i] = false
|
||||
-- Group existing children by Name then ClassName so each virtual child can
|
||||
-- find its candidate matches without scanning every sibling. This is what
|
||||
-- keeps hydration fast for parents with thousands of children. Nesting the
|
||||
-- two tables (rather than a combined key) keeps the Name and ClassName checks
|
||||
-- exact, with no way for one to bleed into the other.
|
||||
local buckets = {}
|
||||
for _, childInstance in existingChildren do
|
||||
-- We guard accessing Name and ClassName in order to avoid tripping over
|
||||
-- children of DataModel that Rojo won't have permissions to access at all.
|
||||
local accessSuccess, name, className = pcall(function()
|
||||
return childInstance.Name, childInstance.ClassName
|
||||
end)
|
||||
if not accessSuccess then
|
||||
continue
|
||||
end
|
||||
|
||||
local bucketsByClassName = buckets[name]
|
||||
if bucketsByClassName == nil then
|
||||
bucketsByClassName = {}
|
||||
buckets[name] = bucketsByClassName
|
||||
end
|
||||
|
||||
local bucket = bucketsByClassName[className]
|
||||
if bucket == nil then
|
||||
bucket = { cursor = 1, instances = {} }
|
||||
bucketsByClassName[className] = bucket
|
||||
end
|
||||
|
||||
table.insert(bucket.instances, childInstance)
|
||||
end
|
||||
|
||||
-- Tracks which existing children have already been paired, so one instance
|
||||
-- isn't matched to two different virtual instances.
|
||||
local visited = {}
|
||||
|
||||
for _, childId in ipairs(virtualInstance.Children) do
|
||||
local virtualChild = virtualInstances[childId]
|
||||
|
||||
for childIndex, childInstance in existingChildren do
|
||||
if not isExistingChildVisited[childIndex] then
|
||||
-- We guard accessing Name and ClassName in order to avoid
|
||||
-- tripping over children of DataModel that Rojo won't have
|
||||
-- permissions to access at all.
|
||||
local accessSuccess, name, className = pcall(function()
|
||||
return childInstance.Name, childInstance.ClassName
|
||||
end)
|
||||
local bucketsByClassName = buckets[virtualChild.Name]
|
||||
local bucket = bucketsByClassName and bucketsByClassName[virtualChild.ClassName]
|
||||
if bucket == nil then
|
||||
continue
|
||||
end
|
||||
|
||||
-- This rule is very conservative and could be loosened in the
|
||||
-- future, or more heuristics could be introduced.
|
||||
if accessSuccess and name == virtualChild.Name and className == virtualChild.ClassName then
|
||||
isExistingChildVisited[childIndex] = true
|
||||
hydrate(instanceMap, virtualInstances, childId, childInstance)
|
||||
break
|
||||
local instances = bucket.instances
|
||||
|
||||
-- Advance past any leading children that have already been paired. The
|
||||
-- cursor makes order-based matching amortized O(1) per child even for
|
||||
-- very large groups, rather than rescanning the visited prefix.
|
||||
while bucket.cursor <= #instances and visited[instances[bucket.cursor]] do
|
||||
bucket.cursor += 1
|
||||
end
|
||||
if bucket.cursor > #instances then
|
||||
continue
|
||||
end
|
||||
|
||||
-- The cursor points at the earliest unvisited child, so the slots from
|
||||
-- here to the end bound how many candidates remain. Visited children
|
||||
-- after the cursor (gaps) only appear once a group is small enough to be
|
||||
-- scored -- the order-based path below always takes the earliest, which
|
||||
-- keeps the visited region a contiguous prefix. So whenever this count
|
||||
-- exceeds the cap it is exact, and we can pick the earliest match without
|
||||
-- collecting anything.
|
||||
local remaining = #instances - bucket.cursor + 1
|
||||
|
||||
local match
|
||||
if remaining > MAX_CANDIDATES_TO_SCORE then
|
||||
-- Too many to score affordably; take the earliest in child order,
|
||||
-- reproducing the original Name + ClassName behavior.
|
||||
match = instances[bucket.cursor]
|
||||
else
|
||||
-- Collect the (at most `remaining`) unvisited candidates.
|
||||
local candidates = {}
|
||||
for index = bucket.cursor, #instances do
|
||||
local childInstance = instances[index]
|
||||
if not visited[childInstance] then
|
||||
table.insert(candidates, childInstance)
|
||||
end
|
||||
end
|
||||
|
||||
if #candidates == 1 then
|
||||
-- Only one candidate, so there's nothing to disambiguate.
|
||||
match = candidates[1]
|
||||
else
|
||||
-- Break the tie by choosing the candidate whose properties best
|
||||
-- match the virtual instance, falling back to the earliest in
|
||||
-- child order when scores are equal.
|
||||
local bestScore = -1
|
||||
for _, childInstance in candidates do
|
||||
local score = countMatchingProperties(childInstance, virtualChild, instanceMap)
|
||||
if score > bestScore then
|
||||
bestScore = score
|
||||
match = childInstance
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
visited[match] = true
|
||||
hydrate(instanceMap, virtualInstances, childId, match)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue