ue-mcp 1.1.21 → 1.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/plugin-cli.js +164 -3
- package/dist/plugin-cli.js.map +1 -1
- package/dist/tool-counts.json +9 -9
- package/dist/tools/asset.js +1 -0
- package/dist/tools/asset.js.map +1 -1
- package/dist/tools/audio.js +6 -1
- package/dist/tools/audio.js.map +1 -1
- package/dist/tools/editor.js +16 -5
- package/dist/tools/editor.js.map +1 -1
- package/dist/tools/gameplay.js +1 -1
- package/dist/tools/gameplay.js.map +1 -1
- package/dist/tools/landscape.js +4 -0
- package/dist/tools/landscape.js.map +1 -1
- package/dist/tools/level.js +7 -2
- package/dist/tools/level.js.map +1 -1
- package/dist/ue-mcp.default.yml +62 -8
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +6 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +108 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +3 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers.cpp +127 -17
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers.h +2 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +258 -5
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +7 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_Input.cpp +75 -32
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +100 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +4 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +79 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +2 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +15 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +7 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +15 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +2 -0
- package/skills/ue-mcp-blueprint/SKILL.md +13 -0
- package/skills/ue-mcp-epic-routing/SKILL.md +37 -0
|
@@ -171,6 +171,9 @@ void FAssetHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
|
|
|
171
171
|
Registry.RegisterHandler(TEXT("delete_asset_batch"), &DeleteAssetBatch);
|
|
172
172
|
Registry.RegisterHandler(TEXT("bulk_rename_assets"), &BulkRename);
|
|
173
173
|
Registry.RegisterHandler(TEXT("create_data_asset"), &CreateDataAsset);
|
|
174
|
+
// #726: generic create-any-concrete-UObject-class asset (physical materials,
|
|
175
|
+
// curves, settings objects) - not restricted to UDataAsset subclasses.
|
|
176
|
+
Registry.RegisterHandler(TEXT("create_asset_by_class"), &CreateAssetByClass);
|
|
174
177
|
Registry.RegisterHandler(TEXT("save_asset"), &SaveAsset);
|
|
175
178
|
Registry.RegisterHandler(TEXT("save_all_dirty"), &SaveAllDirty);
|
|
176
179
|
Registry.RegisterHandler(TEXT("list_textures"), &ListTextures);
|
|
@@ -1869,6 +1872,111 @@ TSharedPtr<FJsonValue> FAssetHandlers::CreateDataAsset(const TSharedPtr<FJsonObj
|
|
|
1869
1872
|
return MCPResult(Result);
|
|
1870
1873
|
}
|
|
1871
1874
|
|
|
1875
|
+
// #726: create an asset of any concrete UObject class. create_data_asset only
|
|
1876
|
+
// accepts UDataAsset subclasses, so "settings-object" classes (UPhysicalMaterial
|
|
1877
|
+
// subclasses, curves, etc.) had no native route and required execute_python with
|
|
1878
|
+
// a hand-picked UFactory. This resolves the class, creates the asset (IAssetTools
|
|
1879
|
+
// CreateAsset with a null factory NewObjects the exact class), applies optional
|
|
1880
|
+
// properties, and saves.
|
|
1881
|
+
TSharedPtr<FJsonValue> FAssetHandlers::CreateAssetByClass(const TSharedPtr<FJsonObject>& Params)
|
|
1882
|
+
{
|
|
1883
|
+
FString Name;
|
|
1884
|
+
if (auto Err = RequireString(Params, TEXT("name"), Name)) return Err;
|
|
1885
|
+
FString PackagePath = OptionalString(Params, TEXT("packagePath"), TEXT("/Game"));
|
|
1886
|
+
FString ClassName;
|
|
1887
|
+
if (auto Err = RequireStringAlt(Params, TEXT("className"), TEXT("class"), ClassName)) return Err;
|
|
1888
|
+
|
|
1889
|
+
// Resolve the class by path or (loaded) name - same resolution as create_data_asset.
|
|
1890
|
+
UClass* AssetClass = nullptr;
|
|
1891
|
+
if (ClassName.StartsWith(TEXT("/")))
|
|
1892
|
+
{
|
|
1893
|
+
AssetClass = LoadClass<UObject>(nullptr, *ClassName);
|
|
1894
|
+
if (!AssetClass) AssetClass = LoadObject<UClass>(nullptr, *ClassName);
|
|
1895
|
+
}
|
|
1896
|
+
if (!AssetClass)
|
|
1897
|
+
{
|
|
1898
|
+
FString Trimmed = ClassName;
|
|
1899
|
+
Trimmed.RemoveFromEnd(TEXT("_C"));
|
|
1900
|
+
for (TObjectIterator<UClass> It; It; ++It)
|
|
1901
|
+
{
|
|
1902
|
+
if (It->GetName() == Trimmed || It->GetName() == ClassName)
|
|
1903
|
+
{
|
|
1904
|
+
AssetClass = *It;
|
|
1905
|
+
break;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
if (!AssetClass)
|
|
1910
|
+
{
|
|
1911
|
+
return MCPError(FString::Printf(TEXT("Class not found: %s (pass full /Script/Module.ClassName or a loaded class name)"), *ClassName));
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// Guard classes that cannot be standalone assets or need a specialized flow.
|
|
1915
|
+
if (AssetClass->HasAnyClassFlags(CLASS_Abstract))
|
|
1916
|
+
{
|
|
1917
|
+
return MCPError(FString::Printf(TEXT("Class %s is abstract and cannot be instantiated as an asset"), *ClassName));
|
|
1918
|
+
}
|
|
1919
|
+
if (AssetClass->IsChildOf(AActor::StaticClass()) || AssetClass->IsChildOf(UActorComponent::StaticClass()))
|
|
1920
|
+
{
|
|
1921
|
+
return MCPError(FString::Printf(TEXT("Class %s is an actor/component, not an asset - use level(place_actor) / level(add_component_to_actor)"), *ClassName));
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
const FString FullPath = FString::Printf(TEXT("%s/%s.%s"), *PackagePath, *Name, *Name);
|
|
1925
|
+
const FString OnConflict = OptionalString(Params, TEXT("onConflict"), TEXT("skip"));
|
|
1926
|
+
|
|
1927
|
+
auto Created = MCPCreateAssetIdempotent<UObject>(Name, PackagePath, OnConflict, AssetClass->GetName(), AssetClass, nullptr);
|
|
1928
|
+
if (Created.EarlyReturn) return Created.EarlyReturn;
|
|
1929
|
+
UObject* NewAsset = Created.Asset;
|
|
1930
|
+
|
|
1931
|
+
// Optional properties (recursive JSON-to-property setter), mirroring create_data_asset.
|
|
1932
|
+
const TSharedPtr<FJsonObject>* PropsObj = nullptr;
|
|
1933
|
+
int32 SetCount = 0;
|
|
1934
|
+
TArray<FString> PropErrors;
|
|
1935
|
+
if (Params->TryGetObjectField(TEXT("properties"), PropsObj) && PropsObj && (*PropsObj).IsValid())
|
|
1936
|
+
{
|
|
1937
|
+
for (const auto& Pair : (*PropsObj)->Values)
|
|
1938
|
+
{
|
|
1939
|
+
FProperty* Prop = AssetClass->FindPropertyByName(FName(*Pair.Key));
|
|
1940
|
+
if (!Prop)
|
|
1941
|
+
{
|
|
1942
|
+
PropErrors.Add(FString::Printf(TEXT("Property not found: %s"), *Pair.Key));
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
void* Addr = Prop->ContainerPtrToValuePtr<void>(NewAsset);
|
|
1946
|
+
FString SetErr;
|
|
1947
|
+
if (MCPJsonProperty::SetJsonOnProperty(Prop, Addr, Pair.Value, SetErr))
|
|
1948
|
+
{
|
|
1949
|
+
SetCount++;
|
|
1950
|
+
}
|
|
1951
|
+
else
|
|
1952
|
+
{
|
|
1953
|
+
PropErrors.Add(FString::Printf(TEXT("Failed to set %s: %s"), *Pair.Key, *SetErr));
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
UEditorAssetLibrary::SaveAsset(FullPath);
|
|
1959
|
+
|
|
1960
|
+
auto Result = MCPSuccess();
|
|
1961
|
+
MCPSetCreated(Result);
|
|
1962
|
+
Result->SetStringField(TEXT("assetPath"), FullPath);
|
|
1963
|
+
Result->SetStringField(TEXT("name"), Name);
|
|
1964
|
+
Result->SetStringField(TEXT("className"), AssetClass->GetName());
|
|
1965
|
+
Result->SetNumberField(TEXT("propertiesSet"), SetCount);
|
|
1966
|
+
if (PropErrors.Num() > 0)
|
|
1967
|
+
{
|
|
1968
|
+
TArray<TSharedPtr<FJsonValue>> Errs;
|
|
1969
|
+
for (const FString& E : PropErrors) Errs.Add(MakeShared<FJsonValueString>(E));
|
|
1970
|
+
Result->SetArrayField(TEXT("propertyErrors"), Errs);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
1974
|
+
Payload->SetStringField(TEXT("assetPath"), FullPath);
|
|
1975
|
+
MCPSetRollback(Result, TEXT("delete_asset"), Payload);
|
|
1976
|
+
|
|
1977
|
+
return MCPResult(Result);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1872
1980
|
TSharedPtr<FJsonValue> FAssetHandlers::SaveAsset(const TSharedPtr<FJsonObject>& Params)
|
|
1873
1981
|
{
|
|
1874
1982
|
FString AssetPath;
|
|
@@ -23,6 +23,9 @@ private:
|
|
|
23
23
|
static TSharedPtr<FJsonValue> DeleteAssetBatch(const TSharedPtr<FJsonObject>& Params);
|
|
24
24
|
static TSharedPtr<FJsonValue> BulkRename(const TSharedPtr<FJsonObject>& Params);
|
|
25
25
|
static TSharedPtr<FJsonValue> CreateDataAsset(const TSharedPtr<FJsonObject>& Params);
|
|
26
|
+
// #726: create an asset of any concrete UObject class via its registered
|
|
27
|
+
// factory (or NewObject fallback), not just UDataAsset subclasses.
|
|
28
|
+
static TSharedPtr<FJsonValue> CreateAssetByClass(const TSharedPtr<FJsonObject>& Params);
|
|
26
29
|
static TSharedPtr<FJsonValue> SaveAsset(const TSharedPtr<FJsonObject>& Params);
|
|
27
30
|
// #429: bulk save of every dirty package - one-shot end-of-workflow flush.
|
|
28
31
|
static TSharedPtr<FJsonValue> SaveAllDirty(const TSharedPtr<FJsonObject>& Params);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include "HandlerUtils.h"
|
|
4
4
|
#include "HandlerAssetCreate.h"
|
|
5
5
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
6
|
+
#include "AssetRegistry/ARFilter.h"
|
|
6
7
|
#include "AssetToolsModule.h"
|
|
7
8
|
#include "IAssetTools.h"
|
|
8
9
|
#include "UObject/UObjectGlobals.h"
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
#include "Factories/SoundCueFactoryNew.h"
|
|
16
17
|
#include "AssetImportTask.h"
|
|
17
18
|
#include "Misc/Paths.h"
|
|
19
|
+
#include "Misc/Base64.h"
|
|
18
20
|
#include "Dom/JsonObject.h"
|
|
19
21
|
#include "Dom/JsonValue.h"
|
|
20
22
|
#include "Kismet/GameplayStatics.h"
|
|
@@ -25,6 +27,7 @@
|
|
|
25
27
|
void FAudioHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
|
|
26
28
|
{
|
|
27
29
|
Registry.RegisterHandler(TEXT("list_sound_assets"), &ListSoundAssets);
|
|
30
|
+
Registry.RegisterHandler(TEXT("extract_sound_wave_pcm"), &ExtractSoundWavePCM);
|
|
28
31
|
Registry.RegisterHandler(TEXT("import_audio"), &ImportAudio);
|
|
29
32
|
Registry.RegisterHandler(TEXT("create_sound_cue"), &CreateSoundCue);
|
|
30
33
|
Registry.RegisterHandler(TEXT("create_metasound_source"), &CreateMetaSoundSource);
|
|
@@ -151,37 +154,144 @@ TSharedPtr<FJsonValue> FAudioHandlers::ListSoundAssets(const TSharedPtr<FJsonObj
|
|
|
151
154
|
{
|
|
152
155
|
auto Result = MCPSuccess();
|
|
153
156
|
|
|
154
|
-
|
|
157
|
+
// #730: the old implementation ignored `directory`, had no result cap, and
|
|
158
|
+
// serialized SoundWave + SoundCue + MetaSoundSource for the whole project in
|
|
159
|
+
// one response. On projects with hundreds of SoundWaves that response could
|
|
160
|
+
// exceed the WebSocket framing threshold and drop the bridge. Honor the
|
|
161
|
+
// directory, filter recursively via a single FARFilter query, and paginate.
|
|
162
|
+
const FString Directory = OptionalString(Params, TEXT("directory"), TEXT("/Game"));
|
|
163
|
+
const bool bRecursive = OptionalBool(Params, TEXT("recursive"), true);
|
|
164
|
+
int32 MaxResults = OptionalInt(Params, TEXT("maxResults"), 1000);
|
|
165
|
+
if (MaxResults <= 0) MaxResults = 1000;
|
|
166
|
+
int32 Offset = OptionalInt(Params, TEXT("offset"), 0);
|
|
167
|
+
if (Offset < 0) Offset = 0;
|
|
155
168
|
|
|
156
169
|
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
|
|
157
170
|
|
|
158
|
-
|
|
159
|
-
ClassPaths.Add(FTopLevelAssetPath(TEXT("/Script/Engine"), TEXT("SoundWave")));
|
|
160
|
-
ClassPaths.Add(FTopLevelAssetPath(TEXT("/Script/Engine"), TEXT("SoundCue")));
|
|
161
|
-
ClassPaths.Add(FTopLevelAssetPath(TEXT("/Script/MetasoundEngine"), TEXT("MetaSoundSource")));
|
|
171
|
+
FARFilter Filter;
|
|
172
|
+
Filter.ClassPaths.Add(FTopLevelAssetPath(TEXT("/Script/Engine"), TEXT("SoundWave")));
|
|
173
|
+
Filter.ClassPaths.Add(FTopLevelAssetPath(TEXT("/Script/Engine"), TEXT("SoundCue")));
|
|
174
|
+
Filter.ClassPaths.Add(FTopLevelAssetPath(TEXT("/Script/MetasoundEngine"), TEXT("MetaSoundSource")));
|
|
175
|
+
Filter.bRecursiveClasses = true;
|
|
176
|
+
Filter.PackagePaths.Add(FName(*Directory));
|
|
177
|
+
Filter.bRecursivePaths = bRecursive;
|
|
162
178
|
|
|
179
|
+
TArray<FAssetData> AssetDataList;
|
|
180
|
+
AssetRegistry.GetAssets(Filter, AssetDataList);
|
|
181
|
+
|
|
182
|
+
// Stable ordering so pagination is deterministic across calls.
|
|
183
|
+
AssetDataList.Sort([](const FAssetData& A, const FAssetData& B)
|
|
184
|
+
{
|
|
185
|
+
return A.GetObjectPathString() < B.GetObjectPathString();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const int32 Total = AssetDataList.Num();
|
|
163
189
|
TArray<TSharedPtr<FJsonValue>> AssetsArray;
|
|
190
|
+
for (int32 Index = Offset; Index < Total && AssetsArray.Num() < MaxResults; ++Index)
|
|
191
|
+
{
|
|
192
|
+
const FAssetData& AssetData = AssetDataList[Index];
|
|
193
|
+
TSharedPtr<FJsonObject> AssetObj = MakeShared<FJsonObject>();
|
|
194
|
+
AssetObj->SetStringField(TEXT("name"), AssetData.AssetName.ToString());
|
|
195
|
+
AssetObj->SetStringField(TEXT("path"), AssetData.GetObjectPathString());
|
|
196
|
+
AssetObj->SetStringField(TEXT("class"), AssetData.AssetClassPath.GetAssetName().ToString());
|
|
197
|
+
AssetObj->SetStringField(TEXT("packagePath"), AssetData.PackagePath.ToString());
|
|
198
|
+
AssetsArray.Add(MakeShared<FJsonValueObject>(AssetObj));
|
|
199
|
+
}
|
|
164
200
|
|
|
165
|
-
|
|
201
|
+
const int32 NextOffset = Offset + AssetsArray.Num();
|
|
202
|
+
Result->SetArrayField(TEXT("assets"), AssetsArray);
|
|
203
|
+
Result->SetNumberField(TEXT("count"), AssetsArray.Num());
|
|
204
|
+
Result->SetNumberField(TEXT("total"), Total);
|
|
205
|
+
Result->SetNumberField(TEXT("offset"), Offset);
|
|
206
|
+
Result->SetNumberField(TEXT("maxResults"), MaxResults);
|
|
207
|
+
Result->SetBoolField(TEXT("hasMore"), NextOffset < Total);
|
|
208
|
+
if (NextOffset < Total)
|
|
166
209
|
{
|
|
167
|
-
|
|
168
|
-
|
|
210
|
+
Result->SetNumberField(TEXT("nextOffset"), NextOffset);
|
|
211
|
+
}
|
|
212
|
+
Result->SetStringField(TEXT("directory"), Directory);
|
|
213
|
+
|
|
214
|
+
return MCPResult(Result);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// #729: decode a USoundWave's imported audio to in-memory PCM. UE Python does
|
|
218
|
+
// not expose USoundWave::GetImportedSoundWaveData, so a semantic-search pipeline
|
|
219
|
+
// (CLAP etc.) previously had no way to reach the samples without relying on the
|
|
220
|
+
// original import file, which may have moved. This returns interleaved signed
|
|
221
|
+
// 16-bit PCM, base64-encoded, plus the format metadata needed to feed a model.
|
|
222
|
+
TSharedPtr<FJsonValue> FAudioHandlers::ExtractSoundWavePCM(const TSharedPtr<FJsonObject>& Params)
|
|
223
|
+
{
|
|
224
|
+
FString SoundPath;
|
|
225
|
+
if (auto Err = RequireString(Params, TEXT("soundPath"), SoundPath)) return Err;
|
|
226
|
+
|
|
227
|
+
USoundWave* Wave = LoadObject<USoundWave>(nullptr, *SoundPath);
|
|
228
|
+
if (!Wave)
|
|
229
|
+
{
|
|
230
|
+
return MCPError(FString::Printf(TEXT("SoundWave not found: %s"), *SoundPath));
|
|
231
|
+
}
|
|
169
232
|
|
|
170
|
-
|
|
233
|
+
#if WITH_EDITOR
|
|
234
|
+
TArray<uint8> RawPCM;
|
|
235
|
+
uint32 SampleRate = 0;
|
|
236
|
+
uint16 NumChannels = 0;
|
|
237
|
+
if (!Wave->GetImportedSoundWaveData(RawPCM, SampleRate, NumChannels)
|
|
238
|
+
|| RawPCM.Num() == 0 || SampleRate == 0 || NumChannels == 0)
|
|
239
|
+
{
|
|
240
|
+
return MCPError(TEXT("Failed to decode imported SoundWave data (no editor source data available for this asset)"));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// RawPCM is interleaved signed 16-bit little-endian across NumChannels.
|
|
244
|
+
int32 TotalFrames = (RawPCM.Num() / (int32)sizeof(int16)) / NumChannels;
|
|
245
|
+
|
|
246
|
+
// Optional decode window so callers can bound the response size (CLAP-style
|
|
247
|
+
// pipelines only need a few seconds). Default is the whole asset.
|
|
248
|
+
const double MaxSeconds = OptionalNumber(Params, TEXT("maxSeconds"), 0.0);
|
|
249
|
+
if (MaxSeconds > 0.0)
|
|
250
|
+
{
|
|
251
|
+
const int32 FrameCap = FMath::Clamp(FMath::FloorToInt(MaxSeconds * (double)SampleRate), 0, TotalFrames);
|
|
252
|
+
TotalFrames = FrameCap;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const bool bDownmix = OptionalBool(Params, TEXT("downmixMono"), false);
|
|
256
|
+
const int16* Samples = reinterpret_cast<const int16*>(RawPCM.GetData());
|
|
257
|
+
|
|
258
|
+
TArray<uint8> OutBytes;
|
|
259
|
+
int32 OutChannels = NumChannels;
|
|
260
|
+
if (bDownmix && NumChannels > 1)
|
|
261
|
+
{
|
|
262
|
+
OutChannels = 1;
|
|
263
|
+
OutBytes.SetNumUninitialized(TotalFrames * (int32)sizeof(int16));
|
|
264
|
+
int16* Dst = reinterpret_cast<int16*>(OutBytes.GetData());
|
|
265
|
+
for (int32 Frame = 0; Frame < TotalFrames; ++Frame)
|
|
171
266
|
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
267
|
+
int32 Acc = 0;
|
|
268
|
+
for (int32 Ch = 0; Ch < NumChannels; ++Ch)
|
|
269
|
+
{
|
|
270
|
+
Acc += Samples[Frame * NumChannels + Ch];
|
|
271
|
+
}
|
|
272
|
+
Dst[Frame] = static_cast<int16>(Acc / NumChannels);
|
|
178
273
|
}
|
|
179
274
|
}
|
|
275
|
+
else
|
|
276
|
+
{
|
|
277
|
+
const int32 ByteCount = TotalFrames * NumChannels * (int32)sizeof(int16);
|
|
278
|
+
OutBytes.Append(RawPCM.GetData(), ByteCount);
|
|
279
|
+
}
|
|
180
280
|
|
|
181
|
-
|
|
182
|
-
Result->SetNumberField(TEXT("count"), AssetsArray.Num());
|
|
281
|
+
const FString Base64 = FBase64::Encode(OutBytes);
|
|
183
282
|
|
|
283
|
+
auto Result = MCPSuccess();
|
|
284
|
+
Result->SetStringField(TEXT("soundPath"), SoundPath);
|
|
285
|
+
Result->SetNumberField(TEXT("sampleRate"), static_cast<double>(SampleRate));
|
|
286
|
+
Result->SetNumberField(TEXT("numChannels"), static_cast<double>(OutChannels));
|
|
287
|
+
Result->SetNumberField(TEXT("numFrames"), static_cast<double>(TotalFrames));
|
|
288
|
+
Result->SetNumberField(TEXT("durationSeconds"), SampleRate > 0 ? static_cast<double>(TotalFrames) / static_cast<double>(SampleRate) : 0.0);
|
|
289
|
+
Result->SetStringField(TEXT("format"), TEXT("pcm_s16le"));
|
|
290
|
+
Result->SetStringField(TEXT("pcmBase64"), Base64);
|
|
184
291
|
return MCPResult(Result);
|
|
292
|
+
#else
|
|
293
|
+
return MCPError(TEXT("extract_sound_wave_pcm requires an editor build"));
|
|
294
|
+
#endif
|
|
185
295
|
}
|
|
186
296
|
|
|
187
297
|
TSharedPtr<FJsonValue> FAudioHandlers::CreateSoundCue(const TSharedPtr<FJsonObject>& Params)
|
|
@@ -12,6 +12,8 @@ public:
|
|
|
12
12
|
private:
|
|
13
13
|
// ── Assets + playback ──────────────────────────────────────────────
|
|
14
14
|
static TSharedPtr<FJsonValue> ListSoundAssets(const TSharedPtr<FJsonObject>& Params);
|
|
15
|
+
// #729: decode a USoundWave's imported audio to in-memory PCM for semantic search.
|
|
16
|
+
static TSharedPtr<FJsonValue> ExtractSoundWavePCM(const TSharedPtr<FJsonObject>& Params);
|
|
15
17
|
// #664: import a WAV/OGG file as a USoundWave asset.
|
|
16
18
|
static TSharedPtr<FJsonValue> ImportAudio(const TSharedPtr<FJsonObject>& Params);
|
|
17
19
|
static TSharedPtr<FJsonValue> CreateSoundCue(const TSharedPtr<FJsonObject>& Params);
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
#include "Dom/JsonObject.h"
|
|
18
18
|
#include "Dom/JsonValue.h"
|
|
19
19
|
#include "IPythonScriptPlugin.h"
|
|
20
|
+
#include "LevelSequence.h"
|
|
21
|
+
#include "LevelSequenceEditorBlueprintLibrary.h"
|
|
22
|
+
#include "Framework/Docking/TabManager.h"
|
|
23
|
+
#include "Widgets/Docking/SDockTab.h"
|
|
24
|
+
#include "ISettingsModule.h"
|
|
25
|
+
#include "Modules/ModuleManager.h"
|
|
20
26
|
#include "Misc/ConfigCacheIni.h"
|
|
21
27
|
#include "Misc/ConfigContext.h"
|
|
22
28
|
#include "Misc/Paths.h"
|
|
@@ -24,6 +30,7 @@
|
|
|
24
30
|
#include "Misc/FileHelper.h"
|
|
25
31
|
#include "LevelEditorViewport.h"
|
|
26
32
|
#include "UnrealClient.h"
|
|
33
|
+
#include "Engine/GameViewportClient.h"
|
|
27
34
|
#include "ContentStreaming.h"
|
|
28
35
|
#include "RenderingThread.h"
|
|
29
36
|
#include "Misc/AutomationTest.h"
|
|
@@ -211,6 +218,13 @@ void FEditorHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
|
|
|
211
218
|
// Kismet, anything user-defined). Pair with editor.invoke_function to
|
|
212
219
|
// drive GeometryScript ops from MCP without hand-writing each handler.
|
|
213
220
|
Registry.RegisterHandler(TEXT("list_function_libraries"), &ListFunctionLibraries);
|
|
221
|
+
// #718: close the open Level Sequence editor before destructive actor ops.
|
|
222
|
+
Registry.RegisterHandler(TEXT("close_sequence"), &CloseSequence);
|
|
223
|
+
// #719: purge cached embedded-Python modules by prefix for tool-dev iteration.
|
|
224
|
+
Registry.RegisterHandler(TEXT("purge_python_modules"), &PurgePythonModules);
|
|
225
|
+
// #727: open a registered editor tab / Project Settings viewer for visual evidence.
|
|
226
|
+
Registry.RegisterHandler(TEXT("open_tab"), &OpenTab);
|
|
227
|
+
Registry.RegisterHandler(TEXT("open_settings"), &OpenSettings);
|
|
214
228
|
}
|
|
215
229
|
|
|
216
230
|
TSharedPtr<FJsonValue> FEditorHandlers::ExecuteCommand(const TSharedPtr<FJsonObject>& Params)
|
|
@@ -245,9 +259,35 @@ TSharedPtr<FJsonValue> FEditorHandlers::ExecutePython(const TSharedPtr<FJsonObje
|
|
|
245
259
|
|
|
246
260
|
bool bSuccess = PythonPlugin->ExecPythonCommandEx(PythonCommand);
|
|
247
261
|
|
|
262
|
+
// #732: a first-class result channel. In ExecuteFile mode CommandResult is
|
|
263
|
+
// normally empty and a top-level `return` is illegal, so scripts were forced
|
|
264
|
+
// to use print() as transport - mixing application data with diagnostics and
|
|
265
|
+
// duplicating it across log_output/output. When the caller names a
|
|
266
|
+
// resultVariable, evaluate it in the Public (__main__) scope the script just
|
|
267
|
+
// ran in and surface it as `result`, leaving print()/log as diagnostics only.
|
|
268
|
+
FString ResultText = PythonCommand.CommandResult;
|
|
269
|
+
const FString ResultVariable = OptionalString(Params, TEXT("resultVariable"));
|
|
270
|
+
bool bResultVariableResolved = false;
|
|
271
|
+
if (bSuccess && !ResultVariable.IsEmpty())
|
|
272
|
+
{
|
|
273
|
+
FPythonCommandEx EvalCommand;
|
|
274
|
+
EvalCommand.Command = ResultVariable;
|
|
275
|
+
EvalCommand.ExecutionMode = EPythonCommandExecutionMode::EvaluateStatement;
|
|
276
|
+
EvalCommand.FileExecutionScope = EPythonFileExecutionScope::Public;
|
|
277
|
+
if (PythonPlugin->ExecPythonCommandEx(EvalCommand))
|
|
278
|
+
{
|
|
279
|
+
ResultText = EvalCommand.CommandResult;
|
|
280
|
+
bResultVariableResolved = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
248
284
|
TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
249
285
|
Result->SetBoolField(TEXT("success"), bSuccess);
|
|
250
|
-
Result->SetStringField(TEXT("result"),
|
|
286
|
+
Result->SetStringField(TEXT("result"), ResultText);
|
|
287
|
+
if (!ResultVariable.IsEmpty())
|
|
288
|
+
{
|
|
289
|
+
Result->SetBoolField(TEXT("resultVariableResolved"), bResultVariableResolved);
|
|
290
|
+
}
|
|
251
291
|
|
|
252
292
|
TArray<TSharedPtr<FJsonValue>> LogArray;
|
|
253
293
|
for (const FPythonLogOutputEntry& Entry : PythonCommand.LogOutput)
|
|
@@ -313,10 +353,32 @@ TSharedPtr<FJsonValue> FEditorHandlers::RunPythonFile(const TSharedPtr<FJsonObje
|
|
|
313
353
|
|
|
314
354
|
bool bSuccess = PythonPlugin->ExecPythonCommandEx(PythonCommand);
|
|
315
355
|
|
|
356
|
+
// #732: same first-class result channel as execute_python. The file runs in
|
|
357
|
+
// the Public (__main__) scope, so a named resultVariable can be read back.
|
|
358
|
+
FString ResultText = PythonCommand.CommandResult;
|
|
359
|
+
const FString ResultVariable = OptionalString(Params, TEXT("resultVariable"));
|
|
360
|
+
bool bResultVariableResolved = false;
|
|
361
|
+
if (bSuccess && !ResultVariable.IsEmpty())
|
|
362
|
+
{
|
|
363
|
+
FPythonCommandEx EvalCommand;
|
|
364
|
+
EvalCommand.Command = ResultVariable;
|
|
365
|
+
EvalCommand.ExecutionMode = EPythonCommandExecutionMode::EvaluateStatement;
|
|
366
|
+
EvalCommand.FileExecutionScope = EPythonFileExecutionScope::Public;
|
|
367
|
+
if (PythonPlugin->ExecPythonCommandEx(EvalCommand))
|
|
368
|
+
{
|
|
369
|
+
ResultText = EvalCommand.CommandResult;
|
|
370
|
+
bResultVariableResolved = true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
316
374
|
TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
|
|
317
375
|
Result->SetBoolField(TEXT("success"), bSuccess);
|
|
318
376
|
Result->SetStringField(TEXT("path"), FilePath);
|
|
319
|
-
Result->SetStringField(TEXT("result"),
|
|
377
|
+
Result->SetStringField(TEXT("result"), ResultText);
|
|
378
|
+
if (!ResultVariable.IsEmpty())
|
|
379
|
+
{
|
|
380
|
+
Result->SetBoolField(TEXT("resultVariableResolved"), bResultVariableResolved);
|
|
381
|
+
}
|
|
320
382
|
|
|
321
383
|
TArray<TSharedPtr<FJsonValue>> LogArray;
|
|
322
384
|
FString CombinedOutput;
|
|
@@ -335,6 +397,150 @@ TSharedPtr<FJsonValue> FEditorHandlers::RunPythonFile(const TSharedPtr<FJsonObje
|
|
|
335
397
|
return MCPResult(Result);
|
|
336
398
|
}
|
|
337
399
|
|
|
400
|
+
// #719: UE's embedded Python caches imported modules for the whole editor
|
|
401
|
+
// session, so after editing a pipeline tool on disk the editor keeps running
|
|
402
|
+
// stale code until the modules are purged from sys.modules. sys.modules is
|
|
403
|
+
// Python-runtime state with no C++ accessor, so the interpreter (a hard plugin
|
|
404
|
+
// dependency) is the correct owner to drive. We emit per-module markers rather
|
|
405
|
+
// than rely on eval repr/str ambiguity, then rebuild the list in C++.
|
|
406
|
+
TSharedPtr<FJsonValue> FEditorHandlers::PurgePythonModules(const TSharedPtr<FJsonObject>& Params)
|
|
407
|
+
{
|
|
408
|
+
FString Prefix;
|
|
409
|
+
if (auto Err = RequireString(Params, TEXT("prefix"), Prefix)) return Err;
|
|
410
|
+
if (Prefix.TrimStartAndEnd().IsEmpty())
|
|
411
|
+
{
|
|
412
|
+
return MCPError(TEXT("'prefix' must be non-empty (an empty prefix would purge every module)"));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
IPythonScriptPlugin* PythonPlugin = IPythonScriptPlugin::Get();
|
|
416
|
+
if (!PythonPlugin || !PythonPlugin->IsPythonAvailable())
|
|
417
|
+
{
|
|
418
|
+
return MCPError(TEXT("Python scripting is not available"));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Escape the prefix into a Python single-quoted literal.
|
|
422
|
+
FString Escaped = Prefix.Replace(TEXT("\\"), TEXT("\\\\")).Replace(TEXT("'"), TEXT("\\'"));
|
|
423
|
+
|
|
424
|
+
FString Code;
|
|
425
|
+
Code += TEXT("import sys as __mcp_sys\n");
|
|
426
|
+
Code += FString::Printf(TEXT("__mcp_names = [__m for __m in list(__mcp_sys.modules) if __m.startswith('%s')]\n"), *Escaped);
|
|
427
|
+
Code += TEXT("for __m in __mcp_names:\n");
|
|
428
|
+
Code += TEXT(" del __mcp_sys.modules[__m]\n");
|
|
429
|
+
Code += TEXT(" print('MCP_PURGED_ITEM:' + __m)\n");
|
|
430
|
+
|
|
431
|
+
FPythonCommandEx PythonCommand;
|
|
432
|
+
PythonCommand.Command = Code;
|
|
433
|
+
PythonCommand.ExecutionMode = EPythonCommandExecutionMode::ExecuteFile;
|
|
434
|
+
PythonCommand.FileExecutionScope = EPythonFileExecutionScope::Public;
|
|
435
|
+
const bool bSuccess = PythonPlugin->ExecPythonCommandEx(PythonCommand);
|
|
436
|
+
if (!bSuccess)
|
|
437
|
+
{
|
|
438
|
+
return MCPError(TEXT("Failed to purge Python modules (interpreter error)"));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
TArray<TSharedPtr<FJsonValue>> Purged;
|
|
442
|
+
const FString Marker = TEXT("MCP_PURGED_ITEM:");
|
|
443
|
+
for (const FPythonLogOutputEntry& Entry : PythonCommand.LogOutput)
|
|
444
|
+
{
|
|
445
|
+
FString Line = Entry.Output;
|
|
446
|
+
int32 Idx = Line.Find(Marker);
|
|
447
|
+
if (Idx != INDEX_NONE)
|
|
448
|
+
{
|
|
449
|
+
FString Name = Line.RightChop(Idx + Marker.Len()).TrimStartAndEnd();
|
|
450
|
+
if (!Name.IsEmpty())
|
|
451
|
+
{
|
|
452
|
+
Purged.Add(MakeShared<FJsonValueString>(Name));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
auto Result = MCPSuccess();
|
|
458
|
+
Result->SetStringField(TEXT("prefix"), Prefix);
|
|
459
|
+
Result->SetArrayField(TEXT("purged"), Purged);
|
|
460
|
+
Result->SetNumberField(TEXT("count"), Purged.Num());
|
|
461
|
+
return MCPResult(Result);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// #718: close the currently open Level Sequence editor. Open sequences
|
|
465
|
+
// re-resolve possessables by name during actor destruction, which can mis-bind
|
|
466
|
+
// or destabilize the editor, so bulk actor ops want the sequencer closed first.
|
|
467
|
+
TSharedPtr<FJsonValue> FEditorHandlers::CloseSequence(const TSharedPtr<FJsonObject>& /*Params*/)
|
|
468
|
+
{
|
|
469
|
+
ULevelSequence* Current = ULevelSequenceEditorBlueprintLibrary::GetCurrentLevelSequence();
|
|
470
|
+
const bool bWasOpen = Current != nullptr;
|
|
471
|
+
const FString OpenPath = bWasOpen ? Current->GetPathName() : FString();
|
|
472
|
+
|
|
473
|
+
ULevelSequenceEditorBlueprintLibrary::CloseLevelSequence();
|
|
474
|
+
|
|
475
|
+
auto Result = MCPSuccess();
|
|
476
|
+
Result->SetBoolField(TEXT("wasOpen"), bWasOpen);
|
|
477
|
+
if (bWasOpen)
|
|
478
|
+
{
|
|
479
|
+
Result->SetStringField(TEXT("closedSequence"), OpenPath);
|
|
480
|
+
}
|
|
481
|
+
return MCPResult(Result);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// #727: open a registered editor tab by ID (e.g. "ProjectSettings", "OutputLog")
|
|
485
|
+
// so an agent can screenshot editor UI as evidence.
|
|
486
|
+
TSharedPtr<FJsonValue> FEditorHandlers::OpenTab(const TSharedPtr<FJsonObject>& Params)
|
|
487
|
+
{
|
|
488
|
+
FString TabId;
|
|
489
|
+
if (auto Err = RequireString(Params, TEXT("tabId"), TabId)) return Err;
|
|
490
|
+
|
|
491
|
+
TSharedPtr<SDockTab> Tab = FGlobalTabmanager::Get()->TryInvokeTab(FTabId(*TabId));
|
|
492
|
+
|
|
493
|
+
auto Result = MCPSuccess();
|
|
494
|
+
Result->SetStringField(TEXT("tabId"), TabId);
|
|
495
|
+
Result->SetBoolField(TEXT("opened"), Tab.IsValid());
|
|
496
|
+
if (!Tab.IsValid())
|
|
497
|
+
{
|
|
498
|
+
Result->SetBoolField(TEXT("success"), false);
|
|
499
|
+
Result->SetStringField(TEXT("error"), FString::Printf(TEXT("No registered tab with id '%s' (try 'ProjectSettings', 'OutputLog', 'ContentBrowserTab1', ...)"), *TabId));
|
|
500
|
+
}
|
|
501
|
+
return MCPResult(Result);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// #727: open (and navigate) a settings viewer - Project Settings / Editor
|
|
505
|
+
// Preferences. section may be a bare section name (with category) or a dotted
|
|
506
|
+
// "Category.Section" pair for convenience.
|
|
507
|
+
TSharedPtr<FJsonValue> FEditorHandlers::OpenSettings(const TSharedPtr<FJsonObject>& Params)
|
|
508
|
+
{
|
|
509
|
+
FString Container = OptionalString(Params, TEXT("container"), TEXT("Project"));
|
|
510
|
+
FString Category = OptionalString(Params, TEXT("category"));
|
|
511
|
+
FString Section = OptionalString(Params, TEXT("section"));
|
|
512
|
+
|
|
513
|
+
// Accept a combined "Category.Section" in `section` when `category` is absent.
|
|
514
|
+
if (Category.IsEmpty() && Section.Contains(TEXT(".")))
|
|
515
|
+
{
|
|
516
|
+
FString Left, Right;
|
|
517
|
+
if (Section.Split(TEXT("."), &Left, &Right))
|
|
518
|
+
{
|
|
519
|
+
Category = Left;
|
|
520
|
+
Section = Right;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
|
|
525
|
+
if (!SettingsModule)
|
|
526
|
+
{
|
|
527
|
+
return MCPError(TEXT("Settings module not available"));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Make sure the viewer tab exists, then show the requested section.
|
|
531
|
+
FGlobalTabmanager::Get()->TryInvokeTab(FTabId(Container == TEXT("Editor") ? TEXT("EditorSettings") : TEXT("ProjectSettings")));
|
|
532
|
+
if (!Category.IsEmpty())
|
|
533
|
+
{
|
|
534
|
+
SettingsModule->ShowViewer(FName(*Container), FName(*Category), FName(*Section));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
auto Result = MCPSuccess();
|
|
538
|
+
Result->SetStringField(TEXT("container"), Container);
|
|
539
|
+
Result->SetStringField(TEXT("category"), Category);
|
|
540
|
+
Result->SetStringField(TEXT("section"), Section);
|
|
541
|
+
return MCPResult(Result);
|
|
542
|
+
}
|
|
543
|
+
|
|
338
544
|
TSharedPtr<FJsonValue> FEditorHandlers::SetProperty(const TSharedPtr<FJsonObject>& Params)
|
|
339
545
|
{
|
|
340
546
|
// #221/#230: TS schema documents `objectPath` but the dispatcher only
|
|
@@ -1042,9 +1248,37 @@ TSharedPtr<FJsonValue> FEditorHandlers::CaptureScreenshot(const TSharedPtr<FJson
|
|
|
1042
1248
|
|
|
1043
1249
|
if (bUsePie && PieWorld)
|
|
1044
1250
|
{
|
|
1251
|
+
// #724: HighResShot renders the PIE world offscreen and (a) strips the
|
|
1252
|
+
// debug canvas (AddOnScreenDebugMessage overlays) and (b) in
|
|
1253
|
+
// Play-in-New-Window did not reliably resolve to the PIE game window.
|
|
1254
|
+
// Capture the actual PIE game viewport with a normal screenshot request
|
|
1255
|
+
// and bShowUI=true, so we get exactly what the player sees - HUD and the
|
|
1256
|
+
// on-screen debug canvas included. The running game viewport consumes the
|
|
1257
|
+
// pending request on its next Draw, so this targets the PIE window even
|
|
1258
|
+
// in new-window mode. Fall back to HighResShot only if no game viewport.
|
|
1259
|
+
FString FullPath = Filename;
|
|
1260
|
+
if (FPaths::IsRelative(Filename))
|
|
1261
|
+
{
|
|
1262
|
+
FullPath = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Screenshots"), Filename);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
UGameViewportClient* GameViewport = PieWorld->GetGameViewport();
|
|
1266
|
+
if (GameViewport && GameViewport->Viewport)
|
|
1267
|
+
{
|
|
1268
|
+
FScreenshotRequest::RequestScreenshot(FullPath, /*bShowUI=*/true, /*bAddFilenameSuffix=*/false);
|
|
1269
|
+
GameViewport->Viewport->Invalidate();
|
|
1270
|
+
|
|
1271
|
+
auto Result = MCPSuccess();
|
|
1272
|
+
Result->SetStringField(TEXT("filename"), FullPath);
|
|
1273
|
+
Result->SetStringField(TEXT("target"), TEXT("pie"));
|
|
1274
|
+
Result->SetBoolField(TEXT("includesDebugCanvas"), true);
|
|
1275
|
+
Result->SetStringField(TEXT("note"), TEXT("PIE game-viewport screenshot queued (UI + on-screen debug canvas included); written asynchronously."));
|
|
1276
|
+
return MCPResult(Result);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Fallback: no resolvable game viewport (unusual) - dispatch HighResShot.
|
|
1045
1280
|
int32 Width = OptionalInt(Params, TEXT("width"), 1920);
|
|
1046
1281
|
int32 Height = OptionalInt(Params, TEXT("height"), 1080);
|
|
1047
|
-
// Some callers pass a single 'resolution' (long edge); honour it as width.
|
|
1048
1282
|
double ResolutionScalar = 0.0;
|
|
1049
1283
|
if (Params->TryGetNumberField(TEXT("resolution"), ResolutionScalar) && ResolutionScalar > 0)
|
|
1050
1284
|
{
|
|
@@ -1057,7 +1291,8 @@ TSharedPtr<FJsonValue> FEditorHandlers::CaptureScreenshot(const TSharedPtr<FJson
|
|
|
1057
1291
|
Result->SetStringField(TEXT("filename"), Filename);
|
|
1058
1292
|
Result->SetStringField(TEXT("target"), TEXT("pie"));
|
|
1059
1293
|
Result->SetStringField(TEXT("consoleCommand"), ConsoleCmd);
|
|
1060
|
-
Result->
|
|
1294
|
+
Result->SetBoolField(TEXT("includesDebugCanvas"), false);
|
|
1295
|
+
Result->SetStringField(TEXT("note"), TEXT("No game viewport resolved; HighResShot dispatched (debug canvas not captured). Output in Saved/Screenshots/<map>/."));
|
|
1061
1296
|
return MCPResult(Result);
|
|
1062
1297
|
}
|
|
1063
1298
|
|
|
@@ -1509,7 +1744,25 @@ TSharedPtr<FJsonValue> FEditorHandlers::OpenAsset(const TSharedPtr<FJsonObject>&
|
|
|
1509
1744
|
|
|
1510
1745
|
TSharedPtr<FJsonValue> FEditorHandlers::RunStatCommand(const TSharedPtr<FJsonObject>& Params)
|
|
1511
1746
|
{
|
|
1512
|
-
|
|
1747
|
+
// #722: callers naturally pass the stat name (e.g. name="unit"); the old
|
|
1748
|
+
// handler only read "command" and silently defaulted to "stat fps" when the
|
|
1749
|
+
// stat was passed as "name", so "unit" ran the FPS counter. Accept either:
|
|
1750
|
+
// an explicit full "command", or a bare stat "name" that we prefix.
|
|
1751
|
+
FString Command = OptionalString(Params, TEXT("command"));
|
|
1752
|
+
if (Command.IsEmpty())
|
|
1753
|
+
{
|
|
1754
|
+
const FString StatName = OptionalString(Params, TEXT("name"));
|
|
1755
|
+
if (!StatName.IsEmpty())
|
|
1756
|
+
{
|
|
1757
|
+
Command = StatName.StartsWith(TEXT("stat "), ESearchCase::IgnoreCase)
|
|
1758
|
+
? StatName
|
|
1759
|
+
: FString::Printf(TEXT("stat %s"), *StatName);
|
|
1760
|
+
}
|
|
1761
|
+
else
|
|
1762
|
+
{
|
|
1763
|
+
Command = TEXT("stat fps");
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1513
1766
|
|
|
1514
1767
|
REQUIRE_EDITOR_WORLD(World);
|
|
1515
1768
|
|
|
@@ -115,6 +115,13 @@ private:
|
|
|
115
115
|
static TSharedPtr<FJsonValue> ExecutePython(const TSharedPtr<FJsonObject>& Params);
|
|
116
116
|
// #142: run a Python file with __file__/__name__ context populated
|
|
117
117
|
static TSharedPtr<FJsonValue> RunPythonFile(const TSharedPtr<FJsonObject>& Params);
|
|
118
|
+
// #719: purge cached embedded-Python modules by prefix (tool-dev iteration)
|
|
119
|
+
static TSharedPtr<FJsonValue> PurgePythonModules(const TSharedPtr<FJsonObject>& Params);
|
|
120
|
+
// #718: close the currently open Level Sequence editor
|
|
121
|
+
static TSharedPtr<FJsonValue> CloseSequence(const TSharedPtr<FJsonObject>& Params);
|
|
122
|
+
// #727: open a registered editor tab / the Project Settings viewer
|
|
123
|
+
static TSharedPtr<FJsonValue> OpenTab(const TSharedPtr<FJsonObject>& Params);
|
|
124
|
+
static TSharedPtr<FJsonValue> OpenSettings(const TSharedPtr<FJsonObject>& Params);
|
|
118
125
|
static TSharedPtr<FJsonValue> SetProperty(const TSharedPtr<FJsonObject>& Params);
|
|
119
126
|
static TSharedPtr<FJsonValue> GetProperty(const TSharedPtr<FJsonObject>& Params);
|
|
120
127
|
static TSharedPtr<FJsonValue> DescribeObject(const TSharedPtr<FJsonObject>& Params);
|