- Added a new section in audit.md to enforce that all incoming changes must pass through the review dialogue, ensuring user approval for every modification. - Introduced a comprehensive plan for the Reconciler overhaul, addressing issues with MeshPart creation, MeshId swapping, and CSG reconstruction, modeled after the rbxsync implementation. - Detailed todos for verifying Tags/Attributes, handling MeshPart creation via AssetService, and ensuring proper CSG part handling during synchronization. These changes improve the stability and user control of the reconciliation process, enhancing overall plugin functionality.
14 KiB
| name | overview | todos | isProject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Reconciler robustness overhaul | Fix the Atlas plugin Reconciler to handle MeshPart creation via AssetService, MeshId swapping, and CSG reconstruction -- modeled after the proven rbxsync Sync.luau / CSGHandler.luau implementation. |
|
false |
Reconciler Robustness Overhaul
Tags/Attributes: Already Working (Verification Only)
Tags and Attributes ARE correctly handled through the RbxDom customProperties.lua system (plugin/rbx_dom_lua/customProperties.lua lines 51-120):
- Tags (lines 97-120): Uses
CollectionService:GetTags()for read,CollectionService:AddTag()/RemoveTag()for write. Properly diffs existing tags against new tags. - Attributes (lines 51-96): Uses
instance:GetAttributes()for read,instance:SetAttribute()for write. Clears removed attributes. Skips reservedRBXprefixed names.
They arrive in the Properties map from the server (e.g. Properties.Tags, Properties.Attributes) and flow through setProperty -> RbxDom PropertyDescriptor:write -> custom handler. The property application order within a single instance doesn't matter per user confirmation.
No code changes needed -- just verify during implementation that this path is exercised.
Problem 1: MeshPart Creation Fails Silently
File: plugin/src/Reconciler/reify.lua line 63
local createSuccess, instance = pcall(Instance.new, virtualInstance.ClassName)
Instance.new("MeshPart") creates an instance with empty MeshId. MeshId is read-only after creation in plugins. The server sends MeshId in Properties, but setProperty fails silently (property added to unappliedProperties). Result: every MeshPart synced from server has no mesh geometry.
rbxsync reference: Sync.luau lines 624-681 use AssetService:CreateMeshPartAsync(meshId), then apply remaining properties with MeshId excluded.
Fix in reify.lua: Before Instance.new, check for MeshPart and use AssetService:CreateMeshPartAsync:
local instance
local meshIdAlreadySet = false
if virtualInstance.ClassName == "MeshPart" then
local meshIdValue = virtualInstance.Properties.MeshId
if meshIdValue then
local decodeSuccess, meshId = decodeValue(meshIdValue, instanceMap)
if decodeSuccess and type(meshId) == "string" and meshId ~= "" then
local AssetService = game:GetService("AssetService")
local ok, meshPart = pcall(function()
return AssetService:CreateMeshPartAsync(meshId)
end)
if ok and meshPart then
instance = meshPart
meshIdAlreadySet = true
end
end
end
end
if instance == nil then
local createSuccess
createSuccess, instance = pcall(Instance.new, virtualInstance.ClassName)
if not createSuccess then
addAllToPatch(unappliedPatch, virtualInstances, id)
return
end
end
Then in the property loop, skip MeshId if already set:
if meshIdAlreadySet and propertyName == "MeshId" then
continue
end
Problem 2: MeshId Swap on Existing MeshPart
File: plugin/src/Reconciler/applyPatch.lua lines 250-277
When an update changes MeshId on an existing MeshPart, setProperty fails (read-only). The property ends up in unappliedUpdate -- the MeshPart keeps the old mesh.
rbxsync reference: Sync.luau lines 852-941 detect MeshId change, recreate via CreateMeshPartAsync, copy properties/children, destroy old.
Fix in applyPatch.lua: Add a MeshPart recreation block before the normal update logic:
-- Before the normal property update loop:
if instance:IsA("MeshPart") and update.changedProperties
and update.changedProperties.MeshId then
local decodeSuccess, newMeshId = decodeValue(update.changedProperties.MeshId, instanceMap)
if decodeSuccess and type(newMeshId) == "string" and newMeshId ~= "" then
local AssetService = game:GetService("AssetService")
local ok, newMeshPart = pcall(function()
return AssetService:CreateMeshPartAsync(newMeshId)
end)
if ok and newMeshPart then
-- Apply name
newMeshPart.Name = update.changedName or instance.Name
-- Apply changed properties (excluding MeshId)
for propName, propValue in pairs(update.changedProperties) do
if propName ~= "MeshId" then
local ds, dv = decodeValue(propValue, instanceMap)
if ds then setProperty(newMeshPart, propName, dv) end
end
end
-- Move children
for _, child in ipairs(instance:GetChildren()) do
child.Parent = newMeshPart
end
-- Swap in instanceMap
local parent = instance.Parent
newMeshPart.Parent = parent
instanceMap:insert(update.id, newMeshPart)
instance.Parent = nil -- Not Destroy(), allows undo
continue -- Skip normal update path
end
end
end
Problem 3: CSG Parts Created as Empty Shells
Root Cause
The server filters out CSG geometry data before sending to the plugin:
**MeshData**(BinaryString) -- can't be serialized to JSON/MessagePack (src/web/api.rs line 3606-3608)**PhysicalConfigData**(SharedString) -- explicitly filtered (src/web/interface.rs lines 147-153)
Even if these bytes reached the plugin, they're not scriptable -- no Roblox API exists to set MeshData or PhysicalConfigData on instances from Lua. No plugin API exists to load raw .rbxm bytes into instances either.
The only way to create CSG geometry in Studio is BasePart:UnionAsync() / BasePart:SubtractAsync() from component parts. This is exactly what rbxsync does.
CSG Data Flow
┌─────────────────────────────────────────────────────────────────────┐
│ TWO-WAY SYNC (Studio → Server → Studio) │
│ │
│ 1. User edits CSG in Studio │
│ 2. Plugin detects change via ChangeBatcher │
│ 3. Plugin calls Plugin:Separate() to decompose → component parts │
│ 4. Plugin encodes component parts + CSG metadata │
│ 5. Server stores component data alongside instance │
│ 6. On next forward sync: server sends component data │
│ 7. Plugin reconstructs via UnionAsync/SubtractAsync │
│ │
│ LIMITATION: .rbxm-sourced CSG that was NEVER synced from Studio │
│ will appear as empty shells until first two-way sync. │
│ A warning is logged for these cases. │
└─────────────────────────────────────────────────────────────────────┘
Implementation: 4 parts
Part A: Port CSGHandler module
Create plugin/src/CSGHandler.lua, ported from rbxsync CSGHandler.luau.
Key functions:
CSGHandler.isCSGOperation(instance)-- check if UnionOperation/IntersectOperation/NegateOperationCSGHandler.separate(pluginInstance, union)-- decompose viaPlugin:Separate(), returns{addParts, subtractParts}CSGHandler.reconstruct(basePart, additionalParts, subtractParts)-- rebuild viaUnionAsync/SubtractAsync
Part B: CSG decomposition in two-way sync (Plugin → Server)
File: plugin/src/ChangeBatcher/encodeInstance.lua
When encoding a CSG instance for syncback, decompose it:
- Call
CSGHandler.separate(plugin, instance)to get component parts - Encode each component part as a child instance under a special
_csgkey - Include CSG metadata (operation types: add vs subtract)
- Send to server as part of the normal write patch
The server write handler (src/web/api.rs) needs to recognize the _csg data and store it.
Part C: Server-side CSG storage and retrieval
File: src/web/api.rs and src/change_processor.rs
When the server receives CSG component data via /api/write:
- Store component part snapshots alongside the CSG instance (e.g., as metadata or in a
_csg/subdirectory in the snapshot tree) - During forward sync, when sending a CSG instance to the plugin, include the component data in the instance's properties/metadata
This needs a new mechanism -- CSG component data is not a normal property. Options:
- Add a
csgComponentsfield to theInstancestruct in src/web/interface.rs - Or encode as a special metadata entry in
InstanceMetadata
Part D: CSG reconstruction in reify.lua
File: plugin/src/Reconciler/reify.lua and plugin/src/Reconciler/applyPatch.lua
When reifying a CSG instance that has component data:
- Instead of
Instance.new("UnionOperation"), queue it for deferred CSG reconstruction - Create the component parts first (as normal instances, unparented)
- After all instances are created, reconstruct CSG via
CSGHandler.reconstruct() - Apply properties (skip CFrame -- geometry defines position, per rbxsync line 1499-1509)
- Parent the reconstructed union
For CSG instances WITHOUT component data (from .rbxm, never two-way synced):
- Create the empty shell via
Instance.new()as today - Log a warning: "CSG instance 'X' has no component data -- appears as empty shell"
Implementation Order
The changes are independent enough to implement incrementally:
- MeshPart creation (reify.lua) -- immediate high-value fix, no server changes
- MeshId swap (applyPatch.lua) -- immediate high-value fix, no server changes
- CSGHandler module (new file) -- pure plugin code, self-contained
- CSG warning (reify.lua) -- trivial addition
- CSG decomposition (encodeInstance.lua + server) -- requires server changes
- CSG server storage (api.rs + interface.rs) -- requires protocol thinking
- CSG reconstruction (reify.lua) -- requires steps 5+6
Steps 1-4 can ship as one PR. Steps 5-7 require more design and could be a separate PR.
Files Modified
| File | Changes |
|---|---|
| plugin/src/Reconciler/reify.lua | MeshPart creation, CSG warning, CSG deferred reconstruction |
| plugin/src/Reconciler/applyPatch.lua | MeshId swap detection + recreation |
| plugin/src/CSGHandler.lua | NEW -- ported from rbxsync |
| plugin/src/ChangeBatcher/encodeInstance.lua | CSG decomposition during two-way sync |
| src/web/interface.rs | CSG component data in Instance struct |
| src/web/api.rs | CSG storage on write, CSG data on read |
Files Verified (no changes)
| File | Status |
|---|---|
| plugin/rbx_dom_lua/customProperties.lua | Tags (lines 97-120) and Attributes (lines 51-96) correctly handled |
| plugin/src/Reconciler/setProperty.lua | Routes to RbxDom descriptors, including custom handlers |
| plugin/src/Reconciler/decodeValue.lua | Decodes all property types via RbxDom |
| plugin/src/Reconciler/diff.lua | Compares Tags/Attributes via Properties map |