local Q={workspace,Players}local I={}I.__index=I local v={}local function X(Q,I)local v=#Q Q[I]=Q[v]Q[v]=nil end local function M(I)for Q,v in ipairs(Q)do if I:IsDescendantOf(v)then return true end end return false end function I.FromTag(Q)return v[Q]end function I.Auto(Q)local function v(Q)local v=require(Q)assert(type(v)=="\116\097\098\108\101","\069\120\112\101\099\116\101\100 \116\097\098\108\101 \102\111\114 \099\111\109\112\111\110\101\110\116")assert(type(v.Tag)=="\115\116\114\105\110\103","\069\120\112\101\099\116\101\100 \046\084\097\103 \112\114\111\112\101\114\116\121")I.new(v.Tag,v,v.RenderPriority)end for Q,I in ipairs(Q:GetDescendants())do if I:IsA("\077\111\100\117\108\101\083\099\114\105\112\116")then v(I)end end Q.DescendantAdded:Connect(function(Q)if Q:IsA("\077\111\100\117\108\101\083\099\114\105\112\116")then v(Q)end end)end function I.new(Q,X,O)assert(type(Q)=="\115\116\114\105\110\103","\065\114\103\117\109\101\110\116 \035\049 \040\116\097\103\041 \115\104\111\117\108\100 \098\101 \097 \115\116\114\105\110\103\059 \103\111\116 "..type(Q))assert(type(X)=="\116\097\098\108\101","\065\114\103\117\109\101\110\116 \035\050 \040\099\108\097\115\115\041 \115\104\111\117\108\100 \098\101 \097 \116\097\098\108\101\059 \103\111\116 "..type(X))assert(type(X.new)=="\102\117\110\099\116\105\111\110","\067\108\097\115\115 \109\117\115\116 \099\111\110\116\097\105\110 \097 \046\110\101\119 \099\111\110\115\116\114\117\099\116\111\114 \102\117\110\099\116\105\111\110")assert(type(X.Destroy)=="\102\117\110\099\116\105\111\110","\067\108\097\115\115 \109\117\115\116 \099\111\110\116\097\105\110 \097 \058\068\101\115\116\114\111\121 \102\117\110\099\116\105\111\110")assert(v[Q]==nil,"\067\111\109\112\111\110\101\110\116 \097\108\114\101\097\100\121 \098\111\117\110\100 \116\111 \116\104\105\115 \116\097\103")local r=setmetatable({},I)r.Added=Signal.new()r.Removed=Signal.new()r._maid=Maid.new()r._lifecycleMaid=Maid.new()r._tag=Q r._class=X r._objects={}r._instancesToObjects={}r._hasHeartbeatUpdate=type(X.HeartbeatUpdate)=="\102\117\110\099\116\105\111\110"r._hasSteppedUpdate=type(X.SteppedUpdate)=="\102\117\110\099\116\105\111\110"r._hasRenderUpdate=type(X.RenderUpdate)=="\102\117\110\099\116\105\111\110"r._hasInit=type(X.Init)=="\102\117\110\099\116\105\111\110"r._hasDeinit=type(X.Deinit)=="\102\117\110\099\116\105\111\110"r._renderPriority=O or Enum.RenderPriority.Last.Value r._lifecycle=false r._nextId=0 r._maid:GiveTask((CollectionService:GetInstanceAddedSignal(Q)):Connect(function(Q)if M(Q)then r:_instanceAdded(Q)end end))r._maid:GiveTask((CollectionService:GetInstanceRemovedSignal(Q)):Connect(function(Q)r:_instanceRemoved(Q)end))r._maid:GiveTask(r._lifecycleMaid)do local I=Instance.new("\066\105\110\100\097\098\108\101\069\118\101\110\116")for Q,v in ipairs(CollectionService:GetTagged(Q))do if M(v)then local Q=I.Event:Connect(function()r:_instanceAdded(v)end)I:Fire()Q:Disconnect()end end I:Destroy()end v[Q]=r r._maid:GiveTask(function()v[Q]=nil end)return r end function I._startHeartbeatUpdate(Q)local I=Q._objects Q._heartbeatUpdate=RunService.Heartbeat:Connect(function(Q)for I,v in ipairs(I)do v:HeartbeatUpdate(Q)end end)Q._lifecycleMaid:GiveTask(Q._heartbeatUpdate)end function I._startSteppedUpdate(Q)local I=Q._objects Q._steppedUpdate=RunService.Stepped:Connect(function(Q,v)for Q,I in ipairs(I)do I:SteppedUpdate(v)end end)Q._lifecycleMaid:GiveTask(Q._steppedUpdate)end function I._startRenderUpdate(Q)local I=Q._objects Q._renderName=Q._tag.."\082\101\110\100\101\114\085\112\100\097\116\101"RunService:BindToRenderStep(Q._renderName,Q._renderPriority,function(Q)for I,v in ipairs(I)do v:RenderUpdate(Q)end end)Q._lifecycleMaid:GiveTask(function()RunService:UnbindFromRenderStep(Q._renderName)end)end function I._startLifecycle(Q)Q._lifecycle=true if Q._hasHeartbeatUpdate then Q:_startHeartbeatUpdate()end if Q._hasSteppedUpdate then Q:_startSteppedUpdate()end if Q._hasRenderUpdate then Q:_startRenderUpdate()end end function I._stopLifecycle(Q)Q._lifecycle=false Q._lifecycleMaid:DoCleaning()end function I._instanceAdded(I,Q)if I._instancesToObjects[Q]then return end if not I._lifecycle then I:_startLifecycle()end I._nextId=I._nextId+1 local v=I._tag..tostring(I._nextId)if IS_SERVER then Q:SetAttribute(ATTRIBUTE_ID_NAME,v)end local X=I._class.new(Q)X.Instance=Q X._id=v I._instancesToObjects[Q]=X table.insert(I._objects,X)if I._hasInit then Thread.Spawn(function()if I._instancesToObjects[Q]~=X then return end X:Init()end)end I.Added:Fire(X)return X end function I._instanceRemoved(I,Q)I._instancesToObjects[Q]=nil for v,M in ipairs(I._objects)do if M.Instance==Q then if I._hasDeinit then M:Deinit()end if IS_SERVER and(Q.Parent and Q:GetAttribute(ATTRIBUTE_ID_NAME)~=nil)then Q:SetAttribute(ATTRIBUTE_ID_NAME,nil)end I.Removed:Fire(M)M:Destroy()M._destroyed=true X(I._objects,v)break end end if#I._objects==0 and I._lifecycle then I:_stopLifecycle()end end function I.GetAll(Q)return TableUtil.CopyShallow(Q._objects)end function I.GetFromInstance(I,Q)return I._instancesToObjects[Q]end function I.GetFromID(I,Q)for I,v in ipairs(I._objects)do if v._id==Q then return v end end return nil end function I.Filter(I,Q)return TableUtil.Filter(I._objects,Q)end function I.WaitFor(v,Q,I)local X=type(Q)=="\115\116\114\105\110\103"local function M(I)return X and I.Instance.Name==Q or not X and I.Instance==Q end for Q,I in ipairs(v._objects)do if M(I)then return Promise.resolve(I)end end local O=nil return((Promise.FromEvent(v.Added,function(Q)O=Q return M(Q)end)):Then(function()return O end)):Timeout(I or DEFAULT_WAIT_FOR_TIMEOUT)end function I.Destroy(Q)Q._maid:Destroy()end return I

