UE5 Seamless travel bug found. We found the cause, and also have a workaround

There is a bug that occurs in UE5 when seamless traveling that occurs if a client beats the server in loading the next level. There’s a race condition that causes the server to never see that client as having loaded the new level, therefore never transitions it’s player controller over to the new level, and the server and all the clients hang.

The following is for Epic. If you are interested in the workaround, it’s at the bottom

Cause:
This seems due to an interaction with some state variables on PlayerController, and the methods:

void APlayerController::ServerNotifyLoadedWorld_Implementation(FName WorldPackageName)

and

void AGameModeBase::PostSeamlessTravel()

ServerNotifyLoadedWorld is an RPC that gets called from the client when they finish transitioning to a new level (either the transition map or the destination map)
If the client calls this AFTER the server has loaded the map, all the checks in the code pass and GameMode::HandleSeamlessTravelPlayer is called and everything works fine.
If the Server is STILL transitioning, this method is not called

So the clients who beat the server to the new level are in a sort of limbo state because the server is not ready to transition them. The server eventually loads the next level and calls GameMode::PostSeamlessTravel, which has the responsibility of transitioning all players who beat the server to the next level.

However, the way the server is detecting that the client beat the server is not correct. It does so by calling PlayerController::HasClientLoadedCurrentWorld, and here is where the issue lies. This method checks a number of things, but eventually this code executes:

if (SeamlessTravelCount > 0)
{
	// In the case where seamless travel has occurred, make sure the client has actually completed the travel
	return bInCorrectWorld && (LastCompletedSeamlessTravelCount == SeamlessTravelCount);
}

This check fails:
(LastCompletedSeamlessTravelCount == SeamlessTravelCount). In our testing, SeamlessTravelCount is incremented at the start of the seamless travel, however LastCompletedSeamlessTravelCount is not incremented until GameMode::HandleSeamlessTravelPlayer is called (which eventually calls APlayerController::PostSeamlessTravel(), which does the increment). So PlayerController::HasClientLoadedCurrentWorld never returns true because HandleSeamlessTravelPlayer is never called, and HandleSeamlessTravelPlayer is never called because HasClientLoadedCurrentWorld never returns true

Workaround:
Subclass PlayerController if you have not already done so. add the following code to the header file:

    virtual void NotifyLoadedWorld(FName WorldPackageName, bool bFinalDest) override;

    UFUNCTION(Reliable, Server, WithValidation, SealedEvent)
    void ServerNotifyLoadedWorldWorkaround(FName WorldPackageName);

And in the .cpp file add the following code:

void AYourPlayerControllerName::NotifyLoadedWorld(FName WorldPackageName, bool bFinalDest)
{
    Super::NotifyLoadedWorld(WorldPackageName, bFinalDest);

    // Call custom ServerNotifyLoadedWorld
    ServerNotifyLoadedWorldWorkaround(WorldPackageName);
}

bool AYourPlayerControllerName::ServerNotifyLoadedWorld_Validate(FName WorldPackageName)
{
	RPC_VALIDATE( WorldPackageName.IsValid() );
	return true;
}

