mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-20 20:55:50 +00:00
239 lines
5.6 KiB
Lua
239 lines
5.6 KiB
Lua
local RunService = game:GetService("RunService")
|
|
|
|
local Packages = script.Parent.Parent.Packages
|
|
local Log = require(Packages.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]
|
|
|
|
local descendants = instance:GetDescendants()
|
|
instance:Destroy()
|
|
|
|
-- After the instance is successfully destroyed,
|
|
-- we can remove all the id mappings
|
|
|
|
if id ~= nil then
|
|
self:removeId(id)
|
|
end
|
|
|
|
for _, descendantInstance in descendants do
|
|
self:removeInstance(descendantInstance)
|
|
end
|
|
end
|
|
|
|
function InstanceMap:destroyId(id)
|
|
local instance = self.fromIds[id]
|
|
if instance ~= nil then
|
|
self:destroyInstance(instance)
|
|
else
|
|
-- There is no instance with this id, so we can just remove the id
|
|
-- without worrying about instance destruction
|
|
self:removeId(id)
|
|
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
|