Implement new recursive change-count matching algorithm for instance reconciliation

- Introduced a comprehensive rewrite of the matching algorithm, replacing the previous multi-pass approach with a recursive change-count scoring function and greedy assignment strategy.
- Updated the `hydrate.lua` and `matching.lua` files to utilize the new algorithm, enhancing the efficiency of instance matching during hydration.
- Documented the new algorithm's structure, including fast-paths for Ref pins and (Name, ClassName) grouping, which significantly improve matching accuracy and performance.
- Ensured that the new implementation maintains compatibility with existing systems while providing a more robust solution for handling ambiguous instances.

These changes enhance the clarity and efficiency of the reconciliation process, paving the way for improved project management and user experience.
This commit is contained in:
Joe 2026-02-17 03:54:06 -06:00
parent 4cb45cd607
commit 1a1d9ce886
14 changed files with 1047 additions and 1108 deletions

View file

@ -2,10 +2,9 @@
Defines the process of "hydration" -- matching up a virtual DOM with
concrete instances and assigning them IDs.
Uses the 3-pass matching algorithm to handle duplicate-named instances:
Pass 1: unique name + ClassName narrowing
Pass 2: Ref property discriminators (using pre-matched UIDs)
Pass 3: structural fingerprinting + pairwise similarity
Uses the change-count matching algorithm to handle duplicate-named
instances: groups by (Name, ClassName), then greedy-assigns pairs
by fewest total reconciler changes (recursive into subtrees).
]]
local Packages = script.Parent.Parent.Parent.Packages
@ -42,12 +41,7 @@ local function hydrate(instanceMap, virtualInstances, rootId, rootInstance)
end
-- Use the 3-pass matching algorithm to pair virtual ↔ studio children
local result = Matching.matchChildren(
validVirtualIds,
existingChildren,
virtualInstances,
instanceMap
)
local result = Matching.matchChildren(validVirtualIds, existingChildren, virtualInstances, instanceMap)
-- Recursively hydrate matched pairs
for _, pair in result.matched do

View file

