Skip to content

Instantly share code, notes, and snippets.

@bruteforks
Last active January 31, 2024 22:55

Revisions

  1. bruteforks revised this gist Jan 31, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion LyraGameMode.cpp
    Original file line number Diff line number Diff line change
    @@ -503,7 +503,7 @@ void ALyraGameMode::FailedToRestartPlayer(AController* NewPlayer)
    {
    Super::FailedToRestartPlayer(NewPlayer);

    // If we tried to s. a pawn and it failed, lets try again *note* check if there's actually a pawn class
    // If we tried to spawn a pawn and it failed, lets try again *note* check if there's actually a pawn class
    // before we try this forever.
    if (UClass* PawnClass = GetDefaultPawnClassForController(NewPlayer))
    {
  2. bruteforks revised this gist Jan 31, 2024. 2 changed files with 14 additions and 2 deletions.
    5 changes: 5 additions & 0 deletions LyraExperienceDefinition.h
    Original file line number Diff line number Diff line change
    @@ -42,6 +42,11 @@ class ULyraExperienceDefinition : public UPrimaryDataAsset
    UPROPERTY(EditDefaultsOnly, Category=Gameplay)
    TObjectPtr<const ULyraPawnData> DefaultPawnData;

    /** Online solution for separate pawn class to spawn for players https://forums.unrealengine.com/t/lyra-ue5-1-spawn-different-ai-pawns-setpawndata/740673/10 https://forums.unrealengine.com/t/lyra-ue5-1-spawn-different-ai-pawns-setpawndata/740673/6 */
    //@TODO: Make soft?
    UPROPERTY(EditDefaultsOnly, Category = Gameplay)
    TObjectPtr<const ULyraPawnData> CustomAIPawnData;

    // List of actions to perform as this experience is loaded/activated/deactivated/unloaded
    UPROPERTY(EditDefaultsOnly, Instanced, Category="Actions")
    TArray<TObjectPtr<UGameFeatureAction>> Actions;
    11 changes: 9 additions & 2 deletions LyraGameMode.cpp
    Original file line number Diff line number Diff line change
    @@ -66,7 +66,14 @@ const ULyraPawnData* ALyraGameMode::GetPawnDataForController(const AController*
    const ULyraExperienceDefinition* Experience = ExperienceComponent->GetCurrentExperienceChecked();
    if (Experience->DefaultPawnData != nullptr)
    {
    return Experience->DefaultPawnData;
    if (InController != nullptr && InController->IsA<AAIController>())
    {
    return Experience->CustomAIPawnData;
    }
    else
    {
    return Experience->DefaultPawnData;
    }
    }

    // Experience is loaded and there's still no pawn data, fall back to the default for now
    @@ -496,7 +503,7 @@ void ALyraGameMode::FailedToRestartPlayer(AController* NewPlayer)
    {
    Super::FailedToRestartPlayer(NewPlayer);

    // If we tried to spawn a pawn and it failed, lets try again *note* check if there's actually a pawn class
    // If we tried to s. a pawn and it failed, lets try again *note* check if there's actually a pawn class
    // before we try this forever.
    if (UClass* PawnClass = GetDefaultPawnClassForController(NewPlayer))
    {
  3. bruteforks created this gist Jan 31, 2024.
    52 changes: 52 additions & 0 deletions LyraExperienceDefinition.h
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,52 @@
    // Copyright Epic Games, Inc. All Rights Reserved.

    #pragma once

    #include "Engine/DataAsset.h"
    #include "LyraExperienceDefinition.generated.h"

    class UGameFeatureAction;
    class ULyraPawnData;
    class ULyraExperienceActionSet;

    /**
    * Definition of an experience
    */
    UCLASS(BlueprintType, Const)
    class ULyraExperienceDefinition : public UPrimaryDataAsset
    {
    GENERATED_BODY()

    public:
    ULyraExperienceDefinition();

    //~UObject interface
    #if WITH_EDITOR
    virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
    #endif
    //~End of UObject interface

    //~UPrimaryDataAsset interface
    #if WITH_EDITORONLY_DATA
    virtual void UpdateAssetBundleData() override;
    #endif
    //~End of UPrimaryDataAsset interface

    public:
    // List of Game Feature Plugins this experience wants to have active
    UPROPERTY(EditDefaultsOnly, Category = Gameplay)
    TArray<FString> GameFeaturesToEnable;

    /** The default pawn class to spawn for players */
    //@TODO: Make soft?
    UPROPERTY(EditDefaultsOnly, Category=Gameplay)
    TObjectPtr<const ULyraPawnData> DefaultPawnData;

    // List of actions to perform as this experience is loaded/activated/deactivated/unloaded
    UPROPERTY(EditDefaultsOnly, Instanced, Category="Actions")
    TArray<TObjectPtr<UGameFeatureAction>> Actions;

    // List of additional action sets to compose into this experience
    UPROPERTY(EditDefaultsOnly, Category=Gameplay)
    TArray<TObjectPtr<ULyraExperienceActionSet>> ActionSets;
    };
    524 changes: 524 additions & 0 deletions LyraGameMode.cpp
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,524 @@
    // Copyright Epic Games, Inc. All Rights Reserved.

    #include "LyraGameMode.h"
    #include "AssetRegistry/AssetData.h"
    #include "Engine/GameInstance.h"
    #include "Engine/World.h"
    #include "LyraLogChannels.h"
    #include "Misc/CommandLine.h"
    #include "System/LyraAssetManager.h"
    #include "LyraGameState.h"
    #include "System/LyraGameSession.h"
    #include "Player/LyraPlayerController.h"
    #include "Player/LyraPlayerBotController.h"
    #include "Player/LyraPlayerState.h"
    #include "Character/LyraCharacter.h"
    #include "UI/LyraHUD.h"
    #include "Character/LyraPawnExtensionComponent.h"
    #include "Character/LyraPawnData.h"
    #include "GameModes/LyraWorldSettings.h"
    #include "GameModes/LyraExperienceDefinition.h"
    #include "GameModes/LyraExperienceManagerComponent.h"
    #include "GameModes/LyraUserFacingExperienceDefinition.h"
    #include "Kismet/GameplayStatics.h"
    #include "Development/LyraDeveloperSettings.h"
    #include "Player/LyraPlayerSpawningManagerComponent.h"
    #include "CommonUserSubsystem.h"
    #include "CommonSessionSubsystem.h"
    #include "TimerManager.h"
    #include "GameMapsSettings.h"

    #include UE_INLINE_GENERATED_CPP_BY_NAME(LyraGameMode)

    ALyraGameMode::ALyraGameMode(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
    {
    GameStateClass = ALyraGameState::StaticClass();
    GameSessionClass = ALyraGameSession::StaticClass();
    PlayerControllerClass = ALyraPlayerController::StaticClass();
    ReplaySpectatorPlayerControllerClass = ALyraReplayPlayerController::StaticClass();
    PlayerStateClass = ALyraPlayerState::StaticClass();
    DefaultPawnClass = ALyraCharacter::StaticClass();
    HUDClass = ALyraHUD::StaticClass();
    }

    const ULyraPawnData* ALyraGameMode::GetPawnDataForController(const AController* InController) const
    {
    // See if pawn data is already set on the player state
    if (InController != nullptr)
    {
    if (const ALyraPlayerState* LyraPS = InController->GetPlayerState<ALyraPlayerState>())
    {
    if (const ULyraPawnData* PawnData = LyraPS->GetPawnData<ULyraPawnData>())
    {
    return PawnData;
    }
    }
    }

    // If not, fall back to the the default for the current experience
    check(GameState);
    ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>();
    check(ExperienceComponent);

    if (ExperienceComponent->IsExperienceLoaded())
    {
    const ULyraExperienceDefinition* Experience = ExperienceComponent->GetCurrentExperienceChecked();
    if (Experience->DefaultPawnData != nullptr)
    {
    return Experience->DefaultPawnData;
    }

    // Experience is loaded and there's still no pawn data, fall back to the default for now
    return ULyraAssetManager::Get().GetDefaultPawnData();
    }

    // Experience not loaded yet, so there is no pawn data to be had
    return nullptr;
    }

    void ALyraGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
    {
    Super::InitGame(MapName, Options, ErrorMessage);

    // Wait for the next frame to give time to initialize startup settings
    GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ThisClass::HandleMatchAssignmentIfNotExpectingOne);
    }

    void ALyraGameMode::HandleMatchAssignmentIfNotExpectingOne()
    {
    FPrimaryAssetId ExperienceId;
    FString ExperienceIdSource;

    // Precedence order (highest wins)
    // - Matchmaking assignment (if present)
    // - URL Options override
    // - Developer Settings (PIE only)
    // - Command Line override
    // - World Settings
    // - Dedicated server
    // - Default experience

    UWorld* World = GetWorld();

    if (!ExperienceId.IsValid() && UGameplayStatics::HasOption(OptionsString, TEXT("Experience")))
    {
    const FString ExperienceFromOptions = UGameplayStatics::ParseOption(OptionsString, TEXT("Experience"));
    ExperienceId = FPrimaryAssetId(FPrimaryAssetType(ULyraExperienceDefinition::StaticClass()->GetFName()), FName(*ExperienceFromOptions));
    ExperienceIdSource = TEXT("OptionsString");
    }

    if (!ExperienceId.IsValid() && World->IsPlayInEditor())
    {
    ExperienceId = GetDefault<ULyraDeveloperSettings>()->ExperienceOverride;
    ExperienceIdSource = TEXT("DeveloperSettings");
    }

    // see if the command line wants to set the experience
    if (!ExperienceId.IsValid())
    {
    FString ExperienceFromCommandLine;
    if (FParse::Value(FCommandLine::Get(), TEXT("Experience="), ExperienceFromCommandLine))
    {
    ExperienceId = FPrimaryAssetId::ParseTypeAndName(ExperienceFromCommandLine);
    if (!ExperienceId.PrimaryAssetType.IsValid())
    {
    ExperienceId = FPrimaryAssetId(FPrimaryAssetType(ULyraExperienceDefinition::StaticClass()->GetFName()), FName(*ExperienceFromCommandLine));
    }
    ExperienceIdSource = TEXT("CommandLine");
    }
    }

    // see if the world settings has a default experience
    if (!ExperienceId.IsValid())
    {
    if (ALyraWorldSettings* TypedWorldSettings = Cast<ALyraWorldSettings>(GetWorldSettings()))
    {
    ExperienceId = TypedWorldSettings->GetDefaultGameplayExperience();
    ExperienceIdSource = TEXT("WorldSettings");
    }
    }

    ULyraAssetManager& AssetManager = ULyraAssetManager::Get();
    FAssetData Dummy;
    if (ExperienceId.IsValid() && !AssetManager.GetPrimaryAssetData(ExperienceId, /*out*/ Dummy))
    {
    UE_LOG(LogLyraExperience, Error, TEXT("EXPERIENCE: Wanted to use %s but couldn't find it, falling back to the default)"), *ExperienceId.ToString());
    ExperienceId = FPrimaryAssetId();
    }

    // Final fallback to the default experience
    if (!ExperienceId.IsValid())
    {
    if (TryDedicatedServerLogin())
    {
    // This will start to host as a dedicated server
    return;
    }

    //@TODO: Pull this from a config setting or something
    ExperienceId = FPrimaryAssetId(FPrimaryAssetType("LyraExperienceDefinition"), FName("B_LyraDefaultExperience"));
    ExperienceIdSource = TEXT("Default");
    }

    OnMatchAssignmentGiven(ExperienceId, ExperienceIdSource);
    }

    bool ALyraGameMode::TryDedicatedServerLogin()
    {
    // Some basic code to register as an active dedicated server, this would be heavily modified by the game
    FString DefaultMap = UGameMapsSettings::GetGameDefaultMap();
    UWorld* World = GetWorld();
    UGameInstance* GameInstance = GetGameInstance();
    if (GameInstance && World && World->GetNetMode() == NM_DedicatedServer && World->URL.Map == DefaultMap)
    {
    // Only register if this is the default map on a dedicated server
    UCommonUserSubsystem* UserSubsystem = GameInstance->GetSubsystem<UCommonUserSubsystem>();

    // Dedicated servers may need to do an online login
    UserSubsystem->OnUserInitializeComplete.AddDynamic(this, &ALyraGameMode::OnUserInitializedForDedicatedServer);

    // There are no local users on dedicated server, but index 0 means the default platform user which is handled by the online login code
    if (!UserSubsystem->TryToLoginForOnlinePlay(0))
    {
    OnUserInitializedForDedicatedServer(nullptr, false, FText(), ECommonUserPrivilege::CanPlayOnline, ECommonUserOnlineContext::Default);
    }

    return true;
    }

    return false;
    }

    void ALyraGameMode::HostDedicatedServerMatch(ECommonSessionOnlineMode OnlineMode)
    {
    FPrimaryAssetType UserExperienceType = ULyraUserFacingExperienceDefinition::StaticClass()->GetFName();

    // Figure out what UserFacingExperience to load
    FPrimaryAssetId UserExperienceId;
    FString UserExperienceFromCommandLine;
    if (FParse::Value(FCommandLine::Get(), TEXT("UserExperience="), UserExperienceFromCommandLine) ||
    FParse::Value(FCommandLine::Get(), TEXT("Playlist="), UserExperienceFromCommandLine))
    {
    UserExperienceId = FPrimaryAssetId::ParseTypeAndName(UserExperienceFromCommandLine);
    if (!UserExperienceId.PrimaryAssetType.IsValid())
    {
    UserExperienceId = FPrimaryAssetId(FPrimaryAssetType(UserExperienceType), FName(*UserExperienceFromCommandLine));
    }
    }

    // Search for the matching experience, it's fine to force load them because we're in dedicated server startup
    ULyraAssetManager& AssetManager = ULyraAssetManager::Get();
    TSharedPtr<FStreamableHandle> Handle = AssetManager.LoadPrimaryAssetsWithType(UserExperienceType);
    if (ensure(Handle.IsValid()))
    {
    Handle->WaitUntilComplete();
    }

    TArray<UObject*> UserExperiences;
    AssetManager.GetPrimaryAssetObjectList(UserExperienceType, UserExperiences);
    ULyraUserFacingExperienceDefinition* FoundExperience = nullptr;
    ULyraUserFacingExperienceDefinition* DefaultExperience = nullptr;

    for (UObject* Object : UserExperiences)
    {
    ULyraUserFacingExperienceDefinition* UserExperience = Cast<ULyraUserFacingExperienceDefinition>(Object);
    if (ensure(UserExperience))
    {
    if (UserExperience->GetPrimaryAssetId() == UserExperienceId)
    {
    FoundExperience = UserExperience;
    break;
    }

    if (UserExperience->bIsDefaultExperience && DefaultExperience == nullptr)
    {
    DefaultExperience = UserExperience;
    }
    }
    }

    if (FoundExperience == nullptr)
    {
    FoundExperience = DefaultExperience;
    }

    UGameInstance* GameInstance = GetGameInstance();
    if (ensure(FoundExperience && GameInstance))
    {
    // Actually host the game
    UCommonSession_HostSessionRequest* HostRequest = FoundExperience->CreateHostingRequest();
    if (ensure(HostRequest))
    {
    HostRequest->OnlineMode = OnlineMode;

    // TODO override other parameters?

    UCommonSessionSubsystem* SessionSubsystem = GameInstance->GetSubsystem<UCommonSessionSubsystem>();
    SessionSubsystem->HostSession(nullptr, HostRequest);

    // This will handle the map travel
    }
    }

    }

    void ALyraGameMode::OnUserInitializedForDedicatedServer(const UCommonUserInfo* UserInfo, bool bSuccess, FText Error, ECommonUserPrivilege RequestedPrivilege, ECommonUserOnlineContext OnlineContext)
    {
    UGameInstance* GameInstance = GetGameInstance();
    if (GameInstance)
    {
    // Unbind
    UCommonUserSubsystem* UserSubsystem = GameInstance->GetSubsystem<UCommonUserSubsystem>();
    UserSubsystem->OnUserInitializeComplete.RemoveDynamic(this, &ALyraGameMode::OnUserInitializedForDedicatedServer);

    if (bSuccess)
    {
    // Online login worked, start a full online game
    UE_LOG(LogLyraExperience, Log, TEXT("Dedicated server online login succeeded, starting online server"));
    HostDedicatedServerMatch(ECommonSessionOnlineMode::Online);
    }
    else
    {
    // Go ahead and try to host anyway, but without online support
    // This behavior is fairly game specific, but this behavior provides the most flexibility for testing
    UE_LOG(LogLyraExperience, Log, TEXT("Dedicated server online login failed, starting LAN-only server"));
    HostDedicatedServerMatch(ECommonSessionOnlineMode::LAN);
    }
    }
    }

    void ALyraGameMode::OnMatchAssignmentGiven(FPrimaryAssetId ExperienceId, const FString& ExperienceIdSource)
    {
    if (ExperienceId.IsValid())
    {
    UE_LOG(LogLyraExperience, Log, TEXT("Identified experience %s (Source: %s)"), *ExperienceId.ToString(), *ExperienceIdSource);

    ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>();
    check(ExperienceComponent);
    ExperienceComponent->SetCurrentExperience(ExperienceId);
    }
    else
    {
    UE_LOG(LogLyraExperience, Error, TEXT("Failed to identify experience, loading screen will stay up forever"));
    }
    }

    void ALyraGameMode::OnExperienceLoaded(const ULyraExperienceDefinition* CurrentExperience)
    {
    // Spawn any players that are already attached
    //@TODO: Here we're handling only *player* controllers, but in GetDefaultPawnClassForController_Implementation we skipped all controllers
    // GetDefaultPawnClassForController_Implementation might only be getting called for players anyways
    for (FConstPlayerControllerIterator Iterator = GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator)
    {
    APlayerController* PC = Cast<APlayerController>(*Iterator);
    if ((PC != nullptr) && (PC->GetPawn() == nullptr))
    {
    if (PlayerCanRestart(PC))
    {
    RestartPlayer(PC);
    }
    }
    }
    }

    bool ALyraGameMode::IsExperienceLoaded() const
    {
    check(GameState);
    ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>();
    check(ExperienceComponent);

    return ExperienceComponent->IsExperienceLoaded();
    }

    UClass* ALyraGameMode::GetDefaultPawnClassForController_Implementation(AController* InController)
    {
    if (const ULyraPawnData* PawnData = GetPawnDataForController(InController))
    {
    if (PawnData->PawnClass)
    {
    return PawnData->PawnClass;
    }
    }

    return Super::GetDefaultPawnClassForController_Implementation(InController);
    }

    APawn* ALyraGameMode::SpawnDefaultPawnAtTransform_Implementation(AController* NewPlayer, const FTransform& SpawnTransform)
    {
    FActorSpawnParameters SpawnInfo;
    SpawnInfo.Instigator = GetInstigator();
    SpawnInfo.ObjectFlags |= RF_Transient; // Never save the default player pawns into a map.
    SpawnInfo.bDeferConstruction = true;

    if (UClass* PawnClass = GetDefaultPawnClassForController(NewPlayer))
    {
    if (APawn* SpawnedPawn = GetWorld()->SpawnActor<APawn>(PawnClass, SpawnTransform, SpawnInfo))
    {
    if (ULyraPawnExtensionComponent* PawnExtComp = ULyraPawnExtensionComponent::FindPawnExtensionComponent(SpawnedPawn))
    {
    if (const ULyraPawnData* PawnData = GetPawnDataForController(NewPlayer))
    {
    PawnExtComp->SetPawnData(PawnData);
    }
    else
    {
    UE_LOG(LogLyra, Error, TEXT("Game mode was unable to set PawnData on the spawned pawn [%s]."), *GetNameSafe(SpawnedPawn));
    }
    }

    SpawnedPawn->FinishSpawning(SpawnTransform);

    return SpawnedPawn;
    }
    else
    {
    UE_LOG(LogLyra, Error, TEXT("Game mode was unable to spawn Pawn of class [%s] at [%s]."), *GetNameSafe(PawnClass), *SpawnTransform.ToHumanReadableString());
    }
    }
    else
    {
    UE_LOG(LogLyra, Error, TEXT("Game mode was unable to spawn Pawn due to NULL pawn class."));
    }

    return nullptr;
    }

    bool ALyraGameMode::ShouldSpawnAtStartSpot(AController* Player)
    {
    // We never want to use the start spot, always use the spawn management component.
    return false;
    }

    void ALyraGameMode::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
    {
    // Delay starting new players until the experience has been loaded
    // (players who log in prior to that will be started by OnExperienceLoaded)
    if (IsExperienceLoaded())
    {
    Super::HandleStartingNewPlayer_Implementation(NewPlayer);
    }
    }

    AActor* ALyraGameMode::ChoosePlayerStart_Implementation(AController* Player)
    {
    if (ULyraPlayerSpawningManagerComponent* PlayerSpawningComponent = GameState->FindComponentByClass<ULyraPlayerSpawningManagerComponent>())
    {
    return PlayerSpawningComponent->ChoosePlayerStart(Player);
    }

    return Super::ChoosePlayerStart_Implementation(Player);
    }

    void ALyraGameMode::FinishRestartPlayer(AController* NewPlayer, const FRotator& StartRotation)
    {
    if (ULyraPlayerSpawningManagerComponent* PlayerSpawningComponent = GameState->FindComponentByClass<ULyraPlayerSpawningManagerComponent>())
    {
    PlayerSpawningComponent->FinishRestartPlayer(NewPlayer, StartRotation);
    }

    Super::FinishRestartPlayer(NewPlayer, StartRotation);
    }

    bool ALyraGameMode::PlayerCanRestart_Implementation(APlayerController* Player)
    {
    return ControllerCanRestart(Player);
    }

    bool ALyraGameMode::ControllerCanRestart(AController* Controller)
    {
    if (APlayerController* PC = Cast<APlayerController>(Controller))
    {
    if (!Super::PlayerCanRestart_Implementation(PC))
    {
    return false;
    }
    }
    else
    {
    // Bot version of Super::PlayerCanRestart_Implementation
    if ((Controller == nullptr) || Controller->IsPendingKillPending())
    {
    return false;
    }
    }

    if (ULyraPlayerSpawningManagerComponent* PlayerSpawningComponent = GameState->FindComponentByClass<ULyraPlayerSpawningManagerComponent>())
    {
    return PlayerSpawningComponent->ControllerCanRestart(Controller);
    }

    return true;
    }

    void ALyraGameMode::InitGameState()
    {
    Super::InitGameState();

    // Listen for the experience load to complete
    ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>();
    check(ExperienceComponent);
    ExperienceComponent->CallOrRegister_OnExperienceLoaded(FOnLyraExperienceLoaded::FDelegate::CreateUObject(this, &ThisClass::OnExperienceLoaded));
    }

    void ALyraGameMode::GenericPlayerInitialization(AController* NewPlayer)
    {
    Super::GenericPlayerInitialization(NewPlayer);

    OnGameModePlayerInitialized.Broadcast(this, NewPlayer);
    }

    void ALyraGameMode::RequestPlayerRestartNextFrame(AController* Controller, bool bForceReset)
    {
    if (bForceReset && (Controller != nullptr))
    {
    Controller->Reset();
    }

    if (APlayerController* PC = Cast<APlayerController>(Controller))
    {
    GetWorldTimerManager().SetTimerForNextTick(PC, &APlayerController::ServerRestartPlayer_Implementation);
    }
    else if (ALyraPlayerBotController* BotController = Cast<ALyraPlayerBotController>(Controller))
    {
    GetWorldTimerManager().SetTimerForNextTick(BotController, &ALyraPlayerBotController::ServerRestartController);
    }
    }

    bool ALyraGameMode::UpdatePlayerStartSpot(AController* Player, const FString& Portal, FString& OutErrorMessage)
    {
    // Do nothing, we'll wait until PostLogin when we try to spawn the player for real.
    // Doing anything right now is no good, systems like team assignment haven't even occurred yet.
    return true;
    }

    void ALyraGameMode::FailedToRestartPlayer(AController* NewPlayer)
    {
    Super::FailedToRestartPlayer(NewPlayer);

    // If we tried to spawn a pawn and it failed, lets try again *note* check if there's actually a pawn class
    // before we try this forever.
    if (UClass* PawnClass = GetDefaultPawnClassForController(NewPlayer))
    {
    if (APlayerController* NewPC = Cast<APlayerController>(NewPlayer))
    {
    // If it's a player don't loop forever, maybe something changed and they can no longer restart if so stop trying.
    if (PlayerCanRestart(NewPC))
    {
    RequestPlayerRestartNextFrame(NewPlayer, false);
    }
    else
    {
    UE_LOG(LogLyra, Verbose, TEXT("FailedToRestartPlayer(%s) and PlayerCanRestart returned false, so we're not going to try again."), *GetPathNameSafe(NewPlayer));
    }
    }
    else
    {
    RequestPlayerRestartNextFrame(NewPlayer, false);
    }
    }
    else
    {
    UE_LOG(LogLyra, Verbose, TEXT("FailedToRestartPlayer(%s) but there's no pawn class so giving up."), *GetPathNameSafe(NewPlayer));
    }
    }