Chair/Table lobby system unexpected behavior

i have rewritte this 3 times and am losing my mind ;C

its a tower defense game lobby system player(s) sit at a table on chairs and teleport in the game:

  • i have a client script checking if a player hits the chair and is allowed to sit there
    if yes it fires a remote asking the server to sit on the chair
  • then the server checks if player is allowed to sit there and seat the player to the chair

i have no unseat mechanic since i listen to the occupant changed event to reset the chair options to allow other player to sit on it later on

the problem is the moment a player jumps and stand up all other players in the game unseat from other chairs and unseated player teleport the that chair and sit in it i did not make it that way but for some random reason it tries to sit all players in the game to the seat a player just jumped out of

i have no explenation the client remote is not firing for all players so not that fault and the server remote does fire for all players even tho its not coded that way ??? i don’t know what i am missing here and it is driving me insane sorry for the long paragraph

demostation:

client script

local player = game:GetService("Players").LocalPlayer
local map = game:GetService("Workspace"):FindFirstChild("map")
local tables = map:FindFirstChild("tables")
local remotes = game:GetService("ReplicatedStorage"):FindFirstChild("remotes")
local debounce = false
for _, chairHitbox in pairs(tables:GetDescendants()) do
	if string.match(chairHitbox.Name,"seatBox") and chairHitbox:IsA("BasePart") then
		chairHitbox.Touched:connect(function(hit)
			if debounce == false and player:GetAttribute("sitting") == false then
				if hit.Parent:FindFirstChild("Humanoid") then
					debounce = true
					--print(player,"hits seatBoxparent humanoid ",chairHitbox)
					if chairHitbox.chair.Value ~= nil then
						--print(player,"hits seatBoxparent chair exist",chairHitbox)
						if player:GetAttribute("sitting") == false then
							--print(player,"ask server to sit on chair",chairHitbox)
							remotes.chair:FireServer(chairHitbox)
						else
							--print(player,"not allowed to sit on chair",chairHitbox)
						end
					else
						--print(player,"chair not exist for seatBox",chairHitbox)
					end
				else
					--print(player,"no Humanoid",chairHitbox)
				end
				task.wait(2)
				debounce = false
			end
		end)
	end
end

server remote
listening

local rs = game:GetService("ReplicatedStorage")
local tables = game:GetService("Workspace"):WaitForChild("map"):WaitForChild("table")
local players = game:GetService("Players"):GetPlayers()
local assets = rs:WaitForChild("assets")
local module = game:GetService("ServerScriptService"):WaitForChild("modules")
local codex = require(rs:WaitForChild("codex"))
local scale = require(module.scale)
local maps = codex.maps
local tableData = {}
local hitboxToTableDataReference = {} -- linking hitboxes client touches to tablegroup as link to access table data from a chair touch

local remotes = rs:WaitForChild("remotes")
local debounce = {}

local playerDataFolder = function(player) return rs:WaitForChild("playerData"):WaitForChild(player.UserId) end
local getPlayerFromHumanoid = function(humanoid) return game:GetService("Players"):GetPlayerFromCharacter(humanoid.Parent) end
local timerTime = 30

--


