rojo/plugin/src/ChangeBatcher/encodeInstance.lua
Joe 9d8762bbaf Refactor encoding logic in ChangeBatcher and enhance duplicate handling
- Updated `encodeInstance` and `encodeService` to utilize helper functions for encoding attributes and tags, improving code organization and readability.
- Changed duplicate handling to allow encoding of instances with duplicate-named siblings, with the server managing name collisions through a dedup suffix system.
- Added debug logging for duplicate detection to aid in diagnostics.
- Streamlined property encoding by implementing a helper function to iterate over encodable properties, enhancing maintainability and consistency.

These changes improve the clarity and efficiency of the encoding process while ensuring accurate handling of duplicate instances.
2026-02-18 23:02:43 -06:00

198 lines
6.1 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 are still encoded and sent to the
server. The server handles filesystem name collisions via the dedup suffix
system (~2, ~3, etc.) in syncback/file_names.rs.
- A debug message is logged when duplicates are detected (for diagnostics only).
]]
local Packages = script.Parent.Parent.Parent.Packages
local Log = require(Packages.Log)
local RbxDom = require(Packages.RbxDom)
local encodeProperty = require(script.Parent.encodeProperty)
local Helpers = require(script.Parent.encodeHelpers)
-- Script class names that need Source property
local SCRIPT_CLASS_NAMES = {
Script = true,
LocalScript = true,
ModuleScript = 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
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 = {}
Helpers.encodeAttributes(instance, properties)
Helpers.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 classSkipProps = SKIP_PROPERTIES_BY_CLASS[instance.ClassName]
Helpers.forEachEncodableProperty(instance.ClassName, classSkipProps, function(propertyName, descriptor)
-- Ref properties cannot be encoded during instance addition because the
-- target instance has no server ID yet. They are handled separately by
-- encodePatchUpdate.lua via the InstanceMap.
if descriptor.dataType == "Ref" then
return
end
local encodeSuccess, encodeResult = encodeProperty(instance, propertyName, descriptor)
if encodeSuccess and encodeResult ~= nil then
properties[propertyName] = encodeResult
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