local DESCENDANT_WHITELIST = {workspace, Players}

local Component = {}
Component.__index = Component

local componentsByTag = {}


local function FastRemove(tbl, i)
	local n = #tbl
	tbl[i] = tbl[n]
	tbl[n] = nil
end


local function IsDescendantOfWhitelist(instance)
	for _,v in ipairs(DESCENDANT_WHITELIST) do
		if (instance:IsDescendantOf(v)) then
			return true
		end
	end
	return false
end


function Component.FromTag(tag)
	return componentsByTag[tag]
end


function Component.Auto(folder)
	local function Setup(moduleScript)
		local m = require(moduleScript)
		assert(type(m) == "table", "Expected table for component")
		assert(type(m.Tag) == "string", "Expected .Tag property")
		Component.new(m.Tag, m, m.RenderPriority)
	end
	for _,v in ipairs(folder:GetDescendants()) do
		if (v:IsA("ModuleScript")) then
			Setup(v)
		end
	end
	folder.DescendantAdded:Connect(function(v)
		if (v:IsA("ModuleScript")) then
			Setup(v)
		end
	end)
end


function Component.new(tag, class, renderPriority)

	assert(type(tag) == "string", "Argument #1 (tag) should be a string; got " .. type(tag))
	assert(type(class) == "table", "Argument #2 (class) should be a table; got " .. type(class))
	assert(type(class.new) == "function", "Class must contain a .new constructor function")
	assert(type(class.Destroy) == "function", "Class must contain a :Destroy function")
	assert(componentsByTag[tag] == nil, "Component already bound to this tag")

	local self = setmetatable({}, Component)

	self.Added = Signal.new()
	self.Removed = Signal.new()

	self._maid = Maid.new()
	self._lifecycleMaid = Maid.new()
	self._tag = tag
	self._class = class
	self._objects = {}
	self._instancesToObjects = {}
	self._hasHeartbeatUpdate = (type(class.HeartbeatUpdate) == "function")
	self._hasSteppedUpdate = (type(class.SteppedUpdate) == "function")
	self._hasRenderUpdate = (type(class.RenderUpdate) == "function")
	self._hasInit = (type(class.Init) == "function")
	self._hasDeinit = (type(class.Deinit) == "function")
	self._renderPriority = renderPriority or Enum.RenderPriority.Last.Value
	self._lifecycle = false
	self._nextId = 0

	self._maid:GiveTask(CollectionService:GetInstanceAddedSignal(tag):Connect(function(instance)
		if (IsDescendantOfWhitelist(instance)) then
			self:_instanceAdded(instance)
		end
	end))

	self._maid:GiveTask(CollectionService:GetInstanceRemovedSignal(tag):Connect(function(instance)
		self:_instanceRemoved(instance)
	end))

	self._maid:GiveTask(self._lifecycleMaid)

	do
		local b = Instance.new("BindableEvent")
		for _,instance in ipairs(CollectionService:GetTagged(tag)) do
			if (IsDescendantOfWhitelist(instance)) then
				local c = b.Event:Connect(function()
					self:_instanceAdded(instance)
				end)
				b:Fire()
				c:Disconnect()
			end
		end
		b:Destroy()
	end

	componentsByTag[tag] = self
	self._maid:GiveTask(function()
		componentsByTag[tag] = nil
	end)

	return self

