Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed

- **`editor start_pie` no longer strands MCP behind the Blueprint compile-error dialog.** The action now pre-flights loaded Blueprints using the engine's own unresolved-error condition. Its default `on_compile_errors: "refuse"` policy returns the offending `{name, path}` entries without starting PIE; `"suppress"` explicitly starts anyway under a scoped unattended-script guard, so the confirmation modal cannot block the game thread and the in-process MCP server.

## [0.22.0] - 2026-08-01

### Internal
Expand Down
2 changes: 1 addition & 1 deletion Docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ Execute a console command. Routes to the first PIE `PlayerController` found (so

### `editor.start_pie` · `editor.stop_pie` · NEW in v0.14.10

`start_pie` queues an in-viewport Play-In-Editor session (refuses to queue a duplicate when a PIE world is already alive); response includes `mode: 'in_viewport'`. `stop_pie` calls `RequestEndPlayMap` when a PIE world exists, no-op (`stopped: false`) otherwise. Both take *no parameters*. Pairs with `run_python` / `load_level` for fully automated in-game test flows.
`start_pie` queues an in-viewport Play-In-Editor session (refuses to queue a duplicate when a PIE world is already alive); response includes `mode: 'in_viewport'`. It accepts `on_compile_errors: "refuse" | "suppress"` (`"refuse"` by default). Refuse mode returns the loaded Blueprints with unresolved compiler errors without starting PIE. Suppress mode starts PIE anyway under a narrowly scoped unattended-script guard, preventing the engine's compile-error confirmation from blocking the game thread and MCP server. `stop_pie` calls `RequestEndPlayMap` when a PIE world exists, no-op (`stopped: false`) otherwise. Pairs with `run_python` / `load_level` for fully automated in-game test flows.

### `editor.run_python` · NEW in v0.14.9

Expand Down
2 changes: 1 addition & 1 deletion Docs/specs/SPEC_MonolithEditor.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Pattern table:

| Action | Description |
|--------|-------------|
| `start_pie` | Begin a PIE session pinned to in-viewport mode (`EPlaySessionWorldType::PlayInEditor` + first active level viewport via `FLevelEditorModule::GetFirstActiveViewport`). Independent of the user's `LastExecutedPlayModeType` toolbar choice. Returns `started: true, mode: 'in_viewport'`. Refuses to queue duplicates when PIE is already running. |
| `start_pie` | Begin a PIE session pinned to in-viewport mode (`EPlaySessionWorldType::PlayInEditor` + first active level viewport via `FLevelEditorModule::GetFirstActiveViewport`). Independent of the user's `LastExecutedPlayModeType` toolbar choice. Pre-flights loaded Blueprints for the engine's unresolved-compile-error condition. `on_compile_errors: "refuse"` (default) returns a structured error with `errored_blueprints` and does not start PIE; `"suppress"` starts anyway under a scoped `GIsRunningUnattendedScript` guard so the engine cannot open a modal that blocks the game thread and MCP server. Success returns `started`, `mode`, `compile_error_policy`, `errored_blueprint_count`, and `errored_blueprints`. Refuses to queue duplicates when PIE is already running. |
| `stop_pie` | End the active PIE session via `GUnrealEd->RequestEndPlayMap()`. No-op (returns `stopped: false`) if PIE not active. |
| `run_console_command` | Execute a console command. Routes to the first PIE PlayerController found (multi-client PIE not disambiguated); falls back to `GEngine->Exec` (with null-guard) when no PIE session is active. |

Expand Down
39 changes: 36 additions & 3 deletions Source/MonolithEditor/Private/MonolithEditorActions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,11 @@ void FMonolithEditorActions::RegisterActions(FMonolithLogCapture* LogCapture)
.Build());

Registry.RegisterAction(TEXT("editor"), TEXT("start_pie"),
TEXT("Start a Play-In-Editor session (equivalent to pressing Cmd+P in the editor)."),
TEXT("Start an in-viewport Play-In-Editor session. Pre-flights loaded Blueprint compile errors so the action never opens a blocking modal: on_compile_errors=\"refuse\" (default) returns the offending assets; \"suppress\" starts PIE anyway while silencing that prompt."),
FMonolithActionHandler::CreateStatic(&HandleStartPIE),
MakeShared<FJsonObject>());
FParamSchemaBuilder()
.Optional(TEXT("on_compile_errors"), TEXT("string"), TEXT("Policy when loaded Blueprints have unresolved compile errors: \"refuse\" (default, safe) returns an error + the offending {name,path} list without starting PIE; \"suppress\" starts PIE anyway and silences the engine's blocking compile-error modal."), TEXT("refuse"))
.Build());

