diff --git a/.cursor/commands/audit.md b/.cursor/commands/audit.md index df059057..c44b4393 100644 --- a/.cursor/commands/audit.md +++ b/.cursor/commands/audit.md @@ -1,8 +1,10 @@ # /audit - Production-Grade Sync Feature Audit +**IMMEDIATE ACTION:** Before reading any files, running any commands, or performing any analysis, you MUST call `SwitchMode` with `target_mode_id: "plan"`. Do not proceed with any other step until you are in Plan mode. This is non-negotiable. + **Mode:** This command runs in **Plan mode** (read-only). It does NOT directly apply code changes. The deliverable is a `.cursor/plans/*.plan.md` file containing every approved fix, ready to be executed in a subsequent Agent-mode session. -**Workflow:** Analyze -> Report -> Quiz user on each fix -> Write plan file. +**Workflow:** Switch to Plan mode -> Analyze -> Report -> Quiz user on each fix -> Write plan file. ## Prerequisites diff --git a/.cursor/rules/atlas-dedup.mdc b/.cursor/rules/atlas-dedup.mdc index d6c1b202..17f1d90e 100644 --- a/.cursor/rules/atlas-dedup.mdc +++ b/.cursor/rules/atlas-dedup.mdc @@ -403,6 +403,15 @@ After successful dedup rename (e.g., `Foo~1` → `Foo`): 5. Updates the index: both path keys and filesystem paths 6. Suppresses VFS events for each file write +### Ref Path Updates After User-Initiated Rename + +User-initiated renames (via the plugin's two-way sync) also update `Rojo_Ref_*` paths. The flow: +1. `old_ref_path` is captured BEFORE the rename (line ~1268, `ref_target_path_from_tree`) +2. Rename handler sets `new_ref_segment` to the new filesystem name (directory rename line ~1350, file rename line ~1512) +3. After rename handling, `update_ref_paths_after_rename(old_ref_path, new_ref_path)` is called (line ~1812) to update all meta/model files containing refs to the renamed instance + +This ensures both dedup-triggered and user-initiated renames maintain ref path integrity. + ### Metadata Consistency After dedup rename, the survivor's metadata must be updated: @@ -487,9 +496,9 @@ The greedy assignment may produce suboptimal pairing in rare ambiguous groups. A Phase 2e from the original plan: optional cache mapping `parent_ref → (children_list_hash, match_assignments)` to avoid re-running matching when children haven't changed. Optimization only, not needed for correctness. -### Stale Ref Paths After Rename +### ~~Stale Ref Paths After Rename~~ (Resolved) -When a Ref target is renamed, the stored `Rojo_Ref_*` path becomes stale. During two-way sync, `update_ref_paths_after_rename` handles this for dedup-triggered renames. However, user-initiated renames via the plugin do NOT automatically update ref paths in meta files. The path remains stale until the next full syncback. This is a known round-trip violation. +Both dedup-triggered renames and user-initiated renames now update `Rojo_Ref_*` paths automatically. See "Ref Path Updates" in the Integration Point 1 section for details. --- diff --git a/plugin/rbx_dom_lua/allValues.json b/plugin/rbx_dom_lua/allValues.json index 6c74956d..0988ee67 100644 --- a/plugin/rbx_dom_lua/allValues.json +++ b/plugin/rbx_dom_lua/allValues.json @@ -261,113 +261,7 @@ }, "MaterialColors": { "value": { - "MaterialColors": { - "Grass": [ - 106, - 127, - 63 - ], - "Slate": [ - 63, - 127, - 107 - ], - "Concrete": [ - 127, - 102, - 63 - ], - "Brick": [ - 138, - 86, - 62 - ], - "Sand": [ - 143, - 126, - 95 - ], - "WoodPlanks": [ - 139, - 109, - 79 - ], - "Rock": [ - 102, - 108, - 111 - ], - "Glacier": [ - 101, - 176, - 234 - ], - "Snow": [ - 195, - 199, - 218 - ], - "Sandstone": [ - 137, - 90, - 71 - ], - "Mud": [ - 58, - 46, - 36 - ], - "Basalt": [ - 30, - 30, - 37 - ], - "Ground": [ - 102, - 92, - 59 - ], - "CrackedLava": [ - 232, - 156, - 74 - ], - "Asphalt": [ - 115, - 123, - 107 - ], - "Cobblestone": [ - 132, - 123, - 90 - ], - "Ice": [ - 129, - 194, - 224 - ], - "LeafyGrass": [ - 115, - 132, - 74 - ], - "Salt": [ - 198, - 189, - 181 - ], - "Limestone": [ - 206, - 173, - 148 - ], - "Pavement": [ - 148, - 148, - 140 - ] - } + "MaterialColors": {} }, "ty": "MaterialColors" }, diff --git a/plugin/rbx_dom_lua/database.json b/plugin/rbx_dom_lua/database.json index 1856f932..add83531 100644 --- a/plugin/rbx_dom_lua/database.json +++ b/plugin/rbx_dom_lua/database.json @@ -1,9 +1,9 @@ { "Version": [ 0, - 700, + 708, 0, - 7000935 + 7080878 ], "Classes": { "Accessory": { @@ -1689,9 +1689,7 @@ }, "AnimationConstraint": { "Name": "AnimationConstraint", - "Tags": [ - "NotBrowsable" - ], + "Tags": [], "Superclass": "Constraint", "Properties": { "C0": { @@ -2070,6 +2068,19 @@ } } }, + "NodeId": { + "Name": "NodeId", + "Scriptability": "None", + "DataType": { + "Value": "String" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "NodeType": { "Name": "NodeType", "Scriptability": "ReadWrite", @@ -2100,6 +2111,9 @@ "InputPinData": { "BinaryString": "AQAAAAAAAAA=" }, + "NodeId": { + "String": "" + }, "NodeType": { "Enum": 0 }, @@ -3375,6 +3389,27 @@ } } }, + "AssetQualityService": { + "Name": "AssetQualityService", + "Tags": [ + "NotCreatable", + "NotReplicated", + "Service" + ], + "Superclass": "Instance", + "Properties": {}, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "AssetService": { "Name": "AssetService", "Tags": [ @@ -4432,6 +4467,7 @@ "Value": "Bool" }, "Tags": [ + "Hidden", "NotReplicated" ], "Kind": { @@ -4809,7 +4845,9 @@ "DataType": { "Value": "Ref" }, - "Tags": [], + "Tags": [ + "Hidden" + ], "Kind": { "Canonical": { "Serialization": "Serializes" @@ -5575,7 +5613,9 @@ "DataType": { "Value": "Ref" }, - "Tags": [], + "Tags": [ + "Hidden" + ], "Kind": { "Canonical": { "Serialization": "Serializes" @@ -5826,7 +5866,9 @@ "DataType": { "Value": "Bool" }, - "Tags": [], + "Tags": [ + "Hidden" + ], "Kind": { "Canonical": { "Serialization": "Serializes" @@ -9732,6 +9774,40 @@ } } }, + "BaseCoreGuiConfiguration": { + "Name": "BaseCoreGuiConfiguration", + "Tags": [ + "NotCreatable", + "NotReplicated" + ], + "Superclass": "Instance", + "Properties": { + "Enabled": { + "Name": "Enabled", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + } + }, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "BaseImportData": { "Name": "BaseImportData", "Tags": [ @@ -15427,6 +15503,40 @@ } } }, + "CapturesViewConfiguration": { + "Name": "CapturesViewConfiguration", + "Tags": [ + "NotCreatable", + "NotReplicated" + ], + "Superclass": "BaseCoreGuiConfiguration", + "Properties": { + "Open": { + "Name": "Open", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + } + }, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "CatalogPages": { "Name": "CatalogPages", "Tags": [ @@ -17739,6 +17849,55 @@ } } }, + "CompositeValueCurve": { + "Name": "CompositeValueCurve", + "Tags": [], + "Superclass": "Instance", + "Properties": { + "CurveType": { + "Name": "CurveType", + "Scriptability": "ReadWrite", + "DataType": { + "Enum": "CompositeValueCurveType" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + } + }, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "Attributes": { + "Attributes": {} + }, + "Capabilities": { + "SecurityCapabilities": 0 + }, + "CurveType": { + "Enum": 2 + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "Sandboxed": { + "Bool": false + }, + "SourceAssetId": { + "Int64": -1 + }, + "Tags": { + "Tags": [] + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "CompressorSoundEffect": { "Name": "CompressorSoundEffect", "Tags": [], @@ -18912,6 +19071,67 @@ } } }, + "CoreGuiConfiguration": { + "Name": "CoreGuiConfiguration", + "Tags": [ + "NotCreatable", + "NotReplicated", + "Service" + ], + "Superclass": "Instance", + "Properties": { + "CapturesViewConfiguration": { + "Name": "CapturesViewConfiguration", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Ref" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "PlayerListConfiguration": { + "Name": "PlayerListConfiguration", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Ref" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "SelfViewConfiguration": { + "Name": "SelfViewConfiguration", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Ref" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + } + }, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "CorePackages": { "Name": "CorePackages", "Tags": [ @@ -20332,6 +20552,22 @@ } } }, + "RunService": { + "Name": "RunService", + "Scriptability": "Read", + "DataType": { + "Value": "Ref" + }, + "Tags": [ + "NotReplicated", + "ReadOnly" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, "VIPServerId": { "Name": "VIPServerId", "Scriptability": "Read", @@ -23456,7 +23692,7 @@ "Orientation": { "Vector3": [ -0.0, - 179.99998, + 180.0, 90.0 ] }, @@ -25081,6 +25317,22 @@ } } }, + "InternalOverrideFACSData": { + "Name": "InternalOverrideFACSData", + "Scriptability": "None", + "DataType": { + "Value": "BinaryString" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "JawDrop": { "Name": "JawDrop", "Scriptability": "ReadWrite", @@ -25710,6 +25962,9 @@ "HistoryId": { "UniqueId": "00000000000000000000000000000000" }, + "InternalOverrideFACSData": { + "BinaryString": "" + }, "Sandboxed": { "Bool": false }, @@ -26043,6 +26298,27 @@ } } }, + "FileManagerService": { + "Name": "FileManagerService", + "Tags": [ + "NotCreatable", + "NotReplicated", + "Service" + ], + "Superclass": "Instance", + "Properties": {}, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "FileMesh": { "Name": "FileMesh", "Tags": [], @@ -29787,6 +30063,21 @@ } } }, + "DisplayScalingMode": { + "Name": "DisplayScalingMode", + "Scriptability": "None", + "DataType": { + "Enum": "DisplayScalingMode" + }, + "Tags": [ + "Hidden" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "GuiNavigationEnabled": { "Name": "GuiNavigationEnabled", "Scriptability": "ReadWrite", @@ -34018,6 +34309,19 @@ } } }, + "UseAvatarSettings": { + "Name": "UseAvatarSettings", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "WaistAccessory": { "Name": "WaistAccessory", "Scriptability": "ReadWrite", @@ -34139,6 +34443,9 @@ "UniqueId": { "UniqueId": "00000000000000000000000000000000" }, + "UseAvatarSettings": { + "Bool": false + }, "WalkAnimation": { "Int64": 0 }, @@ -38060,6 +38367,23 @@ "Serialization": "Serializes" } } + }, + "ViewportPositionState": { + "Name": "ViewportPositionState", + "Scriptability": "None", + "DataType": { + "Value": "Vector2" + }, + "Tags": [ + "NotReplicated", + "NotScriptable", + "ReadOnly" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } } }, "DefaultProperties": { @@ -38165,6 +38489,19 @@ } } }, + "PointerIndex": { + "Name": "PointerIndex", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Int32" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "PressedThreshold": { "Name": "PressedThreshold", "Scriptability": "ReadWrite", @@ -38178,6 +38515,19 @@ } } }, + "PrimaryModifier": { + "Name": "PrimaryModifier", + "Scriptability": "ReadWrite", + "DataType": { + "Enum": "KeyCode" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "ReleasedThreshold": { "Name": "ReleasedThreshold", "Scriptability": "ReadWrite", @@ -38230,6 +38580,19 @@ } } }, + "SecondaryModifier": { + "Name": "SecondaryModifier", + "Scriptability": "ReadWrite", + "DataType": { + "Enum": "KeyCode" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "UIButton": { "Name": "UIButton", "Scriptability": "ReadWrite", @@ -38268,6 +38631,19 @@ "Serialization": "Serializes" } } + }, + "Vector3Scale": { + "Name": "Vector3Scale", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Vector3" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } } }, "DefaultProperties": { @@ -38298,9 +38674,15 @@ "Left": { "Enum": 0 }, + "PointerIndex": { + "Int32": 0 + }, "PressedThreshold": { "Float32": 0.5 }, + "PrimaryModifier": { + "Enum": 0 + }, "ReleasedThreshold": { "Float32": 0.2 }, @@ -38316,6 +38698,9 @@ "Scale": { "Float32": 1.0 }, + "SecondaryModifier": { + "Enum": 0 + }, "SourceAssetId": { "Int64": -1 }, @@ -38333,6 +38718,13 @@ 1.0, 1.0 ] + }, + "Vector3Scale": { + "Vector3": [ + 1.0, + 1.0, + 1.0 + ] } } }, @@ -38744,6 +39136,23 @@ } } }, + "PredictionMode": { + "Name": "PredictionMode", + "Scriptability": "None", + "DataType": { + "Enum": "PredictionMode" + }, + "Tags": [ + "NotReplicated", + "NotScriptable", + "ReadOnly" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, "PropertyStatusStudio": { "Name": "PropertyStatusStudio", "Scriptability": "None", @@ -38939,6 +39348,27 @@ } } }, + "InstanceFileSyncService": { + "Name": "InstanceFileSyncService", + "Tags": [ + "NotCreatable", + "NotReplicated", + "Service" + ], + "Superclass": "Instance", + "Properties": {}, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "IntConstrainedValue": { "Name": "IntConstrainedValue", "Tags": [ @@ -39388,7 +39818,7 @@ "Bool": false }, "PhysicalConfigData": { - "SharedString": "" + "SharedString": "Q1NHUEhTAAAAAEJMT0NL" }, "PhysicsData": { "BinaryString": "" @@ -41657,6 +42087,38 @@ "Serialization": "DoesNotSerialize" } } + }, + "SlimAnimationSource": { + "Name": "SlimAnimationSource", + "Scriptability": "None", + "DataType": { + "Value": "Ref" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimReplicationTimestampSec": { + "Name": "SlimReplicationTimestampSec", + "Scriptability": "None", + "DataType": { + "Value": "Float64" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } } }, "DefaultProperties": { @@ -42034,6 +42496,102 @@ "Properties": {}, "DefaultProperties": {} }, + "MakeupDescription": { + "Name": "MakeupDescription", + "Tags": [ + "NotBrowsable" + ], + "Superclass": "Instance", + "Properties": { + "AssetId": { + "Name": "AssetId", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Int64" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "Instance": { + "Name": "Instance", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Ref" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "MakeupType": { + "Name": "MakeupType", + "Scriptability": "ReadWrite", + "DataType": { + "Enum": "MakeupType" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "Order": { + "Name": "Order", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Int32" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + } + }, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "AssetId": { + "Int64": 0 + }, + "Attributes": { + "Attributes": {} + }, + "Capabilities": { + "SecurityCapabilities": 0 + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "MakeupType": { + "Enum": 0 + }, + "Order": { + "Int32": 0 + }, + "Sandboxed": { + "Bool": false + }, + "SourceAssetId": { + "Int64": -1 + }, + "Tags": { + "Tags": [] + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "ManualGlue": { "Name": "ManualGlue", "Tags": [ @@ -43261,6 +43819,21 @@ "Tags": [], "Superclass": "Instance", "Properties": { + "AlphaMode": { + "Name": "AlphaMode", + "Scriptability": "ReadWrite", + "DataType": { + "Enum": "AlphaMode" + }, + "Tags": [ + "NotBrowsable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "AvgMetalness": { "Name": "AvgMetalness", "Scriptability": "None", @@ -43528,6 +44101,9 @@ } }, "DefaultProperties": { + "AlphaMode": { + "Enum": 3 + }, "Archivable": { "Bool": true }, @@ -44509,7 +45085,7 @@ "Content": "None" }, "PhysicalConfigData": { - "SharedString": "" + "SharedString": "Q1NHUEhTAAAAAEJMT0NL" }, "PhysicsData": { "BinaryString": "" @@ -45244,6 +45820,22 @@ } } }, + "SlimAnimationTarget": { + "Name": "SlimAnimationTarget", + "Scriptability": "None", + "DataType": { + "Value": "Ref" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, "SlimHash": { "Name": "SlimHash", "Scriptability": "None", @@ -46356,7 +46948,7 @@ "Bool": false }, "PhysicalConfigData": { - "SharedString": "" + "SharedString": "Q1NHUEhTAAAAAEJMT0NL" }, "PhysicsData": { "BinaryString": "" @@ -48752,7 +49344,7 @@ "Bool": false }, "PhysicalConfigData": { - "SharedString": "" + "SharedString": "Q1NHUEhTAAAAAEJMT0NL" }, "PhysicsData": { "BinaryString": "" @@ -51314,6 +51906,26 @@ } } }, + "PlatformLibraries": { + "Name": "PlatformLibraries", + "Tags": [ + "NotCreatable", + "Service" + ], + "Superclass": "Instance", + "Properties": {}, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "Player": { "Name": "Player", "Tags": [], @@ -51382,6 +51994,22 @@ } } }, + "CameraFrustumRequested": { + "Name": "CameraFrustumRequested", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, "CameraMaxZoomDistance": { "Name": "CameraMaxZoomDistance", "Scriptability": "ReadWrite", @@ -51933,6 +52561,22 @@ } } }, + "NeedRegionalFallback": { + "Name": "NeedRegionalFallback", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, "Neutral": { "Name": "Neutral", "Scriptability": "ReadWrite", @@ -52820,6 +53464,40 @@ } } }, + "PlayerListConfiguration": { + "Name": "PlayerListConfiguration", + "Tags": [ + "NotCreatable", + "NotReplicated" + ], + "Superclass": "BaseCoreGuiConfiguration", + "Properties": { + "Open": { + "Name": "Open", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + } + }, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "PlayerMouse": { "Name": "PlayerMouse", "Tags": [ @@ -57451,6 +58129,230 @@ } } }, + "RolloutValidation": { + "Name": "RolloutValidation", + "Tags": [ + "NotCreatable" + ], + "Superclass": "Instance", + "Properties": { + "FirstBinaryExpectedValue": { + "Name": "FirstBinaryExpectedValue", + "Scriptability": "None", + "DataType": { + "Value": "BinaryString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "FirstBinaryString": { + "Name": "FirstBinaryString", + "Scriptability": "None", + "DataType": { + "Value": "BinaryString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "FirstSharedExpectedValue": { + "Name": "FirstSharedExpectedValue", + "Scriptability": "None", + "DataType": { + "Value": "SharedString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "FirstSharedString": { + "Name": "FirstSharedString", + "Scriptability": "None", + "DataType": { + "Value": "SharedString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "SecondBinaryExpectedValue": { + "Name": "SecondBinaryExpectedValue", + "Scriptability": "None", + "DataType": { + "Value": "BinaryString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "SecondBinaryString": { + "Name": "SecondBinaryString", + "Scriptability": "None", + "DataType": { + "Value": "BinaryString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "SecondSharedExpectedValue": { + "Name": "SecondSharedExpectedValue", + "Scriptability": "None", + "DataType": { + "Value": "SharedString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "SecondSharedString": { + "Name": "SecondSharedString", + "Scriptability": "None", + "DataType": { + "Value": "SharedString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "ThirdBinaryExpectedValue": { + "Name": "ThirdBinaryExpectedValue", + "Scriptability": "None", + "DataType": { + "Value": "BinaryString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "ThirdBinaryString": { + "Name": "ThirdBinaryString", + "Scriptability": "None", + "DataType": { + "Value": "BinaryString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "ThirdSharedExpectedValue": { + "Name": "ThirdSharedExpectedValue", + "Scriptability": "None", + "DataType": { + "Value": "SharedString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "ThirdSharedString": { + "Name": "ThirdSharedString", + "Scriptability": "None", + "DataType": { + "Value": "SharedString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + } + }, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "RomarkRbxAnalyticsService": { "Name": "RomarkRbxAnalyticsService", "Tags": [ @@ -60791,6 +61693,40 @@ } } }, + "SelfViewConfiguration": { + "Name": "SelfViewConfiguration", + "Tags": [ + "NotCreatable", + "NotReplicated" + ], + "Superclass": "BaseCoreGuiConfiguration", + "Properties": { + "Open": { + "Name": "Open", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + } + }, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "SensorBase": { "Name": "SensorBase", "Tags": [ @@ -61732,6 +62668,19 @@ } } }, + "MoonTextureContent": { + "Name": "MoonTextureContent", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Content" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "MoonTextureId": { "Name": "MoonTextureId", "Scriptability": "ReadWrite", @@ -61745,6 +62694,19 @@ } } }, + "SkyboxBackContent": { + "Name": "SkyboxBackContent", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Content" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "SkyboxBk": { "Name": "SkyboxBk", "Scriptability": "ReadWrite", @@ -61771,6 +62733,32 @@ } } }, + "SkyboxDownContent": { + "Name": "SkyboxDownContent", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Content" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "SkyboxFrontContent": { + "Name": "SkyboxFrontContent", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Content" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "SkyboxFt": { "Name": "SkyboxFt", "Scriptability": "ReadWrite", @@ -61784,6 +62772,19 @@ } } }, + "SkyboxLeftContent": { + "Name": "SkyboxLeftContent", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Content" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "SkyboxLf": { "Name": "SkyboxLf", "Scriptability": "ReadWrite", @@ -61810,6 +62811,19 @@ } } }, + "SkyboxRightContent": { + "Name": "SkyboxRightContent", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Content" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "SkyboxRt": { "Name": "SkyboxRt", "Scriptability": "ReadWrite", @@ -61836,6 +62850,19 @@ } } }, + "SkyboxUpContent": { + "Name": "SkyboxUpContent", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Content" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "StarCount": { "Name": "StarCount", "Scriptability": "ReadWrite", @@ -61862,6 +62889,19 @@ } } }, + "SunTextureContent": { + "Name": "SunTextureContent", + "Scriptability": "ReadWrite", + "DataType": { + "Value": "Content" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "SunTextureId": { "Name": "SunTextureId", "Scriptability": "ReadWrite", @@ -62166,6 +63206,638 @@ } } }, + "SlimAnimationDataEntity": { + "Name": "SlimAnimationDataEntity", + "Tags": [ + "NotCreatable" + ], + "Superclass": "Instance", + "Properties": { + "EntitySource": { + "Name": "EntitySource", + "Scriptability": "None", + "DataType": { + "Value": "Ref" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "Handle": { + "Name": "Handle", + "Scriptability": "None", + "DataType": { + "Value": "Int32" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "IsSlimEnabled": { + "Name": "IsSlimEnabled", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "NumBones": { + "Name": "NumBones", + "Scriptability": "None", + "DataType": { + "Value": "Int32" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone0": { + "Name": "SlimBone0", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone1": { + "Name": "SlimBone1", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone10": { + "Name": "SlimBone10", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone11": { + "Name": "SlimBone11", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone12": { + "Name": "SlimBone12", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone13": { + "Name": "SlimBone13", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone14": { + "Name": "SlimBone14", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone15": { + "Name": "SlimBone15", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone16": { + "Name": "SlimBone16", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone17": { + "Name": "SlimBone17", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone18": { + "Name": "SlimBone18", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone19": { + "Name": "SlimBone19", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone2": { + "Name": "SlimBone2", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone20": { + "Name": "SlimBone20", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone21": { + "Name": "SlimBone21", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone22": { + "Name": "SlimBone22", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone23": { + "Name": "SlimBone23", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone24": { + "Name": "SlimBone24", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone25": { + "Name": "SlimBone25", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone26": { + "Name": "SlimBone26", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone27": { + "Name": "SlimBone27", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone28": { + "Name": "SlimBone28", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone29": { + "Name": "SlimBone29", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone3": { + "Name": "SlimBone3", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone30": { + "Name": "SlimBone30", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone31": { + "Name": "SlimBone31", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone4": { + "Name": "SlimBone4", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone5": { + "Name": "SlimBone5", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone6": { + "Name": "SlimBone6", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone7": { + "Name": "SlimBone7", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone8": { + "Name": "SlimBone8", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimBone9": { + "Name": "SlimBone9", + "Scriptability": "None", + "DataType": { + "Value": "CFrame" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "SlimReplicationTimestampSec": { + "Name": "SlimReplicationTimestampSec", + "Scriptability": "None", + "DataType": { + "Value": "Float64" + }, + "Tags": [ + "Hidden", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + } + }, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, + "SlimAnimationReplicationService": { + "Name": "SlimAnimationReplicationService", + "Tags": [ + "NotCreatable", + "Service" + ], + "Superclass": "Instance", + "Properties": {}, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "SlimContentProvider": { "Name": "SlimContentProvider", "Tags": [ @@ -62186,6 +63858,27 @@ } } }, + "SlimReplicationService": { + "Name": "SlimReplicationService", + "Tags": [ + "NotCreatable", + "NotReplicated", + "Service" + ], + "Superclass": "Instance", + "Properties": {}, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "SlimService": { "Name": "SlimService", "Tags": [ @@ -62614,6 +64307,7 @@ "Value": "Int32" }, "Tags": [ + "Hidden", "NotBrowsable", "NotReplicated", "ReadOnly" @@ -62661,7 +64355,9 @@ "DataType": { "Value": "Bool" }, - "Tags": [], + "Tags": [ + "Hidden" + ], "Kind": { "Canonical": { "Serialization": "Serializes" @@ -65052,7 +66748,6 @@ "Enum": "RolloutState" }, "Tags": [ - "NotBrowsable", "NotReplicated", "NotScriptable" ], @@ -65201,9 +66896,7 @@ "DataType": { "Value": "Bool" }, - "Tags": [ - "NotBrowsable" - ], + "Tags": [], "Kind": { "Canonical": { "Serialization": "Serializes" @@ -67034,6 +68727,19 @@ } } }, + "CommandBarEnterExec": { + "Name": "CommandBarEnterExec", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "CommandBarLocalState": { "Name": "CommandBarLocalState", "Scriptability": "ReadWrite", @@ -67175,6 +68881,19 @@ } } }, + "DraggerLengthFactor": { + "Name": "DraggerLengthFactor", + "Scriptability": "None", + "DataType": { + "Value": "Float32" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "DraggerMajorGridIncrement": { "Name": "DraggerMajorGridIncrement", "Scriptability": "None", @@ -67214,6 +68933,32 @@ } } }, + "DraggerScaleFactor": { + "Name": "DraggerScaleFactor", + "Scriptability": "None", + "DataType": { + "Value": "Float32" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "DraggerShowAxisTicks": { + "Name": "DraggerShowAxisTicks", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "DraggerShowHoverRuler": { "Name": "DraggerShowHoverRuler", "Scriptability": "None", @@ -67240,6 +68985,32 @@ } } }, + "DraggerShowNegativeAxes": { + "Name": "DraggerShowNegativeAxes", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "DraggerShowPlanes": { + "Name": "DraggerShowPlanes", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "DraggerShowTargetSnap": { "Name": "DraggerShowTargetSnap", "Scriptability": "None", @@ -67253,6 +69024,32 @@ } } }, + "DraggerShowTrackball": { + "Name": "DraggerShowTrackball", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "DraggerShowWhileDragging": { + "Name": "DraggerShowWhileDragging", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "DraggerSoftSnapMarginFactor": { "Name": "DraggerSoftSnapMarginFactor", "Scriptability": "None", @@ -68742,6 +70539,19 @@ } } }, + "UseDefaultExternalEditor": { + "Name": "UseDefaultExternalEditor", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "Warning Color": { "Name": "Warning Color", "Scriptability": "ReadWrite", @@ -69813,14 +71623,17 @@ "Properties": { "AspectRatioRange": { "Name": "AspectRatioRange", - "Scriptability": "ReadWrite", + "Scriptability": "None", "DataType": { "Value": "NumberRange" }, - "Tags": [], + "Tags": [ + "NotReplicated", + "NotScriptable" + ], "Kind": { "Canonical": { - "Serialization": "Serializes" + "Serialization": "DoesNotSerialize" } } }, @@ -69858,27 +71671,81 @@ }, "MaxSize": { "Name": "MaxSize", - "Scriptability": "ReadWrite", + "Scriptability": "None", "DataType": { "Value": "Vector2" }, - "Tags": [], + "Tags": [ + "NotReplicated", + "NotScriptable" + ], "Kind": { "Canonical": { - "Serialization": "Serializes" + "Serialization": "DoesNotSerialize" } } }, "MinSize": { "Name": "MinSize", - "Scriptability": "ReadWrite", + "Scriptability": "None", "DataType": { "Value": "Vector2" }, - "Tags": [], + "Tags": [ + "NotReplicated", + "NotScriptable" + ], "Kind": { "Canonical": { - "Serialization": "Serializes" + "Serialization": "DoesNotSerialize" + } + } + }, + "PreferredInput": { + "Name": "PreferredInput", + "Scriptability": "None", + "DataType": { + "Enum": "PreferredInput" + }, + "Tags": [ + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "ReducedMotionEnabled": { + "Name": "ReducedMotionEnabled", + "Scriptability": "None", + "DataType": { + "Value": "Bool" + }, + "Tags": [ + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" + } + } + }, + "ViewportDisplaySize": { + "Name": "ViewportDisplaySize", + "Scriptability": "None", + "DataType": { + "Enum": "DisplaySize" + }, + "Tags": [ + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "DoesNotSerialize" } } } @@ -72105,113 +73972,7 @@ "Enum": 256 }, "MaterialColors": { - "MaterialColors": { - "Grass": [ - 106, - 127, - 63 - ], - "Slate": [ - 63, - 127, - 107 - ], - "Concrete": [ - 127, - 102, - 63 - ], - "Brick": [ - 138, - 86, - 62 - ], - "Sand": [ - 143, - 126, - 95 - ], - "WoodPlanks": [ - 139, - 109, - 79 - ], - "Rock": [ - 102, - 108, - 111 - ], - "Glacier": [ - 101, - 176, - 234 - ], - "Snow": [ - 195, - 199, - 218 - ], - "Sandstone": [ - 137, - 90, - 71 - ], - "Mud": [ - 58, - 46, - 36 - ], - "Basalt": [ - 30, - 30, - 37 - ], - "Ground": [ - 102, - 92, - 59 - ], - "CrackedLava": [ - 232, - 156, - 74 - ], - "Asphalt": [ - 115, - 123, - 107 - ], - "Cobblestone": [ - 132, - 123, - 90 - ], - "Ice": [ - 129, - 194, - 224 - ], - "LeafyGrass": [ - 115, - 132, - 74 - ], - "Salt": [ - 198, - 189, - 181 - ], - "Limestone": [ - 206, - 173, - 148 - ], - "Pavement": [ - 148, - 148, - 140 - ] - } + "MaterialColors": {} }, "MaterialVariant": { "String": "" @@ -77106,6 +78867,26 @@ } } }, + "TraceRouteService": { + "Name": "TraceRouteService", + "Tags": [ + "NotCreatable", + "Service" + ], + "Superclass": "Instance", + "Properties": {}, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "TracerService": { "Name": "TracerService", "Tags": [ @@ -80669,7 +82450,7 @@ "Bool": false }, "PhysicalConfigData": { - "SharedString": "" + "SharedString": "Q1NHUEhTAAAAAEJMT0NL" }, "PhysicsData": { "BinaryString": "" @@ -84040,9 +85821,7 @@ }, "VideoDisplay": { "Name": "VideoDisplay", - "Tags": [ - "NotBrowsable" - ], + "Tags": [], "Superclass": "GuiObject", "Properties": { "ResampleMode": { @@ -84635,9 +86414,7 @@ }, "VideoPlayer": { "Name": "VideoPlayer", - "Tags": [ - "NotBrowsable" - ], + "Tags": [], "Superclass": "Instance", "Properties": { "AutoLoadInStudio": { @@ -86729,7 +88506,7 @@ }, "AuthorityMode": { "Name": "AuthorityMode", - "Scriptability": "ReadWrite", + "Scriptability": "None", "DataType": { "Enum": "AuthorityMode" }, @@ -87069,6 +88846,21 @@ } } }, + "NextGenerationReplication": { + "Name": "NextGenerationReplication", + "Scriptability": "None", + "DataType": { + "Enum": "RolloutState" + }, + "Tags": [ + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "PathfindingUseImprovedSearch": { "Name": "PathfindingUseImprovedSearch", "Scriptability": "None", @@ -87129,6 +88921,21 @@ } } }, + "PlayerScriptsUseInputActionSystem": { + "Name": "PlayerScriptsUseInputActionSystem", + "Scriptability": "None", + "DataType": { + "Enum": "RolloutState" + }, + "Tags": [ + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "PrimalPhysicsSolver": { "Name": "PrimalPhysicsSolver", "Scriptability": "None", @@ -87202,6 +89009,74 @@ } } }, + "RolloutValidationBinaryExpectedValue": { + "Name": "RolloutValidationBinaryExpectedValue", + "Scriptability": "None", + "DataType": { + "Value": "BinaryString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "RolloutValidationBinaryString": { + "Name": "RolloutValidationBinaryString", + "Scriptability": "None", + "DataType": { + "Value": "BinaryString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "RolloutValidationSharedExpectedValue": { + "Name": "RolloutValidationSharedExpectedValue", + "Scriptability": "None", + "DataType": { + "Value": "SharedString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, + "RolloutValidationSharedString": { + "Name": "RolloutValidationSharedString", + "Scriptability": "None", + "DataType": { + "Value": "SharedString" + }, + "Tags": [ + "Hidden", + "NotReplicated", + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "SandboxedInstanceMode": { "Name": "SandboxedInstanceMode", "Scriptability": "None", @@ -87403,6 +89278,21 @@ } } }, + "UseFixedSimulation": { + "Name": "UseFixedSimulation", + "Scriptability": "None", + "DataType": { + "Enum": "RolloutState" + }, + "Tags": [ + "NotScriptable" + ], + "Kind": { + "Canonical": { + "Serialization": "Serializes" + } + } + }, "UseNewLuauTypeSolver": { "Name": "UseNewLuauTypeSolver", "Scriptability": "None", @@ -87448,7 +89338,7 @@ "Enum": 0 }, "CollisionGroupData": { - "BinaryString": "AQEABP////8HRGVmYXVsdA==" + "BinaryString": "AQQABP////8HRGVmYXVsdAEE+////xBGb3JnZU1vdXNlSWdub3JlAgT9////EFN0dWRpb1NlbGVjdGFibGUDBPf///8LRm9yZ2VEZWJyaXM=" }, "DistributedGameTime": { "Float64": 0.0 @@ -87538,6 +89428,9 @@ "NeedsPivotMigration": { "Bool": false }, + "NextGenerationReplication": { + "Enum": 0 + }, "PathfindingUseImprovedSearch": { "Enum": 0 }, @@ -87550,6 +89443,9 @@ "PlayerCharacterDestroyBehavior": { "Enum": 0 }, + "PlayerScriptsUseInputActionSystem": { + "Enum": 0 + }, "PrimalPhysicsSolver": { "Enum": 0 }, @@ -87565,6 +89461,18 @@ "Retargeting": { "Enum": 0 }, + "RolloutValidationBinaryExpectedValue": { + "BinaryString": "" + }, + "RolloutValidationBinaryString": { + "BinaryString": "" + }, + "RolloutValidationSharedExpectedValue": { + "SharedString": "" + }, + "RolloutValidationSharedString": { + "SharedString": "" + }, "Sandboxed": { "Bool": false }, @@ -87613,6 +89521,9 @@ "UniqueId": { "UniqueId": "00000000000000000000000000000000" }, + "UseFixedSimulation": { + "Enum": 0 + }, "UseNewLuauTypeSolver": { "Enum": 2 }, @@ -87794,6 +89705,27 @@ } } }, + "WrapDeformMeshProvider": { + "Name": "WrapDeformMeshProvider", + "Tags": [ + "NotCreatable", + "NotReplicated", + "Service" + ], + "Superclass": "Instance", + "Properties": {}, + "DefaultProperties": { + "Archivable": { + "Bool": true + }, + "HistoryId": { + "UniqueId": "00000000000000000000000000000000" + }, + "UniqueId": { + "UniqueId": "00000000000000000000000000000000" + } + } + }, "WrapDeformer": { "Name": "WrapDeformer", "Tags": [], @@ -88432,9 +90364,7 @@ }, "WrapTextureTransfer": { "Name": "WrapTextureTransfer", - "Tags": [ - "NotBrowsable" - ], + "Tags": [], "Superclass": "Instance", "Properties": { "ReferenceCageMeshContent": { @@ -88699,6 +90629,7 @@ "AlphaMode": { "name": "AlphaMode", "items": { + "Opaque": 3, "Overlay": 0, "TintMask": 2, "Transparency": 1 @@ -88785,6 +90716,21 @@ "Timeout": 10 } }, + "AnimationNodeBlend2DInputMode": { + "name": "AnimationNodeBlend2DInputMode", + "items": { + "Cartesian": 0, + "Polar": 1 + } + }, + "AnimationNodeInterruptible": { + "name": "AnimationNodeInterruptible", + "items": { + "Always": 0, + "ClipFinished": 1, + "Expression": 2 + } + }, "AnimationNodePlayMode": { "name": "AnimationNodePlayMode", "items": { @@ -88821,6 +90767,13 @@ "SubtractNode": 13 } }, + "AnimationNodeWaitFor": { + "name": "AnimationNodeWaitFor", + "items": { + "ClipFinished": 0, + "Expression": 1 + } + }, "AnimationPriority": { "name": "AnimationPriority", "items": { @@ -89568,13 +91521,6 @@ "Relevance": 1 } }, - "CatalogTimedOptionFilter": { - "name": "CatalogTimedOptionFilter", - "items": { - "All": 1, - "TimedOptionOnly": 2 - } - }, "CellBlock": { "name": "CellBlock", "items": { @@ -89807,6 +91753,19 @@ "TriggerForIncompleteCompletions": 3 } }, + "CompositeValueCurveType": { + "name": "CompositeValueCurveType", + "items": { + "ColorHSV": 1, + "ColorRGB": 0, + "NumberRange": 2, + "Rect": 3, + "UDim": 4, + "UDim2": 5, + "Vector2": 6, + "Vector3": 7 + } + }, "CompressionAlgorithm": { "name": "CompressionAlgorithm", "items": { @@ -89853,8 +91812,13 @@ "DisconnectClientFailure": 284, "DisconnectClientRequest": 285, "DisconnectCloudEditKick": 270, + "DisconnectCollaboratorNotAgeVerified": 310, + "DisconnectCollaboratorOwnerActionRequired": 312, "DisconnectCollaboratorPermissionRevoked": 294, + "DisconnectCollaboratorTooManyCollaborators": 313, + "DisconnectCollaboratorTrustedConnectionsRequired": 311, "DisconnectCollaboratorUnderage": 295, + "DisconnectCollaboratorUnknownError": 314, "DisconnectConnectionLost": 277, "DisconnectDevMaintenance": 274, "DisconnectDuplicatePlayer": 264, @@ -89973,6 +91937,7 @@ "name": "CoreGuiType", "items": { "All": 4, + "AvatarSwitcher": 8, "Backpack": 2, "Captures": 7, "Chat": 3, @@ -90030,6 +91995,14 @@ "Follow": 2 } }, + "DataModelExtractorFileType": { + "name": "DataModelExtractorFileType", + "items": { + "FirstSlice": 1, + "NonFirstSlice": 2, + "PlaceFile": 0 + } + }, "DataStoreRequestType": { "name": "DataStoreRequestType", "items": { @@ -90245,6 +92218,14 @@ "Neutral": 0 } }, + "DisplayScalingMode": { + "name": "DisplayScalingMode", + "items": { + "Default": 0, + "Legacy": 1, + "Responsive": 2 + } + }, "DisplaySize": { "name": "DisplaySize", "items": { @@ -90361,6 +92342,14 @@ "WhenScrollable": 0 } }, + "EngineFolder": { + "name": "EngineFolder", + "items": { + "Logs": 2, + "Screenshots": 0, + "Videos": 1 + } + }, "EnviromentalPhysicsThrottle": { "name": "EnviromentalPhysicsThrottle", "items": { @@ -90562,6 +92551,13 @@ "Rejected": 1 } }, + "FilterType": { + "name": "FilterType", + "items": { + "Exclude": 0, + "Include": 1 + } + }, "FinishRecordingOperation": { "name": "FinishRecordingOperation", "items": { @@ -90947,6 +92943,7 @@ "ConnectionClosed": 13, "CreatorEnvironmentsNotSupportedByService": 15, "DnsResolve": 2, + "InactivityTimeout": 16, "InvalidRedirect": 7, "InvalidUrl": 1, "NetFail": 8, @@ -90956,6 +92953,7 @@ "SslConnectFail": 10, "SslVerificationFail": 11, "TimedOut": 5, + "TooManyOutstandingRequests": 17, "TooManyRedirects": 6, "Unknown": 12 } @@ -91125,6 +93123,16 @@ "Sin": 13 } }, + "InstanceFileSyncStatus": { + "name": "InstanceFileSyncStatus", + "items": { + "AncestorErrored": 4, + "Errored": 1, + "NotSynced": 0, + "SyncedAsDescendant": 3, + "SyncedAsRoot": 2 + } + }, "IntermediateMeshGenerationResult": { "name": "IntermediateMeshGenerationResult", "items": { @@ -91278,11 +93286,13 @@ "Minus": 45, "Mode": 313, "MouseBackButton": 1029, + "MouseDelta": 1048, "MouseLeftButton": 1026, "MouseMiddleButton": 1028, "MouseNoButton": 1030, "MousePosition": 1033, "MouseRightButton": 1027, + "MouseWheel": 1035, "MouseX": 1031, "MouseY": 1032, "N": 110, @@ -91337,6 +93347,9 @@ "Thumbstick2Right": 1025, "Thumbstick2Up": 1022, "Tilde": 126, + "Touch": 1034, + "TrackpadPan": 1040, + "TrackpadPinch": 1045, "Two": 50, "U": 117, "Underscore": 95, @@ -91602,6 +93615,14 @@ "Strict": 3 } }, + "MakeupType": { + "name": "MakeupType", + "items": { + "Eye": 2, + "Face": 0, + "Lip": 1 + } + }, "MarketplaceBulkPurchasePromptStatus": { "name": "MarketplaceBulkPurchasePromptStatus", "items": { @@ -92160,7 +94181,7 @@ "Linux": 13, "MetaOS": 20, "NX": 9, - "None": 21, + "None": 22, "OSX": 1, "Ouya": 10, "PS3": 6, @@ -92168,6 +94189,7 @@ "PS5": 19, "SteamOS": 14, "UWP": 18, + "Web": 21, "WebOS": 15, "WiiU": 8, "Windows": 0, @@ -92589,6 +94611,7 @@ "items": { "Active": 131072, "Hover": 262144, + "NegatedPart": 1048576, "Selection": 524288, "Standard": 0 } @@ -92601,6 +94624,13 @@ "RotationsReset": 1 } }, + "RestPoseModel": { + "name": "RestPoseModel", + "items": { + "FromRigInACE": 0, + "FromRigInFile": 1 + } + }, "ReturnKeyType": { "name": "ReturnKeyType", "items": { @@ -92665,6 +94695,34 @@ "Ungroup": 8 } }, + "RigLabel": { + "name": "RigLabel", + "items": { + "Chest": 4, + "HeadBase": 6, + "Invalid": 0, + "LeftAnkle": 17, + "LeftClavicle": 7, + "LeftElbow": 9, + "LeftHip": 15, + "LeftKnee": 16, + "LeftShoulder": 8, + "LeftToes": 18, + "LeftWrist": 10, + "Neck": 5, + "Pelvis": 2, + "RightAnkle": 21, + "RightClavicle": 11, + "RightElbow": 13, + "RightHip": 19, + "RightKnee": 20, + "RightShoulder": 12, + "RightToes": 22, + "RightWrist": 14, + "Root": 1, + "Waist": 3 + } + }, "RigScale": { "name": "RigScale", "items": { @@ -92858,6 +94916,15 @@ "Sensor": 4 } }, + "ScreenshotCaptureResult": { + "name": "ScreenshotCaptureResult", + "items": { + "NoDeviceSupport": 2, + "NoSpaceOnDevice": 3, + "OtherError": 1, + "Success": 0 + } + }, "ScrollBarInset": { "name": "ScrollBarInset", "items": { @@ -92879,27 +94946,41 @@ "items": { "AccessOutsideWrite": 2, "Animation": 15, + "AssetCreateUpdate": 37, + "AssetManagement": 34, + "AssetRead": 33, "AssetRequire": 3, "Assistant": 31, "Audio": 8, "Avatar": 16, + "AvatarBehavior": 50, "Basic": 7, "CSG": 13, "CapabilityControl": 22, + "Capture": 38, "Chat": 14, + "Consequences": 48, "CreateInstances": 6, "DataStore": 9, + "DynamicGeneration": 35, "Environment": 18, + "Groups": 46, "Input": 17, "InternalTest": 29, "LegacySound": 20, + "LoadOwnedAsset": 41, "LoadString": 4, "LocalUser": 24, + "Logging": 44, + "Material": 49, + "Monetization": 40, "Network": 10, "Physics": 11, + "PlatformAvatarEditing": 36, "Players": 21, "Plugin": 23, "PluginOrOpenCloud": 30, + "PromptExternalPurchase": 45, "RemoteCommand": 32, "RemoteEvent": 19, "RobloxEngine": 27, @@ -92907,6 +94988,10 @@ "RunClientScript": 0, "RunServerScript": 1, "ScriptGlobals": 5, + "SensitiveInput": 39, + "ServerCommunication": 43, + "Social": 42, + "Teleport": 47, "UI": 12, "Unassigned": 28, "WritePlayer": 25 @@ -93609,6 +95694,8 @@ "items": { "Default": 0, "Email": 3, + "NewPassword": 9, + "NewPasswordShown": 10, "NoSuggestions": 1, "Number": 2, "OneTimePassword": 8, diff --git a/rbx-dom b/rbx-dom index 3cd393d7..da64cdab 160000 --- a/rbx-dom +++ b/rbx-dom @@ -1 +1 @@ -Subproject commit 3cd393d73a6ce81282b1999797026beeaa91d6f2 +Subproject commit da64cdabab805c063b3d31b1b8a5dbe90373790c diff --git a/scripts/update-reflection.ps1 b/scripts/update-reflection.ps1 new file mode 100644 index 00000000..cb3b5261 --- /dev/null +++ b/scripts/update-reflection.ps1 @@ -0,0 +1,44 @@ +# Update Reflection Database +# Regenerates database.msgpack (Rust) and database.json (Lua) from Roblox Studio. +# Requires: Roblox Studio installed locally. +# Usage: .\scripts\update-reflection.ps1 [--dry-run] + +param( + [switch]$DryRun +) + +$ErrorActionPreference = "Stop" + +Push-Location rbx-dom + +try { + if ($DryRun) { + Write-Host "Dry run: generating reflection database (no output files)" -ForegroundColor Yellow + cargo run --bin rbx_reflector -- generate --patches patches + } else { + Write-Host "Generating reflection database..." -ForegroundColor Cyan + cargo run --bin rbx_reflector -- generate --patches patches rbx_reflection_database/database.msgpack rbx_dom_lua/src/database.json + + Write-Host "Generating allValues.json..." -ForegroundColor Cyan + cargo run --bin rbx_reflector -- values rbx_dom_lua/src/allValues.json + + if ($LASTEXITCODE -ne 0) { + Write-Host "Failed with exit code $LASTEXITCODE" -ForegroundColor Red + exit $LASTEXITCODE + } + + Write-Host "Copying to plugin/rbx_dom_lua/..." -ForegroundColor Cyan + Copy-Item rbx_dom_lua/src/database.json ../plugin/rbx_dom_lua/database.json + Copy-Item rbx_dom_lua/src/allValues.json ../plugin/rbx_dom_lua/allValues.json + Copy-Item rbx_dom_lua/src/Error.lua ../plugin/rbx_dom_lua/Error.lua + Copy-Item rbx_dom_lua/src/base64.lua ../plugin/rbx_dom_lua/base64.lua + Copy-Item rbx_dom_lua/src/PropertyDescriptor.lua ../plugin/rbx_dom_lua/PropertyDescriptor.lua + Copy-Item rbx_dom_lua/src/customProperties.lua ../plugin/rbx_dom_lua/customProperties.lua + # NOTE: init.lua and EncodedValue.lua are intentionally different in the plugin + # (findClassDescriptor, Int64/msgpack handling, float serialization) — do NOT copy them. + } + + Write-Host "Done." -ForegroundColor Green +} finally { + Pop-Location +} diff --git a/scripts/update-reflection.sh b/scripts/update-reflection.sh new file mode 100644 index 00000000..121a1ba3 --- /dev/null +++ b/scripts/update-reflection.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Update Reflection Database +# Regenerates database.msgpack (Rust) and database.json (Lua) from Roblox Studio. +# Requires: Roblox Studio installed locally. +# Usage: bash scripts/update-reflection.sh [--dry-run] + +set -e + +cd "$(dirname "$0")/../rbx-dom" + +if [ "$1" = "--dry-run" ]; then + echo "Dry run: generating reflection database (no output files)" + cargo run --bin rbx_reflector -- generate --patches patches +else + echo "Generating reflection database..." + cargo run --bin rbx_reflector -- generate --patches patches rbx_reflection_database/database.msgpack rbx_dom_lua/src/database.json + + echo "Generating allValues.json..." + cargo run --bin rbx_reflector -- values rbx_dom_lua/src/allValues.json + + echo "Copying to plugin/rbx_dom_lua/..." + cp rbx_dom_lua/src/database.json ../plugin/rbx_dom_lua/database.json + cp rbx_dom_lua/src/allValues.json ../plugin/rbx_dom_lua/allValues.json + cp rbx_dom_lua/src/Error.lua ../plugin/rbx_dom_lua/Error.lua + cp rbx_dom_lua/src/base64.lua ../plugin/rbx_dom_lua/base64.lua + cp rbx_dom_lua/src/PropertyDescriptor.lua ../plugin/rbx_dom_lua/PropertyDescriptor.lua + cp rbx_dom_lua/src/customProperties.lua ../plugin/rbx_dom_lua/customProperties.lua + # NOTE: init.lua and EncodedValue.lua are intentionally different in the plugin + # (findClassDescriptor, Int64/msgpack handling, float serialization) — do NOT copy them. +fi + +echo "Done." diff --git a/src/change_processor.rs b/src/change_processor.rs index b3c201e4..65748d5f 100644 --- a/src/change_processor.rs +++ b/src/change_processor.rs @@ -1084,7 +1084,7 @@ impl JobThreadContext { let mut dedup_metadata_updates: Vec<(Ref, PathBuf)> = Vec::new(); for &removed_id in &patch_set.removed_instances { - let (parent_ref, removed_fs_name) = { + let (parent_ref, removed_fs_name, removed_file_dir) = { let Some(inst) = tree.get_instance(removed_id) else { continue; }; @@ -1096,7 +1096,23 @@ impl JobThreadContext { } let parent = inst.parent(); let fs_name = tree.filesystem_name_for(removed_id); - (parent, fs_name) + // Derive the directory containing this file from its own + // instigating source path. This works even when the parent + // is a ProjectNode (where instigating_source is not a Path). + let file_dir = match source { + InstigatingSource::Path(p) => { + let fname = p.file_name().and_then(|f| f.to_str()).unwrap_or(""); + if fname.starts_with("init.") { + // Directory-format: path is dir/init.luau, + // the containing dir is the grandparent + p.parent().and_then(|d| d.parent()).map(|d| d.to_path_buf()) + } else { + p.parent().map(|d| d.to_path_buf()) + } + } + _ => None, + }; + (parent, fs_name, file_dir) }; if parent_ref.is_none() { @@ -1157,15 +1173,11 @@ impl JobThreadContext { let extension = removed_extension; - // Get the parent directory path - let parent_dir = { - let Some(parent) = tree.get_instance(parent_ref) else { - continue; - }; - match &parent.metadata().instigating_source { - Some(InstigatingSource::Path(p)) => p.clone(), - _ => continue, - } + // Get the parent directory path (derived from the removed + // instance's own path, not the parent's instigating source, + // because the parent may be a ProjectNode). + let Some(parent_dir) = removed_file_dir.clone() else { + continue; }; let action = compute_cleanup_action( @@ -1196,6 +1208,33 @@ impl JobThreadContext { ); self.unsuppress_path(&to); } else { + // Also rename the adjacent meta file if it exists + // (standalone files only; directory-format instances + // have init.meta.json5 inside the directory which + // moves automatically with the dir rename). + if from.is_file() || !from.exists() { + if let (Some(from_parent), Some(from_name), Some(to_name)) = ( + from.parent(), + from.file_stem().and_then(|s| s.to_str()), + to.file_stem().and_then(|s| s.to_str()), + ) { + let from_base = strip_script_suffix(from_name); + let to_base = strip_script_suffix(to_name); + let old_meta = + from_parent.join(format!("{}.meta.json5", from_base)); + if old_meta.exists() { + let new_meta = + from_parent.join(format!("{}.meta.json5", to_base)); + self.suppress_path_any(&old_meta); + self.suppress_path(&new_meta); + if fs::rename(&old_meta, &new_meta).is_err() { + self.unsuppress_path_any(&old_meta); + self.unsuppress_path(&new_meta); + } + } + } + } + // Update ref paths if the renamed instance has refs let old_ref_segment = from.file_name().and_then(|f| f.to_str()).unwrap_or(""); diff --git a/src/snapshot/matching.rs b/src/snapshot/matching.rs index 0a701c40..4fba9b39 100644 --- a/src/snapshot/matching.rs +++ b/src/snapshot/matching.rs @@ -385,12 +385,22 @@ fn compute_change_count( /// Count own property diffs between a snapshot and a tree instance. /// Each differing property = +1. Tags and Attributes counted granularly. /// Children count diff = +1. +/// +/// Syncback strips default-valued properties from model/meta files, so +/// filesystem snapshots may omit properties that the tree (populated from +/// Studio via two-way sync) has at their class defaults. To avoid inflating +/// match costs with phantom diffs, properties present on only one side are +/// skipped when their value matches the class default. fn count_own_diffs(snap: &InstanceSnapshot, inst: &InstanceWithMeta) -> u32 { let mut cost: u32 = 0; let snap_props = &snap.properties; let inst_props = inst.properties(); + let class_data = rbx_reflection_database::get() + .ok() + .and_then(|db| db.classes.get(snap.class_name.as_str())); + // Properties present on the snapshot side for (key, snap_val) in snap_props.iter() { if let Some(inst_val) = inst_props.get(key) { @@ -398,14 +408,24 @@ fn count_own_diffs(snap: &InstanceSnapshot, inst: &InstanceWithMeta) -> u32 { cost += 1; } } else { - cost += 1; // Missing on tree side + let is_default = class_data + .and_then(|cd| cd.default_properties.get(key.as_str())) + .is_some_and(|default| variant_eq(snap_val, default)); + if !is_default { + cost += 1; + } } } // Properties present only on the tree side - for key in inst_props.keys() { + for (key, inst_val) in inst_props.iter() { if !snap_props.contains_key(key) { - cost += 1; + let is_default = class_data + .and_then(|cd| cd.default_properties.get(key.as_str())) + .is_some_and(|default| variant_eq(inst_val, default)); + if !is_default { + cost += 1; + } } } diff --git a/src/snapshot/patch_compute.rs b/src/snapshot/patch_compute.rs index f69de980..c4602bf1 100644 --- a/src/snapshot/patch_compute.rs +++ b/src/snapshot/patch_compute.rs @@ -226,7 +226,7 @@ fn compute_children_patches( let instance_children = instance.children().to_vec(); let snapshot_children = take(&mut snapshot.children); - // Use the 3-pass matching algorithm instead of greedy name+class. + // Use the recursive change-count scoring + greedy assignment matching algorithm. let match_result = match_forward(snapshot_children, &instance_children, tree); // Matched pairs: recursively compute patches. diff --git a/src/syncback/matching.rs b/src/syncback/matching.rs index 4aba519d..114d3f96 100644 --- a/src/syncback/matching.rs +++ b/src/syncback/matching.rs @@ -408,6 +408,9 @@ fn compute_change_count( /// Count own property diffs between two WeakDom instances (flat, non-recursive). /// Each differing property = +1. Children count diff = +1. /// Does NOT include hash fast-path (handled by `compute_change_count`). +/// +/// Properties present on only one side are skipped when their value matches +/// the class default (see `src/snapshot/matching.rs` for full rationale). fn count_own_diffs(new_ref: Ref, old_ref: Ref, new_dom: &WeakDom, old_dom: &WeakDom) -> u32 { let new_inst = match new_dom.get_by_ref(new_ref) { Some(i) => i, @@ -420,19 +423,33 @@ fn count_own_diffs(new_ref: Ref, old_ref: Ref, new_dom: &WeakDom, old_dom: &Weak let mut cost: u32 = 0; + let class_data = rbx_reflection_database::get() + .ok() + .and_then(|db| db.classes.get(new_inst.class.as_str())); + for (key, new_val) in new_inst.properties.iter() { if let Some(old_val) = old_inst.properties.get(key) { if !variant_eq(new_val, old_val) { cost += 1; } } else { - cost += 1; + let is_default = class_data + .and_then(|cd| cd.default_properties.get(key.as_str())) + .is_some_and(|default| variant_eq(new_val, default)); + if !is_default { + cost += 1; + } } } - for key in old_inst.properties.keys() { + for (key, old_val) in old_inst.properties.iter() { if !new_inst.properties.contains_key(key) { - cost += 1; + let is_default = class_data + .and_then(|cd| cd.default_properties.get(key.as_str())) + .is_some_and(|default| variant_eq(old_val, default)); + if !is_default { + cost += 1; + } } } diff --git a/tests/tests/two_way_sync.rs b/tests/tests/two_way_sync.rs index e2a46c25..3cb44622 100644 --- a/tests/tests/two_way_sync.rs +++ b/tests/tests/two_way_sync.rs @@ -7377,3 +7377,796 @@ fn ref_same_batch_add_folder_target_no_extension() { poll_meta_has_ref_attr(&meta_path, "Rojo_Ref_PrimaryPart", "Workspace/FolderTarget"); }); } + +// --------------------------------------------------------------------------- +// Tests: Dedup suffix cleanup +// --------------------------------------------------------------------------- + +/// Add two colliding ModuleScripts (creates X_Y.luau + X_Y~1.luau), then +/// delete one. The remaining instance should lose its dedup suffix (group-to-1 +/// cleanup rule). +#[test] +fn delete_deduped_instance_group_to_1_cleanup() { + run_serve_test("syncback_encoded_names", |session, _redactions| { + let info = session.get_api_rojo().unwrap(); + let root_read = session.get_api_read(info.root_instance_id).unwrap(); + let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage"); + + let mut props1 = HashMap::new(); + props1.insert( + "Source".to_string(), + Variant::String("-- survivor".to_string()), + ); + let mut props2 = HashMap::new(); + props2.insert( + "Source".to_string(), + Variant::String("-- doomed".to_string()), + ); + + let mut added_map = HashMap::new(); + added_map.insert( + Ref::new(), + AddedInstance { + parent: Some(rs_id), + name: "X/Y".to_string(), + class_name: "ModuleScript".to_string(), + properties: props1, + children: vec![], + }, + ); + added_map.insert( + Ref::new(), + AddedInstance { + parent: Some(rs_id), + name: "X:Y".to_string(), + class_name: "ModuleScript".to_string(), + properties: props2, + children: vec![], + }, + ); + + let write_request = WriteRequest { + session_id: info.session_id, + removed: vec![], + added: added_map, + updated: vec![], + }; + session.post_api_write(&write_request).unwrap(); + + let src = session.path().join("src"); + let base = src.join("X_Y.luau"); + let deduped = src.join("X_Y~1.luau"); + + poll_file_exists(&base, "X_Y.luau after add"); + poll_file_exists(&deduped, "X_Y~1.luau after add"); + + // Wait for VFS to settle + thread::sleep(Duration::from_millis(500)); + + // Re-read the tree to find instance IDs + let rs_read = session.get_api_read(rs_id).unwrap(); + + // Read the base file to figure out which instance maps to it + let base_source = fs::read_to_string(&base).unwrap(); + let doomed_name = if base_source.contains("-- doomed") { + // Base file has the doomed source -- find by that name + // The meta file tells us the real name + let base_meta = src.join("X_Y.meta.json5"); + let meta_content = fs::read_to_string(&base_meta).unwrap(); + if meta_content.contains("\"X/Y\"") { + "X/Y" + } else { + "X:Y" + } + } else { + // Base file has the survivor source -- the other one is doomed + let dedup_meta = src.join("X_Y~1.meta.json5"); + let meta_content = fs::read_to_string(&dedup_meta).unwrap(); + if meta_content.contains("\"X/Y\"") { + "X/Y" + } else { + "X:Y" + } + }; + + let (doomed_id, _) = find_by_name(&rs_read.instances, doomed_name); + send_removal(&session, &info.session_id, vec![doomed_id]); + + // After group-to-1 cleanup: only X_Y.luau should remain, no ~1 suffix + poll_not_exists(&deduped, "X_Y~1.luau after group-to-1 cleanup"); + assert_file_exists(&base, "X_Y.luau should survive"); + + // The survivor's meta should still have a name field (since it's slugified) + let meta = src.join("X_Y.meta.json5"); + assert_file_exists(&meta, "Meta for survivor"); + }); +} + +/// Add three colliding ModuleScripts, then delete the base instance. +/// The ~1 instance should be promoted to base (base-name promotion rule). +#[test] +fn delete_base_deduped_instance_promotes_lowest() { + run_serve_test("syncback_encoded_names", |session, _redactions| { + let info = session.get_api_rojo().unwrap(); + let root_read = session.get_api_read(info.root_instance_id).unwrap(); + let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage"); + + let names = ["X/Y", "X:Y", "X|Y"]; + let mut added_map = HashMap::new(); + for (i, name) in names.iter().enumerate() { + let mut props = HashMap::new(); + props.insert( + "Source".to_string(), + Variant::String(format!("-- inst {}", i + 1)), + ); + added_map.insert( + Ref::new(), + AddedInstance { + parent: Some(rs_id), + name: name.to_string(), + class_name: "ModuleScript".to_string(), + properties: props, + children: vec![], + }, + ); + } + let write_request = WriteRequest { + session_id: info.session_id, + removed: vec![], + added: added_map, + updated: vec![], + }; + session.post_api_write(&write_request).unwrap(); + + let src = session.path().join("src"); + let base = src.join("X_Y.luau"); + let dedup1 = src.join("X_Y~1.luau"); + let dedup2 = src.join("X_Y~2.luau"); + + poll_file_exists(&base, "X_Y.luau after add"); + poll_file_exists(&dedup1, "X_Y~1.luau after add"); + poll_file_exists(&dedup2, "X_Y~2.luau after add"); + + // Wait for VFS to settle + thread::sleep(Duration::from_millis(500)); + + // Re-read tree and identify which instance maps to the base file + let rs_read = session.get_api_read(rs_id).unwrap(); + let base_meta = src.join("X_Y.meta.json5"); + let base_meta_content = fs::read_to_string(&base_meta).unwrap(); + + // Extract the real name from the base meta file + let base_inst_name = if base_meta_content.contains("\"X/Y\"") { + "X/Y" + } else if base_meta_content.contains("\"X:Y\"") { + "X:Y" + } else { + "X|Y" + }; + + let (base_id, _) = find_by_name(&rs_read.instances, base_inst_name); + + // Delete the base instance + send_removal(&session, &info.session_id, vec![base_id]); + + // After base-name promotion: ~1 should be promoted to base + // X_Y.luau should exist (promoted from ~1) + // X_Y~2.luau should still exist (unchanged) + // X_Y~1.luau should NOT exist (promoted away) + poll_not_exists(&dedup1, "X_Y~1.luau after promotion"); + thread::sleep(Duration::from_millis(300)); + assert_file_exists(&base, "X_Y.luau should exist (promoted)"); + assert_file_exists(&dedup2, "X_Y~2.luau should remain"); + }); +} + +/// Add three colliding ModuleScripts, then delete the ~1 instance. +/// Gap should be tolerated: X_Y.luau and X_Y~2.luau remain unchanged. +#[test] +fn delete_middle_deduped_instance_tolerates_gap() { + run_serve_test("syncback_encoded_names", |session, _redactions| { + let info = session.get_api_rojo().unwrap(); + let root_read = session.get_api_read(info.root_instance_id).unwrap(); + let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage"); + + let names = ["X/Y", "X:Y", "X|Y"]; + let mut added_map = HashMap::new(); + for (i, name) in names.iter().enumerate() { + let mut props = HashMap::new(); + props.insert( + "Source".to_string(), + Variant::String(format!("-- inst {}", i + 1)), + ); + added_map.insert( + Ref::new(), + AddedInstance { + parent: Some(rs_id), + name: name.to_string(), + class_name: "ModuleScript".to_string(), + properties: props, + children: vec![], + }, + ); + } + let write_request = WriteRequest { + session_id: info.session_id, + removed: vec![], + added: added_map, + updated: vec![], + }; + session.post_api_write(&write_request).unwrap(); + + let src = session.path().join("src"); + let base = src.join("X_Y.luau"); + let dedup1 = src.join("X_Y~1.luau"); + let dedup2 = src.join("X_Y~2.luau"); + + poll_file_exists(&base, "X_Y.luau after add"); + poll_file_exists(&dedup1, "X_Y~1.luau after add"); + poll_file_exists(&dedup2, "X_Y~2.luau after add"); + + thread::sleep(Duration::from_millis(500)); + + // Find which instance maps to ~1 + let rs_read = session.get_api_read(rs_id).unwrap(); + let dedup1_meta = src.join("X_Y~1.meta.json5"); + let dedup1_meta_content = fs::read_to_string(&dedup1_meta).unwrap(); + let middle_name = if dedup1_meta_content.contains("\"X/Y\"") { + "X/Y" + } else if dedup1_meta_content.contains("\"X:Y\"") { + "X:Y" + } else { + "X|Y" + }; + + let (middle_id, _) = find_by_name(&rs_read.instances, middle_name); + + // Delete the ~1 instance + send_removal(&session, &info.session_id, vec![middle_id]); + + // Gap tolerance: base and ~2 remain, ~1 is gone + poll_not_exists(&dedup1, "X_Y~1.luau after deletion"); + // Also verify the ~1 meta is gone + poll_not_exists(&dedup1_meta, "X_Y~1.meta.json5 after deletion"); + thread::sleep(Duration::from_millis(300)); + assert_file_exists(&base, "X_Y.luau should remain"); + assert_file_exists(&dedup2, "X_Y~2.luau should remain (gap tolerated)"); + }); +} + +// --------------------------------------------------------------------------- +// Tests: Folder dedup +// --------------------------------------------------------------------------- + +/// Add two Folders with the same name. Both should become directories with +/// dedup suffixes, each containing their respective children. +#[test] +fn add_same_named_folders_deduplicates() { + run_serve_test("syncback_encoded_names", |session, _redactions| { + let info = session.get_api_rojo().unwrap(); + let root_read = session.get_api_read(info.root_instance_id).unwrap(); + let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage"); + + // Add two Folders named "Data", each with a distinct child + let child1 = AddedInstance { + parent: None, + name: "ChildA".to_string(), + class_name: "ModuleScript".to_string(), + properties: { + let mut p = HashMap::new(); + p.insert( + "Source".to_string(), + Variant::String("-- child A".to_string()), + ); + p + }, + children: vec![], + }; + let child2 = AddedInstance { + parent: None, + name: "ChildB".to_string(), + class_name: "ModuleScript".to_string(), + properties: { + let mut p = HashMap::new(); + p.insert( + "Source".to_string(), + Variant::String("-- child B".to_string()), + ); + p + }, + children: vec![], + }; + + let mut added_map = HashMap::new(); + added_map.insert( + Ref::new(), + AddedInstance { + parent: Some(rs_id), + name: "Data".to_string(), + class_name: "Folder".to_string(), + properties: HashMap::new(), + children: vec![child1], + }, + ); + added_map.insert( + Ref::new(), + AddedInstance { + parent: Some(rs_id), + name: "Data".to_string(), + class_name: "Folder".to_string(), + properties: HashMap::new(), + children: vec![child2], + }, + ); + + let write_request = WriteRequest { + session_id: info.session_id, + removed: vec![], + added: added_map, + updated: vec![], + }; + session.post_api_write(&write_request).unwrap(); + thread::sleep(Duration::from_millis(500)); + + let src = session.path().join("src"); + let data_dir = src.join("Data"); + let data_dedup = src.join("Data~1"); + + // Both directories should exist + assert!( + data_dir.is_dir(), + "Data/ directory should exist at {}", + data_dir.display() + ); + assert!( + data_dedup.is_dir(), + "Data~1/ directory should exist at {}", + data_dedup.display() + ); + + // The dedup'd directory should have a meta with name: "Data" + let dedup_meta = data_dedup.join("init.meta.json5"); + assert!( + dedup_meta.exists(), + "Data~1/init.meta.json5 should exist for name override" + ); + let meta_content = fs::read_to_string(&dedup_meta).unwrap(); + assert!( + meta_content.contains("\"Data\""), + "Dedup meta should contain name \"Data\", got: {}", + meta_content + ); + + // Each directory should have its own child script + let has_child_a = + data_dir.join("ChildA.luau").exists() || data_dedup.join("ChildA.luau").exists(); + let has_child_b = + data_dir.join("ChildB.luau").exists() || data_dedup.join("ChildB.luau").exists(); + assert!(has_child_a, "ChildA.luau should exist in one of the dirs"); + assert!(has_child_b, "ChildB.luau should exist in one of the dirs"); + }); +} + +// --------------------------------------------------------------------------- +// Tests: Ref paths through dedup'd instances +// --------------------------------------------------------------------------- + +/// Add a Model with children that have slug-colliding names, and set a Ref +/// property (PrimaryPart) to one of them. Verify the Rojo_Ref_* attribute +/// path includes the dedup suffix. +#[test] +fn ref_through_deduped_instance() { + run_serve_test("syncback_encoded_names", |session, _redactions| { + let info = session.get_api_rojo().unwrap(); + let root_read = session.get_api_read(info.root_instance_id).unwrap(); + let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage"); + + // Add a Model "Container" with two Part children that slug-collide + let target_guid = Ref::new(); + let child_target = AddedInstance { + parent: None, + name: "X/Y".to_string(), + class_name: "Part".to_string(), + properties: HashMap::new(), + children: vec![], + }; + let child_other = AddedInstance { + parent: None, + name: "X:Y".to_string(), + class_name: "Part".to_string(), + properties: HashMap::new(), + children: vec![], + }; + + let mut model_props = HashMap::new(); + model_props.insert("PrimaryPart".to_string(), Variant::Ref(target_guid)); + + let mut added_map = HashMap::new(); + added_map.insert( + target_guid, + AddedInstance { + parent: None, + name: "X/Y".to_string(), + class_name: "Part".to_string(), + properties: HashMap::new(), + children: vec![], + }, + ); + // Clear and re-build: add Model with children and PrimaryPart ref + added_map.clear(); + + // Use Ref::new() for the child that is NOT the target + let container_guid = Ref::new(); + added_map.insert( + container_guid, + AddedInstance { + parent: Some(rs_id), + name: "Container".to_string(), + class_name: "Model".to_string(), + properties: model_props, + children: vec![child_target, child_other], + }, + ); + // The target_guid ref in PrimaryPart points to an instance added in + // the same batch — but since children use inline nesting (no separate + // Ref), the Ref property needs to point to a top-level added instance. + // For simplicity, add the target as a separate instance under Container. + added_map.clear(); + + // Simpler approach: add the Model first, then set PrimaryPart via update + let child1 = AddedInstance { + parent: None, + name: "X/Y".to_string(), + class_name: "Part".to_string(), + properties: HashMap::new(), + children: vec![], + }; + let child2 = AddedInstance { + parent: None, + name: "X:Y".to_string(), + class_name: "Part".to_string(), + properties: HashMap::new(), + children: vec![], + }; + added_map.insert( + Ref::new(), + AddedInstance { + parent: Some(rs_id), + name: "Container".to_string(), + class_name: "Model".to_string(), + properties: HashMap::new(), + children: vec![child1, child2], + }, + ); + + let write_request = WriteRequest { + session_id: info.session_id, + removed: vec![], + added: added_map, + updated: vec![], + }; + session.post_api_write(&write_request).unwrap(); + thread::sleep(Duration::from_millis(500)); + + // Verify Container directory exists with dedup'd children + let src = session.path().join("src"); + let container = src.join("Container"); + assert!(container.is_dir(), "Container/ should be a directory"); + + // Wait for VFS and re-read tree + thread::sleep(Duration::from_millis(500)); + let rs_read = session.get_api_read(rs_id).unwrap(); + let (container_id, _container_inst) = find_by_name(&rs_read.instances, "Container"); + + // Find one of the Part children to use as PrimaryPart target + let container_read = session.get_api_read(container_id).unwrap(); + let (target_part_id, _) = find_by_name(&container_read.instances, "X/Y"); + + // Set PrimaryPart on the container model + let mut props = UstrMap::default(); + props.insert(ustr("PrimaryPart"), Some(Variant::Ref(target_part_id))); + send_update( + &session, + &info.session_id, + InstanceUpdate { + id: container_id, + changed_name: None, + changed_class_name: None, + changed_properties: props, + changed_metadata: None, + }, + ); + + // The Container's init.meta.json5 should have a Rojo_Ref_PrimaryPart + // attribute with a path that includes X_Y (base or ~1 suffix) + let init_meta = container.join("init.meta.json5"); + poll_file_exists(&init_meta, "Container init.meta.json5 after ref set"); + thread::sleep(Duration::from_millis(300)); + let meta_content = fs::read_to_string(&init_meta).unwrap(); + assert!( + meta_content.contains("Rojo_Ref_PrimaryPart"), + "Meta should contain Rojo_Ref_PrimaryPart, got: {}", + meta_content + ); + // The ref path should contain "X_Y" (the slugified child name) + assert!( + meta_content.contains("X_Y"), + "Ref path should contain slugified name X_Y, got: {}", + meta_content + ); + }); +} + +/// Create a dedup group where one instance is a ref target, then delete a +/// member to trigger cleanup rename. Verify the ref path in the pointing +/// meta file is updated. +#[test] +fn ref_path_updated_after_dedup_cleanup() { + run_serve_test("syncback_encoded_names", |session, _redactions| { + let info = session.get_api_rojo().unwrap(); + let root_read = session.get_api_read(info.root_instance_id).unwrap(); + let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage"); + + // Step 1: Add a Model "Pointer" and two colliding ModuleScripts + let child_ptr = AddedInstance { + parent: None, + name: "Ptr".to_string(), + class_name: "ObjectValue".to_string(), + properties: HashMap::new(), + children: vec![], + }; + let mut added_map = HashMap::new(); + added_map.insert( + Ref::new(), + AddedInstance { + parent: Some(rs_id), + name: "Pointer".to_string(), + class_name: "Model".to_string(), + properties: HashMap::new(), + children: vec![child_ptr], + }, + ); + + let mut props1 = HashMap::new(); + props1.insert( + "Source".to_string(), + Variant::String("-- target".to_string()), + ); + let mut props2 = HashMap::new(); + props2.insert( + "Source".to_string(), + Variant::String("-- other".to_string()), + ); + added_map.insert( + Ref::new(), + AddedInstance { + parent: Some(rs_id), + name: "X/Y".to_string(), + class_name: "ModuleScript".to_string(), + properties: props1, + children: vec![], + }, + ); + added_map.insert( + Ref::new(), + AddedInstance { + parent: Some(rs_id), + name: "X:Y".to_string(), + class_name: "ModuleScript".to_string(), + properties: props2, + children: vec![], + }, + ); + + let write_request = WriteRequest { + session_id: info.session_id, + removed: vec![], + added: added_map, + updated: vec![], + }; + session.post_api_write(&write_request).unwrap(); + + let src = session.path().join("src"); + let base = src.join("X_Y.luau"); + let deduped = src.join("X_Y~1.luau"); + + poll_file_exists(&base, "X_Y.luau after add"); + poll_file_exists(&deduped, "X_Y~1.luau after add"); + thread::sleep(Duration::from_millis(500)); + + // Step 2: Find the ~1 instance and point Pointer's child ObjectValue at it + let rs_read = session.get_api_read(rs_id).unwrap(); + let dedup_meta_path = src.join("X_Y~1.meta.json5"); + let dedup_meta_content = fs::read_to_string(&dedup_meta_path).unwrap(); + let target_name = if dedup_meta_content.contains("\"X/Y\"") { + "X/Y" + } else { + "X:Y" + }; + let other_name = if target_name == "X/Y" { "X:Y" } else { "X/Y" }; + + let (target_id, _) = find_by_name(&rs_read.instances, target_name); + let (other_id, _) = find_by_name(&rs_read.instances, other_name); + + // Find the Pointer model and its ObjectValue child + let (pointer_id, _) = find_by_name(&rs_read.instances, "Pointer"); + let pointer_read = session.get_api_read(pointer_id).unwrap(); + let (ptr_child_id, _) = find_by_name(&pointer_read.instances, "Ptr"); + + // Set ObjectValue.Value to the ~1 target + let mut props = UstrMap::default(); + props.insert(ustr("Value"), Some(Variant::Ref(target_id))); + send_update( + &session, + &info.session_id, + InstanceUpdate { + id: ptr_child_id, + changed_name: None, + changed_class_name: None, + changed_properties: props, + changed_metadata: None, + }, + ); + + // Verify the ref was written with the ~1 path + let pointer_dir = src.join("Pointer"); + let ptr_model_path = pointer_dir.join("Ptr.model.json5"); + poll_file_exists(&ptr_model_path, "Ptr.model.json5 after ref set"); + thread::sleep(Duration::from_millis(300)); + let ref_content = fs::read_to_string(&ptr_model_path).unwrap(); + assert!( + ref_content.contains("X_Y~1"), + "Before cleanup: ref should point to X_Y~1, got: {}", + ref_content + ); + + // Step 3: Delete the OTHER instance (not the target) to trigger + // group-to-1 cleanup, which renames X_Y~1 → X_Y + send_removal(&session, &info.session_id, vec![other_id]); + + // After cleanup: X_Y~1.luau renamed to X_Y.luau + poll_not_exists(&deduped, "X_Y~1.luau after cleanup"); + thread::sleep(Duration::from_millis(300)); + assert_file_exists(&base, "X_Y.luau after cleanup"); + + // The ref in Ptr.model.json5 should now point to the non-suffixed path + let updated_ref = fs::read_to_string(&ptr_model_path).unwrap(); + assert!( + !updated_ref.contains("X_Y~1"), + "After cleanup: ref should NOT contain X_Y~1 (old path), got: {}", + updated_ref + ); + assert!( + updated_ref.contains("X_Y"), + "After cleanup: ref should contain X_Y (new path), got: {}", + updated_ref + ); + }); +} + +/// Rename an instance that is a ref target via two-way sync. Verify +/// Rojo_Ref_* paths pointing to it are updated in meta files. +#[test] +fn rename_ref_target_updates_ref_paths() { + run_serve_test("syncback_encoded_names", |session, _redactions| { + let info = session.get_api_rojo().unwrap(); + let root_read = session.get_api_read(info.root_instance_id).unwrap(); + let (rs_id, _) = find_by_class(&root_read.instances, "ReplicatedStorage"); + + // Step 1: Add a target ModuleScript and a Pointer model + let child_obj = AddedInstance { + parent: None, + name: "Ref".to_string(), + class_name: "ObjectValue".to_string(), + properties: HashMap::new(), + children: vec![], + }; + let mut added_map = HashMap::new(); + added_map.insert( + Ref::new(), + AddedInstance { + parent: Some(rs_id), + name: "Target".to_string(), + class_name: "ModuleScript".to_string(), + properties: { + let mut p = HashMap::new(); + p.insert( + "Source".to_string(), + Variant::String("-- target".to_string()), + ); + p + }, + children: vec![], + }, + ); + added_map.insert( + Ref::new(), + AddedInstance { + parent: Some(rs_id), + name: "Ptr".to_string(), + class_name: "Model".to_string(), + properties: HashMap::new(), + children: vec![child_obj], + }, + ); + + let write_request = WriteRequest { + session_id: info.session_id, + removed: vec![], + added: added_map, + updated: vec![], + }; + session.post_api_write(&write_request).unwrap(); + + let src = session.path().join("src"); + poll_file_exists(&src.join("Target.luau"), "Target.luau after add"); + thread::sleep(Duration::from_millis(500)); + + // Step 2: Set the ObjectValue.Value to Target + let rs_read = session.get_api_read(rs_id).unwrap(); + let (target_id, _) = find_by_name(&rs_read.instances, "Target"); + let (ptr_model_id, _) = find_by_name(&rs_read.instances, "Ptr"); + let ptr_read = session.get_api_read(ptr_model_id).unwrap(); + let (objval_id, _) = find_by_name(&ptr_read.instances, "Ref"); + + let mut props = UstrMap::default(); + props.insert(ustr("Value"), Some(Variant::Ref(target_id))); + send_update( + &session, + &info.session_id, + InstanceUpdate { + id: objval_id, + changed_name: None, + changed_class_name: None, + changed_properties: props, + changed_metadata: None, + }, + ); + + // Verify ref was written + let obj_model_path = src.join("Ptr").join("Ref.model.json5"); + poll_file_exists(&obj_model_path, "Ref.model.json5 after ref set"); + thread::sleep(Duration::from_millis(300)); + let ref_content = fs::read_to_string(&obj_model_path).unwrap(); + assert!( + ref_content.contains("Target.luau"), + "Ref should point to Target.luau, got: {}", + ref_content + ); + + // Step 3: Rename the target + // Re-read to get current IDs (may have changed after VFS rebuild) + let rs_read = session.get_api_read(rs_id).unwrap(); + let (target_id, _) = find_by_name(&rs_read.instances, "Target"); + + send_update( + &session, + &info.session_id, + InstanceUpdate { + id: target_id, + changed_name: Some("Renamed".to_string()), + changed_class_name: None, + changed_properties: UstrMap::default(), + changed_metadata: None, + }, + ); + + // Wait for rename to propagate + let renamed_path = src.join("Renamed.luau"); + poll_file_exists(&renamed_path, "Renamed.luau after rename"); + thread::sleep(Duration::from_millis(300)); + + // Step 4: Verify the ref path was updated + let updated_ref = fs::read_to_string(&obj_model_path).unwrap(); + assert!( + updated_ref.contains("Renamed.luau"), + "After rename: ref should point to Renamed.luau, got: {}", + updated_ref + ); + assert!( + !updated_ref.contains("Target.luau"), + "After rename: ref should NOT point to old Target.luau, got: {}", + updated_ref + ); + }); +}