end


function Component:_startHeartbeatUpdate()
	local all = self._objects
	self._heartbeatUpdate = RunService.Heartbeat:Connect(function(dt)
		for _,v in ipairs(all) do
			v:HeartbeatUpdate(dt)
		end
	end)
	self._lifecycleMaid:GiveTask(self._heartbeatUpdate)
end


function Component:_startSteppedUpdate()
	local all = self._objects
	self._steppedUpdate = RunService.Stepped:Connect(function(_, dt)
		for _,v in ipairs(all) do
			v:SteppedUpdate(dt)
		end
	end)
	self._lifecycleMaid:GiveTask(self._steppedUpdate)
end


function Component:_startRenderUpdate()
	local all = self._objects
	self._renderName = (self._tag .. "RenderUpdate")
	RunService:BindToRenderStep(self._renderName, self._renderPriority, function(dt)
		for _,v in ipairs(all) do
			v:RenderUpdate(dt)
		end
	end)
	self._lifecycleMaid:GiveTask(function()
		RunService:UnbindFromRenderStep(self._renderName)
	end)
end


function Component:_startLifecycle()
	self._lifecycle = true
	if (self._hasHeartbeatUpdate) then
		self:_startHeartbeatUpdate()
	end
	if (self._hasSteppedUpdate) then
		self:_startSteppedUpdate()
	end
	if (self._hasRenderUpdate) then
		self:_startRenderUpdate()
	end
end


function Component:_stopLifecycle()
	self._lifecycle = false
	self._lifecycleMaid:DoCleaning()
end


function Component:_instanceAdded(instance)
	if (self._instancesToObjects[instance]) then return end
	if (not self._lifecycle) then
		self:_startLifecycle()
	end
	self._nextId = (self._nextId + 1)
	local id = (self._tag .. tostring(self._nextId))
	if (IS_SERVER) then
		instance:SetAttribute(ATTRIBUTE_ID_NAME, id)
	end
	local obj = self._class.new(instance)
	obj.Instance = instance
	obj._id = id
	self._instancesToObjects[instance] = obj
	table.insert(self._objects, obj)
	if (self._hasInit) then
		Thread.Spawn(function()
			if (self._instancesToObjects[instance] ~= obj) then return end
			obj:Init()
		end)
	end
	self.Added:Fire(obj)
	return obj
end


function Component:_instanceRemoved(instance)
	self._instancesToObjects[instance] = nil
	for i,obj in ipairs(self._objects) do
		if (obj.Instance == instance) then
			if (self._hasDeinit) then
				obj:Deinit()
			end
			if (IS_SERVER and instance.Parent and instance:GetAttribute(ATTRIBUTE_ID_NAME) ~= nil) then
				instance:SetAttribute(ATTRIBUTE_ID_NAME, nil)
			end
			self.Removed:Fire(obj)
			obj:Destroy()
			obj._destroyed = true
			FastRemove(self._objects, i)
			break
		end
	end
	if (#self._objects == 0 and self._lifecycle) then
		self:_stopLifecycle()
	end
end


function Component:GetAll()
	return TableUtil.CopyShallow(self._objects)
end


function Component:GetFromInstance(instance)
	return self._instancesToObjects[instance]
end


function Component:GetFromID(id)
	for _,v in ipairs(self._objects) do
		if (v._id == id) then
			return v
		end
	end
	return nil
end


function Component:Filter(filterFunc)
	return TableUtil.Filter(self._objects, filterFunc)
end


function Component:WaitFor(instance, timeout)
	local isName = (type(instance) == "string")
	local function IsInstanceValid(obj)
		return ((isName and obj.Instance.Name == instance) or ((not isName) and obj.Instance == instance))
	end
	for _,obj in ipairs(self._objects) do
		if (IsInstanceValid(obj)) then
			return Promise.resolve(obj)
		end
	end
	local lastObj = nil
	return Promise.FromEvent(self.Added, function(obj)
		lastObj = obj
		return IsInstanceValid(obj)
	end):Then(function()
		return lastObj
	end):Timeout(timeout or DEFAULT_WAIT_FOR_TIMEOUT)
end


function Component:Destroy()
	self._maid:Destroy()
end


return Component
