Harden MVP menu input and quit flow
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FILES = {
|
||||
"AgrarianMvpFrontendWidget.h": ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.h",
|
||||
"AgrarianMvpFrontendWidget.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.cpp",
|
||||
"AgrarianGamePlayerController.h": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.h",
|
||||
"AgrarianGamePlayerController.cpp": ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp",
|
||||
}
|
||||
|
||||
EXPECTED = {
|
||||
"AgrarianMvpFrontendWidget.h": [
|
||||
"ConfirmActiveScreen",
|
||||
"BackFromActiveScreen",
|
||||
"SaveAndQuit",
|
||||
],
|
||||
"AgrarianMvpFrontendWidget.cpp": [
|
||||
"NativeOnMouseButtonDown",
|
||||
"IsPointInside(LocalMousePosition, MaleCardPosition, CardSize)",
|
||||
"IsPointInside(LocalMousePosition, FemaleCardPosition, CardSize)",
|
||||
"Save & Quit",
|
||||
"ConsoleCommand(TEXT(\"AgrarianSaveWorld\"))",
|
||||
"ConsoleCommand(TEXT(\"quit\"))",
|
||||
"PlayerController->SetIgnoreMoveInput(false)",
|
||||
"PlayerController->SetIgnoreLookInput(false)",
|
||||
],
|
||||
"AgrarianGamePlayerController.h": [
|
||||
"ShowMvpPauseMenu",
|
||||
"HandleMvpEscapeInput",
|
||||
],
|
||||
"AgrarianGamePlayerController.cpp": [
|
||||
"SetIgnoreMoveInput(true)",
|
||||
"SetIgnoreLookInput(true)",
|
||||
"InputComponent->BindKey(EKeys::Enter",
|
||||
"InputComponent->BindKey(EKeys::SpaceBar",
|
||||
"InputComponent->BindKey(EKeys::Escape",
|
||||
"MvpFrontendWidget->IsInViewport()",
|
||||
"ShowMvpPauseMenu();",
|
||||
],
|
||||
}
|
||||
|
||||
FORBIDDEN = {
|
||||
"AgrarianGamePlayerController.cpp": [
|
||||
"BindKey(EKeys::LeftMouseButton",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
failures = []
|
||||
for label, path in FILES.items():
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for snippet in EXPECTED[label]:
|
||||
if snippet not in text:
|
||||
failures.append(f"{label} missing {snippet!r}")
|
||||
|
||||
for snippet in FORBIDDEN.get(label, []):
|
||||
if snippet in text:
|
||||
failures.append(f"{label} should not contain {snippet!r}")
|
||||
|
||||
if failures:
|
||||
raise RuntimeError("MVP menu input and quit flow verification failed: " + "; ".join(failures))
|
||||
|
||||
print("Agrarian MVP menu input and quit flow verification complete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "InputCoreTypes.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "TimerManager.h"
|
||||
@@ -84,6 +85,9 @@ void AAgrarianGamePlayerController::BeginPlay()
|
||||
{
|
||||
if (MvpFrontendStartupDelaySeconds > 0.0f)
|
||||
{
|
||||
SetIgnoreMoveInput(true);
|
||||
SetIgnoreLookInput(true);
|
||||
bShowMouseCursor = false;
|
||||
GetWorldTimerManager().SetTimer(
|
||||
MvpFrontendStartupTimerHandle,
|
||||
this,
|
||||
@@ -121,6 +125,11 @@ void AAgrarianGamePlayerController::SetupInputComponent()
|
||||
{
|
||||
Super::SetupInputComponent();
|
||||
|
||||
InputComponent->BindKey(EKeys::Enter, IE_Pressed, this, &AAgrarianGamePlayerController::HandleMvpConfirmInput);
|
||||
InputComponent->BindKey(EKeys::SpaceBar, IE_Pressed, this, &AAgrarianGamePlayerController::HandleMvpConfirmInput);
|
||||
InputComponent->BindKey(EKeys::BackSpace, IE_Pressed, this, &AAgrarianGamePlayerController::HandleMvpBackInput);
|
||||
InputComponent->BindKey(EKeys::Escape, IE_Pressed, this, &AAgrarianGamePlayerController::HandleMvpEscapeInput);
|
||||
|
||||
// only add IMCs for local player controllers
|
||||
if (IsLocalPlayerController())
|
||||
{
|
||||
@@ -152,7 +161,7 @@ bool AAgrarianGamePlayerController::ShouldUseTouchControls() const
|
||||
|
||||
void AAgrarianGamePlayerController::ShowMvpFrontend()
|
||||
{
|
||||
if (!IsLocalPlayerController() || MvpFrontendWidget)
|
||||
if (!IsLocalPlayerController() || (MvpFrontendWidget && MvpFrontendWidget->IsInViewport()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -162,16 +171,90 @@ void AAgrarianGamePlayerController::ShowMvpFrontend()
|
||||
MvpFrontendWidgetClass = UAgrarianMvpFrontendWidget::StaticClass();
|
||||
}
|
||||
|
||||
MvpFrontendWidget = CreateWidget<UAgrarianMvpFrontendWidget>(this, MvpFrontendWidgetClass);
|
||||
if (!MvpFrontendWidget)
|
||||
{
|
||||
MvpFrontendWidget = CreateWidget<UAgrarianMvpFrontendWidget>(this, MvpFrontendWidgetClass);
|
||||
}
|
||||
|
||||
if (!MvpFrontendWidget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MvpFrontendWidget->SetActiveScreen(EAgrarianMvpFrontendScreen::CharacterSelection);
|
||||
MvpFrontendWidget->AddToPlayerScreen(10);
|
||||
if (!MvpFrontendWidget->IsInViewport())
|
||||
{
|
||||
MvpFrontendWidget->AddToPlayerScreen(10);
|
||||
}
|
||||
SetInputMode(FInputModeGameAndUI().SetWidgetToFocus(MvpFrontendWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock));
|
||||
bShowMouseCursor = true;
|
||||
SetIgnoreMoveInput(true);
|
||||
SetIgnoreLookInput(true);
|
||||
}
|
||||
|
||||
void AAgrarianGamePlayerController::ShowMvpPauseMenu()
|
||||
{
|
||||
if (!IsLocalPlayerController())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MvpFrontendWidgetClass)
|
||||
{
|
||||
MvpFrontendWidgetClass = UAgrarianMvpFrontendWidget::StaticClass();
|
||||
}
|
||||
|
||||
if (!MvpFrontendWidget || !MvpFrontendWidget->IsInViewport())
|
||||
{
|
||||
if (!MvpFrontendWidget)
|
||||
{
|
||||
MvpFrontendWidget = CreateWidget<UAgrarianMvpFrontendWidget>(this, MvpFrontendWidgetClass);
|
||||
}
|
||||
|
||||
if (MvpFrontendWidget)
|
||||
{
|
||||
MvpFrontendWidget->AddToPlayerScreen(10);
|
||||
}
|
||||
}
|
||||
|
||||
if (!MvpFrontendWidget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MvpFrontendWidget->SetActiveScreen(EAgrarianMvpFrontendScreen::MainMenu);
|
||||
SetInputMode(FInputModeGameAndUI().SetWidgetToFocus(MvpFrontendWidget->TakeWidget()).SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock));
|
||||
bShowMouseCursor = true;
|
||||
SetIgnoreMoveInput(true);
|
||||
SetIgnoreLookInput(true);
|
||||
}
|
||||
|
||||
void AAgrarianGamePlayerController::HandleMvpConfirmInput()
|
||||
{
|
||||
if (MvpFrontendWidget && MvpFrontendWidget->IsInViewport())
|
||||
{
|
||||
MvpFrontendWidget->ConfirmActiveScreen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void AAgrarianGamePlayerController::HandleMvpBackInput()
|
||||
{
|
||||
if (MvpFrontendWidget && MvpFrontendWidget->IsInViewport())
|
||||
{
|
||||
MvpFrontendWidget->BackFromActiveScreen();
|
||||
}
|
||||
}
|
||||
|
||||
void AAgrarianGamePlayerController::HandleMvpEscapeInput()
|
||||
{
|
||||
if (MvpFrontendWidget && MvpFrontendWidget->IsInViewport())
|
||||
{
|
||||
MvpFrontendWidget->BackFromActiveScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
ShowMvpPauseMenu();
|
||||
}
|
||||
|
||||
void AAgrarianGamePlayerController::AgrarianGrantItem(FName ItemId, int32 Quantity)
|
||||
|
||||
@@ -62,6 +62,10 @@ protected:
|
||||
/** Returns true if the player should use UMG touch controls */
|
||||
bool ShouldUseTouchControls() const;
|
||||
void ShowMvpFrontend();
|
||||
void ShowMvpPauseMenu();
|
||||
void HandleMvpConfirmInput();
|
||||
void HandleMvpBackInput();
|
||||
void HandleMvpEscapeInput();
|
||||
|
||||
public:
|
||||
UFUNCTION(Exec)
|
||||
|
||||
@@ -33,7 +33,7 @@ FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry,
|
||||
|
||||
if (Key == EKeys::Enter || Key == EKeys::SpaceBar)
|
||||
{
|
||||
ContinueFromActiveScreen();
|
||||
ConfirmActiveScreen();
|
||||
return FReply::Handled();
|
||||
}
|
||||
}
|
||||
@@ -42,13 +42,13 @@ FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry,
|
||||
const FKey Key = InKeyEvent.GetKey();
|
||||
if (Key == EKeys::Enter || Key == EKeys::SpaceBar)
|
||||
{
|
||||
ContinueFromActiveScreen();
|
||||
ConfirmActiveScreen();
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
if (Key == EKeys::BackSpace || Key == EKeys::Escape)
|
||||
{
|
||||
ReturnFromActiveScreen();
|
||||
BackFromActiveScreen();
|
||||
return FReply::Handled();
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,22 @@ FReply UAgrarianMvpFrontendWidget::NativeOnKeyDown(const FGeometry& InGeometry,
|
||||
const FKey Key = InKeyEvent.GetKey();
|
||||
if (Key == EKeys::Enter || Key == EKeys::SpaceBar)
|
||||
{
|
||||
ContinueFromActiveScreen();
|
||||
ConfirmActiveScreen();
|
||||
return FReply::Handled();
|
||||
}
|
||||
}
|
||||
else if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu)
|
||||
{
|
||||
const FKey Key = InKeyEvent.GetKey();
|
||||
if (Key == EKeys::Enter || Key == EKeys::SpaceBar || Key == EKeys::Escape)
|
||||
{
|
||||
ConfirmActiveScreen();
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
if (Key == EKeys::Q)
|
||||
{
|
||||
SaveAndQuit();
|
||||
return FReply::Handled();
|
||||
}
|
||||
}
|
||||
@@ -106,7 +121,7 @@ FReply UAgrarianMvpFrontendWidget::NativeOnMouseButtonDown(const FGeometry& InGe
|
||||
|
||||
if (IsPointInside(LocalMousePosition, ButtonPosition, ButtonSize))
|
||||
{
|
||||
ContinueFromActiveScreen();
|
||||
ConfirmActiveScreen();
|
||||
return FReply::Handled();
|
||||
}
|
||||
}
|
||||
@@ -121,13 +136,13 @@ FReply UAgrarianMvpFrontendWidget::NativeOnMouseButtonDown(const FGeometry& InGe
|
||||
|
||||
if (IsPointInside(LocalMousePosition, ButtonPosition, ButtonSize))
|
||||
{
|
||||
ContinueFromActiveScreen();
|
||||
ConfirmActiveScreen();
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
if (BackButtonSize.X > 96.0f && IsPointInside(LocalMousePosition, BackButtonPosition, BackButtonSize))
|
||||
{
|
||||
ReturnFromActiveScreen();
|
||||
BackFromActiveScreen();
|
||||
return FReply::Handled();
|
||||
}
|
||||
}
|
||||
@@ -140,7 +155,27 @@ FReply UAgrarianMvpFrontendWidget::NativeOnMouseButtonDown(const FGeometry& InGe
|
||||
|
||||
if (IsPointInside(LocalMousePosition, ButtonPosition, ButtonSize))
|
||||
{
|
||||
ContinueFromActiveScreen();
|
||||
ConfirmActiveScreen();
|
||||
return FReply::Handled();
|
||||
}
|
||||
}
|
||||
else if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu)
|
||||
{
|
||||
const float MenuContentX = PanelPosition.X + (48.0f * Scale);
|
||||
const float MenuContentWidth = PanelSize.X - (96.0f * Scale);
|
||||
const FVector2D ResumeButtonPosition(MenuContentX, PanelPosition.Y + (220.0f * Scale));
|
||||
const FVector2D ButtonSize(FMath::Min(MenuContentWidth, 310.0f * Scale), 56.0f * Scale);
|
||||
const FVector2D QuitButtonPosition(MenuContentX, ResumeButtonPosition.Y + (72.0f * Scale));
|
||||
|
||||
if (IsPointInside(LocalMousePosition, ResumeButtonPosition, ButtonSize))
|
||||
{
|
||||
ConfirmActiveScreen();
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
if (IsPointInside(LocalMousePosition, QuitButtonPosition, ButtonSize))
|
||||
{
|
||||
SaveAndQuit();
|
||||
return FReply::Handled();
|
||||
}
|
||||
}
|
||||
@@ -172,6 +207,25 @@ void UAgrarianMvpFrontendWidget::SetHighContrastMode(bool bNewUseHighContrast)
|
||||
InvalidateLayoutAndVolatility();
|
||||
}
|
||||
|
||||
void UAgrarianMvpFrontendWidget::ConfirmActiveScreen()
|
||||
{
|
||||
ContinueFromActiveScreen();
|
||||
}
|
||||
|
||||
void UAgrarianMvpFrontendWidget::BackFromActiveScreen()
|
||||
{
|
||||
ReturnFromActiveScreen();
|
||||
}
|
||||
|
||||
void UAgrarianMvpFrontendWidget::SaveAndQuit()
|
||||
{
|
||||
if (APlayerController* PlayerController = GetOwningPlayer())
|
||||
{
|
||||
PlayerController->ConsoleCommand(TEXT("AgrarianSaveWorld"));
|
||||
PlayerController->ConsoleCommand(TEXT("quit"));
|
||||
}
|
||||
}
|
||||
|
||||
int32 UAgrarianMvpFrontendWidget::NativePaint(
|
||||
const FPaintArgs& Args,
|
||||
const FGeometry& AllottedGeometry,
|
||||
@@ -188,7 +242,7 @@ int32 UAgrarianMvpFrontendWidget::NativePaint(
|
||||
FVector2D PanelPosition = FVector2D::ZeroVector;
|
||||
FVector2D PanelSize = FVector2D::ZeroVector;
|
||||
GetPanelLayout(Size, Scale, PanelPosition, PanelSize);
|
||||
const FLinearColor BackdropColor = bUseHighContrast ? FLinearColor(0.0f, 0.0f, 0.0f, 0.96f) : FLinearColor(0.015f, 0.018f, 0.014f, 0.92f);
|
||||
const FLinearColor BackdropColor = bUseHighContrast ? FLinearColor(0.0f, 0.0f, 0.0f, 1.0f) : FLinearColor(0.015f, 0.018f, 0.014f, 0.99f);
|
||||
const FLinearColor PanelColor = bUseHighContrast ? FLinearColor(0.0f, 0.0f, 0.0f, 0.98f) : FLinearColor(0.035f, 0.045f, 0.034f, 0.96f);
|
||||
const FLinearColor AccentColor = bUseHighContrast ? FLinearColor(0.95f, 0.95f, 0.30f, 1.0f) : FLinearColor(0.45f, 0.72f, 0.40f, 1.0f);
|
||||
|
||||
@@ -279,6 +333,12 @@ void UAgrarianMvpFrontendWidget::ContinueFromActiveScreen()
|
||||
}
|
||||
|
||||
if (ActiveScreen == EAgrarianMvpFrontendScreen::Loading)
|
||||
{
|
||||
CompleteFrontendFlow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu)
|
||||
{
|
||||
CompleteFrontendFlow();
|
||||
}
|
||||
@@ -289,6 +349,12 @@ void UAgrarianMvpFrontendWidget::ReturnFromActiveScreen()
|
||||
if (ActiveScreen == EAgrarianMvpFrontendScreen::JoinServer)
|
||||
{
|
||||
SetActiveScreen(EAgrarianMvpFrontendScreen::CharacterSelection);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ActiveScreen == EAgrarianMvpFrontendScreen::MainMenu)
|
||||
{
|
||||
CompleteFrontendFlow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,6 +364,8 @@ void UAgrarianMvpFrontendWidget::CompleteFrontendFlow()
|
||||
{
|
||||
PlayerController->SetInputMode(FInputModeGameOnly());
|
||||
PlayerController->bShowMouseCursor = false;
|
||||
PlayerController->SetIgnoreMoveInput(false);
|
||||
PlayerController->SetIgnoreLookInput(false);
|
||||
}
|
||||
|
||||
RemoveFromParent();
|
||||
@@ -319,10 +387,10 @@ void UAgrarianMvpFrontendWidget::DrawMainMenu(
|
||||
const FSlateFontInfo HintFont = FCoreStyle::GetDefaultFontStyle("Regular", FMath::RoundToInt(16.0f * Scale));
|
||||
|
||||
DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, MainMenuTitle, FVector2D(ContentX, PanelPosition.Y + (58.0f * Scale)), ContentWidth, TitleFont, FLinearColor(0.92f, 0.98f, 0.84f, 1.0f));
|
||||
DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, MainMenuSubtitle, FVector2D(ContentX + (4.0f * Scale), PanelPosition.Y + (128.0f * Scale)), ContentWidth, SubtitleFont, FLinearColor(0.72f, 0.80f, 0.68f, 1.0f));
|
||||
DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Game menu")), FVector2D(ContentX + (4.0f * Scale), PanelPosition.Y + (128.0f * Scale)), ContentWidth, SubtitleFont, FLinearColor(0.72f, 0.80f, 0.68f, 1.0f));
|
||||
|
||||
const FVector2D ButtonPosition(ContentX, PanelPosition.Y + (230.0f * Scale));
|
||||
const FVector2D ButtonSize(FMath::Min(ContentWidth, 280.0f * Scale), 58.0f * Scale);
|
||||
const FVector2D ButtonPosition(ContentX, PanelPosition.Y + (220.0f * Scale));
|
||||
const FVector2D ButtonSize(FMath::Min(ContentWidth, 310.0f * Scale), 56.0f * Scale);
|
||||
FSlateDrawElement::MakeBox(
|
||||
OutDrawElements,
|
||||
++LayerId,
|
||||
@@ -331,8 +399,19 @@ void UAgrarianMvpFrontendWidget::DrawMainMenu(
|
||||
ESlateDrawEffect::None,
|
||||
FLinearColor(0.35f, 0.58f, 0.30f, 0.95f));
|
||||
|
||||
DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, PrimaryActionLabel, ButtonPosition + FVector2D(22.0f * Scale, 12.0f * Scale), ButtonSize.X - (44.0f * Scale), ButtonFont, FLinearColor(0.96f, 1.0f, 0.90f, 1.0f));
|
||||
DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Placeholder flow: splash, startup credits, character selection, join, loading, then Ground Zero.")), FVector2D(ContentX, PanelPosition.Y + PanelSize.Y - (66.0f * Scale)), ContentWidth, HintFont, FLinearColor(0.62f, 0.68f, 0.58f, 1.0f));
|
||||
DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Resume")), ButtonPosition + FVector2D(22.0f * Scale, 11.0f * Scale), ButtonSize.X - (44.0f * Scale), ButtonFont, FLinearColor(0.96f, 1.0f, 0.90f, 1.0f));
|
||||
|
||||
const FVector2D QuitButtonPosition(ContentX, ButtonPosition.Y + (72.0f * Scale));
|
||||
FSlateDrawElement::MakeBox(
|
||||
OutDrawElements,
|
||||
++LayerId,
|
||||
AllottedGeometry.ToPaintGeometry(FVector2f(ButtonSize), FSlateLayoutTransform(FVector2f(QuitButtonPosition))),
|
||||
FCoreStyle::Get().GetBrush(TEXT("WhiteBrush")),
|
||||
ESlateDrawEffect::None,
|
||||
FLinearColor(0.42f, 0.22f, 0.18f, 0.95f));
|
||||
|
||||
DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Save & Quit")), QuitButtonPosition + FVector2D(22.0f * Scale, 11.0f * Scale), ButtonSize.X - (44.0f * Scale), ButtonFont, FLinearColor(1.0f, 0.92f, 0.84f, 1.0f));
|
||||
DrawTextAt(OutDrawElements, LayerId, AllottedGeometry, FText::FromString(TEXT("Press Escape to open this menu. Save & Quit writes the current world save before closing.")), FVector2D(ContentX, PanelPosition.Y + PanelSize.Y - (66.0f * Scale)), ContentWidth, HintFont, FLinearColor(0.62f, 0.68f, 0.58f, 1.0f));
|
||||
}
|
||||
|
||||
void UAgrarianMvpFrontendWidget::DrawCharacterSelection(
|
||||
|
||||
@@ -64,6 +64,15 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
|
||||
void SetHighContrastMode(bool bNewUseHighContrast);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
|
||||
void ConfirmActiveScreen();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
|
||||
void BackFromActiveScreen();
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI")
|
||||
void SaveAndQuit();
|
||||
|
||||
protected:
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user