Hi,
Thank you for the additional information!
The reason I asked about World Partition is because we did recently fix an issue that was almost identical to this, but it was discovered when moving in and out of World Partition cells quickly. The solution was to make sure that actor channels skip destroying net startup actors when they are closed due to the actor’s level being unloaded (CL 24651313 in UE5/Main).
However, it seems this fix isn’t affecting your situation here. I wasn’t able to find any changes between 5.1 and 5.2 that looked to affect this behavior, but after digging more into this, I believe I’ve tracked down what is causing the problem here.
When the streaming level is unloaded on the server, we intentionally will not close the net startup actors’ channels (UNetDriver::OnLevelRemovedFromWorld). However, when the unloaded streaming level is garbage collected (which in this case appears to be happening on the next frame or so), the NetDriver will close these channels and send the close bunches to the client (UNetDriver::NotifyStreamingLevelUnload). However, the close reason passed to the client here is EChannelCloseReason::Destroyed, so when the client receives the close bunch for the actor channel, it will just destroy that actor.
This normally isn’t an issue, as the client usually will then unload and clean up the streaming level anyway, destroying all the actors in it. Then when the streaming level is reloaded, the client will recreate these actors.
However, as you’ve found, with enough latency and with a small enough delay between the server unloading and loading the level, the client won’t actually unload the streaming level, but it will still receive the close bunches for the actors and destroy them.
Later when the server tries to replicate one of these net startup actors, it expects it to already exist on the client, as it should have been loaded with the level. When the actor isn’t present on the client, the warnings and replication failures occur.
The solution here is likely to make sure the server is sending the correct EChannelCloseReason when the streaming level is unloaded. In your repro project, I tested out this change to UNetDriver::NotifyActorDestroyed, and it did resolve the issue:
if( Channel ) { check(Channel->OpenedLocally); Channel->bClearRecentActorRefs = false; // If the actor is being destroyed due to the level being unloaded, make sure we send that as the reason. // We can check IsSeamlessTravel for this, as this looks to only be set to true when NotifyActorDestroyed is called from NotifyActorLevelUnloaded. Channel->Close( IsSeamlessTravel ? EChannelCloseReason::LevelUnloaded : EChannelCloseReason::Destroyed); }
That being said, this change has not been thoroughly tested, so there may be some side effects I didn’t catch. Hopefully this at least provides a better idea of the problem though.
Thanks,
Alex