Enhance matching module with valid children tracking and code readability improvements

- Added a `validChildren` field to the `VCache` structure to track valid child instances during the matching process.
- Refactored the `cacheVirtual` function to improve readability by formatting parameters across multiple lines.
- Updated the `computePairCost` function to utilize the new `validChildren` field for more accurate cost calculations.
- Simplified the `matchChildren` function by removing redundant code and improving the handling of matched instances.
- Enhanced test cases for the `matchChildren` function to ensure consistent behavior and validate the new changes.

These updates improve the efficiency and clarity of the matching process, ensuring better performance and maintainability.
This commit is contained in:
Joe 2026-02-18 06:40:28 -06:00
parent b687c8862d
commit ddd47b9afa
9 changed files with 287 additions and 363 deletions

View file

@ -59,6 +59,7 @@ type VCache = {
attrs: { [string]: any },
refs: { [string]: RefIdentity },
childCount: number,
validChildren: { string },
}
type SCache = {
@ -120,7 +121,11 @@ end
-- Pre-computation helpers (called once per instance per group)
-- ================================================================
local function cacheVirtual(vInst: VirtualInstance, classKeys: ClassComparisonKeys, virtualInstances: VirtualInstances): VCache
local function cacheVirtual(
vInst: VirtualInstance,
classKeys: ClassComparisonKeys,
virtualInstances: VirtualInstances
): VCache
local decoded: { [string]: any } = {}
local extraProps: { string } = {}
local refs: { [string]: RefIdentity } = {}
@ -172,6 +177,15 @@ local function cacheVirtual(vInst: VirtualInstance, classKeys: ClassComparisonKe
end
end
local validChildren: { string } = {}
if vInst.Children then
for _, childId in ipairs(vInst.Children) do
if virtualInstances[childId] then
table.insert(validChildren, childId)
end
end
end
return {
props = decoded,
extraProps = extraProps,
@ -179,10 +193,16 @@ local function cacheVirtual(vInst: VirtualInstance, classKeys: ClassComparisonKe
attrs = decodedAttrs,
refs = refs,
childCount = if vInst.Children then #vInst.Children else 0,
validChildren = validChildren,
}
end
local function cacheStudio(studioInstance: Instance, classKeys: ClassComparisonKeys, extraPropNames: { string }, refPropNames: { string }): SCache
local function cacheStudio(
studioInstance: Instance,
classKeys: ClassComparisonKeys,
extraPropNames: { string },
refPropNames: { string }
): SCache
local props: { [string]: any } = {}
for _, propName in ipairs(classKeys.propNames) do
@ -244,13 +264,16 @@ local function countOwnDiffs(vCache: VCache, sCache: SCache, classKeys: ClassCom
vVal = defaults[propName]
end
if not trueEquals(vVal, sProps[propName]) then
local sVal = sProps[propName]
if vVal ~= sVal and not trueEquals(vVal, sVal) then
cost += 1
end
end
for _, propName in ipairs(vCache.extraProps) do
if not trueEquals(vProps[propName], sProps[propName]) then
local vVal = vProps[propName]
local sVal = sProps[propName]
if vVal ~= sVal and not trueEquals(vVal, sVal) then
cost += 1
end
end
@ -271,7 +294,8 @@ local function countOwnDiffs(vCache: VCache, sCache: SCache, classKeys: ClassCom
local vAttrs = vCache.attrs
local sAttrs = sCache.attrs
for key, vVal in pairs(vAttrs) do
if not trueEquals(vVal, sAttrs[key]) then
local sVal = sAttrs[key]
if vVal ~= sVal and not trueEquals(vVal, sVal) then
cost += 1
end
end
@ -306,23 +330,6 @@ local function countOwnDiffs(vCache: VCache, sCache: SCache, classKeys: ClassCom
return cost
end
-- ================================================================
-- Utilities
-- ================================================================
local function removeMatched(arr: { any }, matchedIndices: { [number]: boolean }): ()
local write = 1
for read = 1, #arr do
if not matchedIndices[read] then
arr[write] = arr[read]
write += 1
end
end
for i = write, #arr do
arr[i] = nil
end
end
-- ================================================================
-- Core: 2-function mutual recursion
-- ================================================================
@ -362,32 +369,18 @@ local function computePairCost(
return cost
end
local vChildren = vInst.Children
local validVChildren = vCache.validChildren
local studioKids = sCache.children
if (vChildren and #vChildren > 0) or #studioKids > 0 then
if not vChildren or #vChildren == 0 then
if #validVChildren > 0 or #studioKids > 0 then
if #validVChildren == 0 then
cost += #studioKids * UNMATCHED_PENALTY
elseif #studioKids == 0 then
for _, childId in ipairs(vChildren) do
if virtualInstances[childId] then
cost += UNMATCHED_PENALTY
end
end
cost += #validVChildren * UNMATCHED_PENALTY
else
local validVChildren: { string } = {}
for _, childId in ipairs(vChildren) do
if virtualInstances[childId] then
table.insert(validVChildren, childId)
end
end
if #validVChildren > 0 then
local childResult =
matchChildren(session, validVChildren, studioKids, virtualInstances, virtualId, studioInstance)
cost += childResult.totalCost
else
cost += #studioKids * UNMATCHED_PENALTY
end
local childResult =
matchChildren(session, validVChildren, studioKids, virtualInstances, virtualId, studioInstance)
cost += childResult.totalCost
end
end
@ -409,7 +402,6 @@ matchChildren = function(
parentVirtualId: string?,
parentStudioInstance: Instance?
): MatchResult
-- Cache lookup
if parentVirtualId and parentStudioInstance then
local pc = session.matchCache[parentVirtualId]
if pc then
@ -421,14 +413,14 @@ matchChildren = function(
end
local matched: { MatchPair } = {}
local remainingVirtual: { string } = table.clone(virtualChildren)
local remainingStudio: { Instance } = table.clone(studioChildren)
local matchedV: { [number]: boolean } = {}
local matchedS: { [number]: boolean } = {}
-- ============================================================
-- Group by (Name, ClassName) -- direct property access, no pcall
-- Group by (Name, ClassName) once
-- ============================================================
local vByKey: { [string]: { number } } = {}
for i, id in ipairs(remainingVirtual) do
for i, id in ipairs(virtualChildren) do
local vInst = virtualInstances[id]
if vInst then
local key = vInst.Name .. "\0" .. vInst.ClassName
@ -442,7 +434,7 @@ matchChildren = function(
end
local sByKey: { [string]: { number } } = {}
for i, inst in ipairs(remainingStudio) do
for i, inst in ipairs(studioChildren) do
local key = inst.Name .. "\0" .. inst.ClassName
local group = sByKey[key]
if not group then
@ -453,222 +445,160 @@ matchChildren = function(
end
-- ============================================================
-- 1:1 instant match
-- 1:1 instant match + ambiguous scoring in single pass
-- ============================================================
local matchedV: { [number]: boolean } = {}
local matchedS: { [number]: boolean } = {}
for key, vIndices in pairs(vByKey) do
local sIndices = sByKey[key]
if sIndices and #vIndices == 1 and #sIndices == 1 then
if not sIndices then
continue
end
if #vIndices == 1 and #sIndices == 1 then
local vi, si = vIndices[1], sIndices[1]
if not matchedV[vi] and not matchedS[si] then
table.insert(matched, {
virtualId = remainingVirtual[vi],
studioInstance = remainingStudio[si],
})
matchedV[vi] = true
matchedS[si] = true
end
table.insert(matched, {
virtualId = virtualChildren[vi],
studioInstance = studioChildren[si],
})
matchedV[vi] = true
matchedS[si] = true
continue
end
end
removeMatched(remainingVirtual, matchedV)
removeMatched(remainingStudio, matchedS)
local firstVInst = virtualInstances[virtualChildren[vIndices[1]]]
if not firstVInst then
continue
end
local classKeys = RbxDom.getClassComparisonKeys(firstVInst.ClassName)
-- ============================================================
-- Ambiguous groups: change-count scoring + greedy assignment
-- ============================================================
if #remainingVirtual > 0 and #remainingStudio > 0 then
local vByKey2: { [string]: { number } } = {}
for i, id in ipairs(remainingVirtual) do
local vInst = virtualInstances[id]
if vInst then
local key = vInst.Name .. "\0" .. vInst.ClassName
local group = vByKey2[key]
if not group then
group = {}
vByKey2[key] = group
end
table.insert(group, i)
local vCaches: { [number]: VCache } = {}
local allExtraProps: { [string]: boolean } = {}
local allRefProps: { [string]: boolean } = {}
for _, vi in ipairs(vIndices) do
local vInst = virtualInstances[virtualChildren[vi]]
if not vInst then
continue
end
local vCache = cacheVirtual(vInst, classKeys, virtualInstances)
vCaches[vi] = vCache
for _, propName in ipairs(vCache.extraProps) do
allExtraProps[propName] = true
end
for propName, _ in pairs(vCache.refs) do
allRefProps[propName] = true
end
end
local sByKey2: { [string]: { number } } = {}
for i, inst in ipairs(remainingStudio) do
local key = inst.Name .. "\0" .. inst.ClassName
local group = sByKey2[key]
if not group then
group = {}
sByKey2[key] = group
end
table.insert(group, i)
local extraPropNamesArray: { string } = {}
for propName, _ in pairs(allExtraProps) do
table.insert(extraPropNamesArray, propName)
end
local matchedV2: { [number]: boolean } = {}
local matchedS2: { [number]: boolean } = {}
local refPropNamesArray: { string } = {}
for propName, _ in pairs(allRefProps) do
table.insert(refPropNamesArray, propName)
end
for key, vIndices in pairs(vByKey2) do
local sIndices = sByKey2[key]
if not sIndices then
local sCaches: { [number]: SCache } = {}
for _, si in ipairs(sIndices) do
sCaches[si] = cacheStudio(studioChildren[si], classKeys, extraPropNamesArray, refPropNamesArray)
end
local scoredPairs: { ScoredPair } = {}
local pairIdx = 0
local bestSoFar = math.huge
for _, vi in ipairs(vIndices) do
local vCache = vCaches[vi]
if not vCache then
continue
end
local firstVInst = virtualInstances[remainingVirtual[vIndices[1]]]
if not firstVInst then
continue
end
local classKeys = RbxDom.getClassComparisonKeys(firstVInst.ClassName)
local vCaches: { [number]: VCache } = {}
local allExtraProps: { [string]: boolean } = {}
local allRefProps: { [string]: boolean } = {}
for _, vi in ipairs(vIndices) do
if matchedV2[vi] then
continue
end
local vInst = virtualInstances[remainingVirtual[vi]]
if not vInst then
continue
end
local vCache = cacheVirtual(vInst, classKeys, virtualInstances)
vCaches[vi] = vCache
for _, propName in ipairs(vCache.extraProps) do
allExtraProps[propName] = true
end
for propName, _ in pairs(vCache.refs) do
allRefProps[propName] = true
end
end
local extraPropNamesArray: { string } = {}
for propName, _ in pairs(allExtraProps) do
table.insert(extraPropNamesArray, propName)
end
local refPropNamesArray: { string } = {}
for propName, _ in pairs(allRefProps) do
table.insert(refPropNamesArray, propName)
end
local sCaches: { [number]: SCache } = {}
for _, si in ipairs(sIndices) do
if not matchedS2[si] then
sCaches[si] = cacheStudio(remainingStudio[si], classKeys, extraPropNamesArray, refPropNamesArray)
end
end
-- Score all (A, B) pairs
local scoredPairs: { ScoredPair } = {}
local pairIdx = 0
local bestSoFar = math.huge
for _, vi in ipairs(vIndices) do
if matchedV2[vi] then
continue
end
local vCache = vCaches[vi]
if not vCache then
local sCache = sCaches[si]
if not sCache then
continue
end
for _, si in ipairs(sIndices) do
if matchedS2[si] then
continue
end
local sCache = sCaches[si]
if not sCache then
continue
end
pairIdx += 1
local cost = countOwnDiffs(vCache, sCache, classKeys)
pairIdx += 1
if cost < bestSoFar then
local validVChildren = vCache.validChildren
local studioKids = sCache.children
local cost = countOwnDiffs(vCache, sCache, classKeys)
if cost < bestSoFar then
local vInst = virtualInstances[remainingVirtual[vi]]
if vInst then
local vChildren = vInst.Children
local studioKids = sCache.children
if (vChildren and #vChildren > 0) or #studioKids > 0 then
if not vChildren or #vChildren == 0 then
cost += #studioKids * UNMATCHED_PENALTY
elseif #studioKids == 0 then
for _, childId in ipairs(vChildren) do
if virtualInstances[childId] then
cost += UNMATCHED_PENALTY
end
end
else
local validVChildren: { string } = {}
for _, childId in ipairs(vChildren) do
if virtualInstances[childId] then
table.insert(validVChildren, childId)
end
end
if #validVChildren > 0 then
local childResult = matchChildren(
session,
validVChildren,
studioKids,
virtualInstances,
remainingVirtual[vi],
sCache.instance
)
cost += childResult.totalCost
else
cost += #studioKids * UNMATCHED_PENALTY
end
end
end
if #validVChildren > 0 or #studioKids > 0 then
if #validVChildren == 0 then
cost += #studioKids * UNMATCHED_PENALTY
elseif #studioKids == 0 then
cost += #validVChildren * UNMATCHED_PENALTY
else
local childResult = matchChildren(
session,
validVChildren,
studioKids,
virtualInstances,
virtualChildren[vi],
sCache.instance
)
cost += childResult.totalCost
end
end
table.insert(scoredPairs, { vi = vi, si = si, cost = cost, idx = pairIdx })
if cost < bestSoFar then
bestSoFar = cost
end
end
end
table.sort(scoredPairs, function(a: ScoredPair, b: ScoredPair): boolean
if a.cost ~= b.cost then
return a.cost < b.cost
table.insert(scoredPairs, { vi = vi, si = si, cost = cost, idx = pairIdx })
if cost < bestSoFar then
bestSoFar = cost
end
return a.idx < b.idx
end)
for _, pair in ipairs(scoredPairs) do
if matchedV2[pair.vi] or matchedS2[pair.si] then
continue
end
table.insert(matched, {
virtualId = remainingVirtual[pair.vi],
studioInstance = remainingStudio[pair.si],
})
matchedV2[pair.vi] = true
matchedS2[pair.si] = true
end
end
removeMatched(remainingVirtual, matchedV2)
removeMatched(remainingStudio, matchedS2)
table.sort(scoredPairs, function(a: ScoredPair, b: ScoredPair): boolean
if a.cost ~= b.cost then
return a.cost < b.cost
end
return a.idx < b.idx
end)
for _, pair in ipairs(scoredPairs) do
if matchedV[pair.vi] or matchedS[pair.si] then
continue
end
table.insert(matched, {
virtualId = virtualChildren[pair.vi],
studioInstance = studioChildren[pair.si],
})
matchedV[pair.vi] = true
matchedS[pair.si] = true
end
end
-- ============================================================
-- Compute totalCost for ALL matched pairs (session cache helps)
-- Build unmatched lists + compute totalCost
-- ============================================================
local unmatchedVirtual: { string } = {}
for i, id in ipairs(virtualChildren) do
if not matchedV[i] then
table.insert(unmatchedVirtual, id)
end
end
local unmatchedStudio: { Instance } = {}
for i, inst in ipairs(studioChildren) do
if not matchedS[i] then
table.insert(unmatchedStudio, inst)
end
end
local totalCost = 0
for _, pair in ipairs(matched) do
totalCost += computePairCost(session, pair.virtualId, pair.studioInstance, virtualInstances, math.huge)
end
totalCost += (#remainingVirtual + #remainingStudio) * UNMATCHED_PENALTY
totalCost += (#unmatchedVirtual + #unmatchedStudio) * UNMATCHED_PENALTY
local result = {
matched = matched,
unmatchedVirtual = remainingVirtual,
unmatchedStudio = remainingStudio,
unmatchedVirtual = unmatchedVirtual,
unmatchedStudio = unmatchedStudio,
totalCost = totalCost,
}