From 9c6966acff3ac4e8d17b6c59a3b195dd39b9919d Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 17 Feb 2026 07:52:29 -0600 Subject: [PATCH] Add default properties caching and update matching logic - Implemented a caching mechanism for default properties per class in `rbx_dom_lua/init.lua`, allowing for efficient retrieval of encoded default values, including inherited defaults. - Updated the matching logic in `Reconciler/matching.lua` to account for properties present only on the Studio side, ensuring accurate comparisons against default values during reconciliation. - Added new test cases to validate the handling of default-omitted properties, enhancing the robustness of the reconciliation process. These changes improve the efficiency and accuracy of property matching, addressing issues related to default values in the reconciliation workflow. --- plugin/rbx_dom_lua/init.lua | 38 ++ plugin/src/Reconciler/matching.lua | 42 ++ .../Icon/UIAspectRatioConstraint.model.json5 | 3 + .../BillboardGui/Frame/Icon/init.meta.json5 | 46 ++ .../Frame/Info/UIGradient.model.json5 | 36 ++ .../Frame/Info/UIStroke.model.json5 | 6 + .../BillboardGui/Frame/Info/init.meta.json5 | 60 +++ .../Hitbox/BillboardGui/Frame/init.meta.json5 | 45 ++ .../Hitbox/BillboardGui/init.meta.json5 | 26 + .../expected/Hitbox/Water.model.json5 | 16 + .../expected/Hitbox/init.meta.json5 | 29 ++ .../expected/TsunamiWave/Texture.model.json5 | 15 + .../TsunamiWave/Texture~1.model.json5 | 16 + .../TsunamiWave/Texture~10.model.json5 | 16 + .../TsunamiWave/Texture~11.model.json5 | 16 + .../TsunamiWave/Texture~2.model.json5 | 16 + .../TsunamiWave/Texture~3.model.json5 | 15 + .../TsunamiWave/Texture~4.model.json5 | 16 + .../TsunamiWave/Texture~5.model.json5 | 16 + .../TsunamiWave/Texture~6.model.json5 | 16 + .../TsunamiWave/Texture~7.model.json5 | 16 + .../TsunamiWave/Texture~8.model.json5 | 15 + .../TsunamiWave/Texture~9.model.json5 | 16 + .../expected/TsunamiWave/glow.model.json5 | 185 ++++++++ .../expected/TsunamiWave/glow~1.model.json5 | 186 ++++++++ .../expected/TsunamiWave/glow~2.model.json5 | 186 ++++++++ .../expected/TsunamiWave/init.meta.json5 | 60 +++ .../UFOWave_matching/expected/init.meta.json5 | 52 ++ .../UFOWave_matching/input.rbxm | Bin 0 -> 21586 bytes src/snapshot/matching.rs | 443 ++++++++++++++++++ 30 files changed, 1648 insertions(+) create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Icon/UIAspectRatioConstraint.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Icon/init.meta.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Info/UIGradient.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Info/UIStroke.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Info/init.meta.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/init.meta.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/init.meta.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/Water.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/init.meta.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~1.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~10.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~11.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~2.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~3.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~4.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~5.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~6.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~7.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~8.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~9.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~1.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~2.model.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/init.meta.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/expected/init.meta.json5 create mode 100644 rojo-test/syncback-tests/UFOWave_matching/input.rbxm diff --git a/plugin/rbx_dom_lua/init.lua b/plugin/rbx_dom_lua/init.lua index e26e3c2a..ed6ab91d 100644 --- a/plugin/rbx_dom_lua/init.lua +++ b/plugin/rbx_dom_lua/init.lua @@ -6,6 +6,43 @@ local PropertyDescriptor = require(script.PropertyDescriptor) -- Returns nil if the class doesn't exist in the database -- The properties table maps property names to metadata (scriptability, serialization status) -- To get a full PropertyDescriptor for encoding, use findCanonicalPropertyDescriptor +-- Cache for default properties per class (includes inherited defaults). +local defaultPropertiesCache: { [string]: { [string]: any } } = {} + +-- Returns a table mapping property names to their encoded default values +-- for a given class, including inherited defaults from parent classes. +-- Subclass overrides take precedence. Results are cached per class. +local function findDefaultProperties(className: string): { [string]: any } + local cached = defaultPropertiesCache[className] + if cached then + return cached + end + + local defaults: { [string]: any } = {} + local currentClassName: string? = className + + repeat + local currentClass = database.Classes[currentClassName] + if currentClass == nil then + break + end + + local classDefaults = currentClass.DefaultProperties + if classDefaults then + for propName, encodedValue in classDefaults do + if defaults[propName] == nil then + defaults[propName] = encodedValue + end + end + end + + currentClassName = currentClass.Superclass + until currentClassName == nil + + defaultPropertiesCache[className] = defaults + return defaults +end + local function findClassDescriptor(className) local classData = database.Classes[className] if classData == nil then @@ -111,6 +148,7 @@ return { writeProperty = writeProperty, findCanonicalPropertyDescriptor = findCanonicalPropertyDescriptor, findClassDescriptor = findClassDescriptor, + findDefaultProperties = findDefaultProperties, Error = Error, EncodedValue = require(script.EncodedValue), } diff --git a/plugin/src/Reconciler/matching.lua b/plugin/src/Reconciler/matching.lua index 9eb7c0dd..45d8823f 100644 --- a/plugin/src/Reconciler/matching.lua +++ b/plugin/src/Reconciler/matching.lua @@ -330,6 +330,48 @@ function Matching._countOwnDiffs(vInst: any, studioInstance: Instance): number end end + -- Properties present only on the Studio side (not in virtual). + -- Syncback strips default-valued properties from model files, so virtual + -- instances may omit properties that Studio has at non-default values. + -- Without this check, a virtual instance missing e.g. Face scores equally + -- against all Studio instances in the group, causing wrong pairings. + local defaults = RbxDom.findDefaultProperties(vInst.ClassName) + if defaults then + for propName, encodedDefault in defaults do + if propName == "Tags" or propName == "Attributes" then + continue + end + + -- Skip properties already compared from the virtual side + if vProps and vProps[propName] ~= nil then + continue + end + + -- Skip Ref/UniqueId types (not comparable during hydration) + local ty = next(encodedDefault) + if ty == "Ref" or ty == "UniqueId" then + continue + end + + -- pcall guards against unsupported types (e.g. SharedString) + local pcallOk, decodeOk, defaultValue = pcall(RbxDom.EncodedValue.decode, encodedDefault) + if not pcallOk or not decodeOk then + continue + end + + local readOk, studioValue = pcall(function() + return (studioInstance :: any)[propName] + end) + if not readOk then + continue + end + + if not trueEquals(defaultValue, studioValue) then + cost += 1 + end + end + end + -- Tags: count individual adds/removes local vTags = vInst.Properties and vInst.Properties.Tags if vTags then diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Icon/UIAspectRatioConstraint.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Icon/UIAspectRatioConstraint.model.json5 new file mode 100644 index 00000000..cace7a97 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Icon/UIAspectRatioConstraint.model.json5 @@ -0,0 +1,3 @@ +{ + className: "UIAspectRatioConstraint" +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Icon/init.meta.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Icon/init.meta.json5 new file mode 100644 index 00000000..f121b2cc --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Icon/init.meta.json5 @@ -0,0 +1,46 @@ +{ + className: "ImageLabel", + properties: { + AnchorPoint: [ + 0.5, + 0 + ], + BackgroundColor3: [ + 1, + 1, + 1 + ], + BackgroundTransparency: 1, + BorderColor3: [ + 0, + 0, + 0 + ], + BorderSizePixel: 0, + ImageContent: "rbxassetid://131974996946194", + Position: { + UDim2: [ + [ + 0.5, + 0 + ], + [ + 0.5, + 0 + ] + ] + }, + Size: { + UDim2: [ + [ + 0.699999988079071, + 0 + ], + [ + 0.699999988079071, + 0 + ] + ] + } + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Info/UIGradient.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Info/UIGradient.model.json5 new file mode 100644 index 00000000..197d4910 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Info/UIGradient.model.json5 @@ -0,0 +1,36 @@ +{ + className: "UIGradient", + properties: { + Color: { + ColorSequence: { + keypoints: [ + { + color: [ + 0.1882352977991104, + 0.11372549086809158, + 1 + ], + time: 0 + }, + { + color: [ + 0.5490196347236633, + 0, + 1 + ], + time: 0.608996570110321 + }, + { + color: [ + 0.501960813999176, + 0, + 0.9176470637321472 + ], + time: 1 + } + ] + } + }, + Rotation: -90 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Info/UIStroke.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Info/UIStroke.model.json5 new file mode 100644 index 00000000..2afd1ec6 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Info/UIStroke.model.json5 @@ -0,0 +1,6 @@ +{ + className: "UIStroke", + properties: { + Thickness: 2 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Info/init.meta.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Info/init.meta.json5 new file mode 100644 index 00000000..bb5f1ca4 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/Info/init.meta.json5 @@ -0,0 +1,60 @@ +{ + className: "TextLabel", + properties: { + AnchorPoint: [ + 0.5, + 0 + ], + BackgroundColor3: [ + 1, + 1, + 1 + ], + BackgroundTransparency: 1, + BorderColor3: [ + 0, + 0, + 0 + ], + BorderSizePixel: 0, + FontFace: { + cachedFaceId: null, + family: "rbxasset://fonts/families/LuckiestGuy.json", + style: "Normal", + weight: "Regular" + }, + Position: { + UDim2: [ + [ + 0.5, + 0 + ], + [ + 0, + 0 + ] + ] + }, + Size: { + UDim2: [ + [ + 1, + 0 + ], + [ + 0.5, + 0 + ] + ] + }, + Text: "UFO", + TextColor3: [ + 1, + 1, + 1 + ], + TextScaled: true, + TextSize: 14, + TextWrapped: true + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/init.meta.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/init.meta.json5 new file mode 100644 index 00000000..00b3efd0 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/Frame/init.meta.json5 @@ -0,0 +1,45 @@ +{ + className: "Frame", + properties: { + AnchorPoint: [ + 0.5, + 0.5 + ], + BackgroundColor3: [ + 1, + 1, + 1 + ], + BackgroundTransparency: 1, + BorderColor3: [ + 0, + 0, + 0 + ], + BorderSizePixel: 0, + Position: { + UDim2: [ + [ + 0.5, + 0 + ], + [ + 0.5, + 0 + ] + ] + }, + Size: { + UDim2: [ + [ + 1, + 0 + ], + [ + 1, + 0 + ] + ] + } + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/init.meta.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/init.meta.json5 new file mode 100644 index 00000000..7262f7a0 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/BillboardGui/init.meta.json5 @@ -0,0 +1,26 @@ +{ + className: "BillboardGui", + properties: { + Active: true, + LightInfluence: 1, + ResetOnSpawn: false, + Size: { + UDim2: [ + [ + 60, + 0 + ], + [ + 40, + 0 + ] + ] + }, + StudsOffset: [ + 0, + 75, + 0 + ], + ZIndexBehavior: "Sibling" + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/Water.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/Water.model.json5 new file mode 100644 index 00000000..696e6de6 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/Water.model.json5 @@ -0,0 +1,16 @@ +{ + attributes: { + Rojo_Ref_SoundGroup: "ServerStorage/__server_muted.model.json5" + }, + className: "Sound", + properties: { + AudioContent: "rbxassetid://9120549564", + Looped: true, + Playing: true, + SourceAssetId: 9120549564, + Tags: [ + "SoundGroupEffects" + ], + Volume: 3 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/init.meta.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/init.meta.json5 new file mode 100644 index 00000000..afc3c11f --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/Hitbox/init.meta.json5 @@ -0,0 +1,29 @@ +{ + className: "Part", + properties: { + Anchored: true, + BottomSurface: "Smooth", + CFrame: [ + 789.7578125, + 37.375, + 0.00000762939453125, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1 + ], + CanCollide: false, + Size: [ + 25.515625, + 70.75, + 260 + ], + TopSurface: "Smooth", + Transparency: 1 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture.model.json5 new file mode 100644 index 00000000..5e975d2a --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture.model.json5 @@ -0,0 +1,15 @@ +{ + className: "Texture", + properties: { + Color3: [ + 0.41568630933761597, + 0, + 1 + ], + Face: "Top", + StudsPerTileU: 200, + StudsPerTileV: 200, + TextureContent: "rbxassetid://10025668350", + Transparency: 0.6000000238418579 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~1.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~1.model.json5 new file mode 100644 index 00000000..11e28cd8 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~1.model.json5 @@ -0,0 +1,16 @@ +{ + className: "Texture", + name: "Texture", + properties: { + Color3: [ + 0.41568630933761597, + 0, + 1 + ], + Face: "Right", + StudsPerTileU: 200, + StudsPerTileV: 200, + TextureContent: "rbxassetid://10025668350", + Transparency: 0.6000000238418579 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~10.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~10.model.json5 new file mode 100644 index 00000000..dc4b910d --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~10.model.json5 @@ -0,0 +1,16 @@ +{ + className: "Texture", + name: "Texture", + properties: { + Color3: [ + 0.40000003576278687, + 0, + 1 + ], + Face: "Right", + StudsPerTileU: 200, + StudsPerTileV: 200, + TextureContent: "rbxassetid://10023635072", + Transparency: 0.6499999761581421 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~11.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~11.model.json5 new file mode 100644 index 00000000..df4f761f --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~11.model.json5 @@ -0,0 +1,16 @@ +{ + className: "Texture", + name: "Texture", + properties: { + Color3: [ + 0.40000003576278687, + 0, + 1 + ], + Face: "Top", + StudsPerTileU: 200, + StudsPerTileV: 200, + TextureContent: "rbxassetid://10023635072", + Transparency: 0.6499999761581421 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~2.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~2.model.json5 new file mode 100644 index 00000000..9f5fd1c3 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~2.model.json5 @@ -0,0 +1,16 @@ +{ + className: "Texture", + name: "Texture", + properties: { + Color3: [ + 0.41568630933761597, + 0, + 1 + ], + Face: "Left", + StudsPerTileU: 200, + StudsPerTileV: 200, + TextureContent: "rbxassetid://10025668350", + Transparency: 0.6000000238418579 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~3.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~3.model.json5 new file mode 100644 index 00000000..cc60c294 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~3.model.json5 @@ -0,0 +1,15 @@ +{ + className: "Texture", + name: "Texture", + properties: { + Color3: [ + 0.41568630933761597, + 0, + 1 + ], + StudsPerTileU: 200, + StudsPerTileV: 200, + TextureContent: "rbxassetid://10025668350", + Transparency: 0.6000000238418579 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~4.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~4.model.json5 new file mode 100644 index 00000000..cec50fee --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~4.model.json5 @@ -0,0 +1,16 @@ +{ + className: "Texture", + name: "Texture", + properties: { + Color3: [ + 0.41568630933761597, + 0, + 1 + ], + Face: "Bottom", + StudsPerTileU: 200, + StudsPerTileV: 200, + TextureContent: "rbxassetid://10025668350", + Transparency: 0.6000000238418579 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~5.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~5.model.json5 new file mode 100644 index 00000000..30761828 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~5.model.json5 @@ -0,0 +1,16 @@ +{ + className: "Texture", + name: "Texture", + properties: { + Color3: [ + 0.41568630933761597, + 0, + 1 + ], + Face: "Back", + StudsPerTileU: 200, + StudsPerTileV: 200, + TextureContent: "rbxassetid://10025668350", + Transparency: 0.6000000238418579 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~6.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~6.model.json5 new file mode 100644 index 00000000..015bfdce --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~6.model.json5 @@ -0,0 +1,16 @@ +{ + className: "Texture", + name: "Texture", + properties: { + Color3: [ + 0.40000003576278687, + 0, + 1 + ], + Face: "Back", + StudsPerTileU: 200, + StudsPerTileV: 200, + TextureContent: "rbxassetid://10023635072", + Transparency: 0.6499999761581421 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~7.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~7.model.json5 new file mode 100644 index 00000000..c9f537bd --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~7.model.json5 @@ -0,0 +1,16 @@ +{ + className: "Texture", + name: "Texture", + properties: { + Color3: [ + 0.40000003576278687, + 0, + 1 + ], + Face: "Bottom", + StudsPerTileU: 200, + StudsPerTileV: 200, + TextureContent: "rbxassetid://10023635072", + Transparency: 0.6499999761581421 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~8.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~8.model.json5 new file mode 100644 index 00000000..869ecc79 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~8.model.json5 @@ -0,0 +1,15 @@ +{ + className: "Texture", + name: "Texture", + properties: { + Color3: [ + 0.40000003576278687, + 0, + 1 + ], + StudsPerTileU: 200, + StudsPerTileV: 200, + TextureContent: "rbxassetid://10023635072", + Transparency: 0.6499999761581421 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~9.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~9.model.json5 new file mode 100644 index 00000000..470357ae --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/Texture~9.model.json5 @@ -0,0 +1,16 @@ +{ + className: "Texture", + name: "Texture", + properties: { + Color3: [ + 0.40000003576278687, + 0, + 1 + ], + Face: "Left", + StudsPerTileU: 200, + StudsPerTileV: 200, + TextureContent: "rbxassetid://10023635072", + Transparency: 0.6499999761581421 + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow.model.json5 new file mode 100644 index 00000000..f62c6a95 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow.model.json5 @@ -0,0 +1,185 @@ +{ + attributes: { + Size_Curve: { + BinaryString: "AAAAAAAAgD8AAAAAyEWiPWTpPj9Q1747AACAPwAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + Size_End: 60, + Size_Envelope: 5, + Size_Start: 45 + }, + className: "ParticleEmitter", + properties: { + Color: { + ColorSequence: { + keypoints: [ + { + color: [ + 0.6666666865348816, + 0, + 1 + ], + time: 0 + }, + { + color: [ + 0.6666666865348816, + 0, + 1 + ], + time: 1 + } + ] + } + }, + Lifetime: { + NumberRange: [ + 0.25, + 0.6000000238418579 + ] + }, + LightEmission: 1, + LockedToPart: true, + Size: { + NumberSequence: { + keypoints: [ + { + envelope: 5, + time: 0, + value: 45 + }, + { + envelope: 5, + time: 0.001580123440362513, + value: 46.08238983154297 + }, + { + envelope: 5, + time: 0.006548703648149967, + value: 47.16343307495117 + }, + { + envelope: 5, + time: 0.015281829982995987, + value: 48.24135971069336 + }, + { + envelope: 5, + time: 0.028204884380102158, + value: 49.313777923583984 + }, + { + envelope: 5, + time: 0.045797109603881836, + value: 50.37750244140625 + }, + { + envelope: 5, + time: 0.06858612596988678, + value: 51.42829513549805 + }, + { + envelope: 5, + time: 0.09713879972696304, + value: 52.4605827331543 + }, + { + envelope: 5, + time: 0.13203296065330505, + value: 53.46717071533203 + }, + { + envelope: 5, + time: 0.17381449043750763, + value: 54.438995361328125 + }, + { + envelope: 5, + time: 0.22292612493038177, + value: 55.36511993408203 + }, + { + envelope: 5, + time: 0.27961885929107666, + value: 56.23305892944336 + }, + { + envelope: 5, + time: 0.34385624527931213, + value: 57.029701232910156 + }, + { + envelope: 5, + time: 0.4152511656284332, + value: 57.74271774291992 + }, + { + envelope: 5, + time: 0.49307429790496826, + value: 58.362186431884766 + }, + { + envelope: 5, + time: 0.5763297080993652, + value: 58.88185501098633 + }, + { + envelope: 5, + time: 0.6638965010643005, + value: 59.29954147338867 + }, + { + envelope: 5, + time: 0.7546600699424744, + value: 59.6165657043457 + }, + { + envelope: 5, + time: 0.8476089239120483, + value: 59.83663558959961 + }, + { + envelope: 5, + time: 1, + value: 59.964595794677734 + } + ] + } + }, + Speed: { + NumberRange: [ + 0.0010000000474974513, + 0.0010000000474974513 + ] + }, + Tags: [ + "_VFX" + ], + Texture: "rbxassetid://77024587778878", + Transparency: { + NumberSequence: { + keypoints: [ + { + envelope: 0, + time: 0, + value: 1 + }, + { + envelope: 0, + time: 0.09989999979734421, + value: 0.75 + }, + { + envelope: 0, + time: 0.6000000238418579, + value: 0.75 + }, + { + envelope: 0, + time: 1, + value: 1 + } + ] + } + } + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~1.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~1.model.json5 new file mode 100644 index 00000000..943be0f1 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~1.model.json5 @@ -0,0 +1,186 @@ +{ + attributes: { + Size_Curve: { + BinaryString: "AAAAAAAAgD8AAAAAyEWiPWTpPj9Q1747AACAPwAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + Size_End: 60, + Size_Envelope: 5, + Size_Start: 45 + }, + className: "ParticleEmitter", + name: "glow", + properties: { + Color: { + ColorSequence: { + keypoints: [ + { + color: [ + 0.364705890417099, + 0.11372549086809158, + 1 + ], + time: 0 + }, + { + color: [ + 0.364705890417099, + 0.11372549086809158, + 1 + ], + time: 1 + } + ] + } + }, + Lifetime: { + NumberRange: [ + 0.25, + 0.6000000238418579 + ] + }, + LightEmission: 1, + LockedToPart: true, + Size: { + NumberSequence: { + keypoints: [ + { + envelope: 5, + time: 0, + value: 45 + }, + { + envelope: 5, + time: 0.001580123440362513, + value: 46.08238983154297 + }, + { + envelope: 5, + time: 0.006548703648149967, + value: 47.16343307495117 + }, + { + envelope: 5, + time: 0.015281829982995987, + value: 48.24135971069336 + }, + { + envelope: 5, + time: 0.028204884380102158, + value: 49.313777923583984 + }, + { + envelope: 5, + time: 0.045797109603881836, + value: 50.37750244140625 + }, + { + envelope: 5, + time: 0.06858612596988678, + value: 51.42829513549805 + }, + { + envelope: 5, + time: 0.09713879972696304, + value: 52.4605827331543 + }, + { + envelope: 5, + time: 0.13203296065330505, + value: 53.46717071533203 + }, + { + envelope: 5, + time: 0.17381449043750763, + value: 54.438995361328125 + }, + { + envelope: 5, + time: 0.22292612493038177, + value: 55.36511993408203 + }, + { + envelope: 5, + time: 0.27961885929107666, + value: 56.23305892944336 + }, + { + envelope: 5, + time: 0.34385624527931213, + value: 57.029701232910156 + }, + { + envelope: 5, + time: 0.4152511656284332, + value: 57.74271774291992 + }, + { + envelope: 5, + time: 0.49307429790496826, + value: 58.362186431884766 + }, + { + envelope: 5, + time: 0.5763297080993652, + value: 58.88185501098633 + }, + { + envelope: 5, + time: 0.6638965010643005, + value: 59.29954147338867 + }, + { + envelope: 5, + time: 0.7546600699424744, + value: 59.6165657043457 + }, + { + envelope: 5, + time: 0.8476089239120483, + value: 59.83663558959961 + }, + { + envelope: 5, + time: 1, + value: 59.964595794677734 + } + ] + } + }, + Speed: { + NumberRange: [ + 0.0010000000474974513, + 0.0010000000474974513 + ] + }, + Tags: [ + "_VFX" + ], + Texture: "rbxassetid://77024587778878", + Transparency: { + NumberSequence: { + keypoints: [ + { + envelope: 0, + time: 0, + value: 1 + }, + { + envelope: 0, + time: 0.09989999979734421, + value: 0.75 + }, + { + envelope: 0, + time: 0.6000000238418579, + value: 0.75 + }, + { + envelope: 0, + time: 1, + value: 1 + } + ] + } + } + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~2.model.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~2.model.json5 new file mode 100644 index 00000000..25ec3941 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/glow~2.model.json5 @@ -0,0 +1,186 @@ +{ + attributes: { + Size_Curve: { + BinaryString: "AAAAAAAAgD8AAAAAyEWiPWTpPj9Q1747AACAPwAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + Size_End: 60, + Size_Envelope: 5, + Size_Start: 45 + }, + className: "ParticleEmitter", + name: "glow", + properties: { + Color: { + ColorSequence: { + keypoints: [ + { + color: [ + 0.6666666865348816, + 0, + 1 + ], + time: 0 + }, + { + color: [ + 0.6666666865348816, + 0, + 1 + ], + time: 1 + } + ] + } + }, + Lifetime: { + NumberRange: [ + 0.25, + 0.6000000238418579 + ] + }, + LightEmission: 1, + LockedToPart: true, + Size: { + NumberSequence: { + keypoints: [ + { + envelope: 5, + time: 0, + value: 45 + }, + { + envelope: 5, + time: 0.001580123440362513, + value: 46.08238983154297 + }, + { + envelope: 5, + time: 0.006548703648149967, + value: 47.16343307495117 + }, + { + envelope: 5, + time: 0.015281829982995987, + value: 48.24135971069336 + }, + { + envelope: 5, + time: 0.028204884380102158, + value: 49.313777923583984 + }, + { + envelope: 5, + time: 0.045797109603881836, + value: 50.37750244140625 + }, + { + envelope: 5, + time: 0.06858612596988678, + value: 51.42829513549805 + }, + { + envelope: 5, + time: 0.09713879972696304, + value: 52.4605827331543 + }, + { + envelope: 5, + time: 0.13203296065330505, + value: 53.46717071533203 + }, + { + envelope: 5, + time: 0.17381449043750763, + value: 54.438995361328125 + }, + { + envelope: 5, + time: 0.22292612493038177, + value: 55.36511993408203 + }, + { + envelope: 5, + time: 0.27961885929107666, + value: 56.23305892944336 + }, + { + envelope: 5, + time: 0.34385624527931213, + value: 57.029701232910156 + }, + { + envelope: 5, + time: 0.4152511656284332, + value: 57.74271774291992 + }, + { + envelope: 5, + time: 0.49307429790496826, + value: 58.362186431884766 + }, + { + envelope: 5, + time: 0.5763297080993652, + value: 58.88185501098633 + }, + { + envelope: 5, + time: 0.6638965010643005, + value: 59.29954147338867 + }, + { + envelope: 5, + time: 0.7546600699424744, + value: 59.6165657043457 + }, + { + envelope: 5, + time: 0.8476089239120483, + value: 59.83663558959961 + }, + { + envelope: 5, + time: 1, + value: 59.964595794677734 + } + ] + } + }, + Speed: { + NumberRange: [ + 0.0010000000474974513, + 0.0010000000474974513 + ] + }, + Tags: [ + "_VFX" + ], + Texture: "rbxassetid://77024587778878", + Transparency: { + NumberSequence: { + keypoints: [ + { + envelope: 0, + time: 0, + value: 1 + }, + { + envelope: 0, + time: 0.09989999979734421, + value: 0.75 + }, + { + envelope: 0, + time: 0.6000000238418579, + value: 0.75 + }, + { + envelope: 0, + time: 1, + value: 1 + } + ] + } + } + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/init.meta.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/init.meta.json5 new file mode 100644 index 00000000..017fe065 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/TsunamiWave/init.meta.json5 @@ -0,0 +1,60 @@ +{ + className: "UnionOperation", + properties: { + Anchored: true, + AssetId: "http://www.roblox.com//asset/?id=212226088", + CFrame: [ + 801.715576171875, + 36.375, + 0.00000762939453125, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 1 + ], + CanCollide: false, + CanQuery: false, + CanTouch: false, + CastShadow: false, + Color: { + Color3uint8: [ + 48, + 12, + 71 + ] + }, + FormFactor: "Symmetric", + InitialSize: [ + 18.943885803222656, + 6.7999587059021, + 39.99999237060547 + ], + Size: [ + 49.43058776855469, + 72.75, + 260 + ], + UnscaledCofm: [ + -1.2760223150253296, + -0.917913019657135, + 0.000006928649327164749 + ], + UnscaledVolInertiaDiags: [ + 321672.25, + 343861.1875, + 37455.859375 + ], + UnscaledVolInertiaOffDiags: [ + 5263.43994140625, + 0.014713685028254986, + -0.0022491957060992718 + ], + UnscaledVolume: 2355.29296875, + UsePartColor: true + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/expected/init.meta.json5 b/rojo-test/syncback-tests/UFOWave_matching/expected/init.meta.json5 new file mode 100644 index 00000000..36ad4609 --- /dev/null +++ b/rojo-test/syncback-tests/UFOWave_matching/expected/init.meta.json5 @@ -0,0 +1,52 @@ +{ + attributes: { + Rojo_Ref_PrimaryPart: "ServerStorage/UFOWave/TsunamiWave", + Speed: 10, + ZonesPassed: { + Float32: 0 + } + }, + className: "Model", + properties: { + ModelMeshCFrame: [ + -740.2847900390625, + -71.00001525878906, + -369.2483825683594, + -2.6309862732887268e-8, + 0, + -1.000003695487976, + 0, + 1, + 0, + 1.000003695487976, + 0, + -2.6309862732887268e-8 + ], + WorldPivotData: { + OptionalCFrame: { + orientation: [ + [ + 1, + 0, + 0 + ], + [ + 0, + 1, + 0 + ], + [ + 0, + 0, + 1 + ] + ], + position: [ + 801.715576171875, + 36.375, + 0.00000762939453125 + ] + } + } + } +} diff --git a/rojo-test/syncback-tests/UFOWave_matching/input.rbxm b/rojo-test/syncback-tests/UFOWave_matching/input.rbxm new file mode 100644 index 0000000000000000000000000000000000000000..d51f387dcbdce11441c5bf2430c839a61cd42223 GIT binary patch literal 21586 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 -> (RojoTree, Vec) { + let root_snap = InstanceSnapshot { + snapshot_id: Ref::none(), + metadata: InstanceMetadata::default(), + name: Cow::Borrowed("DataModel"), + class_name: ustr("DataModel"), + properties: Default::default(), + children, + }; + let tree = RojoTree::new(root_snap); + let root_id = tree.get_root_id(); + let child_ids: Vec = tree.get_instance(root_id).unwrap().children().to_vec(); + (tree, child_ids) + } + + // ================================================================ + // Default-omission matching tests + // + // These reproduce the scenario where syncback strips default-valued + // properties from model files, creating an asymmetry between the + // snapshot (sparse) and tree (full properties from Studio). + // + // The matching must still pair instances correctly despite some + // snapshots missing properties that the tree has at their defaults. + // ================================================================ + + #[test] + fn reflection_database_has_expected_defaults() { + // Validates the reflection database has the defaults we rely on. + use rbx_dom_weak::types::Variant; + + let db = rbx_reflection_database::get().expect("reflection database should load"); + let texture = db.classes.get("Texture").expect("Texture class should exist"); + let face_default = texture + .default_properties + .get("Face") + .expect("Texture should have Face default"); + match face_default { + Variant::Enum(e) => assert_eq!(e.to_u32(), 5, "Face default should be Front (5)"), + other => panic!("Face default should be Enum, got {:?}", other), + } + + let part = db.classes.get("Part").expect("Part class should exist"); + let anchored_default = part + .default_properties + .get("Anchored") + .expect("Part should have Anchored default"); + assert_eq!( + anchored_default, + &Variant::Bool(false), + "Anchored default should be false" + ); + } + + #[test] + fn texture_face_six_instances_default_omitted() { + // Exact reproduction of the Texture Face bug: + // 6 Textures all named "Texture", each with a different Face. + // Snapshot side: 5 have explicit Face, 1 omits Face (Front=default). + // Tree side: all 6 have explicit Face (populated from Studio). + // + // Without the default-aware fix, the snapshot without Face scores + // equally against all tree instances, steals one by iteration order, + // and displaces the correct match. + use rbx_dom_weak::types::{Enum, Variant}; + + let faces = [ + ("Top", Enum::from_u32(1)), + ("Right", Enum::from_u32(0)), + ("Left", Enum::from_u32(3)), + ("Bottom", Enum::from_u32(4)), + ("Back", Enum::from_u32(2)), + ]; + let face_front = Enum::from_u32(5); + + let shared_props = vec![("Transparency", Variant::Float32(0.65))]; + + // Snapshots: 5 with explicit Face + 1 without (Front default) + let mut snaps: Vec = faces + .iter() + .map(|(_, face_enum)| { + let mut props = shared_props.clone(); + props.push(("Face", Variant::Enum(face_enum.clone()))); + make_snapshot_with_props("Texture", "Texture", props) + }) + .collect(); + // The Front one: NO Face property (default omitted by syncback) + snaps.push(make_snapshot_with_props( + "Texture", + "Texture", + shared_props.clone(), + )); + + // Tree: all 6 with explicit Face (Studio always has all properties) + let mut tree_children: Vec = faces + .iter() + .map(|(_, face_enum)| { + let mut props = shared_props.clone(); + props.push(("Face", Variant::Enum(face_enum.clone()))); + make_snapshot_with_props("Texture", "Texture", props) + }) + .collect(); + tree_children.push(make_snapshot_with_props( + "Texture", + "Texture", + { + let mut props = shared_props.clone(); + props.push(("Face", Variant::Enum(face_front.clone()))); + props + }, + )); + + let (tree, tree_refs) = make_tree_from_snapshots(tree_children); + let result = match_forward(snaps, &tree_refs, &tree); + + assert_eq!(result.matched.len(), 6); + assert!(result.unmatched_snapshot.is_empty()); + assert!(result.unmatched_tree.is_empty()); + + // Verify each pair has matching Face values. + for (snap, tree_ref) in &result.matched { + let inst = tree.get_instance(*tree_ref).unwrap(); + let snap_face = snap.properties.get(&ustr("Face")); + let tree_face = inst.properties().get(&ustr("Face")).unwrap(); + + match snap_face { + Some(sv) => { + assert!( + variant_eq(sv, tree_face), + "Face mismatch: snap={:?}, tree={:?}", + sv, + tree_face + ); + } + None => { + // Snapshot omitted Face = must match tree Face=Front (5) + assert!( + variant_eq(tree_face, &Variant::Enum(face_front.clone())), + "Snapshot without Face should match Front, got {:?}", + tree_face + ); + } + } + } + } + + #[test] + fn texture_face_twelve_instances_two_groups() { + // Exact reproduction of the user's real scenario: + // 12 Textures split into two groups of 6 by TextureContent. + // Each group covers all 6 faces, with Front omitted (default). + // Tree has all 12 with explicit Face. + use rbx_dom_weak::types::{Enum, Variant}; + + let face_values: Vec = vec![ + Enum::from_u32(1), // Top + Enum::from_u32(0), // Right + Enum::from_u32(3), // Left + Enum::from_u32(4), // Bottom + Enum::from_u32(2), // Back + ]; + let face_front = Enum::from_u32(5); + + let mut snaps = Vec::new(); + let mut tree_children = Vec::new(); + + for group_transparency in [0.6_f32, 0.65_f32] { + // 5 with explicit Face + for face in &face_values { + snaps.push(make_snapshot_with_props( + "Texture", + "Texture", + vec![ + ("Transparency", Variant::Float32(group_transparency)), + ("Face", Variant::Enum(face.clone())), + ], + )); + tree_children.push(make_snapshot_with_props( + "Texture", + "Texture", + vec![ + ("Transparency", Variant::Float32(group_transparency)), + ("Face", Variant::Enum(face.clone())), + ], + )); + } + // 1 without Face (Front default) + snaps.push(make_snapshot_with_props( + "Texture", + "Texture", + vec![("Transparency", Variant::Float32(group_transparency))], + )); + tree_children.push(make_snapshot_with_props( + "Texture", + "Texture", + vec![ + ("Transparency", Variant::Float32(group_transparency)), + ("Face", Variant::Enum(face_front.clone())), + ], + )); + } + + // Reverse tree order to make it harder + tree_children.reverse(); + + let (tree, tree_refs) = make_tree_from_snapshots(tree_children); + let result = match_forward(snaps, &tree_refs, &tree); + + assert_eq!(result.matched.len(), 12); + assert!(result.unmatched_snapshot.is_empty()); + assert!(result.unmatched_tree.is_empty()); + + for (snap, tree_ref) in &result.matched { + let inst = tree.get_instance(*tree_ref).unwrap(); + let snap_face = snap.properties.get(&ustr("Face")); + let tree_face = inst.properties().get(&ustr("Face")).unwrap(); + + // Transparency must match (distinguishes the two groups) + let snap_t = snap.properties.get(&ustr("Transparency")).unwrap(); + let tree_t = inst.properties().get(&ustr("Transparency")).unwrap(); + assert!( + variant_eq(snap_t, tree_t), + "Group mismatch: snap Transparency={:?}, tree={:?}", + snap_t, + tree_t + ); + + match snap_face { + Some(sv) => assert!( + variant_eq(sv, tree_face), + "Face mismatch: snap={:?}, tree={:?}", + sv, + tree_face + ), + None => assert!( + variant_eq(tree_face, &Variant::Enum(face_front.clone())), + "Omitted Face should match Front, got {:?}", + tree_face + ), + } + } + } + + #[test] + fn part_anchored_default_omitted() { + // 4 Parts named "Block": 3 have Anchored=true (explicit), + // 1 omits Anchored (false is the default). Tree has all 4. + use rbx_dom_weak::types::Variant; + + let mut snaps = Vec::new(); + let mut tree_children = Vec::new(); + + for i in 0..3 { + let props = vec![ + ("Transparency", Variant::Float32(i as f32 * 0.1)), + ("Anchored", Variant::Bool(true)), + ]; + snaps.push(make_snapshot_with_props("Block", "Part", props.clone())); + tree_children.push(make_snapshot_with_props("Block", "Part", props)); + } + // The one with default Anchored=false: snapshot omits it + snaps.push(make_snapshot_with_props( + "Block", + "Part", + vec![("Transparency", Variant::Float32(0.3))], + )); + tree_children.push(make_snapshot_with_props( + "Block", + "Part", + vec![ + ("Transparency", Variant::Float32(0.3)), + ("Anchored", Variant::Bool(false)), + ], + )); + + tree_children.reverse(); + + let (tree, tree_refs) = make_tree_from_snapshots(tree_children); + let result = match_forward(snaps, &tree_refs, &tree); + + assert_eq!(result.matched.len(), 4); + + for (snap, tree_ref) in &result.matched { + let inst = tree.get_instance(*tree_ref).unwrap(); + let snap_t = snap.properties.get(&ustr("Transparency")).unwrap(); + let tree_t = inst.properties().get(&ustr("Transparency")).unwrap(); + assert!( + variant_eq(snap_t, tree_t), + "Transparency mismatch: snap={:?}, tree={:?}", + snap_t, + tree_t + ); + } + } + + #[test] + fn multiple_defaults_omitted_all_at_once() { + // Tree instances have MANY extra properties all at their class defaults. + // Snapshots omit all of them. The match should not be thrown off. + use rbx_dom_weak::types::Variant; + + // Two sparse snapshots differing only by Transparency + let snap_a = make_snapshot_with_props( + "Wall", + "Part", + vec![("Transparency", Variant::Float32(0.0))], + ); + let snap_b = make_snapshot_with_props( + "Wall", + "Part", + vec![("Transparency", Variant::Float32(1.0))], + ); + + // Tree has Transparency + many known Part defaults from Studio. + // These are all at their default values, so count_own_diffs should + // skip them (not inflate match cost). + let extra_defaults: Vec<(&str, Variant)> = vec![ + ("Anchored", Variant::Bool(false)), + ("CastShadow", Variant::Bool(true)), + ("CanCollide", Variant::Bool(true)), + ("CanTouch", Variant::Bool(true)), + ("Locked", Variant::Bool(false)), + ("Massless", Variant::Bool(false)), + ]; + + let mut tree_props_a = vec![("Transparency", Variant::Float32(0.0))]; + tree_props_a.extend(extra_defaults.iter().cloned()); + let mut tree_props_b = vec![("Transparency", Variant::Float32(1.0))]; + tree_props_b.extend(extra_defaults.iter().cloned()); + + // Tree in reversed order + let (tree, tree_refs) = make_tree_from_snapshots(vec![ + make_snapshot_with_props("Wall", "Part", tree_props_b), + make_snapshot_with_props("Wall", "Part", tree_props_a), + ]); + let result = match_forward(vec![snap_a, snap_b], &tree_refs, &tree); + + assert_eq!(result.matched.len(), 2, "Both should match"); + + for (snap, tree_ref) in &result.matched { + let inst = tree.get_instance(*tree_ref).unwrap(); + let snap_t = snap.properties.get(&ustr("Transparency")).unwrap(); + let tree_t = inst.properties().get(&ustr("Transparency")).unwrap(); + assert!( + variant_eq(snap_t, tree_t), + "Part with 6 extra default props mismatched: snap={:?}, tree={:?}", + snap_t, + tree_t + ); + } + } + + #[test] + fn ten_ambiguous_textures_stress() { + // 10 Textures: 8 with unique Face values (some repeated across + // groups), 2 with Front omitted. Two distinguishing groups by + // Transparency. The matcher must handle this without mis-pairing. + use rbx_dom_weak::types::{Enum, Variant}; + + let faces_group_a = [ + Some(Enum::from_u32(1)), // Top + Some(Enum::from_u32(0)), // Right + Some(Enum::from_u32(3)), // Left + None, // Front (default, omitted) + Some(Enum::from_u32(4)), // Bottom + ]; + let faces_group_b = [ + Some(Enum::from_u32(2)), // Back + Some(Enum::from_u32(4)), // Bottom + None, // Front (default, omitted) + Some(Enum::from_u32(1)), // Top + Some(Enum::from_u32(3)), // Left + ]; + let face_front = Enum::from_u32(5); + + let mut snaps = Vec::new(); + let mut tree_children = Vec::new(); + + for (group_t, faces) in [(0.5_f32, &faces_group_a[..]), (0.8_f32, &faces_group_b[..])] { + for face_opt in faces { + let mut snap_props = vec![("Transparency", Variant::Float32(group_t))]; + let mut tree_props = vec![("Transparency", Variant::Float32(group_t))]; + + if let Some(face) = face_opt { + snap_props.push(("Face", Variant::Enum(face.clone()))); + tree_props.push(("Face", Variant::Enum(face.clone()))); + } else { + // Snapshot omits Face; tree has Front + tree_props.push(("Face", Variant::Enum(face_front.clone()))); + } + + snaps.push(make_snapshot_with_props("Texture", "Texture", snap_props)); + tree_children.push(make_snapshot_with_props( + "Texture", + "Texture", + tree_props, + )); + } + } + + // Shuffle tree order (reverse) + tree_children.reverse(); + + let (tree, tree_refs) = make_tree_from_snapshots(tree_children); + let result = match_forward(snaps, &tree_refs, &tree); + + assert_eq!(result.matched.len(), 10); + assert!(result.unmatched_snapshot.is_empty()); + assert!(result.unmatched_tree.is_empty()); + + for (snap, tree_ref) in &result.matched { + let inst = tree.get_instance(*tree_ref).unwrap(); + + // Transparency must match (group discriminator) + let snap_t = snap.properties.get(&ustr("Transparency")).unwrap(); + let tree_t = inst.properties().get(&ustr("Transparency")).unwrap(); + assert!( + variant_eq(snap_t, tree_t), + "Group mismatch: snap={:?}, tree={:?}", + snap_t, + tree_t + ); + + // Face must match + let snap_face = snap.properties.get(&ustr("Face")); + let tree_face = inst.properties().get(&ustr("Face")).unwrap(); + match snap_face { + Some(sv) => assert!( + variant_eq(sv, tree_face), + "Face mismatch: snap={:?}, tree={:?}", + sv, + tree_face + ), + None => assert!( + variant_eq(tree_face, &Variant::Enum(face_front.clone())), + "Omitted Face should match Front, got {:?}", + tree_face + ), + } + } + } + #[test] fn depth_limit_completes_quickly() { // Deeply nested tree (depth=6) with same-named instances at each level.