Registry.RegisterAction(TEXT("editor"), TEXT("stop_pie"),
TEXT("Stop the active Play-In-Editor session."),
Expand Down Expand Up @@ -2017,15 +2019,46 @@ FMonolithActionResult FMonolithEditorActions::HandleStartPIE(const TSharedPtr<FJ
return FMonolithActionResult::Success(AlreadyRunning);
}

FString CompileMode = TEXT("refuse");
if (Params.IsValid())
{
Params->TryGetStringField(TEXT("on_compile_errors"), CompileMode);
}
const bool bRefuseCompileErrors = CompileMode.Equals(TEXT("refuse"), ESearchCase::IgnoreCase);
const bool bSuppressCompileErrors = CompileMode.Equals(TEXT("suppress"), ESearchCase::IgnoreCase);
if (!bRefuseCompileErrors && !bSuppressCompileErrors)
{
return FMonolithActionResult::Error(
FString::Printf(TEXT("Invalid on_compile_errors policy '%s'. Expected 'refuse' or 'suppress'."), *CompileMode),
-32602);
}

TArray<FErroredBlueprintEntry> Errored;
ScanErroredBlueprints(Errored);
if (Errored.Num() > 0 && bRefuseCompileErrors)
{
TSharedPtr<FJsonObject> ErrorData = MakeShared<FJsonObject>();
ErrorData->SetNumberField(TEXT("errored_blueprint_count"), Errored.Num());
ErrorData->SetArrayField(TEXT("errored_blueprints"), ErroredBlueprintsToJson(Errored));
return FMonolithActionResult::Error(
FString::Printf(TEXT("start_pie refused: %d Blueprint(s) have unresolved compile errors. ")
TEXT("Fix them, or pass on_compile_errors=\"suppress\" to start PIE without opening the blocking modal."),
Errored.Num()))
.WithErrorData(ErrorData);
}

FString StartError;
if (!StartPieInternal(StartError))
if (!StartPieInternal(StartError, bSuppressCompileErrors))
{
return FMonolithActionResult::Error(StartError);
}

TSharedPtr<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetBoolField(TEXT("started"), true);
Root->SetStringField(TEXT("mode"), TEXT("in_viewport"));
Root->SetStringField(TEXT("compile_error_policy"), bSuppressCompileErrors ? TEXT("suppress") : TEXT("refuse"));
Root->SetNumberField(TEXT("errored_blueprint_count"), Errored.Num());
Root->SetArrayField(TEXT("errored_blueprints"), ErroredBlueprintsToJson(Errored));
return FMonolithActionResult::Success(Root);
}

Expand Down
74 changes: 74 additions & 0 deletions Source/MonolithEditor/Private/Tests/MonolithStartPieTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright tumourlove. All Rights Reserved.

#if WITH_DEV_AUTOMATION_TESTS

#include "CoreMinimal.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Engine/Blueprint.h"
#include "Misc/AutomationTest.h"
#include "MonolithEditorActions.h"
#include "MonolithToolRegistry.h"

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FMonolithStartPieRejectsInvalidCompilePolicyTest,
"Monolith.Editor.PIE.StartPieRejectsInvalidCompilePolicy",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)

bool FMonolithStartPieRejectsInvalidCompilePolicyTest::RunTest(const FString& /*Parameters*/)
{
TSharedPtr<FJsonObject> Params = MakeShared<FJsonObject>();
Params->SetStringField(TEXT("on_compile_errors"), TEXT("prompt"));

const FMonolithActionResult Result = FMonolithEditorActions::HandleStartPIE(Params);
TestFalse(TEXT("Invalid compile-error policy is rejected"), Result.bSuccess);
TestEqual(TEXT("Invalid policy is an invalid-params error"), Result.ErrorCode, -32602);
TestTrue(TEXT("Error names the accepted policies"),
Result.ErrorMessage.Contains(TEXT("refuse")) && Result.ErrorMessage.Contains(TEXT("suppress")));
return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FMonolithStartPieRefusesErroredBlueprintTest,
"Monolith.Editor.PIE.StartPieRefusesErroredBlueprint",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)

bool FMonolithStartPieRefusesErroredBlueprintTest::RunTest(const FString& /*Parameters*/)
{
UBlueprint* BrokenBlueprint = NewObject<UBlueprint>(
GetTransientPackage(),
TEXT("MonolithStartPieBrokenBlueprint"));
BrokenBlueprint->Status = BS_Error;
BrokenBlueprint->bDisplayCompilePIEWarning = true;

const FMonolithActionResult Result = FMonolithEditorActions::HandleStartPIE(MakeShared<FJsonObject>());
TestFalse(TEXT("Default policy refuses an errored Blueprint"), Result.bSuccess);
TestTrue(TEXT("Refusal carries structured error data"), Result.ErrorData.IsValid());

bool bFoundTestBlueprint = false;
if (Result.ErrorData.IsValid() && Result.ErrorData->Type == EJson::Object)
{
const TSharedPtr<FJsonObject> ErrorData = Result.ErrorData->AsObject();
const TArray<TSharedPtr<FJsonValue>>* Blueprints = nullptr;
if (ErrorData.IsValid() && ErrorData->TryGetArrayField(TEXT("errored_blueprints"), Blueprints) && Blueprints)
{
for (const TSharedPtr<FJsonValue>& Value : *Blueprints)
{
const TSharedPtr<FJsonObject> Entry = Value.IsValid() ? Value->AsObject() : nullptr;
if (Entry.IsValid() && Entry->GetStringField(TEXT("name")) == BrokenBlueprint->GetName())
{
bFoundTestBlueprint = true;
break;
}
}
}
}
TestTrue(TEXT("Structured refusal lists the offending Blueprint"), bFoundTestBlueprint);

BrokenBlueprint->bDisplayCompilePIEWarning = false;
BrokenBlueprint->Status = BS_UpToDate;
BrokenBlueprint->MarkAsGarbage();
return true;
}

#endif // WITH_DEV_AUTOMATION_TESTS