mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 22:11:38 +00:00
- Finalized the transition from a duplicate-name blacklist system to a comprehensive 3-pass matching algorithm for both syncback and forward operations, enhancing the handling of ambiguous paths. - Integrated a new `matching.lua` module to facilitate instance matching during hydration, utilizing unique name matching, class name narrowing, and structural fingerprinting. - Updated various components, including `ChangeBatcher`, `Reconciler`, and `snapshot` modules, to leverage the new matching logic, ensuring robust file management and improved performance. - Removed legacy duplicate handling code and streamlined the encoding process for instances with duplicate names. These changes significantly enhance the clarity and efficiency of file operations, paving the way for improved project management and user experience.
282 lines
8.9 KiB
Lua
282 lines
8.9 KiB
Lua
--[[
|
|
Encodes a Roblox Studio instance into a format suitable for transmission
|
|
to the Rojo server for syncback (creating files from Studio instances).
|
|
|
|
Supports ALL instance types:
|
|
- Scripts (ModuleScript, Script, LocalScript) -> .luau files or directories with init files if has children
|
|
- Folders -> directories (with Attributes if present)
|
|
- All other instances -> .model.json files or directories
|
|
|
|
Children are recursively encoded and included.
|
|
|
|
DUPLICATE HANDLING:
|
|
- Instances with duplicate-named siblings at ANY level of the path are skipped
|
|
- This matches the behavior in ref_properties.rs::is_path_unique
|
|
- A path is ambiguous if duplicates exist anywhere from the instance to the root
|
|
- A warning is logged when duplicates are encountered
|
|
]]
|
|
|
|
local Packages = script.Parent.Parent.Parent.Packages
|
|
local Log = require(Packages.Log)
|
|
local RbxDom = require(Packages.RbxDom)
|
|
|
|
local encodeProperty = require(script.Parent.encodeProperty)
|
|
local UNENCODABLE_DATA_TYPES = require(script.Parent.propertyFilter)
|
|
|
|
-- Script class names that need Source property
|
|
local SCRIPT_CLASS_NAMES = {
|
|
Script = true,
|
|
LocalScript = true,
|
|
ModuleScript = true,
|
|
}
|
|
|
|
-- Properties to always skip when encoding (internal Roblox properties that never serialize)
|
|
local SKIP_PROPERTIES = {
|
|
Parent = true,
|
|
Name = true, -- We handle this separately
|
|
Archivable = true,
|
|
}
|
|
|
|
-- Properties to skip per class (matches filter_out_property in Rust syncback)
|
|
-- These are properties whose values are encoded in the file itself, not metadata
|
|
local SKIP_PROPERTIES_BY_CLASS = {
|
|
Script = {
|
|
Source = true, -- Encoded in the .luau file
|
|
ScriptGuid = true, -- Internal
|
|
RunContext = true, -- Encoded in file suffix (.server.luau, .client.luau, etc.)
|
|
},
|
|
LocalScript = {
|
|
Source = true, -- Encoded in the .luau file
|
|
ScriptGuid = true, -- Internal
|
|
},
|
|
ModuleScript = {
|
|
Source = true, -- Encoded in the .luau file
|
|
ScriptGuid = true, -- Internal
|
|
},
|
|
LocalizationTable = {
|
|
Contents = true, -- Encoded in the .csv file
|
|
},
|
|
StringValue = {
|
|
Value = true, -- Encoded in the .txt file
|
|
},
|
|
}
|
|
|
|
-- Finds duplicate names among a list of instances
|
|
-- Returns a set of names that appear more than once
|
|
local function findDuplicateNames(children: { Instance }): { [string]: boolean }
|
|
local nameCounts: { [string]: number } = {}
|
|
local duplicates: { [string]: boolean } = {}
|
|
|
|
for _, child in children do
|
|
local name = child.Name
|
|
nameCounts[name] = (nameCounts[name] or 0) + 1
|
|
if nameCounts[name] > 1 then
|
|
duplicates[name] = true
|
|
end
|
|
end
|
|
|
|
return duplicates
|
|
end
|
|
|
|
-- Checks if an instance has duplicate-named siblings
|
|
local function hasDuplicateSiblings(instance: Instance): boolean
|
|
local parent = instance.Parent
|
|
if not parent then
|
|
return false
|
|
end
|
|
|
|
local siblings = parent:GetChildren()
|
|
local duplicates = findDuplicateNames(siblings)
|
|
return duplicates[instance.Name] == true
|
|
end
|
|
|
|
-- Checks if the path to an instance is unique by walking up the tree.
|
|
-- Returns false if ANY ancestor (including the instance itself) has duplicate siblings.
|
|
-- This matches the behavior of is_path_unique in ref_properties.rs
|
|
local function isPathUnique(instance: Instance): boolean
|
|
local current = instance
|
|
|
|
while current do
|
|
-- Check if current has duplicate-named siblings
|
|
if hasDuplicateSiblings(current) then
|
|
return false
|
|
end
|
|
|
|
-- Move up to parent
|
|
current = current.Parent
|
|
|
|
-- Stop at game (DataModel) - we've reached the root
|
|
if current and current == game then
|
|
break
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Forward declaration for recursion
|
|
local encodeInstance
|
|
|
|
-- Encode Attributes if present on any instance
|
|
local function encodeAttributes(instance, properties)
|
|
-- Try to get Attributes - this works for all instance types
|
|
local success, attributes = pcall(function()
|
|
return instance:GetAttributes()
|
|
end)
|
|
|
|
if success and attributes and next(attributes) then
|
|
-- Attributes need to be encoded specially
|
|
-- The server expects them in the properties map under "Attributes"
|
|
local attributeDescriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, "Attributes")
|
|
if attributeDescriptor then
|
|
local encodeSuccess, encodeResult = encodeProperty(instance, "Attributes", attributeDescriptor)
|
|
if encodeSuccess and encodeResult ~= nil then
|
|
properties.Attributes = encodeResult
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Encode Tags if present on any instance
|
|
local function encodeTags(instance, properties)
|
|
-- Try to get Tags - this works for all instance types
|
|
local success, tags = pcall(function()
|
|
return instance:GetTags()
|
|
end)
|
|
|
|
if success and tags and #tags > 0 then
|
|
-- Tags need to be encoded specially
|
|
local tagDescriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, "Tags")
|
|
if tagDescriptor then
|
|
local encodeSuccess, encodeResult = encodeProperty(instance, "Tags", tagDescriptor)
|
|
if encodeSuccess and encodeResult ~= nil then
|
|
properties.Tags = encodeResult
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
encodeInstance = function(instance, parentId, _skipPathCheck)
|
|
-- Log duplicate-named siblings as a debug message (server handles them via
|
|
-- dedup suffixes) but do NOT skip encoding.
|
|
if not _skipPathCheck and not isPathUnique(instance) then
|
|
Log.debug(
|
|
"Instance '{}' ({}) has duplicate-named siblings in path (server will handle via dedup)",
|
|
instance:GetFullName(),
|
|
instance.ClassName
|
|
)
|
|
end
|
|
|
|
-- Log encoding at trace level (very detailed)
|
|
if not _skipPathCheck then
|
|
Log.trace("Encoding for syncback: {} ({})", instance:GetFullName(), instance.ClassName)
|
|
end
|
|
|
|
local properties = {}
|
|
|
|
-- Always try to encode Attributes and Tags for any instance type
|
|
encodeAttributes(instance, properties)
|
|
encodeTags(instance, properties)
|
|
|
|
-- For scripts, encode the Source property first (required for script files)
|
|
if SCRIPT_CLASS_NAMES[instance.ClassName] then
|
|
local sourceDescriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, "Source")
|
|
if sourceDescriptor then
|
|
local encodeSuccess, encodeResult = encodeProperty(instance, "Source", sourceDescriptor)
|
|
if encodeSuccess then
|
|
properties.Source = encodeResult
|
|
else
|
|
Log.warn("Failed to encode Source property for {:?}", instance)
|
|
return nil
|
|
end
|
|
else
|
|
Log.warn("Could not find Source property descriptor for {:?}", instance)
|
|
return nil
|
|
end
|
|
end
|
|
|
|
-- Encode ALL properties for ANY instance type (matching Rust syncback behavior)
|
|
-- The server handles filtering defaults and serialization
|
|
local classDescriptor = RbxDom.findClassDescriptor(instance.ClassName)
|
|
if classDescriptor then
|
|
-- Get class-specific properties to skip
|
|
local classSkipProps = SKIP_PROPERTIES_BY_CLASS[instance.ClassName] or {}
|
|
|
|
for propertyName, propertyMeta in pairs(classDescriptor.properties) do
|
|
-- Skip universally skipped properties
|
|
if SKIP_PROPERTIES[propertyName] then
|
|
continue
|
|
end
|
|
|
|
-- Skip class-specific properties (Source, RunContext for scripts, etc.)
|
|
if classSkipProps[propertyName] then
|
|
continue
|
|
end
|
|
|
|
-- Skip Attributes and Tags since we handle them separately above
|
|
if propertyName == "Attributes" or propertyName == "Tags" then
|
|
continue
|
|
end
|
|
|
|
-- Only encode properties that:
|
|
-- 1. Are readable (ReadWrite or Read scriptability)
|
|
-- 2. Actually serialize (not "DoesNotSerialize")
|
|
local isReadable = propertyMeta.scriptability == "ReadWrite" or propertyMeta.scriptability == "Read"
|
|
local doesSerialize = propertyMeta.serialization ~= "DoesNotSerialize"
|
|
|
|
if isReadable and doesSerialize then
|
|
-- Get the full PropertyDescriptor for encoding
|
|
local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
|
|
if descriptor then
|
|
if UNENCODABLE_DATA_TYPES[descriptor.dataType] then
|
|
continue
|
|
end
|
|
|
|
-- Ref properties cannot be encoded during instance addition
|
|
-- because the target instance has no server ID yet. They are
|
|
-- handled separately by encodePatchUpdate.lua during property
|
|
-- update encoding via the InstanceMap.
|
|
if descriptor.dataType == "Ref" then
|
|
continue
|
|
end
|
|
|
|
local encodeSuccess, encodeResult = encodeProperty(instance, propertyName, descriptor)
|
|
if encodeSuccess and encodeResult ~= nil then
|
|
properties[propertyName] = encodeResult
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Recursively encode children (including those with duplicate names;
|
|
-- the server handles duplicates via dedup suffix system)
|
|
local children = {}
|
|
local childInstances = instance:GetChildren()
|
|
|
|
for _, child in ipairs(childInstances) do
|
|
local encodedChild = encodeInstance(child, nil, true)
|
|
if encodedChild then
|
|
table.insert(children, encodedChild)
|
|
end
|
|
end
|
|
|
|
-- Log property count for debugging
|
|
local propCount = 0
|
|
for _ in pairs(properties) do
|
|
propCount += 1
|
|
end
|
|
Log.trace(" Encoded {} with {} properties, {} children", instance.Name, propCount, #children)
|
|
|
|
return {
|
|
parent = parentId,
|
|
name = instance.Name,
|
|
className = instance.ClassName,
|
|
properties = properties,
|
|
children = children, -- Include children array (may be empty)
|
|
}
|
|
end
|
|
|
|
return function(instance, parentId)
|
|
return encodeInstance(instance, parentId, false)
|
|
end
|