Rojo enables Roblox developers to use professional-grade software engineering tools https://rojo.space
Find a file
boatbomber 85655ca84f
Heuristic hydration matching instead of hard dependency on GetChildren ordering (#1266)
### Summary

When two or more sibling instances share the same `Name` and
`ClassName`, Rojo's reconciler previously paired them with their
server-side counterparts purely by child order (first-unvisited match in
`GetChildren()` order). If the workspace child order ever diverged from
the server's, the wrong instance got paired so each duplicate could
inherit a sibling's properties.

This is the root of the #1257 bug: the welded parts would oscillate
between positions on each connect/disconnect because hydration kept
mis-pairing them. (#1265 stopped the sync fallback from scrambling child
order in the first place but this PR makes hydration robust even when
order *does* diverge.)

This PR makes `hydrate` break ties by comparing properties: when several
existing children match on `Name`+`ClassName`, it scores each candidate
by how many of the virtual instance's properties match the candidate's
live values, and picks the best. Order remains the tiebreak when scores
are equal, so behavior is unchanged for uniquely-named instances and for
indistinguishable siblings.

### Changes

- `trueEquals.lua`: extracted verbatim from `diff.lua` (the fuzzy
value-equality helper) so it can be shared. No behavior change;
`diff.lua` now requires it.
- `countMatchingProperties.lua`: added
`countMatchingProperties(instance, virtualInstance, instanceMap) ->
number`. Skips `Ref` properties (the instanceMap isn't fully built
mid-hydrate, so refs can't be decoded reliably, and they're a poor
disambiguator anyway) and any property that can't be read or decoded.
- `hydrate.lua`: See details below.

### Hydrate Changes

This touches `hydrate`, which runs over the whole tree on every
connect/resync, so I want state clearly that **the common path is faster
than before, not slower** even for parents with thousands of children!

The old algorithm was a nested scan: for each of `V` virtual children,
scan existing children until the first unvisited `Name`+`ClassName`
match. Two costs stand out:
- A `pcall` (to guard DataModel permission errors) ran on every
comparison (up to `V*E` `pcall`s per parent).
- Even for in-order trees the re-scanning of the visited prefix made it
`O(V^2)`.

The new algorithm does a single bucketing pass, then `O(1)` lookups:
1. One `O(E)` pass groups existing children into nested
`buckets[name][className]` tables. This runs exactly `E` `pcall`s total
(one per child), down from the `V*E` worst case.
2. Each virtual child does an `O(1)` bucket lookup to find its
candidates.
3. A per-bucket cursor skips already-paired children, so order-based
matching is amortized `O(1)` per child instead of rescanning.

| Scenario | Old | New |
| ------------------------------------------------ |
--------------------------------------- |
--------------------------------------------- |
| Unique-named children (typical, incl. thousands) | `O(V^2)`, plus up
to `V*E` pcalls | `O(V + E)`, plus exactly `E` pcalls |
| `C <= 32` candidates | `O(C^2)` | `O(P * C^2)` scoring |
| `C > 32` candidates | `O(C^2)` | `O(C)` |

Property scoring (`getProperty`/`decodeValue`/`trueEquals`) is the only
new expense, and it's gated two ways:
- It runs only when a `Name`+`ClassName` group has >=2 candidates (i.e.
never for uniquely-named instances).
- A cap, `MAX_CANDIDATES_TO_SCORE = 32`, means scoring only kicks in
once a group has <=32 unvisited candidates. A folder of thousands of
identically-named parts therefore does not trigger scoring; it falls
back to the original order-based pairing. The worst-case added scoring
work is bounded to roughly `32^2` property comparisons per group,
independent of group size.

So overall this is faster when you have unique names or many children.
It is slower but more robust when you have small groups of duplicate
names. Memory usage is increased as it creates the candidate buckets.
2026-06-01 17:29:22 -07:00
.cargo Use new Cargo config.toml convention 2021-07-12 13:19:36 -04:00
.github/workflows Upload plugin as part of release workflow (#1227) 2026-02-14 11:10:36 -08:00
.lune Upload plugin as part of release workflow (#1227) 2026-02-14 11:10:36 -08:00
.vscode Plugin dev ux improvements (#992) 2024-11-10 15:53:58 -08:00
assets Update default gitignores to include sourcemap (#1145) 2025-10-28 17:28:55 -07:00
benches Add plugin build flag (#735) 2023-07-17 19:15:40 -07:00
build/windows Support long file paths on Windows (past 256 limit) (#464) 2021-08-23 15:21:01 -04:00
crates Implement VFS Path normalization for improved cross-platform tree synchronization (#1201) 2026-01-19 15:04:59 -08:00
plugin Heuristic hydration matching instead of hard dependency on GetChildren ordering (#1266) 2026-06-01 17:29:22 -07:00
rojo-test Ensure that pruned Instances aren't treated as existing in syncback (#1179) 2025-11-29 21:21:48 -08:00
scripts Plugin dev ux improvements (#992) 2024-11-10 15:53:58 -08:00
src Upload plugin as part of release workflow (#1227) 2026-02-14 11:10:36 -08:00
test-projects Improves sourcemap path handling with pathdiff (#1217) 2026-02-12 19:17:28 -08:00
tests Use msgpack for API (#1176) 2026-02-12 18:37:24 -08:00
.dir-locals.el Plugin dev ux improvements (#992) 2024-11-10 15:53:58 -08:00
.editorconfig Changed file extensions of init command from lua to luau (#831) 2024-01-05 16:00:49 -08:00
.git-blame-ignore-revs Create .git-blame-ignore-revs & Add Stylua pr (#787) 2023-09-19 05:56:39 +00:00
.gitattributes Set linguist language of lua files to luau (#956) 2024-08-02 10:03:57 -07:00
.gitignore Implement VFS Path normalization for improved cross-platform tree synchronization (#1201) 2026-01-19 15:04:59 -08:00
.gitmodules Upload plugin as part of release workflow (#1227) 2026-02-14 11:10:36 -08:00
build.rs Upload plugin as part of release workflow (#1227) 2026-02-14 11:10:36 -08:00
Cargo.lock Improves sourcemap path handling with pathdiff (#1217) 2026-02-12 19:17:28 -08:00
Cargo.toml Improves sourcemap path handling with pathdiff (#1217) 2026-02-12 19:17:28 -08:00
CHANGELOG.md Heuristic hydration matching instead of hard dependency on GetChildren ordering (#1266) 2026-06-01 17:29:22 -07:00
CONTRIBUTING.md Improves sourcemap path handling with pathdiff (#1217) 2026-02-12 19:17:28 -08:00
LICENSE.txt Tweak bottom of README, move LICENSE to LICENSE.txt 2019-01-18 20:57:19 -08:00
plugin.project.json Upload plugin as part of release workflow (#1227) 2026-02-14 11:10:36 -08:00
README.md Add JSONC Support for Project, Meta, and Model JSON files (#1144) 2025-10-28 17:29:57 -07:00
rokit.toml Upload plugin as part of release workflow (#1227) 2026-02-14 11:10:36 -08:00
selene.toml Move to using Rokit, update tools, and don't install unnecessary tools (#1109) 2025-08-29 18:45:15 -07:00
testez.yml Update selene (#550) 2022-06-12 03:41:49 -04:00

Rojo
 
Actions status Latest server version Rojo Documentation

Rojo is a tool designed to enable Roblox developers to use professional-grade software engineering tools.

With Rojo, it's possible to use industry-leading tools like Visual Studio Code and Git.

Rojo is designed for power users who want to use the best tools available for building games, libraries, and plugins.

Features

Rojo enables:

  • Working on scripts and models from the filesystem, in your favorite editor
  • Versioning your game, library, or plugin using Git or another VCS
  • Streaming rbxmx and rbxm models into your game in real time
  • Packaging and deploying your project to Roblox.com from the command line

In the future, Rojo will be able to:

  • Sync instances from Roblox Studio to the filesystem
  • Automatically convert your existing game to work with Rojo
  • Import custom instances like MoonScript code

Documentation

Documentation is hosted in the rojo.space repository.

Contributing

Check out our contribution guide for detailed instructions for helping work on Rojo!

Pull requests are welcome!

Rojo supports Rust 1.88 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.

License

Rojo is available under the terms of the Mozilla Public License, Version 2.0. See LICENSE.txt for details.