Signal Module by sleitnick causes cyclic tables

I am modifying a queueing module I found to use the Signal Module by sleitnick.

Before this, the script used bindable events instanced by the script and worked perfectly fine. I saw that HitboxClass used Signal and I wanted to be able to use that too (since it’s also way easier to work with plus i will be able to :Destroy() connections)

The script worked fine when using BindableEvents, but after using Signal.new() it throws a cyclic table error.

Something interesting to note is that roblox only throws the cyclic table error when I put the queue inside of a table. I was testing stuff out and found this out (My main system put a queue in a table for organization purposes lol)

My question now is, how can I prevent this? The obvious solution is to take the queue out of the table, but i would like to keep it inside the table lmao
Should I be using a different signal module? Anything will help. Thanks

Also I might publish the modified version when its finished but we’ll see ig

--uQueueService--
--Services--
local teleportService = game:GetService("TeleportService")
local runService = game:GetService("RunService")
local players = game:GetService("Players")

--Modules--
local Janitor = require(script:WaitForChild("Janitor"))
local Signal = require(script:WaitForChild("Signal"))

--Private--
export type QueueParams = {
	MinimumPlayers: number,
	MaximumPlayers: number,
	PlaceId: number,
	Countdown: number,
	TeleportData:any
}

local newJanitor = Janitor.new()

local tpOptions = Instance.new("TeleportOptions")
tpOptions.ShouldReserveServer = true

local function teleportQueue(queue,placeId,tpData)
	local success, err = pcall(function()
		table.insert(tpData["PlayerAmt"],#queue)
		tpOptions:SetTeleportData(tpData)
		local result:TeleportAsyncResult = teleportService:TeleportAsync(placeId,queue,tpOptions)
		return result.PrivateServerId
	end)
	if success then
		return true
	else
		return false
	end
end

local function getWholeNum(num)
	if num % 10 == 0 then
		return num / 10
	end
end

--Public--

local methods = {}

function methods:AddPlayer(plr:Player)
	local queued = self.queued
	if plr then
		if not table.find(queued,plr) then
			if #queued < self.maxPlayers then
				print("Inserting player..")
				table.insert(queued,plr)
				print(queued)
				self.PlayerAdded:Fire(plr)
			else
				warn("Queue is full!","Amount of players queued:",#queued,"Maximum players:",self.maxPlayers)
			end
		else
			warn("Player is already in queue!")
		end
	else
		warn("Attempt to add player to queue but player was nil!")
	end
end

function methods:RemovePlayer(plr)
	local queued = self.queued
	if plr then
		local plrIndex = table.find(queued,plr)
		if plrIndex and not self.finished then
			table.remove(queued,plrIndex)
			self.PlayerRemoved:Fire(plr)
		end
	else
		warn("Attempt to remove player from queue but player was nil")
	end
end

function methods:Teleport()
	if self.placeId then
		local success = teleportQueue(self.queued,self.placeId, self.tpData)
		if not success then
			warn("Players could not be teleported")
		end
	end
end

function methods:Destroy()
	newJanitor:Cleanup()
	
	setmetatable(self,nil)
	table.clear(self)
end

function methods:Reset()
	self.minPlayers = nil
	self.maxPlayers = nil
	self.placeId = nil
	self.countdown = nil
	self.tpData = nil
end

local QueueService = {}

function QueueService.new(queueParams:QueueParams)
	local object = {
		--Events--
		PlayerAdded = Signal.new(),
		PlayerRemoved = Signal.new(),
		Initiated = Signal.new(),
		CountTick = Signal.new(),

		--Properties--
		minPlayers = queueParams["MinimumPlayers"],
		maxPlayers = queueParams["MaximumPlayers"],
		placeId = queueParams["PlaceId"],
		countdown = queueParams["Countdown"],
		tpData = queueParams["TeleportData"],
		ticking = false,
		queued = {},
		connections = {}
	}
	
	newJanitor:Add(object.PlayerAdded,"Destroy")
	newJanitor:Add(object.PlayerRemoved,"Destroy")
	newJanitor:Add(object.Initiated,"Destroy")
	newJanitor:Add(object.CountTick,"Destroy")
	
	if object.maxPlayers == nil then
		object.maxPlayers = 999
	end
	if object.countdown == nil then
		object.countdown = 5
	end
	
	assert(object.minPlayers,"Need to provide minimum amount of players!")
	
	local left = players.PlayerRemoving:Connect(function(plr)
		local index = table.find(object.queued,plr)
		if index then
			object.PlayerRemoved:Fire(plr)
			table.remove(object.queued,index)
		else
			warn("Could not remove player from queue: Player not found in queue!")
		end
	end)
	
	local heartbeat = runService.Heartbeat:Connect(function()
		--print(#object.queued < object.minPlayers, #object.queued, object.minPlayers)
		
		if #object.queued < object.minPlayers then
			object.ticking = false
		end
		
		if #object.queued >= object.minPlayers and not object.ticking then
			task.spawn(function()
				object.ticking = true
				print("firing tickStarted",object.ticking)

				for i = object.countdown * 10,0,-1 do
					wait(0.1)
					--print(object.ticking,i)
					if not object.ticking then break end
					local num = getWholeNum(i)
					if num then
						object.CountTick:Fire(num)
					end
				end

				if not object.ticking then return end

				object.Initiated:Fire()

				--print(object.placeId)
				if runService:IsStudio() then
					warn("Running Studio. Player will not be teleported")
				else
					if object.placeId then
						teleportQueue(object.queued,object.placeId, object.tpData)
					end
				end
			end)
		end
	end)
	
	heartbeat = newJanitor:Add(heartbeat,"Disconnect")
	left = newJanitor:Add(left,"Disconnect")
	
	return setmetatable(object,{
		__index = methods,
		__tostring = function() return "Queue" end
	})
end

return QueueService
-- A snippet of code in the main queueing system -- 
local QueueParams:QueueService.QueueParams = {
	MinimumPlayers = params["Players"],
	PlaceId = placeId,
	TeleportData = {
		Params = params
	}
}
	
local newQueue = QueueService.new(QueueParams)
	
Queue = {
	["Params"] = params,
	["Queue"] = newQueue,
	["Host"] = firstPlr,
}
2 Likes

You could try Roblox’s signal library:

As for the cyclic table error, I’m not sure. As you might already know, the error comes from storing a table within itself, which Roblox can’t handle when it encodes the table. I can’t find where that would be happening in your code.

The BindableEvents are probably different because they’re encoded as references, so when Roblox encodes the table, their data is just some kind of reference to the instance and not the actual data.

It’s possible the version of sleitnick’s signal you’re using hasn’t been updated for deffered events (I recall that broke quite a few signal implementations) though I don’t know why that would result in the error you’re seeing.

1 Like

Actually, I think the signal module does have a :FireDeferred() method

Btw how do i use this library lol

1 Like

I am still stuck at a dead end. Maybe i will try using bindable events and making a special function to make connections, because I don’t know how else to do this.

I found out how to replicate the error. Just fire a RemoteEvent with the queue as a parameter and it causes the error. I’ll move the module into ReplicatedStorage and see if it works

I asked the creator of HitboxClass and it seems I should just be sending properties of the OOP object through instead of the actual object, since there are several factors that cause errors when trying to run tables through a RemoteEvent. (It works with BindableEvents though)

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.