hypertwist/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistFirstRunLaunchPlayerController.cpp
2026-07-23 17:23:24 +00:00

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();
}