mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
- 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.
198 lines
6.1 KiB
Lua
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
|