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
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
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.
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.