forked from rojo-rbx/rojo
* Implement ChangeBatcher * Use ChangeBatcher for two-way sync * Pause updates during patch application * I can English good * Break after encountering a nil Parent change This prevents __flush from erroring out when an instance's Parent is changed to nil and it has other property changes in the same batch. * Update rbx_dom_lua * Don't connect changed listeners in a running game #468 made me realize how bad of an idea this is in general... * Update TestEZ and fix sibling Ref reification test * Add ChangeBatcher tests * Test instance unpausing by breaking functionality out to __cycle * Break up the module a bit and improve tests * Shuffle requires around and edit comment * Break out more stuff, rename createChangePatch -> createPatchSet * Make ChangeBatcher responsible for unpausing all paused instances This somewhat improves the situation (of course, it would preferrable to not have to hack around this problem with Source at all). It also sets us up nicely if we come across any other properties that do anything similar. * Remove old reference to pausedBatchInstances * Use RenderStepped instead of Heartbeat and trash multi-frame pauses I probably should have done this in the first place... ChangeBatcher still needs to unpause instances, but we don't need to hold pauses for any longer than one cycle. * Remove useless branch * if not next(x) -> if next(x) == nil * Add InstanceMap:unpauseAllInstances, use it in ChangeBatcher * Move IsRunning check to InstanceMap:__maybeFireInstanceChanged
236 lines
5.5 KiB
Lua
236 lines
5.5 KiB
Lua
local RunService = game:GetService("RunService")
|
|
|
|
local Log = require(script.Parent.Parent.Log)
|
|
|
|
--[[
|
|
A bidirectional map between instance IDs and Roblox instances. It lets us
|
|
keep track of every instance we know about.
|
|
|
|
TODO: Track ancestry to catch when stuff moves?
|
|
]]
|
|
local InstanceMap = {}
|
|
InstanceMap.__index = InstanceMap
|
|
|
|
function InstanceMap.new(onInstanceChanged)
|
|
local self = {
|
|
-- A map from IDs to instances.
|
|
fromIds = {},
|
|
|
|
-- A map from instances to IDs.
|
|
fromInstances = {},
|
|
|
|
-- A set of all instances that updates should be paused for. This set
|
|
-- should generally be empty, and will be filled by pauseInstance
|
|
-- temporarily.
|
|
pausedUpdateInstances = {},
|
|
|
|
-- A map from instances to a signal or list of signals connected to it.
|
|
instancesToSignal = {},
|
|
|
|
-- Callback that's invoked whenever an instance is changed and it was
|
|
-- not paused.
|
|
onInstanceChanged = onInstanceChanged,
|
|
}
|
|
|
|
return setmetatable(self, InstanceMap)
|
|
end
|
|
|
|
function InstanceMap:size()
|
|
local size = 0
|
|
|
|
for _ in pairs(self.fromIds) do
|
|
size = size + 1
|
|
end
|
|
|
|
return size
|
|
end
|
|
|
|
--[[
|
|
Disconnect all connections and release all instance references.
|
|
]]
|
|
function InstanceMap:stop()
|
|
-- I think this is safe.
|
|
for instance in pairs(self.fromInstances) do
|
|
self:removeInstance(instance)
|
|
end
|
|
end
|
|
|
|
function InstanceMap:__fmtDebug(output)
|
|
output:writeLine("InstanceMap {{")
|
|
output:indent()
|
|
|
|
-- Collect all of the entries in the InstanceMap and sort them by their
|
|
-- label, which helps make our output deterministic.
|
|
local entries = {}
|
|
for id, instance in pairs(self.fromIds) do
|
|
local label = string.format("%s (%s)", instance:GetFullName(), instance.ClassName)
|
|
|
|
table.insert(entries, {id, label})
|
|
end
|
|
|
|
table.sort(entries, function(a, b)
|
|
return a[2] < b[2]
|
|
end)
|
|
|
|
for _, entry in ipairs(entries) do
|
|
output:writeLine("{}: {}", entry[1], entry[2])
|
|
end
|
|
|
|
output:unindent()
|
|
output:write("}")
|
|
end
|
|
|
|
function InstanceMap:insert(id, instance)
|
|
self:removeId(id)
|
|
self:removeInstance(instance)
|
|
|
|
self.fromIds[id] = instance
|
|
self.fromInstances[instance] = id
|
|
self:__connectSignals(instance)
|
|
end
|
|
|
|
function InstanceMap:removeId(id)
|
|
local instance = self.fromIds[id]
|
|
|
|
if instance ~= nil then
|
|
self:__disconnectSignals(instance)
|
|
self.fromIds[id] = nil
|
|
self.fromInstances[instance] = nil
|
|
end
|
|
end
|
|
|
|
function InstanceMap:removeInstance(instance)
|
|
local id = self.fromInstances[instance]
|
|
self:__disconnectSignals(instance)
|
|
|
|
if id ~= nil then
|
|
self.fromInstances[instance] = nil
|
|
self.fromIds[id] = nil
|
|
end
|
|
end
|
|
|
|
function InstanceMap:destroyInstance(instance)
|
|
local id = self.fromInstances[instance]
|
|
|
|
if id ~= nil then
|
|
self:removeId(id)
|
|
end
|
|
|
|
for _, descendantInstance in ipairs(instance:GetDescendants()) do
|
|
self:removeInstance(descendantInstance)
|
|
end
|
|
|
|
instance:Destroy()
|
|
end
|
|
|
|
function InstanceMap:destroyId(id)
|
|
local instance = self.fromIds[id]
|
|
self:removeId(id)
|
|
|
|
if instance ~= nil then
|
|
for _, descendantInstance in ipairs(instance:GetDescendants()) do
|
|
self:removeInstance(descendantInstance)
|
|
end
|
|
|
|
instance:Destroy()
|
|
end
|
|
end
|
|
|
|
--[[
|
|
Pause updates for an instance.
|
|
]]
|
|
function InstanceMap:pauseInstance(instance)
|
|
local id = self.fromInstances[instance]
|
|
|
|
-- If we don't know about this instance, ignore it.
|
|
if id == nil then
|
|
return
|
|
end
|
|
|
|
self.pausedUpdateInstances[instance] = true
|
|
end
|
|
|
|
--[[
|
|
Unpause updates for an instance.
|
|
]]
|
|
function InstanceMap:unpauseInstance(instance)
|
|
self.pausedUpdateInstances[instance] = nil
|
|
end
|
|
|
|
--[[
|
|
Unpause updates for all instances.
|
|
]]
|
|
function InstanceMap:unpauseAllInstances()
|
|
table.clear(self.pausedUpdateInstances)
|
|
end
|
|
|
|
function InstanceMap:__connectSignals(instance)
|
|
-- ValueBase instances have an overriden version of the Changed signal that
|
|
-- only detects changes to their Value property.
|
|
--
|
|
-- We can instead connect listener to each individual property that we care
|
|
-- about on those objects (Name and Value) to emulate the same idea.
|
|
if instance:IsA("ValueBase") then
|
|
local signals = {
|
|
instance:GetPropertyChangedSignal("Name"):Connect(function()
|
|
self:__maybeFireInstanceChanged(instance, "Name")
|
|
end),
|
|
|
|
instance:GetPropertyChangedSignal("Value"):Connect(function()
|
|
self:__maybeFireInstanceChanged(instance, "Value")
|
|
end),
|
|
|
|
instance:GetPropertyChangedSignal("Parent"):Connect(function()
|
|
self:__maybeFireInstanceChanged(instance, "Parent")
|
|
end),
|
|
}
|
|
|
|
self.instancesToSignal[instance] = signals
|
|
else
|
|
self.instancesToSignal[instance] = instance.Changed:Connect(function(propertyName)
|
|
self:__maybeFireInstanceChanged(instance, propertyName)
|
|
end)
|
|
end
|
|
end
|
|
|
|
function InstanceMap:__maybeFireInstanceChanged(instance, propertyName)
|
|
Log.trace("{}.{} changed", instance:GetFullName(), propertyName)
|
|
|
|
if self.pausedUpdateInstances[instance] then
|
|
return
|
|
end
|
|
|
|
if self.onInstanceChanged == nil then
|
|
return
|
|
end
|
|
|
|
if RunService:IsRunning() then
|
|
-- We probably don't want to pick up property changes to save to the
|
|
-- filesystem in a running game.
|
|
return
|
|
end
|
|
|
|
self.onInstanceChanged(instance, propertyName)
|
|
end
|
|
|
|
function InstanceMap:__disconnectSignals(instance)
|
|
local signals = self.instancesToSignal[instance]
|
|
|
|
if signals ~= nil then
|
|
-- In most cases, we only have a single signal, so we avoid keeping
|
|
-- around the extra table. ValueBase objects force us to use multiple
|
|
-- signals to emulate the Instance.Changed event, however.
|
|
if typeof(signals) == "table" then
|
|
for _, signal in ipairs(signals) do
|
|
signal:Disconnect()
|
|
end
|
|
else
|
|
signals:Disconnect()
|
|
end
|
|
|
|
self.instancesToSignal[instance] = nil
|
|
end
|
|
end
|
|
|
|
return InstanceMap
|