local function changeTableMap(tableGroup,mapId)
	if mapId then
		--change to specific map
		tableGroup["map"] = maps[mapId]
	else
		-- get random map
		local lastMap = tableGroup["map"]["name"]
		repeat tableGroup["map"] = maps[math.random(1,#maps)] until lastMap ~= tableGroup["map"]["name"]
	end
	local minimapHolder = tableGroup["model"].minimap
	--cleaning minimap
	for _, object in pairs(minimapHolder:GetChildren()) do
		object:Destroy()
	end
	local newMinimap = assets.minimap:FindFirstChild(tableGroup["map"]["name"]):Clone()
	newMinimap.PrimaryPart.Transparency = 1
	--newMinimap:SetPrimaryPartCFrame(tableGroup["model"].map.CFrame)
	scale(newMinimap,tableGroup["tableScale"],tableGroup["model"].map.CFrame)
	newMinimap.Parent = minimapHolder

	local minimapStats = assets.gui.minimapStats:Clone()
	minimapStats.mapName.value.Text = tableGroup["map"]["name"]
	minimapStats.timer.value.Text = timerTime
	minimapStats.Parent = newMinimap.PrimaryPart
	minimapStats.Adornee = newMinimap.PrimaryPart
	tableGroup["mapRoot"] = minimapStats
	tableGroup["timer"][2] = true
	-- change map and minmap to new random picked map
	-- keep changing untill not same map twice in a row
end




local function updateSeatState(chairHitBox,tableGroup)
	--print("seat change UPDATE")
	if chairHitBox.Name == "seatBox" then
		if chairHitBox.chair and chairHitBox.chair.Value ~= nil then
			if chairHitBox.chair.Value.seat.Occupant then
				--has occupant
				--print("chair as occupant",getPlayerFromHumanoid(chairHitBox.chair.Value.seat.Occupant))
			else
				--print("chair empty")
				--no occupant
			end
		end
		local tempPlayerList,hostStillThere = {},false
		for _, chair in pairs(tableGroup["chairs"]) do
			if chair.chair.Value and chair.chair.Value.seat and chair.chair.Value.seat.Occupant then
				tempPlayerList[getPlayerFromHumanoid(chair.chair.Value.seat.Occupant)] = true	
				if tableGroup["host"] == getPlayerFromHumanoid(chair.chair.Value.seat.Occupant) then
					--print("host still there")
					hostStillThere = true
				end
			end
		end
		local oldestSitter = {nil,0}
		local duplicates =  {}
		local playerCounter = 0
		for index, player in pairs(tableGroup["players"]) do
			--print(index,player)
			if tempPlayerList[index] then
				playerCounter += 1
				-- player still in lobby
				if hostStillThere == false then
					--print("sitter no hostanymore hoststill there = false")
					if oldestSitter[1] == nil then
						oldestSitter[1] = index
						oldestSitter[2] = player
					elseif oldestSitter[2] < player then
						--new player is sitting longer around table then other player
						--print("newHost",player)
						oldestSitter[1] = index
						oldestSitter[2] = player
					end
				end
			else
				-- player no longer in lobby left!
				--print("HIDE",tableGroup["players"][index])
				--if player is still in game then hide ui
				--print(tableGroup["players"][index],index)
				if tableGroup["host"] == index then
					--print("remote")
					remotes.client:FireClient(tableGroup["host"],{["hideUI"]={"hostGame","lobbySettings"}})
					-- enabling timer again resetting chairs
					tableGroup["invites"] = {}
					tableGroup["timer"][2] = true
				end	
				--table.remove(tableGroup["players"],index)
				tableGroup["players"][index] = nil
			end
		end
		if tableGroup["mapRoot"] then
			tableGroup["mapRoot"].players.value.Text = playerCounter
		end
		if oldestSitter[1] and hostStillThere == false then
			tableGroup["host"] = oldestSitter[1]
			tableGroup["invites"] = {}
			-- check if host is allowed to change lobby settings
			if playerDataFolder(tableGroup["host"]) and playerDataFolder(tableGroup["host"]):FindFirstChild("gamepass") and playerDataFolder(tableGroup["host"]):FindFirstChild("gamepass"):FindFirstChild("host") and playerDataFolder(tableGroup["host"]):FindFirstChild("gamepass"):FindFirstChild("host").Value == true then
				-- is allowed to change lobby settings showing lobby button
				remotes.client:FireClient(tableGroup["host"],{["openUI"]="hostGame"})
			end
		end
		-- if no players left reset lobby
		if playerCounter == 0 then
			--print("EMPTY LOBBY")
			tableGroup["host"] = nil
			tableGroup["teleportLock"] = false
			tableGroup["type"] = "public"
			tableGroup["invites"] = {}
			tableGroup["timer"][2] = true

		end
		print(tableGroup)
	end
end




local tableManager = {}

local startTimeCalc = math.floor(timerTime / #tables:GetChildren() + .5)
tableManager.setup = function()
	for index, tableGroup in pairs(tables:GetChildren()) do

		-- TABLE
		local TABLE = tableGroup:FindFirstChild("table")
		local newTableModel = nil
		tableData[TABLE] = {
			["teleportLock"] = false,
			["model"] = tableGroup,
			["chairs"] = {},
			["host"] = nil,
			["players"] = {},
			["type"] = "public", --public,private,friends
			["invited"] = {},
			["slots"] = 1,
			["modifier"] = nil,
			["map"] = maps[math.random(1,#maps)],
			["mapRoot"] = nil,
			["tableScale"] = 1,
			["timer"] = {startTimeCalc*index,true},
			["status"] = "",
		}
		--changeTableMap(tableGroup,nil)
		if string.match(tableGroup.Name,"small") then
			newTableModel = assets.table.small.default:Clone()
		elseif string.match(tableGroup.Name,"normal") then
			tableData[TABLE]["tableScale"] = 1.3
			newTableModel = assets.table.normal.default:Clone()
		elseif string.match(tableGroup.Name,"large") then
			tableData[TABLE]["tableScale"] = 1.8
			newTableModel = assets.table.large.default:Clone()
		end
		newTableModel.Parent = tableGroup.board
		newTableModel:SetPrimaryPartCFrame(TABLE.CFrame)
		TABLE.table.Value = newTableModel
		tableGroup.map.Transparency = 1
		tableGroup.table.Transparency = 1


		-- END TABLE
		for _, object in pairs(tableGroup:GetChildren()) do
			-- CHAIRS + MAP
			if string.match(object.Name,"seatBox") then --is a seat
				table.insert(tableData[TABLE]["chairs"],object) -- second argument is when someone seated to see who seated the longest
				--seat
				local newChairModel = assets.chair.default:Clone()
				newChairModel:SetPrimaryPartCFrame(object.CFrame)
				newChairModel.Parent = tableGroup.slots
				object.chair.Value = newChairModel
				object.Transparency = 1
				newChairModel.seat:GetPropertyChangedSignal("Occupant"):Connect(function()
					updateSeatState(object,tableData[TABLE])
				end)
				hitboxToTableDataReference[object] = TABLE
			end
			-- END CHAIRS + MAP
		end
		tableData[TABLE]["slots"] = #tableData[TABLE]["chairs"]
		changeTableMap(tableData[TABLE],nil)
	end
	print("first gen",tableData)
end




tableManager.remote = function()
	remotes.host.OnServerEvent:Connect(function(player,...)
		local args = ...
		--print(args)
		-- figuring out the tableGroup player belongs to and see if he is actualy a host
		local playerTableGroup = player.Character.Humanoid.SeatPart:FindFirstAncestor("slots")
		if playerTableGroup and playerTableGroup.Parent and playerTableGroup.Parent:FindFirstChild("table") then -- checking if slots folder exist no fake data from exploiter and check if parent exist wich is the actual tableGroupModel
			playerTableGroup = playerTableGroup.Parent:FindFirstChild("table")
		end
		
		if args["cancelLobby"] then
			-- resume timer player canceled creating lobby
			if tableData[playerTableGroup]["host"] == player then
				--print("HOST CONFIRMED resume timer",player,playerTableGroup)
				tableData[playerTableGroup]["timer"][2] = true -- disable countdown
				tableData[playerTableGroup]["status"] = ""
			end
		elseif args["setupGame"] then
			--host asked to setup game with parameters
			if tableData[playerTableGroup]["host"] == player then
				--make sure player is host and has gamepass or whitelist in case of exploit
				if rs:WaitForChild("playerData"):WaitForChild(player.UserId):WaitForChild("gamepass"):FindFirstChild("host") and rs:WaitForChild("playerData"):WaitForChild(player.UserId):WaitForChild("gamepass"):FindFirstChild("host").Value == true then
					-- player owns gamepass or whitelist
					--check if all options are there
					if args["setupGame"] == true and args["slots"] and args["type"] and args["map"] then
						--ok setup game
						--tableData[playerTableGroup]["map"] = maps[args["map"]]
						changeTableMap(tableData[playerTableGroup],args["map"])
						tableData[playerTableGroup]["slots"] = args["slots"]
						tableData[playerTableGroup]["type"] = args["type"]
						tableData[playerTableGroup]["timer"][1] = 30
						tableData[playerTableGroup]["timer"][2] = true
						remotes.client:FireClient(
							player,{["hideUI"]={"lobbySettings"},["openUI"]="hostGame",})
					end
				end	
			end
			
			
		else
			-- cancel lobby has priority otherwise we can asume seat is just for pausing the timer
			if args["seat"] then
				-- pause timer host setting up
				if tableData[playerTableGroup]["host"] == player then
					--print("HOST CONFIRMED before pause timer",player,playerTableGroup)
					tableData[playerTableGroup]["timer"][2] = false -- disable countdown
					tableData[playerTableGroup]["status"] = "Configuring"
				end
			end
		end
		
	end)
	
	
	remotes.chair.OnServerEvent:Connect(function(player,chairHitBox)
		print(player)
		if not debounce[player] then
			debounce[player] = true
			print("player not debounced")
			if tableData[hitboxToTableDataReference[chairHitBox]] and chairHitBox.chair.Value 	and chairHitBox.chair.Value.seat and chairHitBox.chair.Value.seat.Occupant == nil then
				local allowedToSit = false
				print("player seat not occupied yet")
				if tableData[hitboxToTableDataReference[chairHitBox]]["teleportLock"] == false and tableData[hitboxToTableDataReference[chairHitBox]]["slots"] > #tableData[hitboxToTableDataReference[chairHitBox]]["players"] then
					--print("no teleport lock and slot available")
					-- teleporting / lock phase before teleport (teleportLock) and slots > #players already in
					if tableData[hitboxToTableDataReference[chairHitBox]]["type"] == "public" then
						print("public")
						--public lobby
						allowedToSit = true
					elseif tableData[hitboxToTableDataReference[chairHitBox]]["type"] == "private" and tableData[hitboxToTableDataReference[chairHitBox]]["host"] ~= nil then
						print("private")
						-- invite only
						if tableData[hitboxToTableDataReference[chairHitBox]]["invited"][player] and tableData[hitboxToTableDataReference[chairHitBox]]["invited"][player] == true then
							--print("player invited")
							allowedToSit = true
						end
					elseif tableData[hitboxToTableDataReference[chairHitBox]]["type"] == "friends" and tableData[hitboxToTableDataReference[chairHitBox]]["host"] ~= nil then
						-- private but friends can join withoud invite
						print("friends")
						if tableData[hitboxToTableDataReference[chairHitBox]]["invited"][player] or tableData[hitboxToTableDataReference[chairHitBox]]["host"]:IsFriendsWith(player.UserId) then
							print("friends and invited or friend of host")
							allowedToSit = true
						end
					end
				end


				if allowedToSit then
					--table.insert(tableData[hitboxToTableDataReference[chairHitBox]]["players"],{player,time()})
					tableData[hitboxToTableDataReference[chairHitBox]]["players"][player] = time()
					print("SIT",player,tableData[hitboxToTableDataReference[chairHitBox]])
					chairHitBox.chair.Value.seat:Sit(player.Character.Humanoid)
					print("after gen",tableData)
				end
			end
			task.wait(3)
			debounce[player] = nil
		end
	end)
end


task.spawn(function()
	while true do
		for tableIndex,tableGroup in pairs(tableData) do
			if tableGroup["timer"][2] == true then
				tableGroup["timer"][1] = tableGroup["timer"][1] - 1
				if tableGroup["mapRoot"] then
					if tableGroup["timer"][1] < 1 then
						tableGroup["mapRoot"].timer.value.Text = "0"
					else
						tableGroup["mapRoot"].timer.value.Text = tableGroup["timer"][1]
					end
					--changing timer color to orange or red when close to 0
					
					tableGroup["mapRoot"].timer.value.TextColor3 = Color3.fromRGB(255,255,255)
					local percentage = tableGroup["timer"][1]*100/timerTime
					local red = math.clamp(255 - (percentage*255/100),200,255)
					tableGroup["mapRoot"].timer.value.TextColor3 = Color3.fromRGB(red,math.clamp(percentage*255/100,150,255),math.clamp(percentage*255/100,150,255))
					tableGroup["mapRoot"].timer.icon.Visible = true
				end
				if tableGroup["timer"][1] < 1 then
					tableGroup["timer"][1] = timerTime
					tableGroup["timer"][2] = false
					changeTableMap(tableGroup,nil)
				end
			else
				-- timer disable display status instead
				if tableGroup["mapRoot"] then
					tableGroup["mapRoot"].timer.icon.Visible = false
					tableGroup["mapRoot"].timer.value.Text = tableGroup["status"]
					tableGroup["mapRoot"].timer.value.TextColor3 = Color3.fromRGB(255,255,255)
				end
			end
		end
		task.wait(1)
	end
end)


return tableManager

i posted my first attempt since the 3th i split it all up in different module script to hopefully prevent that unexpected behavior but it doesn’t this version is more self contained and one server script

1 Like