Stabilize investor frontend entry

This commit is contained in:
2026-05-19 20:46:09 -07:00
parent d59f613e2b
commit d2b8185333
9 changed files with 169 additions and 47 deletions
Binary file not shown.
@@ -0,0 +1,57 @@
# Investor Readiness Audit - 2026-05-19
## Current Verdict
The current build should not be presented as investor visual MVP until it passes
a real-GPU packaged-client visual and stability check. The systems are further
along than the presentation, but the player-facing first impression is still at
risk.
## Findings
- Packaged crash logs show a Slate/UI paint crash while the character-selection
frontend is active. The crash callstack centers on `SButton::OnPaint` and
`FSlateDrawElement::MakeBox`, so the immediate fix removes custom button brush
styling, defers frontend actions to the next tick, and shortens the investor
path from character selection directly into Ground Zero.
- The project content library still contains only placeholder meshes plus the
Manny/Quinn mannequin assets for characters. The current character and object
visuals are proxy-quality, not final investor-quality realism.
- Ground Zero editor verification confirms landscape, water/resource actors,
foliage variation actors, and environment materials exist, but this does not
prove the packaged executable looks good through the real GPU path.
- Cooked runtime logs warned that tree, shrub, and grass materials were missing
instanced-static-mesh usage. Those flags are now set on the assets and in the
repeatable map setup script.
- The generated investor roadmap/report can show old milestone completion state
if it is not regenerated from the current roadmap after fixes. The source
roadmap still contains explicit visual QA acceptance items that remain open
until real packaged screenshots or clips are captured.
## Required Before Investor-Ready Label
- Launch the packaged Windows demo through the real GPU desktop path.
- Capture startup credits, character selection, first spawn, terrain, vegetation,
water, campfire, shelter, pause menu, and save/quit.
- Confirm selecting a character and entering Ground Zero does not crash.
- Confirm the first player view is not ground/legs, the menu does not appear
after gameplay starts, water is visible, foliage is visible, and debug-looking
primitives are not the dominant read.
- Replace proxy/mannequin art with real or production-directed free/internal art
assets before calling the build visually investor-ready.
## Verification Completed In This Pass
- `verify_mvp_menu_input_and_quit_flow.py`
- `verify_mvp_character_archetype_choice.py`
- `verify_mvp_character_proxies.py`
- `verify_mvp_frontend_umg_flow.py`
- `verify_ground_zero_natural_environment_pass.py`
- Windows package BuildCookRun completed successfully after the fixes.
## Remaining Risk
The packaged build still needs a real interactive visual pass through
Sunshine/Moonlight or direct Windows display access. Static checks and editor
commandlets are not enough to clear the user-reported crash and visual quality
concerns.
@@ -0,0 +1,43 @@
import unreal
MATERIAL_PATHS = [
"/Game/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak",
"/Game/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush",
"/Game/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal",
]
def set_instanced_usage(material):
applied = False
for property_name in (
"used_with_instanced_static_meshes",
"bUsedWithInstancedStaticMeshes",
):
try:
material.set_editor_property(property_name, True)
applied = True
except Exception:
pass
return applied
dirty_assets = []
for material_path in MATERIAL_PATHS:
material = unreal.EditorAssetLibrary.load_asset(material_path)
if not material:
raise RuntimeError(f"Missing material: {material_path}")
if not set_instanced_usage(material):
raise RuntimeError(f"Could not set instanced static mesh usage on {material_path}")
unreal.MaterialEditingLibrary.recompile_material(material)
dirty_assets.append(material_path)
for material_path in dirty_assets:
if not unreal.EditorAssetLibrary.save_asset(material_path, only_if_is_dirty=False):
raise RuntimeError(f"Failed to save updated material: {material_path}")
print("Updated instanced static mesh usage for Ground Zero foliage materials:")
for material_path in dirty_assets:
print(f" - {material_path}")
+7
View File
@@ -52,16 +52,19 @@ ENVIRONMENT_MATERIALS = {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Tree_CoastalOak",
"color": unreal.LinearColor(0.18, 0.31, 0.16, 1.0),
"roughness": 0.88,
"used_with_instanced_static_meshes": True,
},
"shrub": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Shrub_CoyoteBrush",
"color": unreal.LinearColor(0.31, 0.39, 0.20, 1.0),
"roughness": 0.9,
"used_with_instanced_static_meshes": True,
},
"grass": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Grass_DryCoastal",
"color": unreal.LinearColor(0.47, 0.42, 0.23, 1.0),
"roughness": 0.95,
"used_with_instanced_static_meshes": True,
},
"wood_resource": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Wood_Resource",
@@ -807,6 +810,10 @@ def ensure_environment_materials():
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"])
unreal.log(f"Created Ground Zero environment material: {spec['path']}")
if spec.get("used_with_instanced_static_meshes"):
material.set_editor_property("used_with_instanced_static_meshes", True)
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
created_or_loaded[key] = material
return created_or_loaded
+2 -3
View File
@@ -37,9 +37,8 @@ def main() -> None:
"UButton::StaticClass()",
"UTextBlock::StaticClass()",
"OnClicked.AddDynamic",
"OnHovered.AddDynamic",
"ButtonStyle.Hovered.TintColor",
"ButtonStyle.Pressed.TintColor",
"SetBackgroundColor",
"DeferFrontendAction",
"SetIsFocusable(true)",
"SetKeyboardFocus()",
"BackFromActiveScreen()",
@@ -17,22 +17,6 @@
#include "Styling/CoreStyle.h"
#include "TimerManager.h"
namespace
{
FButtonStyle MakeAgrarianButtonStyle(const FLinearColor& NormalColor, const FLinearColor& HoveredColor)
{
FButtonStyle ButtonStyle = FCoreStyle::Get().GetWidgetStyle<FButtonStyle>(TEXT("Button"));
ButtonStyle.Normal.TintColor = FSlateColor(NormalColor);
ButtonStyle.Hovered.TintColor = FSlateColor(HoveredColor);
ButtonStyle.Pressed.TintColor = FSlateColor(FLinearColor(
FMath::Min(HoveredColor.R + 0.12f, 1.0f),
FMath::Min(HoveredColor.G + 0.12f, 1.0f),
FMath::Min(HoveredColor.B + 0.12f, 1.0f),
HoveredColor.A));
return ButtonStyle;
}
}
void UAgrarianMvpFrontendWidget::NativeConstruct()
{
Super::NativeConstruct();
@@ -175,7 +159,7 @@ void UAgrarianMvpFrontendWidget::ContinueFromActiveScreen()
{
if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection)
{
SetActiveScreen(EAgrarianMvpFrontendScreen::JoinServer);
CompleteFrontendFlow();
return;
}
@@ -201,11 +185,6 @@ void UAgrarianMvpFrontendWidget::ContinueFromActiveScreen()
CompleteFrontendFlow();
return;
}
if (ActiveScreen == EAgrarianMvpFrontendScreen::SavingAndQuit)
{
return;
}
}
void UAgrarianMvpFrontendWidget::ReturnFromActiveScreen()
@@ -226,7 +205,7 @@ void UAgrarianMvpFrontendWidget::CompleteFrontendFlow()
{
if (APlayerController* PlayerController = GetOwningPlayer())
{
if (ActiveScreen == EAgrarianMvpFrontendScreen::Loading)
if (ActiveScreen == EAgrarianMvpFrontendScreen::CharacterSelection || ActiveScreen == EAgrarianMvpFrontendScreen::Loading)
{
PlayerController->ConsoleCommand(SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale
? TEXT("AgrarianSelectCharacter female")
@@ -291,7 +270,6 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
AddText(Panel, FText::FromString(TEXT("Gameplay is paused while this menu is active.")), FMath::RoundToInt(22.0f * Scale), false, MutedTextColor, 72.0f * Scale);
PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Resume")), ButtonColor, ButtonHoverColor, 16.0f * Scale);
PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked);
PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton);
UButton* QuitButton = AddButton(Panel, FText::FromString(TEXT("Save & Quit")), QuitButtonColor, FLinearColor(0.58f, 0.28f, 0.22f, 1.0f), 34.0f * Scale);
QuitButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleSaveAndQuitClicked);
@@ -314,7 +292,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
const bool bMaleSelected = SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultMale;
const bool bFemaleSelected = SelectedCharacterArchetype == EAgrarianMvpCharacterArchetype::YoungAdultFemale;
UButton* MaleButton = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("MalePioneerButton"));
MaleButton->SetStyle(MakeAgrarianButtonStyle(bMaleSelected ? ButtonHoverColor : SecondaryButtonColor, ButtonHoverColor));
MaleButton->SetBackgroundColor(bMaleSelected ? ButtonHoverColor : SecondaryButtonColor);
MaleButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleMaleCharacterClicked);
UVerticalBox* MaleStack = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("MalePioneerStack"));
MaleButton->SetContent(MaleStack);
@@ -328,7 +306,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
}
UButton* FemaleButton = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("FemalePioneerButton"));
FemaleButton->SetStyle(MakeAgrarianButtonStyle(bFemaleSelected ? ButtonHoverColor : SecondaryButtonColor, ButtonHoverColor));
FemaleButton->SetBackgroundColor(bFemaleSelected ? ButtonHoverColor : SecondaryButtonColor);
FemaleButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleFemaleCharacterClicked);
UVerticalBox* FemaleStack = WidgetTree->ConstructWidget<UVerticalBox>(UVerticalBox::StaticClass(), TEXT("FemalePioneerStack"));
FemaleButton->SetContent(FemaleStack);
@@ -341,9 +319,8 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
FemaleSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
}
PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Continue")), ButtonColor, ButtonHoverColor, 10.0f * Scale);
PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Enter Ground Zero")), ButtonColor, ButtonHoverColor, 10.0f * Scale);
PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked);
PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton);
AddText(Panel, FText::Format(FText::FromString(TEXT("Selected {0}: {1}. Click a card or use Left/Right, then continue.")), GetSelectedRoleLabel(), GetSelectedCharacterLabel()), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 0.0f);
return;
}
@@ -363,9 +340,8 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
}
PrimaryFocusButton = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("ContinueToLoadingButton"));
PrimaryFocusButton->SetStyle(MakeAgrarianButtonStyle(ButtonColor, ButtonHoverColor));
PrimaryFocusButton->SetBackgroundColor(ButtonColor);
PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked);
PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton);
UTextBlock* ContinueLabel = AddText(nullptr, FText::FromString(TEXT("Continue to loading")), FMath::RoundToInt(20.0f * Scale), true, TextColor, 0.0f);
PrimaryFocusButton->SetContent(ContinueLabel);
if (UButtonSlot* ContinueLabelSlot = Cast<UButtonSlot>(ContinueLabel->Slot))
@@ -380,7 +356,7 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
}
UButton* BackButton = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass(), TEXT("BackButton"));
BackButton->SetStyle(MakeAgrarianButtonStyle(SecondaryButtonColor, ButtonHoverColor));
BackButton->SetBackgroundColor(SecondaryButtonColor);
BackButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleBackClicked);
UTextBlock* BackLabel = AddText(nullptr, FText::FromString(TEXT("Back")), FMath::RoundToInt(20.0f * Scale), true, TextColor, 0.0f);
BackButton->SetContent(BackLabel);
@@ -413,7 +389,6 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree()
AddText(Panel, FText::Format(FText::FromString(TEXT("{0}: {1} | Server: {2}")), GetSelectedRoleLabel(), GetSelectedCharacterLabel(), JoinServerAddress), FMath::RoundToInt(18.0f * Scale), false, TextColor, 34.0f * Scale);
PrimaryFocusButton = AddButton(Panel, FText::FromString(TEXT("Enter Ground Zero")), ButtonColor, ButtonHoverColor, 14.0f * Scale);
PrimaryFocusButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked);
PrimaryFocusButton->OnHovered.AddDynamic(this, &UAgrarianMvpFrontendWidget::FocusPrimaryButton);
AddText(Panel, FText::FromString(TEXT("Click or press Enter to close the MVP menu and begin testing.")), FMath::RoundToInt(15.0f * Scale), false, MutedTextColor, 0.0f);
}
@@ -440,7 +415,7 @@ UTextBlock* UAgrarianMvpFrontendWidget::AddText(UVerticalBox* Parent, const FTex
UButton* UAgrarianMvpFrontendWidget::AddButton(UVerticalBox* Parent, const FText& Text, const FLinearColor& NormalColor, const FLinearColor& HoveredColor, float BottomPadding)
{
UButton* Button = WidgetTree->ConstructWidget<UButton>(UButton::StaticClass());
Button->SetStyle(MakeAgrarianButtonStyle(NormalColor, HoveredColor));
Button->SetBackgroundColor(NormalColor);
Button->SetClickMethod(EButtonClickMethod::MouseDown);
Button->SetTouchMethod(EButtonTouchMethod::Down);
@@ -481,27 +456,66 @@ void UAgrarianMvpFrontendWidget::FocusPrimaryButton()
void UAgrarianMvpFrontendWidget::HandlePrimaryActionClicked()
{
ConfirmActiveScreen();
DeferFrontendAction([this]()
{
ConfirmActiveScreen();
});
}
void UAgrarianMvpFrontendWidget::HandleBackClicked()
{
BackFromActiveScreen();
DeferFrontendAction([this]()
{
BackFromActiveScreen();
});
}
void UAgrarianMvpFrontendWidget::HandleSaveAndQuitClicked()
{
SaveAndQuit();
DeferFrontendAction([this]()
{
SaveAndQuit();
});
}
void UAgrarianMvpFrontendWidget::HandleMaleCharacterClicked()
{
SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale);
DeferFrontendAction([this]()
{
SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultMale);
});
}
void UAgrarianMvpFrontendWidget::HandleFemaleCharacterClicked()
{
SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale);
DeferFrontendAction([this]()
{
SetSelectedCharacterArchetype(EAgrarianMvpCharacterArchetype::YoungAdultFemale);
});
}
void UAgrarianMvpFrontendWidget::DeferFrontendAction(TFunction<void()> Action)
{
if (!Action)
{
return;
}
if (UWorld* World = GetWorld())
{
FTimerDelegate DeferredAction;
DeferredAction.BindLambda([WeakThis = TWeakObjectPtr<UAgrarianMvpFrontendWidget>(this), Action = MoveTemp(Action)]() mutable
{
if (WeakThis.IsValid())
{
Action();
}
});
World->GetTimerManager().SetTimerForNextTick(DeferredAction);
return;
}
Action();
}
FText UAgrarianMvpFrontendWidget::GetSelectedCharacterLabel() const
@@ -112,6 +112,8 @@ private:
UFUNCTION()
void HandleFemaleCharacterClicked();
void DeferFrontendAction(TFunction<void()> Action);
UPROPERTY()
TObjectPtr<UButton> PrimaryFocusButton;