void AYourPlayerControllerName::ServerNotifyLoadedWorldWorkaround_Implementation(FName WorldPackageName)
{
    UE_LOG(LogPlayerController, Verbose, TEXT("AYourPlayerControllerName::ServerNotifyLoadedWorldWorkaround_Implementation: Client loaded %s"), *WorldPackageName.ToString());

    UWorld *CurWorld = GetWorld();

    // Only valid for calling, for PC's in the process of seamless traveling
    // NOTE: SeamlessTravelCount tracks client seamless travel, through the serverside gameplay code; this should not be replaced.
    if (CurWorld != NULL && !CurWorld->IsNetMode(NM_Client) && SeamlessTravelCount > 0 && LastCompletedSeamlessTravelCount < SeamlessTravelCount)
    {
        // Update our info on what world the client is in
        UNetConnection *const Connection = Cast<UNetConnection>(Player);

        if (Connection != NULL)
        {
            Connection->SetClientWorldPackageName(WorldPackageName);
        }

        // if both the server and this client have completed the transition, handle it
        FSeamlessTravelHandler &SeamlessTravelHandler = GEngine->SeamlessTravelHandlerForWorld(CurWorld);
        AGameModeBase *CurGameMode = CurWorld->GetAuthGameMode();

        if (!SeamlessTravelHandler.IsInTransition() && WorldPackageName == CurWorld->GetOutermost()->GetFName() && CurGameMode != NULL)
        {
            AController *TravelPlayer = this;
            CurGameMode->HandleSeamlessTravelPlayer(TravelPlayer);
        }
        else
        {
            // This is the seamless travel fix for client loading before server and softlocking
            // Skip this for TransitionMapName
            FString TransitionMapString = GetDefault<UGameMapsSettings>()->TransitionMap.GetLongPackageName();
            FName TransitionMapName(*TransitionMapString);
            if (TransitionMapName != WorldPackageName)
            {
                LastCompletedSeamlessTravelCount = SeamlessTravelCount;
            }
        }
    }
}

You will incur an extra RPC during level transitions, but the impact of this should be minimal.

12 Likes

I’ve been struggling with this issue in 5.0.2, and had also narrowed it down to the server not receiving the ServerNotifyLoadedWorld RPC calls (but hadn’t got any further than that). It’s a huge relief to see that you’ve found a workaround!

I am having some trouble implementing it and I’m not incredibly experienced with c++, so apologies if this is a basic question. I did already have a c++ class that inherits and overrides some functions from PlayerController.h (by including it in the header), but when I paste the workaround in and switch the PlayerController names to the names of my player controller there are a few errors:

image

My PlayerControllerBase.h file is as follows:

#pragma once

include “CoreMinimal.h”
include “GameFramework/PlayerController.h”
include “PlayerControllerBase.generated.h”

UCLASS()
class APlayerControllerBase : public APlayerController
{
GENERATED_BODY()

public:
virtual void OnRep_Pawn() override;

UFUNCTION(BlueprintCallable, Category = “Start Spectating Player Controller”)
void StartSpectating();

UFUNCTION(Client, Reliable)
void Client_StartSpectating();

UFUNCTION(BlueprintCallable, Category = “Start Spectating Player Controller”)
void StartPlaying();

virtual void NotifyLoadedWorld(FName WorldPackageName, bool bFinalDest) override;

UFUNCTION(Reliable, Server, WithValidation, SealedEvent)
void ServerNotifyLoadedWorldWorkaround(FName WorldPackageName);
};

And the .cpp file includes PlayerControllerBase.h and I’ve just added the workaround to the end, and also tried including Net/UnrealNetwork.h and GameFramework/GameModeBase.h. Any pointers in the right direction would be greatly appreciated. Thanks!

1 Like

I’m having a similar issue, but when implementing your solution I see that the Last and Current travel counts are now the same (previously were not), however bInCorrectWorld still returns false for me preventing completion and still only when the client loads first.

Before your change

LogPlayerController: Verbose: APlayerController::HasClientLoadedCurrentWorld: Client /Game/Maps/MAP_01_P, World: /Game/Maps/MAP_01_P
LogPlayerController: Verbose: Last Count 0, Current Count 1

After your change, it appears to hold the temp transition level in GetClientWorldPackageName().

LogPlayerController: Verbose: APlayerController::HasClientLoadedCurrentWorld: Client /Temp/Untitled_1, World: /Game/Maps/MAP_01_P
LogPlayerController: Verbose: Last Count 1, Current Count 1

This line probably needs a bit of adjustment to handle the case where no specific transition map is specified.

const FString TransitionMapString = GetDefault<UGameMapsSettings>()->TransitionMap.GetLongPackageName();
1 Like