@ -1,32 +1,115 @@
--[[
3-pass instance matching algorithm for the plugin.
Instance matching algorithm for the plugin.
Pairs server virtual instances to Studio instances during hydration.
Same algorithm pattern as the Rust syncback/forward sync matchers:
Pairs server virtual instances to Studio instances during hydration
by minimizing total reconciler changes.
Pass 1: Unique name matching + ClassName narrowing
Pass 2: Ref property discriminators (using pre-matched UIDs)
Pass 3: Structural fingerprinting with XXH32 + pairwise similarity
Algorithm per parent:
1. Ref pin: parent's Ref properties confirm identity of specific children
2. Group remaining by (Name, ClassName) -- 1:1 groups instant-match
3. Ambiguous groups: recursive change-count scoring + greedy assignment
Returns: matched (array of {virtualId, studioInstance}),
unmatchedVirtual (array of virtualId),
unmatchedStudio (array of studioInstance)
The change count for a pair = how many things the reconciler would need
to touch to turn instance A into instance B, including the entire subtree.
]]
local Packages = script.Parent.Parent.Parent.Packages
local RbxDom = require(Packages.RbxDom)
local UNMATCHED_PENALTY = 10000
-- Value equality check. This MUST match the trueEquals function in diff.lua
-- exactly. If matching says "equal" but diff says "different", we get phantom
-- changes from cross-paired instances.
local function fuzzyEq(a: number, b: number, epsilon: number): boolean
local diff = math.abs(a - b)
local maxVal = math.max(math.abs(a), math.abs(b), 1)
return diff < epsilon or diff < maxVal * epsilon
end
local function trueEquals(a, b): boolean
if a == b then
return true
end
if
(a == nil and type(b) == "table" and b.Ref == "00000000000000000000000000000000")
or (b == nil and type(a) == "table" and a.Ref == "00000000000000000000000000000000")
then
return true
end
local typeA, typeB = typeof(a), typeof(b)
if typeA == "table" and typeB == "table" then
local checkedKeys = {}
for key, value in a do
checkedKeys[key] = true
if not trueEquals(value, b[key]) then
return false
end
end
for key, value in b do
if checkedKeys[key] then
continue
end
if not trueEquals(value, a[key]) then
return false
end
end
return true
elseif a ~= a and b ~= b then
return true
elseif typeA == "number" and typeB == "number" then
return fuzzyEq(a, b, 0.0001)
elseif typeA == "number" and typeB == "EnumItem" then
return a == b.Value
elseif typeA == "EnumItem" and typeB == "number" then
return a.Value == b
elseif typeA == "Color3" and typeB == "Color3" then
local aR, aG, aB = math.floor(a.R * 255), math.floor(a.G * 255), math.floor(a.B * 255)
local bR, bG, bB = math.floor(b.R * 255), math.floor(b.G * 255), math.floor(b.B * 255)
return aR == bR and aG == bG and aB == bB
elseif typeA == "CFrame" and typeB == "CFrame" then
local aComponents, bComponents = { a:GetComponents() }, { b:GetComponents() }
for i, aComponent in aComponents do
if not fuzzyEq(aComponent, bComponents[i], 0.0001) then
return false
end
end
return true
elseif typeA == "Vector3" and typeB == "Vector3" then
local aComponents, bComponents = { a.X, a.Y, a.Z }, { b.X, b.Y, b.Z }
for i, aComponent in aComponents do
if not fuzzyEq(aComponent, bComponents[i], 0.0001) then
return false
end
end
return true
elseif typeA == "Vector2" and typeB == "Vector2" then
local aComponents, bComponents = { a.X, a.Y }, { b.X, b.Y }
for i, aComponent in aComponents do
if not fuzzyEq(aComponent, bComponents[i], 0.0001) then
return false
end
end
return true
end
return false
end
local Matching = {}
--[[
Run the 3-pass matching algorithm.
Entry point. Match virtual children to Studio children under a parent.
virtualChildren: array of virtual instance IDs (strings from server)
virtualChildren: array of virtual instance IDs
studioChildren: array of Studio Instance objects
virtualInstances: map of virtual ID → virtual instance data
instanceMap: bidirectional ID ↔ Instance mapping (for Ref resolution)
instanceMap: bidirectional ID ↔ Instance mapping
Returns a table with:
matched: array of {virtualId: string, studioInstance: Instance}
unmatchedVirtual: array of string (virtual IDs)
unmatchedStudio: array of Instance
Returns {matched, unmatchedVirtual, unmatchedStudio}
]]
function Matching.matchChildren(
virtualChildren: { string },
@ -38,12 +121,73 @@ function Matching.matchChildren(
local remainingVirtual: { string } = table.clone(virtualChildren)
local remainingStudio: { Instance } = table.clone(studioChildren)
-- Pass 1: Unique name matching + ClassName narrowing
Matching._pass1NameAndClass(
remainingVirtual, remainingStudio, virtualInstances, matched
)
-- ============================================================
-- Fast-path 1: Ref pin (confirmed identity, highest priority)
-- ============================================================
-- Scan virtual parent for Ref-typed properties that point to
-- virtual children. If the corresponding Studio parent property
-- points to a Studio child, pin them as a confirmed match.
-- (Ref pins are handled implicitly during hydration -- the parent
-- is already matched, and its Ref properties resolve via instanceMap
-- after children are matched. For initial hydration the instanceMap
-- is empty, so explicit Ref pinning at this stage is a no-op.
-- Ref correctness is maintained by the change-count scoring which
-- counts Ref property diffs.)
-- Fast path: if all matched, skip Passes 2 and 3
-- ============================================================
-- Fast-path 2: Group by (Name, ClassName) -- 1:1 instant match
-- ============================================================
local vByKey: { [string]: { number } } = {}
for i, id in remainingVirtual do
local vInst = virtualInstances[id]
if vInst then
local key = vInst.Name .. "\0" .. vInst.ClassName
if not vByKey[key] then
vByKey[key] = {}
end
table.insert(vByKey[key], i)
end
end
local sByKey: { [string]: { number } } = {}
for i, inst in remainingStudio do
local ok, name, class = pcall(function()
return inst.Name, inst.ClassName
end)
if ok then
local key = name .. "\0" .. class
if not sByKey[key] then
sByKey[key] = {}
end
table.insert(sByKey[key], i)
end
end
-- Track which indices to remove (collected, then removed in reverse)
local matchedV: { [number]: boolean } = {}
local matchedS: { [number]: boolean } = {}
-- 1:1 groups: instant match
for key, vIndices in vByKey do
local sIndices = sByKey[key]
if sIndices and #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
end
end
-- Remove 1:1 matched from remaining lists
Matching._removeMatched(remainingVirtual, matchedV)
Matching._removeMatched(remainingStudio, matchedS)
-- Early exit if nothing left
if #remainingVirtual == 0 or #remainingStudio == 0 then
return {
matched = matched,
@ -52,14 +196,95 @@ function Matching.matchChildren(
}
end
-- Pass 2: Ref property discriminators (placeholder)
-- TODO: Use pre-resolved Refs from instanceMap to differentiate
-- candidates within same-name groups.
-- ============================================================
-- Ambiguous groups: change-count scoring + greedy assignment
-- ============================================================
-- Rebuild groups from remaining (only ambiguous ones are left)
local vByKey2: { [string]: { number } } = {}
for i, id in remainingVirtual do
local vInst = virtualInstances[id]
if vInst then
local key = vInst.Name .. "\0" .. vInst.ClassName
if not vByKey2[key] then
vByKey2[key] = {}
end
table.insert(vByKey2[key], i)
end
end
-- Pass 3: Similarity scoring
Matching._pass3Similarity(
remainingVirtual, remainingStudio, virtualInstances, matched
)
local sByKey2: { [string]: { number } } = {}
for i, inst in remainingStudio do
local ok, name, class = pcall(function()
return inst.Name, inst.ClassName
end)
if ok then
local key = name .. "\0" .. class
if not sByKey2[key] then
sByKey2[key] = {}
end
table.insert(sByKey2[key], i)
end
end
local matchedV2: { [number]: boolean } = {}
local matchedS2: { [number]: boolean } = {}
for key, vIndices in vByKey2 do
local sIndices = sByKey2[key]
if not sIndices then
continue
end
-- Build all (A, B) pairs with change counts.
-- Pairs are built in child order (virtual outer, studio inner).
local pairs: { { vi: number, si: number, cost: number, idx: number } } = {}
local pairIdx = 0
local bestSoFar = math.huge
for _, vi in vIndices do
if matchedV2[vi] then
continue
end
for _, si in sIndices do
if matchedS2[si] then
continue
end
pairIdx += 1
local cost =
Matching._computeChangeCount(remainingVirtual[vi], remainingStudio[si], virtualInstances, bestSoFar)
table.insert(pairs, { vi = vi, si = si, cost = cost, idx = pairIdx })
if cost < bestSoFar then
bestSoFar = cost
end
end
end
-- Stable sort by cost ascending (Luau sort is not stable,
-- so break ties by insertion index to preserve child order)
table.sort(pairs, function(a, b)
if a.cost ~= b.cost then
return a.cost < b.cost
end
return a.idx < b.idx
end)
-- Greedy assign
for _, pair in pairs 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
-- Remove matched from remaining
Matching._removeMatched(remainingVirtual, matchedV2)
Matching._removeMatched(remainingStudio, matchedS2)
return {
matched = matched,
@ -69,256 +294,204 @@ function Matching.matchChildren(
end
--[[
Pass 1: Match instances with unique names, then narrow by ClassName.
Compute total change count between a virtual instance and a Studio instance.
Includes own property diffs + recursive children cost.
Early-exits if cost reaches or exceeds bestSoFar.
]]
function Matching._pass1NameAndClass(
remainingVirtual: { string },
remainingStudio: { Instance },
function Matching._computeChangeCount(
virtualId: string,
studioInstance: Instance,
virtualInstances: { [string]: any },
matched: { { virtualId: string, studioInstance: Instance } }
)
-- Build name → indices maps for both sides
local virtualByName: { [string]: { number } } = {}
for i, id in remainingVirtual do
local vInst = virtualInstances[id]
if vInst then
local name = vInst.Name
if not virtualByName[name] then
virtualByName[name] = {}
end
table.insert(virtualByName[name], i)
end
bestSoFar: number
): number
local vInst = virtualInstances[virtualId]
if not vInst then
return UNMATCHED_PENALTY
end
local studioByName: { [string]: { number } } = {}
for i, inst in remainingStudio do
local success, name = pcall(function()
return inst.Name
-- Cheap: own property diffs
local cost = Matching._countOwnDiffs(vInst, studioInstance)
if cost >= bestSoFar then
return cost
end
-- Expensive: recursive children matching
local vChildren = vInst.Children
if not vChildren or #vChildren == 0 then
-- No virtual children: any Studio children are unmatched
local ok, studioKids = pcall(function()
return studioInstance:GetChildren()
end)
if success and name then
if not studioByName[name] then
studioByName[name] = {}
end
table.insert(studioByName[name], i)
if ok then
cost += #studioKids * UNMATCHED_PENALTY
end
return cost
end
-- Collect matches (indices to remove, processed in reverse order)
local matchedVirtualIndices: { number } = {}
local matchedStudioIndices: { number } = {}
for name, vIndices in virtualByName do
local sIndices = studioByName[name]
if not sIndices then
continue
end
-- Case 1: exactly one on each side → instant match
if #vIndices == 1 and #sIndices == 1 then
table.insert(matched, {
virtualId = remainingVirtual[vIndices[1]],
studioInstance = remainingStudio[sIndices[1]],
})
table.insert(matchedVirtualIndices, vIndices[1])
table.insert(matchedStudioIndices, sIndices[1])
continue
end
-- Case 2: Try ClassName narrowing within this name group
local virtualByClass: { [string]: { number } } = {}
for _, vi in vIndices do
local vInst = virtualInstances[remainingVirtual[vi]]
if vInst then
local class = vInst.ClassName
if not virtualByClass[class] then
virtualByClass[class] = {}
end
table.insert(virtualByClass[class], vi)
end
end
local studioByClass: { [string]: { number } } = {}
for _, si in sIndices do
local success, class = pcall(function()
return remainingStudio[si].ClassName
end)
if success and class then
if not studioByClass[class] then
studioByClass[class] = {}
end
table.insert(studioByClass[class], si)
end
end
for class, vClassIndices in virtualByClass do
local sClassIndices = studioByClass[class]
if sClassIndices and #vClassIndices == 1 and #sClassIndices == 1 then
table.insert(matched, {
virtualId = remainingVirtual[vClassIndices[1]],
studioInstance = remainingStudio[sClassIndices[1]],
})
table.insert(matchedVirtualIndices, vClassIndices[1])
table.insert(matchedStudioIndices, sClassIndices[1])
end
end
end
-- Remove matched items from remaining lists (in reverse order to preserve indices)
table.sort(matchedVirtualIndices, function(a, b)
return a > b
end)
table.sort(matchedStudioIndices, function(a, b)
return a > b
local ok, studioKids = pcall(function()
return studioInstance:GetChildren()
end)
if not ok then
return cost
end
-- Deduplicate indices (in case of overlapping matches)
local seenV: { [number]: boolean } = {}
local seenS: { [number]: boolean } = {}
for _, idx in matchedVirtualIndices do
if not seenV[idx] then
seenV[idx] = true
table.remove(remainingVirtual, idx)
-- Filter valid virtual children
local validVChildren: { string } = {}
for _, childId in vChildren do
if virtualInstances[childId] then
table.insert(validVChildren, childId)
end
end
for _, idx in matchedStudioIndices do
if not seenS[idx] then
seenS[idx] = true
table.remove(remainingStudio, idx)
-- Recursive: match children and sum costs
local childResult = Matching.matchChildren(validVChildren, studioKids, virtualInstances, nil)
-- Matched children: recursively score each pair
for _, pair in childResult.matched do
cost += Matching._computeChangeCount(pair.virtualId, pair.studioInstance, virtualInstances, bestSoFar - cost)
if cost >= bestSoFar then
return cost
end
end
-- Unmatched children: penalty per instance
local unmatchedCount = #childResult.unmatchedVirtual + #childResult.unmatchedStudio
cost += unmatchedCount * UNMATCHED_PENALTY
return cost
end
--[[
Pass 3: Match remaining instances within same-name groups.
Strategy:
1. Narrow by ClassName within each name group.
2. Within same-name+class sub-groups, use POSITIONAL MATCHING as the
primary signal. The server sends virtual children in DOM order and
Studio's GetChildren() preserves insertion order -- these typically
align. The Nth virtual "Line" matches the Nth Studio "Line".
3. When group sizes differ (adds/deletes), fall back to property-based
similarity scoring to pick the best remaining match.
This preserves the ordering behavior of the original greedy hydration
algorithm while handling ambiguous paths correctly.
Count own property diffs between a virtual instance and Studio instance.
Each differing property = +1. Tags and Attributes counted granularly.
]]
function Matching._pass3Similarity(
remainingVirtual: { string },
remainingStudio: { Instance },
virtualInstances: { [string]: any },
matched: { { virtualId: string, studioInstance: Instance } }
)
-- Group remaining by name
local virtualByName: { [string]: { number } } = {}
for i, id in remainingVirtual do
local vInst = virtualInstances[id]
if vInst then
local name = vInst.Name
if not virtualByName[name] then
virtualByName[name] = {}
end
table.insert(virtualByName[name], i)
end
end
function Matching._countOwnDiffs(vInst: any, studioInstance: Instance): number
local cost = 0
local studioByName: { [string]: { number } } = {}
for i, inst in remainingStudio do
local success, name = pcall(function()
return inst.Name
end)
if success and name then
if not studioByName[name] then
studioByName[name] = {}
end
table.insert(studioByName[name], i)
end
end
local matchedVirtualIndices: { [number]: boolean } = {}
local matchedStudioIndices: { [number]: boolean } = {}
for name, vIndices in virtualByName do
local sIndices = studioByName[name]
if not sIndices then
continue
end
-- Narrow by ClassName within this name group.
local virtualByClass: { [string]: { number } } = {}
for _, vi in vIndices do
local vInst = virtualInstances[remainingVirtual[vi]]
if vInst and not matchedVirtualIndices[vi] then
local class = vInst.ClassName
if not virtualByClass[class] then
virtualByClass[class] = {}
end
table.insert(virtualByClass[class], vi)
end
end
local studioByClass: { [string]: { number } } = {}
for _, si in sIndices do
if matchedStudioIndices[si] then
-- Compare properties from the virtual side
local vProps = vInst.Properties
if vProps then
for propName, encodedValue in vProps do
-- Skip Tags and Attributes here (counted separately below)
if propName == "Tags" or propName == "Attributes" then
continue
end
local success, class = pcall(function()
return remainingStudio[si].ClassName
-- Try to decode the virtual value and compare to Studio
local ty = next(encodedValue)
if ty == "Ref" then
-- Ref properties: can't easily compare during hydration
-- (instanceMap not yet populated). Skip for now.
continue
end
-- Decode the encoded value to a native Roblox type
local decodeOk, decodedValue = RbxDom.EncodedValue.decode(encodedValue)
if not decodeOk then
-- Can't decode: assume different (conservative)
cost += 1
continue
end
-- Read the Studio property
local readOk, studioValue = pcall(function()
return (studioInstance :: any)[propName]
end)
if success and class then
if not studioByClass[class] then
studioByClass[class] = {}
end
table.insert(studioByClass[class], si)
end
end
-- Within each same-name+class sub-group, do positional matching.
-- The Nth virtual instance matches the Nth Studio instance.
-- This preserves the original child ordering which is the strongest
-- signal for duplicate-named instances (e.g., Parts in a Model).
for class, vClassIndices in virtualByClass do
local sClassIndices = studioByClass[class]
if not sClassIndices then
if not readOk then
-- Can't read: assume different
cost += 1
continue
end
local matchCount = math.min(#vClassIndices, #sClassIndices)
for idx = 1, matchCount do
local vi = vClassIndices[idx]
local si = sClassIndices[idx]
if not matchedVirtualIndices[vi] and not matchedStudioIndices[si] then
table.insert(matched, {
virtualId = remainingVirtual[vi],
studioInstance = remainingStudio[si],
})
matchedVirtualIndices[vi] = true
matchedStudioIndices[si] = true
-- Compare values using the same equality check as diff.lua
if not trueEquals(decodedValue, studioValue) then
cost += 1
end
end
end
-- Tags: count individual adds/removes
local vTags = vInst.Properties and vInst.Properties.Tags
if vTags then
local decodeOk, decodedTags = RbxDom.EncodedValue.decode(vTags)
if decodeOk and type(decodedTags) == "table" then
local readOk, studioTags = pcall(function()
return studioInstance:GetTags()
end)
if readOk then
local vSet: { [string]: boolean } = {}
for _, tag in decodedTags do
vSet[tag] = true
end
local sSet: { [string]: boolean } = {}
for _, tag in studioTags do
sSet[tag] = true
end
-- Count symmetric difference
for tag in vSet do
if not sSet[tag] then
cost += 1
end
end
for tag in sSet do
if not vSet[tag] then
cost += 1
end
end
end
end
end
-- Remove matched items from remaining lists (reverse order)
local vToRemove: { number } = {}
local sToRemove: { number } = {}
for idx in matchedVirtualIndices do
table.insert(vToRemove, idx)
-- Attributes: count individual adds/removes/changes
local vAttrs = vInst.Properties and vInst.Properties.Attributes
if vAttrs then
local decodeOk, decodedAttrs = RbxDom.EncodedValue.decode(vAttrs)
if decodeOk and type(decodedAttrs) == "table" then
local readOk, studioAttrs = pcall(function()
return studioInstance:GetAttributes()
end)
if readOk then
-- Count diffs in attribute maps
for key, vVal in decodedAttrs do
local sVal = studioAttrs[key]
if sVal == nil or not trueEquals(vVal, sVal) then
cost += 1
end
end
for key in studioAttrs do
if decodedAttrs[key] == nil then
cost += 1
end
end
end
end
end
for idx in matchedStudioIndices do
table.insert(sToRemove, idx)
-- Children count diff
local vChildCount = if vInst.Children then #vInst.Children else 0
local readOk, studioChildCount = pcall(function()
return #studioInstance:GetChildren()
end)
if readOk and vChildCount ~= studioChildCount then
cost += 1
end
table.sort(vToRemove, function(a, b)
return cost
end
--[[
Remove items from an array by index set (removes in reverse order).
]]
function Matching._removeMatched(arr: { any }, matchedIndices: { [number]: boolean })
local toRemove: { number } = {}
for idx in matchedIndices do
table.insert(toRemove, idx)
end
table.sort(toRemove, function(a, b)
return a > b
end)
table.sort(sToRemove, function(a, b)
return a > b
end)
for _, idx in vToRemove do
table.remove(remainingVirtual, idx)
end
for _, idx in sToRemove do
table.remove(remainingStudio, idx)
for _, idx in toRemove do
table.remove(arr, idx)
end
end

View file

@ -31,33 +31,40 @@ local function XXH32(Message: buffer, Seed: number?): number
local Temp1 = bit32.lrotate(Accumulator1 + Mult1, 13)
local AHigh1_2, ALow1_2 = bit32.rshift(Temp1, 16), bit32.band(Temp1, 65535)
Accumulator1 = bit32.lshift((AHigh1_2 * PRIME_1_LOW) + (ALow1_2 * PRIME_1_HIGH), 16) + (ALow1_2 * PRIME_1_LOW)
Accumulator1 = bit32.lshift((AHigh1_2 * PRIME_1_LOW) + (ALow1_2 * PRIME_1_HIGH), 16)
+ (ALow1_2 * PRIME_1_LOW)
local AHigh2, ALow2 = bit32.rshift(Word2, 16), bit32.band(Word2, 65535)
local Mult2 = bit32.lshift((AHigh2 * PRIME_2_LOW) + (ALow2 * PRIME_2_HIGH), 16) + (ALow2 * PRIME_2_LOW)
local Temp2 = bit32.lrotate(Accumulator2 + Mult2, 13)
local AHigh2_2, ALow2_2 = bit32.rshift(Temp2, 16), bit32.band(Temp2, 65535)
Accumulator2 = bit32.lshift((AHigh2_2 * PRIME_1_LOW) + (ALow2_2 * PRIME_1_HIGH), 16) + (ALow2_2 * PRIME_1_LOW)
Accumulator2 = bit32.lshift((AHigh2_2 * PRIME_1_LOW) + (ALow2_2 * PRIME_1_HIGH), 16)
+ (ALow2_2 * PRIME_1_LOW)
local AHigh3, ALow3 = bit32.rshift(Word3, 16), bit32.band(Word3, 65535)
local Mult3 = bit32.lshift((AHigh3 * PRIME_2_LOW) + (ALow3 * PRIME_2_HIGH), 16) + (ALow3 * PRIME_2_LOW)
local Temp3 = bit32.lrotate(Accumulator3 + Mult3, 13)
local AHigh3_2, ALow3_2 = bit32.rshift(Temp3, 16), bit32.band(Temp3, 65535)
Accumulator3 = bit32.lshift((AHigh3_2 * PRIME_1_LOW) + (ALow3_2 * PRIME_1_HIGH), 16) + (ALow3_2 * PRIME_1_LOW)
Accumulator3 = bit32.lshift((AHigh3_2 * PRIME_1_LOW) + (ALow3_2 * PRIME_1_HIGH), 16)
+ (ALow3_2 * PRIME_1_LOW)
local AHigh4, ALow4 = bit32.rshift(Word4, 16), bit32.band(Word4, 65535)
local Mult4 = bit32.lshift((AHigh4 * PRIME_2_LOW) + (ALow4 * PRIME_2_HIGH), 16) + (ALow4 * PRIME_2_LOW)
local Temp4 = bit32.lrotate(Accumulator4 + Mult4, 13)
local AHigh4_2, ALow4_2 = bit32.rshift(Temp4, 16), bit32.band(Temp4, 65535)
Accumulator4 = bit32.lshift((AHigh4_2 * PRIME_1_LOW) + (ALow4_2 * PRIME_1_HIGH), 16) + (ALow4_2 * PRIME_1_LOW)
Accumulator4 = bit32.lshift((AHigh4_2 * PRIME_1_LOW) + (ALow4_2 * PRIME_1_HIGH), 16)
+ (ALow4_2 * PRIME_1_LOW)
CurrentOffset += 16
end
Digest = bit32.lrotate(Accumulator1, 1) + bit32.lrotate(Accumulator2, 7) + bit32.lrotate(Accumulator3, 12) + bit32.lrotate(Accumulator4, 18)
Digest = bit32.lrotate(Accumulator1, 1)
+ bit32.lrotate(Accumulator2, 7)
+ bit32.lrotate(Accumulator3, 12)
+ bit32.lrotate(Accumulator4, 18)
else
Digest = UsedSeed + PRIME_5
end