From d12120bab6ec11c861c7b17f832df163ead17738 Mon Sep 17 00:00:00 2001 From: Floyd Horng Date: Thu, 11 Jun 2026 02:47:50 +0100 Subject: [PATCH] Fix syncback not removing stale $properties at engine defaults (#1244) --- CHANGELOG.md | 2 + ...ject_default_properties_remove-stdout.snap | 5 ++ ...roperties_remove-default.project.json.snap | 59 +++++++++++++++ .../input-project/default.project.json | 21 ++++++ .../input.rbxl | Bin 0 -> 44361 bytes src/snapshot_middleware/project.rs | 70 +++++++++++++++--- tests/tests/syncback.rs | 3 + 7 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_default_properties_remove-stdout.snap create mode 100644 rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_default_properties_remove-default.project.json.snap create mode 100644 rojo-test/syncback-tests/project_default_properties_remove/input-project/default.project.json create mode 100644 rojo-test/syncback-tests/project_default_properties_remove/input.rbxl diff --git a/CHANGELOG.md b/CHANGELOG.md index cff39688..00524693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Making a new release? Simply add the new header with the version and date undern * Instances that share a name and class are now robustly matched on resync by comparing their properties, instead of relying on child order alone. ([#1266]) * Rojo now reports a clear error instead of panicking in several cases, including when the `serve` port is already in use, when a synced file is read-only or locked, when the filesystem watcher can't be created, and when the working directory is inaccessible. ([#1267]) * `rojo serve` now validates the `Host`/`Origin` headers to protect the local/private server against DNS rebinding, gates `/api/open` to local clients, and warns when bound to a network-reachable address. The accepted hosts can be extended with the `--allowed-hosts` option or a project's `serveAllowedHosts` field, for example to reach a network-exposed server by hostname. ([#1270]) +* Fixed syncback not removing stale `$properties` entries when Studio resets a property to its engine default. ([#1244]) [#1176]: https://github.com/rojo-rbx/rojo/pull/1176 [#1179]: https://github.com/rojo-rbx/rojo/pull/1179 @@ -60,6 +61,7 @@ Making a new release? Simply add the new header with the version and date undern [#1266]: https://github.com/rojo-rbx/rojo/pull/1266 [#1267]: https://github.com/rojo-rbx/rojo/pull/1267 [#1270]: https://github.com/rojo-rbx/rojo/pull/1270 +[#1244]: https://github.com/rojo-rbx/rojo/pull/1244 ## [7.7.0-rc.1] (November 27th, 2025) diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_default_properties_remove-stdout.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_default_properties_remove-stdout.snap new file mode 100644 index 00000000..3ef76951 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_default_properties_remove-stdout.snap @@ -0,0 +1,5 @@ +--- +source: tests/rojo_test/syncback_util.rs +expression: "String::from_utf8_lossy(&output.stdout)" +--- +Writing default.project.json diff --git a/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_default_properties_remove-default.project.json.snap b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_default_properties_remove-default.project.json.snap new file mode 100644 index 00000000..1b8d07d2 --- /dev/null +++ b/rojo-test/syncback-test-snapshots/end_to_end__tests__syncback__project_default_properties_remove-default.project.json.snap @@ -0,0 +1,59 @@ +--- +source: tests/tests/syncback.rs +expression: default.project.json +--- +{ + "name": "SyncbackTest", + "tree": { + "$className": "DataModel", + "Workspace": { + "$className": "Workspace", + "TestPart": { + "$className": "Part", + "$properties": { + "Anchored": true, + "Color": { + "Color3uint8": [ + 0, + 0, + 255 + ] + } + } + }, + "$properties": { + "EnableSLIMAvatars": { + "Enum": 0 + }, + "ImprovedAnimationConstraint": { + "Enum": 0 + }, + "ImprovedPhysicsReplication": { + "Enum": 0 + }, + "LayeredClothingCacheOptimizations": { + "Enum": 0 + }, + "MeshStreamingAndImprovedLods": { + "Enum": 0 + }, + "NextGenerationReplication": { + "Enum": 0 + }, + "PlayerScriptsUseInputActionSystem": { + "Enum": 0 + }, + "UseFixedSimulation": { + "Enum": 0 + }, + "UseNewLuauTypeSolver": "Disabled", + "ValidateEnabledProximityPrompt": { + "Enum": 0 + } + }, + "$attributes": { + "Rojo_Target_CurrentCamera": "302d573157260ee80a3baa32000003b5" + } + } + } +} diff --git a/rojo-test/syncback-tests/project_default_properties_remove/input-project/default.project.json b/rojo-test/syncback-tests/project_default_properties_remove/input-project/default.project.json new file mode 100644 index 00000000..6f0b1ade --- /dev/null +++ b/rojo-test/syncback-tests/project_default_properties_remove/input-project/default.project.json @@ -0,0 +1,21 @@ +{ + "name": "SyncbackTest", + "tree": { + "$className": "DataModel", + "Workspace": { + "$className": "Workspace", + "TestPart": { + "$className": "Part", + "$properties": { + "Transparency": 1.0, + "Anchored": true, + "Color": [ + 1.0, + 0.0, + 0.0 + ] + } + } + } + } +} \ No newline at end of file diff --git a/rojo-test/syncback-tests/project_default_properties_remove/input.rbxl b/rojo-test/syncback-tests/project_default_properties_remove/input.rbxl new file mode 100644 index 0000000000000000000000000000000000000000..7ca3700a7bd1b8c41ba7aa37e4e615585f43e662 GIT binary patch literal 44361 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 let mut new_child_map = HashMap::new(); let mut node_changed_map = Vec::new(); + // Tracks whether any stale default-valued properties were removed from + // project nodes. If so, we must reserialize even if + // project_node_should_reserialize wouldn't otherwise detect a change + // (it only compares node properties forward, not in reverse). + let mut removed_stale_properties = false; let mut node_queue = VecDeque::with_capacity(1); node_queue.push_back((&mut project.tree, old_inst, snapshot.new_inst())); @@ -402,10 +407,12 @@ pub fn syncback_project<'sync>( // We only want to set properties if it needs it. if !middleware.handles_own_properties() { - project_node_property_syncback_path(snapshot, new_inst, node); + removed_stale_properties |= + project_node_property_syncback_path(snapshot, new_inst, node); } } else { - project_node_property_syncback_no_path(snapshot, new_inst, node); + removed_stale_properties |= + project_node_property_syncback_no_path(snapshot, new_inst, node); } for child_ref in new_inst.children() { @@ -507,12 +514,18 @@ pub fn syncback_project<'sync>( } let mut fs_snapshot = FsSnapshot::new(); - for (node_properties, node_attributes, old_inst) in node_changed_map { - if project_node_should_reserialize(node_properties, node_attributes, old_inst)? { - fs_snapshot.add_file(project_path, serde_json::to_vec_pretty(&project)?); - break; + let mut needs_reserialize = removed_stale_properties; + if !needs_reserialize { + for (node_properties, node_attributes, old_inst) in node_changed_map { + if project_node_should_reserialize(node_properties, node_attributes, old_inst)? { + needs_reserialize = true; + break; + } } } + if needs_reserialize { + fs_snapshot.add_file(project_path, serde_json::to_vec_pretty(&project)?); + } Ok(SyncbackReturn { fs_snapshot, @@ -521,15 +534,18 @@ pub fn syncback_project<'sync>( }) } +/// Syncs properties from the new instance into the project node. +/// Returns `true` if any stale properties were removed (i.e. properties +/// that existed in the project node but are now at their engine default). fn project_node_property_syncback( _snapshot: &SyncbackSnapshot, filtered_properties: UstrMap<&Variant>, new_inst: &Instance, node: &mut ProjectNode, -) { +) -> bool { let properties = &mut node.properties; let mut attributes = BTreeMap::new(); - for (name, value) in filtered_properties { + for (&name, &value) in &filtered_properties { match value { Variant::Attributes(attrs) => { for (attr_name, attr_value) in attrs.iter() { @@ -552,14 +568,48 @@ fn project_node_property_syncback( } } } + + // Remove stale properties: entries that exist in the project node's + // $properties but are no longer in the filtered (non-default) properties + // from the instance. This handles the case where Studio resets a property + // to its engine default — filter_properties won't include it, so we need + // to clean up the now-stale project entry. + let class_data = rbx_reflection_database::get() + .ok() + .and_then(|db| db.classes.get(new_inst.class.as_str())); + let len_before = properties.len(); + properties.retain(|prop_name, _| { + if filtered_properties.contains_key(prop_name) { + return true; + } + // Only remove if the property has a known default value in the + // reflection database. If there's no default, the property might be + // absent from the instance for other reasons (e.g. unknown property), + // so we conservatively keep it. + if let Some(data) = &class_data { + if data.default_properties.contains_key(prop_name.as_str()) { + log::debug!( + "Removing stale property '{}' from project node for class '{}': \ + value has been reset to engine default", + prop_name, + new_inst.class + ); + return false; + } + } + true + }); + let removed_stale = properties.len() < len_before; + node.attributes = attributes; + removed_stale } fn project_node_property_syncback_path( snapshot: &SyncbackSnapshot, new_inst: &Instance, node: &mut ProjectNode, -) { +) -> bool { let filtered_properties = snapshot .get_path_filtered_properties(new_inst.referent()) .unwrap(); @@ -570,7 +620,7 @@ fn project_node_property_syncback_no_path( snapshot: &SyncbackSnapshot, new_inst: &Instance, node: &mut ProjectNode, -) { +) -> bool { let filtered_properties = filter_properties(snapshot.project(), new_inst); project_node_property_syncback(snapshot, filtered_properties, new_inst, node) } diff --git a/tests/tests/syncback.rs b/tests/tests/syncback.rs index 31553496..cffccccb 100644 --- a/tests/tests/syncback.rs +++ b/tests/tests/syncback.rs @@ -86,4 +86,7 @@ syncback_tests! { sync_rules => ["src/module.modulescript", "src/text.text"], // Ensures that the `syncUnscriptable` setting works unscriptable_properties => ["default.project.json"], + // Ensures that syncback correctly removes default values from projects rather + // than leaving an incorrect value. + project_default_properties_remove => ["default.project.json"], }