Hi,
Using 4.13.2 with the ShooterGame demo for testing I’ve noticed that if I use seamless ServerTravel to reload the same level twice then things go bad. AGameMode::PostSeamlessTravel seems to call HandleSeamlessTravelPlayer immediately for remote players who have not started to travel, since the ClientWorldPackageName is the same both before and after the transition.
This means that APlayerController’s LastCompletedSeamlessTravelCount and SeamlessTravelCount are equal already for remote clients when the server’s PostLoadMap event triggers.
Normally for ShooterGame this doesn’t seem to be an issue but I am writing a plugin that spawns an entity from the PostMapLoad event and doing that in this situation completely breaks the remote clients’ replication, but works perfectly otherwise.
First seamless travel, looks good and works fine:
[OnPostLoadMap] Client: 000002AE98DEAB00 LCSTC: 0 STC: 1
Seamless travel again to the same level, the remote client is stuck and replication doesn’t work:
[OnPostLoadMap] Client: 000002AE98DEAB00 LCSTC: 2 STC: 2
LogPlayerController:Verbose: APlayerController::ServerNotifyLoadedWorld_Implementation: Client loaded /Temp/Untitled_3
LogNet: Server connection received: ActorChannelFailure
LogNet: Server connection received: ActorChannelFailure
LogPlayerController:Verbose: APlayerController::ServerNotifyLoadedWorld_Implementation: Client loaded /Game/Maps/Highrise
Relevant engine code:
void AGameMode::PostSeamlessTravel()
{
if ( GameSession != NULL )
{
GameSession->PostSeamlessTravel();
}
// We have to make a copy of the controller list, since the code after this will destroy
// and create new controllers in the world's list
TArray<TAutoWeakObjectPtr<class AController> > OldControllerList;
for (auto It = GetWorld()->GetControllerIterator(); It; ++It)
{
OldControllerList.Add(*It);
}
// handle players that are already loaded
for( FConstControllerIterator Iterator = OldControllerList.CreateConstIterator(); Iterator; ++Iterator )
{
AController* Controller = *Iterator;
if (Controller->PlayerState)
{
APlayerController* PlayerController = Cast<APlayerController>(Controller);
if (PlayerController == NULL)
{
HandleSeamlessTravelPlayer(Controller);
}
else
{
if (Controller->PlayerState->bOnlySpectator)
{
// The spectator count must be incremented here, instead of in HandleSeamlessTravelPlayer,
// as otherwise spectators can 'hide' from player counters, by making HasClientLoadedCurrentWorld return false
NumSpectators++;
}
else
{
NumTravellingPlayers++;
}
if (PlayerController->HasClientLoadedCurrentWorld())
{
HandleSeamlessTravelPlayer(Controller);
}
}
}
}
}
…
bool APlayerController::HasClientLoadedCurrentWorld()
{
UNetConnection* Connection = Cast<UNetConnection>(Player);
if (Connection == NULL && UNetConnection::GNetConnectionBeingCleanedUp != NULL && UNetConnection::GNetConnectionBeingCleanedUp->PlayerController == this)
{
Connection = UNetConnection::GNetConnectionBeingCleanedUp;
}
if (Connection != NULL)
{
// NOTE: To prevent exploits, child connections must not use the parent connections ClientWorldPackageName value at all.
return (Connection->ClientWorldPackageName == GetWorld()->GetOutermost()->GetFName());
}
else
{
// if we have no client connection, we're local, so we always have the current world
return true;
}
}
Thanks!