Quick update on this. Since I couldn’t get the fix working on my Player Controller subclass, I have built the engine from source and added the fix straight into the PlayerController.cpp and now the server is consistently calling HandleSeamlessTravelPlayer. Thanks so much for sharing the fix!

If anyone is able to point me towards the correct way to implement this into the subclass I’d be extremely grateful, so that I can avoid building the engine from source.

1 Like

@gollymashed I apologize. The above solution has a typo and there’s another step (Other than those missing header files, sorry about that too)

You need to open “YourProjectName.Build.cs” and you should have a line in there that looks like this

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", ... }

Just add “EngineSettings” to that array.

And then there’s an error in the code I posted

bool AYourPlayerControllerName::ServerNotifyLoadedWorld_Validate(FName WorldPackageName)

Should be

bool AYourPlayerControllerName::ServerNotifyLoadedWorldWorkaround_Validate(FName WorldPackageName)

We wanted to avoid compiling the engine as well, so I hope that works for you! If not let me know.

Right you are, we have a transition map so we didn’t run into that issue!

Also, the fix worked for me @liquid.mike thanks! I applied the code directly to engine source.

1 Like

@liquid.mike Wow I’ve been banging my head against the wall for almost a month after noticing this issue appear after upgrading to UE5.
How long has this been an issue and if it was reintroduced, how has this not been more posted about? This seems like a critical error regarding all games that are using UE5.

1 Like

How has this not been fixed yet!?

Can you post the whole cpp and header files, getting errors trying to implement this and think I am missing something

There were some errors in the original post. Please see my reply UE5 Seamless travel bug found. We found the cause, and also have a workaround - #5 by liquid.mike

If anyone else runs into trouble here is what to include in your cpp along with the corrections shown above

include “Net/UnrealNetwork.h”
include “GameFramework/GameModeBase.h”
include “GameMapsSettings.h”

1 Like

Ran into this problem as well. I believe there is a side effect happening with this workaround. Namely, InitSeamlessTravelPlayer is now getting called twice for each player. This results in GameMode’s NumPlayers being double-counted, so it always thinks twice as many players are connected compared to how many actually are. We use NumPlayers in our multiplayer logic, and while I could probably work further around this, that feels like a bad idea. Has anyone else encountered this?

I think I ran into this problem too, when I host the game in my PC everyone travels to the map, when I host the game on my brother’s PC the same, but when I host the game on my less powered notebook I and my brother keep stuck in the loading.

So my question is, if the bug has been reported in Jun 16, why its still happening now in Sep 21?

Edit1: It seems that the unreleased 5.1 addresses this, the function HasClientLoadedCurrentWorld check return is now

return (Connection->GetClientWorldPackageName() == GetWorld()->GetOutermost()->GetFName());

If possible I would like to know if the unreleased 5.1 actually fixed this issue or not, did you test it or have any information on this?

Just to know if it’s worth waiting for full 5.1 release (5.1 Preview 2 currently available to download, so it shouldn’ take that long hopefully).

So we don’t have to implement this workaround, where as JEFFREY_EPSTEIN mentioned above, this workaround causes some side effects (if true).

Thank you!

I didn’t tested the 5.1 yet due to project plugins. I suggest you ask in the Unreal Slackers discord if anyone knows it the problem was fixed.

Might I ask if this bug was reported somewhere here: https://issues.unrealengine.com/ ? I’m asking before I report it myself.

Still happens on 5.0.3 for me.

Okay so I missed what was noted by @havenking . They seem to have fixed it in 5.1. They have also added one line to ServerNotifyLoadedWorld which handles remapping a package name for networking.

1 Like

Do you happen to have the commit hash for the fix?

EDIT: Here’s the commit if anyone is curious: https://github.com/EpicGames/UnrealEngine/commit/503690c293cbfa284611c2d1e58abf79fa9b97bc

1 Like