// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. #include "GameModes/OLSGameMode.h" #include "CommonUserSubsystem.h" #include "GameMapsSettings.h" #include "DataAssets/OLSExperienceDefinitionDataAsset.h" #include "DataAssets/OLSPawnDataAsset.h" #include "GameModes/OLSExperienceManagerComponent.h" #include "Kismet/GameplayStatics.h" #include "Player/OLSPlayerState.h" #include "Systems/OLSAssetManager.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(OLSGameMode) // AOLSGameMode::AOLSGameMode(const FObjectInitializer& objectInitializer) : Super(objectInitializer) // { // // @TODO: Implement this. // // GameStateClass = ALyraGameState::StaticClass(); // // GameSessionClass = ALyraGameSession::StaticClass(); // // PlayerControllerClass = ALyraPlayerController::StaticClass(); // // ReplaySpectatorPlayerControllerClass = ALyraReplayPlayerController::StaticClass(); // // PlayerStateClass = ALyraPlayerState::StaticClass(); // // DefaultPawnClass = ALyraCharacter::StaticClass(); // // HUDClass = ALyraHUD::StaticClass(); // } const UOLSPawnDataAsset* AOLSGameMode::GetPawnDataForController(const AController* controller) const { // See if pawn data is already set on the player state if (controller) { if (const AOLSPlayerState* playerState = controller->GetPlayerState()) { if (const UOLSPawnDataAsset* pawnData = playerState->GetPawnData()) { return pawnData; } } } // If not, fall back to the the default for the current experience check(GameState); UOLSExperienceManagerComponent* experienceComponent = GameState->FindComponentByClass(); check(experienceComponent); if (experienceComponent->IsExperienceLoaded()) { const UOLSExperienceDefinitionDataAsset* experienceDataAsset = experienceComponent->GetCurrentExperienceChecked(); if (experienceDataAsset->DefaultPawnData) { return experienceDataAsset->DefaultPawnData; } // Experience is loaded and there's still no pawn data, fall back to the default for now return UOLSAssetManager::Get().GetDefaultPawnData(); } // Experience not loaded yet, so there is no pawn data to be had return nullptr; } void AOLSGameMode::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); } UClass* AOLSGameMode::GetDefaultPawnClassForController_Implementation(AController* controller) { if (const UOLSPawnDataAsset* pawnData = GetPawnDataForController(controller)) { if (pawnData->PawnClass) { return pawnData->PawnClass; } } return Super::GetDefaultPawnClassForController_Implementation(controller); } APawn* AOLSGameMode::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(pawnClass, spawnTransform, spawnInfo)) { // @TODO: Implement this as well as implement UOLSawnExtensionComponent // if (UOLSawnExtensionComponent* PawnExtComp = UOLSawnExtensionComponent::FindPawnExtensionComponent(spawnedPawn)) // { // if (const ULyraPawnData* PawnData = GetPawnDataForController(newPlayer)) // { // PawnExtComp->SetPawnData(PawnData); // } // else // { // @TODO: Replace this with out custom log. // 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 { // @TODO: Replace this with out custom log. // UE_LOG(LogLyra, Error, TEXT("Game mode was unable to spawn Pawn of class [%s] at [%s]."), *GetNameSafe(pawnClass), *spawnTransform.ToHumanReadableString()); } } else { // @TODO: Replace this with out custom log. // UE_LOG(LogLyra, Error, TEXT("Game mode was unable to spawn Pawn due to NULL pawn class.")); } return nullptr; } bool AOLSGameMode::ShouldSpawnAtStartSpot(AController* Player) { // We never want to use the start spot, always use the spawn management component. return false; } void AOLSGameMode::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* AOLSGameMode::ChoosePlayerStart_Implementation(AController* player) { // @TODO: Implement this as well as implement UOLSPlayerSpawningManagerComponent // if (UOLSPlayerSpawningManagerComponent* PlayerSpawningComponent = GameState->FindComponentByClass()) // { // return PlayerSpawningComponent->ChoosePlayerStart(player); // } return Super::ChoosePlayerStart_Implementation(player); } void AOLSGameMode::FinishRestartPlayer(AController* newPlayer, const FRotator& startRotation) { // @TODO: Implement this as well as implement UOLSPlayerSpawningManagerComponent // if (UOLSPlayerSpawningManagerComponent* PlayerSpawningComponent = GameState->FindComponentByClass()) // { // PlayerSpawningComponent->FinishRestartPlayer(NewPlayer, StartRotation); // } Super::FinishRestartPlayer(newPlayer, startRotation); } bool AOLSGameMode::PlayerCanRestart_Implementation(APlayerController* player) { return ControllerCanRestart(player); } void AOLSGameMode::InitGameState() { Super::InitGameState(); // Listen for the experience load to complete UOLSExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass(); check(ExperienceComponent); ExperienceComponent->CallOrRegister_OnExperienceLoaded(FOLSExperienceLoadedNativeDelegate::FDelegate::CreateUObject(this, &ThisClass::OnExperienceLoaded)); } bool AOLSGameMode::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 AOLSGameMode::GenericPlayerInitialization(AController* newPlayer) { Super::GenericPlayerInitialization(newPlayer); OnGameModePlayerInitialized.Broadcast(this, newPlayer); } void AOLSGameMode::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(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 { // @TODO: Replace this with our custom log. // 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 { // @TODO: Replace this with our custom log. // UE_LOG(LogLyra, Verbose, TEXT("FailedToRestartPlayer(%s) but there's no pawn class so giving up."), *GetPathNameSafe(newPlayer)); } } void AOLSGameMode::RequestPlayerRestartNextFrame(AController* controller, bool shouldForceReset) { if (shouldForceReset && controller) { controller->Reset(); } if (APlayerController* playerController = Cast(controller)) { GetWorldTimerManager().SetTimerForNextTick(playerController, &APlayerController::ServerRestartPlayer_Implementation); } // else if (AOLSPlayerBotController* BotController = Cast(controller)) // { // GetWorldTimerManager().SetTimerForNextTick(BotController, &ALyraPlayerBotController::ServerRestartController); // } } bool AOLSGameMode::ControllerCanRestart(AController* controller) { if (APlayerController* playerController = Cast(controller)) { if (!Super::PlayerCanRestart_Implementation(playerController)) { return false; } } else { // Bot version of Super::PlayerCanRestart_Implementation if ((controller == nullptr) || controller->IsPendingKillPending()) { return false; } } // @TODO: Implement this after having PlayerSpawningManagerComponent // if (ULyraPlayerSpawningManagerComponent* PlayerSpawningComponent = GameState->FindComponentByClass()) // { // return PlayerSpawningComponent->ControllerCanRestart(controller); // } return true; } void AOLSGameMode::OnExperienceLoaded(const UOLSExperienceDefinitionDataAsset* 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* playerController = Cast(*iterator); if (playerController && playerController->GetPawn()) { if (PlayerCanRestart(playerController)) { RestartPlayer(playerController); } } } } bool AOLSGameMode::IsExperienceLoaded() const { check(GameState); UOLSExperienceManagerComponent* experienceComponent = GameState->FindComponentByClass< UOLSExperienceManagerComponent>(); check(experienceComponent); return experienceComponent->IsExperienceLoaded(); } void AOLSGameMode::OnMatchAssignmentGiven(FPrimaryAssetId experienceId, const FString& experienceIdSource) { if (experienceId.IsValid()) { // @TODO: Replace this with our custom. // UE_LOG(LogLyraExperience, Log, TEXT("Identified experience %s (Source: %s)"), *ExperienceId.ToString(), *ExperienceIdSource); UOLSExperienceManagerComponent* experienceComponent = GameState->FindComponentByClass(); check(experienceComponent); experienceComponent->SetCurrentExperience(experienceId); } else { // @TODO: Replace this with our custom. // UE_LOG(LogLyraExperience, Error, TEXT("Failed to identify experience, loading screen will stay up forever")); } } void AOLSGameMode::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(UOLSExperienceDefinitionDataAsset::StaticClass()->GetFName()), FName(*ExperienceFromOptions)); experienceIdSource = TEXT("OptionsString"); } if (!experienceId.IsValid() && World->IsPlayInEditor()) { // @TODO: Implement this when UOLSDeveloperSettings is implemented. // experienceId = GetDefault()->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(UOLSExperienceDefinitionDataAsset::StaticClass()->GetFName()), FName(*ExperienceFromCommandLine)); } experienceIdSource = TEXT("CommandLine"); } } // see if the world settings has a default experience if (!experienceId.IsValid()) { // @TODO: Implement this when AOLSWorldSettings is implemented. // if (AOLSWorldSettings* TypedWorldSettings = Cast(GetWorldSettings())) // { // experienceId = TypedWorldSettings->GetDefaultGameplayExperience(); // experienceIdSource = TEXT("WorldSettings"); // } } UOLSAssetManager& assetManager = UOLSAssetManager::Get(); FAssetData dummy; if (experienceId.IsValid() && !assetManager.GetPrimaryAssetData(experienceId, /*out*/ dummy)) { // @TODO: Replace this with our custom. // UE_LOG(LogLyraExperience, Error, TEXT("EXPERIENCE: Wanted to use %s but couldn't find it, falling back to the default)"), *experienceIding()); 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 AOLSGameMode::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(); // // // Dedicated servers may need to do an online login // userSubsystem->OnUserInitializeComplete.AddDynamic(this, &ThisClass::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 AOLSGameMode::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 Handle = AssetManager.LoadPrimaryAssetsWithType(UserExperienceType); // if (ensure(Handle.IsValid())) // { // Handle->WaitUntilComplete(); // } // // TArray UserExperiences; // AssetManager.GetPrimaryAssetObjectList(UserExperienceType, UserExperiences); // ULyraUserFacingExperienceDefinition* FoundExperience = nullptr; // ULyraUserFacingExperienceDefinition* DefaultExperience = nullptr; // // for (UObject* Object : UserExperiences) // { // ULyraUserFacingExperienceDefinition* UserExperience = Cast(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(this); // if (ensure(HostRequest)) // { // HostRequest->OnlineMode = OnlineMode; // // // TODO override other parameters? // // UCommonSessionSubsystem* SessionSubsystem = GameInstance->GetSubsystem(); // SessionSubsystem->HostSession(nullptr, HostRequest); // // // This will handle the map travel // } // } } void AOLSGameMode::OnUserInitializedForDedicatedServer( const UCommonUserInfo* userInfo, bool isSuccess, FText error, ECommonUserPrivilege requestedPrivilege, ECommonUserOnlineContext onlineContext) { UGameInstance* gameInstance = GetGameInstance(); if (gameInstance) { // Unbind UCommonUserSubsystem* UserSubsystem = gameInstance->GetSubsystem(); UserSubsystem->OnUserInitializeComplete.RemoveDynamic(this, &ThisClass::OnUserInitializedForDedicatedServer); // Dedicated servers do not require user login, but some online subsystems may expect it if (isSuccess && ensure(userInfo)) { // @TODO: replace this with our custom. // UE_LOG(LogLyraExperience, Log, TEXT("Dedicated server user login succeeded for id %s, starting online server"), *UserInfo->GetNetId().ToString()); } else { // @TODO: replace this with our custom. // UE_LOG(LogLyraExperience, Log, TEXT("Dedicated server user login unsuccessful, starting online server as login is not required")); } HostDedicatedServerMatch(ECommonSessionOnlineMode::Online); } }