698 lines
20 KiB
C++
698 lines
20 KiB
C++
#include "HyperTwistTraining/HyperTwistFirstRunLaunchPlayerController.h"
|
|
|
|
#include "Blueprint/UserWidget.h"
|
|
#include "Engine/Engine.h"
|
|
#include "EngineUtils.h"
|
|
#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"
|
|
#include "HyperTwistTraining/HyperTwistCoachDashboardActor.h"
|
|
#include "HyperTwistTraining/HyperTwistFirstRunLaunchLibrary.h"
|
|
#include "HyperTwistTraining/HyperTwistFirstRunLaunchWidget.h"
|
|
#include "Kismet/GameplayStatics.h"
|
|
#include "Misc/CommandLine.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/Paths.h"
|
|
#include "TimerManager.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogHyperTwistFirstRunLaunch, Log, All);
|
|
|
|
AHyperTwistFirstRunLaunchPlayerController::AHyperTwistFirstRunLaunchPlayerController()
|
|
{
|
|
bShowMouseCursor = true;
|
|
bEnableClickEvents = true;
|
|
bEnableMouseOverEvents = true;
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::SetupInputComponent()
|
|
{
|
|
Super::SetupInputComponent();
|
|
if (InputComponent == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
InputComponent->BindKey(
|
|
EKeys::Escape,
|
|
IE_Pressed,
|
|
this,
|
|
&AHyperTwistFirstRunLaunchPlayerController::ReturnFromAdvancedDashboard);
|
|
InputComponent->BindKey(
|
|
EKeys::F10,
|
|
IE_Pressed,
|
|
this,
|
|
&AHyperTwistFirstRunLaunchPlayerController::ReturnFromAdvancedDashboard);
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
|
|
InitializeStartupDiagnostics();
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("BeginPlay"),
|
|
FString::Printf(TEXT("First-run controller booted on world '%s'."), *DescribeCurrentWorldName())
|
|
);
|
|
|
|
ApplyFirstRunInputMode();
|
|
if (TryOpenCommandLineStartupRoute())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (bShowFirstRunLaunchOnBeginPlay)
|
|
{
|
|
if (ShowFirstRunLaunchMenu() == nullptr)
|
|
{
|
|
TryRecoverFromFirstRunFailure(TEXT("Failed to create the first-run launch widget on begin play."));
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("BeginPlay"),
|
|
TEXT("The first-run launch menu was intentionally disabled on begin play.")
|
|
);
|
|
PersistStartupDiagnostics(TEXT("launch-menu-disabled"));
|
|
}
|
|
|
|
bool AHyperTwistFirstRunLaunchPlayerController::TryOpenCommandLineStartupRoute()
|
|
{
|
|
FHyperTwistFirstRunLaunchRoute Route;
|
|
FString FailureReason;
|
|
const EHyperTwistPackagedStartupRouteParseResult ParseResult =
|
|
UHyperTwistFirstRunLaunchLibrary::ParsePackagedStartupRouteArgument(
|
|
FCommandLine::Get(),
|
|
Route,
|
|
FailureReason
|
|
);
|
|
if (ParseResult == EHyperTwistPackagedStartupRouteParseResult::NotPresent)
|
|
{
|
|
return false;
|
|
}
|
|
if (ParseResult == EHyperTwistPackagedStartupRouteParseResult::Invalid)
|
|
{
|
|
const FString Message = FString::Printf(
|
|
TEXT("Rejected packaged startup-route override: %s Continuing with the visible first-run menu."),
|
|
FailureReason.IsEmpty() ? TEXT("invalid argument.") : *FailureReason
|
|
);
|
|
AppendStartupDiagnosticsLine(TEXT("PackagedStartupRoute"), Message, true);
|
|
HyperTwistRuntimeDiagnostics::AppendEvent(TEXT("PackagedStartupRoute"), Message);
|
|
PersistStartupDiagnostics(TEXT("command-line-route-rejected"));
|
|
return false;
|
|
}
|
|
|
|
const FString AcceptedMessage = FString::Printf(
|
|
TEXT("Accepted packaged startup route '%s': map='%s', game_mode='%s'."),
|
|
*Route.RouteId,
|
|
Route.MapAssetPath.IsEmpty() ? TEXT("<dashboard>") : *Route.MapAssetPath,
|
|
Route.GameModeClassPath.IsEmpty() ? TEXT("<none>") : *Route.GameModeClassPath
|
|
);
|
|
AppendStartupDiagnosticsLine(TEXT("PackagedStartupRoute"), AcceptedMessage);
|
|
HyperTwistRuntimeDiagnostics::AppendEvent(TEXT("PackagedStartupRoute"), AcceptedMessage);
|
|
if (OpenFirstRunRoute(Route.RouteId))
|
|
{
|
|
const FString OpenedMessage = FString::Printf(
|
|
TEXT("Opened packaged startup route '%s' through first-run product authority."),
|
|
*Route.RouteId
|
|
);
|
|
AppendStartupDiagnosticsLine(TEXT("PackagedStartupRoute"), OpenedMessage);
|
|
HyperTwistRuntimeDiagnostics::AppendEvent(TEXT("PackagedStartupRoute"), OpenedMessage);
|
|
PersistStartupDiagnostics(TEXT("command-line-route-opened"));
|
|
return true;
|
|
}
|
|
|
|
const FString OpenFailureMessage = FString::Printf(
|
|
TEXT("Could not open packaged startup route '%s'; continuing with the visible first-run menu."),
|
|
*Route.RouteId
|
|
);
|
|
AppendStartupDiagnosticsLine(TEXT("PackagedStartupRoute"), OpenFailureMessage, true);
|
|
HyperTwistRuntimeDiagnostics::AppendEvent(
|
|
TEXT("PackagedStartupRoute"),
|
|
OpenFailureMessage
|
|
);
|
|
PersistStartupDiagnostics(TEXT("command-line-route-open-failed"));
|
|
return false;
|
|
}
|
|
|
|
UHyperTwistFirstRunLaunchWidget*
|
|
AHyperTwistFirstRunLaunchPlayerController::ShowFirstRunLaunchMenu()
|
|
{
|
|
if (ActiveFirstRunLaunchWidget != nullptr)
|
|
{
|
|
ActiveFirstRunLaunchWidget->RebuildFirstRunLaunchSurface();
|
|
if (!ActiveFirstRunLaunchWidget->IsFirstRunLaunchSurfaceReady())
|
|
{
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("ShowFirstRunLaunchMenu"),
|
|
TEXT("The existing first-run widget has no renderable root tree."),
|
|
true
|
|
);
|
|
PersistStartupDiagnostics(TEXT("launch-widget-root-missing"));
|
|
return nullptr;
|
|
}
|
|
if (!ActiveFirstRunLaunchWidget->IsInViewport())
|
|
{
|
|
ActiveFirstRunLaunchWidget->AddToViewport(FirstRunLaunchZOrder);
|
|
}
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("ShowFirstRunLaunchMenu"),
|
|
TEXT("Prepared the existing first-run launch widget for layout validation.")
|
|
);
|
|
ScheduleFirstRunSurfaceValidation();
|
|
return ActiveFirstRunLaunchWidget;
|
|
}
|
|
|
|
TSubclassOf<UHyperTwistFirstRunLaunchWidget> ResolvedWidgetClass = FirstRunLaunchWidgetClass;
|
|
if (*ResolvedWidgetClass == nullptr)
|
|
{
|
|
ResolvedWidgetClass = UHyperTwistFirstRunLaunchWidget::StaticClass();
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("ShowFirstRunLaunchMenu"),
|
|
TEXT("Fell back to the native first-run widget class.")
|
|
);
|
|
}
|
|
if (*ResolvedWidgetClass == nullptr)
|
|
{
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("ShowFirstRunLaunchMenu"),
|
|
TEXT("No first-run widget class was available."),
|
|
true
|
|
);
|
|
PersistStartupDiagnostics(TEXT("launch-widget-class-missing"));
|
|
return nullptr;
|
|
}
|
|
|
|
ActiveFirstRunLaunchWidget = CreateWidget<UHyperTwistFirstRunLaunchWidget>(
|
|
this,
|
|
ResolvedWidgetClass
|
|
);
|
|
if (ActiveFirstRunLaunchWidget == nullptr)
|
|
{
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("ShowFirstRunLaunchMenu"),
|
|
TEXT("CreateWidget returned null for the first-run launch surface."),
|
|
true
|
|
);
|
|
PersistStartupDiagnostics(TEXT("launch-widget-create-failed"));
|
|
return nullptr;
|
|
}
|
|
|
|
ActiveFirstRunLaunchWidget->OnFirstRunRouteRequested.AddDynamic(
|
|
this,
|
|
&AHyperTwistFirstRunLaunchPlayerController::HandleFirstRunRouteRequested
|
|
);
|
|
ActiveFirstRunLaunchWidget->RebuildFirstRunLaunchSurface();
|
|
if (!ActiveFirstRunLaunchWidget->IsFirstRunLaunchSurfaceReady())
|
|
{
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("ShowFirstRunLaunchMenu"),
|
|
TEXT("The first-run widget could not construct its renderable root tree."),
|
|
true
|
|
);
|
|
ActiveFirstRunLaunchWidget = nullptr;
|
|
PersistStartupDiagnostics(TEXT("launch-widget-root-missing"));
|
|
return nullptr;
|
|
}
|
|
|
|
ActiveFirstRunLaunchWidget->AddToViewport(FirstRunLaunchZOrder);
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("ShowFirstRunLaunchMenu"),
|
|
FString::Printf(
|
|
TEXT("Added the first-run launch widget to the viewport at z-order %d."),
|
|
FirstRunLaunchZOrder
|
|
)
|
|
);
|
|
ApplyFirstRunInputMode();
|
|
ScheduleFirstRunSurfaceValidation();
|
|
return ActiveFirstRunLaunchWidget;
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::RemoveFirstRunLaunchMenu()
|
|
{
|
|
if (ActiveFirstRunLaunchWidget != nullptr)
|
|
{
|
|
GetWorldTimerManager().ClearTimer(FirstRunSurfaceValidationTimerHandle);
|
|
FirstRunSurfaceValidationAttempt = 0;
|
|
ActiveFirstRunLaunchWidget->RemoveFromParent();
|
|
ActiveFirstRunLaunchWidget = nullptr;
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("RemoveFirstRunLaunchMenu"),
|
|
TEXT("Removed the active first-run launch widget from the viewport.")
|
|
);
|
|
PersistStartupDiagnostics(TEXT("launch-widget-removed"));
|
|
}
|
|
}
|
|
|
|
bool AHyperTwistFirstRunLaunchPlayerController::OpenFirstRunRoute(const FString& RouteId)
|
|
{
|
|
FHyperTwistFirstRunLaunchRoute Route;
|
|
if (!UHyperTwistFirstRunLaunchLibrary::TryFindFirstRunLaunchRoute(RouteId, Route)
|
|
|| !Route.IsStructurallyValid())
|
|
{
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("OpenFirstRunRoute"),
|
|
FString::Printf(TEXT("Rejected invalid first-run route '%s'."), *RouteId),
|
|
true
|
|
);
|
|
PersistStartupDiagnostics(TEXT("invalid-route-rejected"));
|
|
return false;
|
|
}
|
|
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("OpenFirstRunRoute"),
|
|
FString::Printf(
|
|
TEXT("Opening route '%s' (%s)."),
|
|
*Route.RouteId,
|
|
Route.bOpensDashboard ? TEXT("dashboard") : TEXT("dedicated-map")
|
|
)
|
|
);
|
|
|
|
if (Route.bOpensDashboard)
|
|
{
|
|
RemoveFirstRunLaunchMenu();
|
|
if (EnsureCoachDashboard() != nullptr)
|
|
{
|
|
PersistStartupDiagnostics(TEXT("dashboard-opened"));
|
|
return true;
|
|
}
|
|
|
|
TryRecoverFromFirstRunFailure(
|
|
FString::Printf(TEXT("Failed to open dashboard route '%s'."), *Route.RouteId)
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (Route.bLaunchesDedicatedMap)
|
|
{
|
|
OpenMapRoute(Route.MapAssetPath, Route.GameModeClassPath);
|
|
PersistStartupDiagnostics(TEXT("map-route-opened"));
|
|
return true;
|
|
}
|
|
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("OpenFirstRunRoute"),
|
|
FString::Printf(TEXT("Route '%s' did not match a supported launch action."), *Route.RouteId),
|
|
true
|
|
);
|
|
PersistStartupDiagnostics(TEXT("route-without-launch-action"));
|
|
return false;
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::HandleFirstRunRouteRequested(
|
|
const FString& RouteId
|
|
)
|
|
{
|
|
if (!OpenFirstRunRoute(RouteId))
|
|
{
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("HandleFirstRunRouteRequested"),
|
|
FString::Printf(TEXT("The requested route '%s' did not open successfully."), *RouteId),
|
|
true
|
|
);
|
|
}
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::ReturnFromAdvancedDashboard()
|
|
{
|
|
if (ActiveDashboardActor == nullptr || !ActiveDashboardActor->HasActiveDashboard())
|
|
{
|
|
return;
|
|
}
|
|
|
|
ActiveDashboardActor->RemoveDashboard();
|
|
if (UHyperTwistFirstRunLaunchWidget* LaunchWidget = ShowFirstRunLaunchMenu())
|
|
{
|
|
LaunchWidget->ShowPage(EHyperTwistMainMenuPage::Advanced);
|
|
}
|
|
ApplyFirstRunInputMode();
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("ReturnFromAdvancedDashboard"),
|
|
TEXT("Closed the engineering dashboard and restored the main menu."));
|
|
PersistStartupDiagnostics(TEXT("dashboard-returned-to-main-menu"));
|
|
}
|
|
|
|
AHyperTwistCoachDashboardActor*
|
|
AHyperTwistFirstRunLaunchPlayerController::EnsureCoachDashboard()
|
|
{
|
|
if (ActiveDashboardActor == nullptr)
|
|
{
|
|
ActiveDashboardActor = FindExistingDashboardActor();
|
|
}
|
|
|
|
if (ActiveDashboardActor == nullptr)
|
|
{
|
|
ActiveDashboardActor = SpawnDashboardActor();
|
|
}
|
|
|
|
if (ActiveDashboardActor != nullptr)
|
|
{
|
|
ApplyDashboardDefaults(ActiveDashboardActor);
|
|
ActiveDashboardActor->CreateAndShowDashboard();
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("EnsureCoachDashboard"),
|
|
TEXT("Dashboard actor is ready and the dashboard surface was requested.")
|
|
);
|
|
PersistStartupDiagnostics(TEXT("dashboard-surface-requested"));
|
|
}
|
|
else
|
|
{
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("EnsureCoachDashboard"),
|
|
TEXT("Dashboard actor could not be found or spawned."),
|
|
true
|
|
);
|
|
}
|
|
|
|
ApplyFirstRunInputMode();
|
|
return ActiveDashboardActor;
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::ApplyFirstRunInputMode()
|
|
{
|
|
bShowMouseCursor = true;
|
|
bEnableClickEvents = true;
|
|
bEnableMouseOverEvents = true;
|
|
|
|
FInputModeGameAndUI InputMode;
|
|
InputMode.SetHideCursorDuringCapture(false);
|
|
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
|
|
SetInputMode(InputMode);
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("ApplyFirstRunInputMode"),
|
|
TEXT("Applied game-and-UI input mode with unlocked cursor capture.")
|
|
);
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::OpenMapRoute(
|
|
const FString& MapAssetPath,
|
|
const FString& GameModeClassPath
|
|
)
|
|
{
|
|
if (MapAssetPath.IsEmpty())
|
|
{
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("OpenMapRoute"),
|
|
TEXT("Refused to open an empty map route."),
|
|
true
|
|
);
|
|
return;
|
|
}
|
|
|
|
FString Options;
|
|
if (!GameModeClassPath.IsEmpty())
|
|
{
|
|
Options = FString::Printf(TEXT("game=%s"), *GameModeClassPath);
|
|
}
|
|
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("OpenMapRoute"),
|
|
FString::Printf(
|
|
TEXT("Opening map '%s' with options '%s'."),
|
|
*MapAssetPath,
|
|
Options.IsEmpty() ? TEXT("<none>") : *Options
|
|
)
|
|
);
|
|
UGameplayStatics::OpenLevel(this, FName(*MapAssetPath), true, Options);
|
|
}
|
|
|
|
AHyperTwistCoachDashboardActor*
|
|
AHyperTwistFirstRunLaunchPlayerController::FindExistingDashboardActor()
|
|
{
|
|
if (GetWorld() == nullptr)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
for (TActorIterator<AHyperTwistCoachDashboardActor> ActorIt(GetWorld()); ActorIt; ++ActorIt)
|
|
{
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("FindExistingDashboardActor"),
|
|
TEXT("Reused an existing dashboard actor from the world.")
|
|
);
|
|
return *ActorIt;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
AHyperTwistCoachDashboardActor*
|
|
AHyperTwistFirstRunLaunchPlayerController::SpawnDashboardActor()
|
|
{
|
|
if (GetWorld() == nullptr)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
TSubclassOf<AHyperTwistCoachDashboardActor> ResolvedActorClass = DashboardActorClass;
|
|
if (*ResolvedActorClass == nullptr)
|
|
{
|
|
ResolvedActorClass = AHyperTwistCoachDashboardActor::StaticClass();
|
|
}
|
|
if (*ResolvedActorClass == nullptr)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
FTransform SpawnTransform;
|
|
AHyperTwistCoachDashboardActor* DashboardActor =
|
|
GetWorld()->SpawnActorDeferred<AHyperTwistCoachDashboardActor>(
|
|
ResolvedActorClass,
|
|
SpawnTransform,
|
|
nullptr,
|
|
nullptr,
|
|
ESpawnActorCollisionHandlingMethod::AlwaysSpawn
|
|
);
|
|
if (DashboardActor == nullptr)
|
|
{
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("SpawnDashboardActor"),
|
|
TEXT("SpawnActorDeferred returned null for the dashboard actor."),
|
|
true
|
|
);
|
|
return nullptr;
|
|
}
|
|
|
|
ApplyDashboardDefaults(DashboardActor);
|
|
DashboardActor->FinishSpawning(SpawnTransform);
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("SpawnDashboardActor"),
|
|
TEXT("Spawned a new dashboard actor for the first-run shell.")
|
|
);
|
|
return DashboardActor;
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::ApplyDashboardDefaults(
|
|
AHyperTwistCoachDashboardActor* DashboardActor
|
|
) const
|
|
{
|
|
if (DashboardActor == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DashboardActor->DashboardDefaultUserId = DashboardDefaultUserId;
|
|
DashboardActor->DashboardDefaultSessionId = DashboardDefaultSessionId;
|
|
DashboardActor->DashboardDefaultMode = DashboardDefaultMode;
|
|
DashboardActor->DashboardDefaultCoachMaxCases = DashboardDefaultCoachMaxCases;
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::InitializeStartupDiagnostics()
|
|
{
|
|
if (!StartupDiagnosticsLines.IsEmpty() && !StartupSessionDiagnosticsLogPath.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FString LogDirectory = FPaths::ProjectLogDir();
|
|
RuntimeLogPath = FPaths::Combine(
|
|
LogDirectory,
|
|
UHyperTwistFirstRunLaunchLibrary::GetRuntimeLogFileName()
|
|
);
|
|
StartupDiagnosticsLogPath = FPaths::Combine(
|
|
LogDirectory,
|
|
UHyperTwistFirstRunLaunchLibrary::GetStartupDiagnosticsLogFileName()
|
|
);
|
|
const FString SessionToken = FDateTime::UtcNow().ToString(TEXT("%Y%m%dT%H%M%SZ"));
|
|
StartupSessionDiagnosticsLogPath = FPaths::Combine(
|
|
LogDirectory,
|
|
FString::Printf(TEXT("HyperTwistFirstRunLaunch-%s.log"), *SessionToken)
|
|
);
|
|
|
|
StartupDiagnosticsLines.Reset();
|
|
StartupDiagnosticsLines.Add(FString::Printf(TEXT("session=%s"), *SessionToken));
|
|
StartupDiagnosticsLines.Add(FString::Printf(TEXT("world=%s"), *DescribeCurrentWorldName()));
|
|
StartupDiagnosticsLines.Add(FString::Printf(TEXT("runtime_log=%s"), *RuntimeLogPath));
|
|
StartupDiagnosticsLines.Add(FString::Printf(TEXT("latest_diagnostics_log=%s"), *StartupDiagnosticsLogPath));
|
|
PersistStartupDiagnostics(TEXT("startup-initialized"));
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::AppendStartupDiagnosticsLine(
|
|
const FString& Stage,
|
|
const FString& Message,
|
|
const bool bTreatAsError
|
|
)
|
|
{
|
|
InitializeStartupDiagnostics();
|
|
|
|
const FString Line = FString::Printf(
|
|
TEXT("[%s] [%s] %s"),
|
|
*FDateTime::UtcNow().ToString(TEXT("%Y-%m-%dT%H:%M:%SZ")),
|
|
*Stage,
|
|
*Message
|
|
);
|
|
StartupDiagnosticsLines.Add(Line);
|
|
|
|
if (bTreatAsError)
|
|
{
|
|
LastStartupFailureReason = Message;
|
|
UE_LOG(LogHyperTwistFirstRunLaunch, Error, TEXT("%s"), *Line);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogHyperTwistFirstRunLaunch, Display, TEXT("%s"), *Line);
|
|
}
|
|
|
|
PersistStartupDiagnostics(bTreatAsError ? TEXT("startup-error") : TEXT("startup-progress"));
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::PersistStartupDiagnostics(
|
|
const FString& ResultStatus
|
|
) const
|
|
{
|
|
if (StartupDiagnosticsLogPath.IsEmpty() || StartupSessionDiagnosticsLogPath.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
TArray<FString> LinesToWrite = StartupDiagnosticsLines;
|
|
LinesToWrite.Add(FString::Printf(TEXT("result=%s"), *ResultStatus));
|
|
if (!LastStartupFailureReason.IsEmpty())
|
|
{
|
|
LinesToWrite.Add(FString::Printf(TEXT("last_failure=%s"), *LastStartupFailureReason));
|
|
}
|
|
|
|
FFileHelper::SaveStringArrayToFile(LinesToWrite, *StartupDiagnosticsLogPath);
|
|
FFileHelper::SaveStringArrayToFile(LinesToWrite, *StartupSessionDiagnosticsLogPath);
|
|
}
|
|
|
|
bool AHyperTwistFirstRunLaunchPlayerController::TryRecoverFromFirstRunFailure(
|
|
const FString& FailureReason
|
|
)
|
|
{
|
|
LastStartupFailureReason = FailureReason;
|
|
AppendStartupDiagnosticsLine(TEXT("StartupRecovery"), FailureReason, true);
|
|
|
|
if (bStartupRecoveryAttempted)
|
|
{
|
|
PersistStartupDiagnostics(TEXT("startup-recovery-already-attempted"));
|
|
return false;
|
|
}
|
|
|
|
bStartupRecoveryAttempted = true;
|
|
const FString FallbackRouteId = UHyperTwistFirstRunLaunchLibrary::GetStartupFallbackRouteId();
|
|
const FString RecoveryMessage = FString::Printf(
|
|
TEXT("HyperTwist could not show the first-run launch surface. ")
|
|
TEXT("Inspect Saved/Logs/%s and Saved/Logs/%s. ")
|
|
TEXT("Attempting automatic fallback route '%s'."),
|
|
*UHyperTwistFirstRunLaunchLibrary::GetRuntimeLogFileName(),
|
|
*UHyperTwistFirstRunLaunchLibrary::GetStartupDiagnosticsLogFileName(),
|
|
*FallbackRouteId
|
|
);
|
|
|
|
if (GEngine != nullptr)
|
|
{
|
|
GEngine->AddOnScreenDebugMessage(
|
|
-1,
|
|
15.0f,
|
|
FColor::Yellow,
|
|
RecoveryMessage
|
|
);
|
|
}
|
|
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("StartupRecovery"),
|
|
FString::Printf(TEXT("Attempting fallback route '%s'."), *FallbackRouteId)
|
|
);
|
|
|
|
const bool bRecovered = OpenFirstRunRoute(FallbackRouteId);
|
|
PersistStartupDiagnostics(bRecovered ? TEXT("startup-recovered") : TEXT("startup-recovery-failed"));
|
|
return bRecovered;
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::ScheduleFirstRunSurfaceValidation()
|
|
{
|
|
FirstRunSurfaceValidationAttempt = 0;
|
|
GetWorldTimerManager().ClearTimer(FirstRunSurfaceValidationTimerHandle);
|
|
GetWorldTimerManager().SetTimer(
|
|
FirstRunSurfaceValidationTimerHandle,
|
|
this,
|
|
&AHyperTwistFirstRunLaunchPlayerController::ValidateFirstRunSurfaceLayout,
|
|
FMath::Max(FirstRunSurfaceValidationIntervalSeconds, 0.01f),
|
|
false
|
|
);
|
|
PersistStartupDiagnostics(TEXT("launch-widget-added-pending-layout"));
|
|
}
|
|
|
|
void AHyperTwistFirstRunLaunchPlayerController::ValidateFirstRunSurfaceLayout()
|
|
{
|
|
if (ActiveFirstRunLaunchWidget == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
++FirstRunSurfaceValidationAttempt;
|
|
const FVector2D LocalSize = ActiveFirstRunLaunchWidget->GetCachedGeometry().GetLocalSize();
|
|
const bool bHasRenderableLayout = ActiveFirstRunLaunchWidget->IsInViewport()
|
|
&& ActiveFirstRunLaunchWidget->IsVisible()
|
|
&& ActiveFirstRunLaunchWidget->IsFirstRunLaunchSurfaceReady()
|
|
&& LocalSize.X > 1.0f
|
|
&& LocalSize.Y > 1.0f;
|
|
if (bHasRenderableLayout)
|
|
{
|
|
AppendStartupDiagnosticsLine(
|
|
TEXT("ValidateFirstRunSurfaceLayout"),
|
|
FString::Printf(
|
|
TEXT("Confirmed a renderable first-run layout at %.0fx%.0f after %d attempt(s)."),
|
|
LocalSize.X,
|
|
LocalSize.Y,
|
|
FirstRunSurfaceValidationAttempt
|
|
)
|
|
);
|
|
PersistStartupDiagnostics(TEXT("launch-menu-renderable"));
|
|
HyperTwistRuntimeDiagnostics::RequestReadyFrameCapture(TEXT("first-run-launch-ready"));
|
|
return;
|
|
}
|
|
|
|
if (FirstRunSurfaceValidationAttempt < FMath::Max(MaxFirstRunSurfaceValidationAttempts, 1))
|
|
{
|
|
GetWorldTimerManager().SetTimer(
|
|
FirstRunSurfaceValidationTimerHandle,
|
|
this,
|
|
&AHyperTwistFirstRunLaunchPlayerController::ValidateFirstRunSurfaceLayout,
|
|
FMath::Max(FirstRunSurfaceValidationIntervalSeconds, 0.01f),
|
|
false
|
|
);
|
|
return;
|
|
}
|
|
|
|
TryRecoverFromFirstRunFailure(
|
|
FString::Printf(
|
|
TEXT("The first-run launch widget never produced a visible non-zero layout after %d attempts (last size %.0fx%.0f)."),
|
|
FirstRunSurfaceValidationAttempt,
|
|
LocalSize.X,
|
|
LocalSize.Y
|
|
)
|
|
);
|
|
}
|
|
|
|
FString AHyperTwistFirstRunLaunchPlayerController::DescribeCurrentWorldName() const
|
|
{
|
|
const UWorld* World = GetWorld();
|
|
if (World == nullptr)
|
|
{
|
|
return TEXT("null-world");
|
|
}
|
|
|
|
return World->GetMapName();
|
|
}
|