In the above clip, the black block is marked as Persistent, but is manually “streamed” in and out by locally parenting it to Workspace or Lighting depending on if the local character is in the blue region. The black ball is streamed. When the black ball touches the black block, they get welded together.
When the user walks far enough away, the black ball and black block are unloaded. When the user then walks back into the region, the black ball never re-appears.
A part spawner to reproduce this issue with a single player. This is what the above video was taken with.
A seat in the manually “streamed” region, to simulate this with two players. If player 2 sits in the seat and player 1 walks away and back, this will occur with player 2’s character.
Why we need this to work
We need this functionality to work because “Adopt Me!” currently uses this “manually streaming” trick for all of its separate regions/interiors. We want to move to StreamingEnabled, but to minimize impact on the game we want to move over gradually. We were hoping to make pets stream, but we ran into this issue which is a blocker on doing this gradually. Because of this issue, we would have to also move characters, vehicles, seats, and anything a player or pet grabs over to streaming as well. This is hard for a multi-year-old game with tens of thousands of lines of code. Moving multiple systems over at once is also riskier in terms of introducing game-breaking bugs.
Workarounds
We can trigger a streaming update by changing the parent on the server side. Moving a model between folders in workspace is enough to trigger a streaming update. This involves sending the update to everyone that has the model streamed in, which is not ideal, but it may be better than moving almost every physical model over to streaming.
Expected behavior
I expect the black ball to re-appear either [before the black block re-appears] or [when the black block re-appears] or [very shortly after the black block re-appears]. I do not expect it to never re-appear, as it does right now.
The behavior that is occurring in the repo place is a combination of two factors, a desync between the client and the server as a result of the client script reparenting the persistent model, and a result of how parts joined into an assembly are handled.
If two non-anchored parts are joined together via a weld or other joint they become an assembly. As discussed in this post:
When parts are joined into an assembly they are always streamed in or out together. This means if there are two parts joined together into an assembly and one of those parts belongs to a persistent model than all parts in the assembly are considered persistent and not subject to stream out.
This is problematic in your repro place because one of the parts is reparented under Lighting, but not the other. This results in the remaining part being considered by the client as no longer persistent and subject to stream out.
Here is the sequence of steps:
Ball is spawned and welded to the persistent block. Ball is treated as persistent on both client and server.
Persistent block is reparented under Lighting (on the client only), but ball remains under workspace. Ball is no longer considered persistent on the client because it is no longer welded to a persistent part, but from the server’s perspective is still persistent.
Ball is streamed out on client. From the server’s perspective this should never have happened. The ball is never resent because from the server perspective it should never have been streamed out in the first place.
We have a change under development, but not yet enabled, that would cause the server to resend instances that should be persistent but are for some reason GC’d on the client. This would fix the repro place that was provided, but would have the undesirable side effect that the ball would be continuously streamed in and out over and over as the client would immediately stream it back out again every time the server sends it, until the client is close enough to the ball that it is not streamed out.
If this client/server desync behavior is required then my recommendation would be either:
Store the ball part under the same model as the ManuallyStreamedRegion model
Store the ball under its own persistent model
Ensure the ball is also reparented under Lighting at the same time the connected part is reparented. (Probably would need to happen before the persistent part otherwise you risk having the ball streamed out on the client)
Hopefully my explanation makes sense, please let me know if anything isn’t clear.
Right now, my workaround was going to be to trigger the “cause the server to resend instances that should be persistent but are for some reason GC’d on the client” behavior myself: if we change the parent of the streamed model between different folders in Workspace then it appears to trigger re-sending the model to the clients which have GC’d it.
This is the easiest solution for us – we can just trigger the update any time a player moves between regions/interiors in our game.
I suspect this won’t be a problem with “the undesirable side effect that the ball would be continuously streamed in and out over and over” behavior, because Adopt Me has a significant difference from this repro place. In Adopt Me, “manually streamed” regions are significantly larger than the max streaming radius, and further away than the max streaming radius. We would never be in a situation where Roblox’s streaming wants to stream in part of an assembly, but we are not “manually streaming” in the other parts. We’d always be “manually streaming” in the other parts before Roblox is trying to stream the rest in.
With this in mind, do you think
For now, manually triggering models to be restreamed via reparenting
and Later, relying on Roblox to re-stream these when the new behavior is introduced
is a good a solution? Or are there consequences you can foresee that I can’t?
I’m not a fan of allowing any desync that would result in an instance being GC’d on the client when the server thinks it should not be GC’able. This could potentially result in repeatedly streaming instances out/in continuously leading to increased CPU cost on server and client, as well as bandwidth costs. I would prefer one of the recommendations I made to avoid the instance becoming GC’able at all.
Reparenting models between different folders in order to trigger a resend is going to result in increased server CPU time as it processes those updates, and if you cause that to scale based on the number of players and how they are moving between regions that could be problematic. You are basically making the server view these as entirely new persistent models that need to be evaluated, and having to check that for all players. This is unlikely to be a good workaround due to the performance impact.
Also keep in mind that the new re-streaming behavior I mentioned might not be possible to introduce if this kind of desync is common and would result in continuous restreaming.
Okay, I think I’ll spend some time to evaluate if making all items that could attach to pets in Adopt Me streamable. This might be a pain, but it is the end goal. This involves characters, vehicles, pet toys, and seats. I don’t want to put a lot of work into this partial streaming approach if it’s comparable work to the end goal of making everything stream.
I’ve closed this ticket out as “By Design”. The underlying issue here is a desync being created between the client and the server. The server views the part as persistent and it should never be GC’d by a client. The change on the client causes the part to no longer be linked to the persistent model, resulting in it being GC’d.
Even if we changed the server to resend the part again in this situation depending on the type of desync induced on the client it could immediately result in the part being GC’d again, potentially in a repeating cycle.