STRANGE behaviour!

The problem is that Player.PlayerRemoved fires after the bound function finishes executing, meaning that the player list should still be intact. Its not - in the case of single-player leaving.

Unfortunately, many games don’t automatically save data, so data is deleted when the server loses connection randomly.

If its helpful, it seems like a race condition to me, because PlayerRemoving is called and while that function is running, the Player instance’s Parent is set to nil and then BindToClose is run when there are no Player Instances parented to game.Players. This means that both PlayerRemoving and BindToClose can run at the same time but GetPlayers will be empty but there is still a reference to the removing player.
This was my test code if it helps

local Players = game:GetService("Players")

Players.PlayerRemoving:Connect(function (player)
	print(player.Parent)
	local t = time()
	while player.Parent do wait() end
	print("PlayerRemoving - Player is gone", time()-t)
end)

game:BindToClose(function()
	print(Players:GetPlayers())
	local t = time()
	if #Players:GetPlayers() > 0 then
		Players.ChildRemoved:Wait()
	end
	print("BindToClose - Player is gone", time()-t)
	print(Players:GetPlayers())
end)

And my output

  14:53:36.900  {}  -  Server - Script:11
  14:53:36.900  BindToClose - Player is gone 0  -  Server - Script:16
  14:53:36.900  {}  -  Server - Script:17
  14:53:36.889  Disconnect from ::ffff:127.0.0.1|50232  -  Studio
  14:53:36.890  Players  -  Server
  14:53:36.939  PlayerRemoving - Player is gone 0.03750000335276127  -  Server
1 Like

This is perfect evidence that on a normal server close, that player instances are completely gone.

This is why I said earlier that the only option is a workaround. Sorry @Ninomae_InanisHoloEN, but it’s probably gonna be the only way.

If you want to dig deeper, I suggest DataModel:BindToClose().

So PlayerRemoving is racing with BindToClose, and BindToClose seems to win most of the time! BindToClose is racing with GC to index player list, and GC wins most of the time. What about in the case of a forced shutdown, why does GC always lose?

I. Don’t. Need. A. Workaround.

Because the server closes unexpectedly, preserving the rest of the instances that might still be located in it. Basically, the server is force closed, meaning that there is not enough time to prepare for PlayerRemoving.

How many times do I have to tell you this? It looks like that there is only a workaround!

If you want to dig deeper, then look at DataModel:BindToClose(). But, that seems like all that’s left possible.

I will no longer be replying or reading any of your future posts. I have stated more than enough times already that I DO NOT NEED A WORKAROUND.

You do you, but I think personally that it’s all left. You can find an alternative, someday.

I’m actually not sure it starts as a race condition anymore because the order of events seems to be
Player tries to leave → PlayerRemoving is called (in a new thread) → Player is parented to nil → BindToClose is called (in a new thread) → game ends

There is a 0.0375s wait between PlayerRemoving being called and the Player’s parent being nil at which point there are no players, so BindToClose is called. I think the GC will remove the Player once there are no references to it, so PlayerRemoving has to return for the Player to actually be destroyed.

But I don’t know the internals of this so I can’t be sure

1 Like

You can manually destroy a player, but even that won’t beat the GC. Correct me if I’m wrong.

I don’t think I understand beating the GC, the garbage collector collects any objects that are not in use (no references) and frees it. The GC will, be definition, always come last because after it the object is gone and can’t be recovered.

strong reference = no gc
In that case, I’m sure of this to be a bug. Bindtoclose should not be running before playeremoved fires, and even if it were to run, the player reference in players should not still exist - in some cases it does! whats even more strange is that a forced shutdown will result in the players being indexed by the func bound with bindtoclose. by logic, this is due to shutdown happening before players are remove and their parents set to nil (by logic of order)

yeah, i agree. but if the parent is set to nil, getplayers wont work - which makes sense - but it does sometimes

well, since this is confirmed to be a bug, and im not a Regular, i’ll ask others to report this

I’m going to go before I lose my time, sanity, and composure.

1 Like

Why can’t you run the function for each player when BindToClose is called? Is there a specific reason not to do so?

Is BindToClose running before PlayerRemoving though?
With this test

local Players = game:GetService("Players")

local serverIsClosed = false

Players.PlayerRemoving:Connect(function(player)
	if serverIsClosed then return end
	print("Bye", player)
end)

game:BindToClose(function()
	serverIsClosed = true
	print(#Players:GetPlayers())
end)

Heres my output

  15:23:39.305  Baseplate auto-recovery file was created  -  Studio
  15:23:42.618  0  -  Server - Script2:12
  15:23:42.606  Disconnect from ::ffff:127.0.0.1|51630  -  Studio
  15:23:42.607  Bye yes35go  -  Server

The outputs are out of order but the PlayerRemoving is called first (.607 vs. .618)? But I might also be confused too

Hey guys, here’s a summary and final update:

As long as Roblox doesn’t clarify the chronology of these events, we’d have to use logical deduction and guesstimation. BindToClose and PlayerRemoving are pretty unpredictable. One thing that is guaranteed is that PlayerRemoving will always be able to index the player leaving.

The optimal way to minimize data loss would be to use a custom server-based player list table in this format:
["PlayerName"] = PlayerDataTable

in which PlayerDataTable is a table of a similar format as the following:

["DataName"] = DataValue

Thanks everyone.

1 Like