ultimate-unreal-engine-mcp 0.1.9 → 0.1.11
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/dist/tools/editor/index.js +76 -0
- package/package.json +1 -1
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +187 -5
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +2 -2
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +247 -0
|
@@ -110,6 +110,82 @@ export function registerEditorTools(server, bridge) {
|
|
|
110
110
|
});
|
|
111
111
|
}));
|
|
112
112
|
// --------------------------------------------------------------------------
|
|
113
|
+
// ue_set_actor_property
|
|
114
|
+
// --------------------------------------------------------------------------
|
|
115
|
+
server.registerTool('ue_set_actor_property', {
|
|
116
|
+
title: 'Set Actor Property',
|
|
117
|
+
description: '[requires_plugin] Set a UPROPERTY value on an actor by label. Supports bool, int, float, string, name, text, actor (reference by label), and asset (reference by path).',
|
|
118
|
+
inputSchema: z.object({
|
|
119
|
+
actor_label: z.string().describe('The editor label of the target actor'),
|
|
120
|
+
property_name: z.string().describe('The UPROPERTY name to set (e.g., RoomID, RoomDoor, DoorCurve)'),
|
|
121
|
+
value: z.union([z.string(), z.number(), z.boolean()]).describe('The value to set — string for name/text/actor label/asset path, number for int/float, boolean for bool'),
|
|
122
|
+
value_type: z.enum(['bool', 'int', 'float', 'string', 'name', 'text', 'actor', 'asset']).describe('Type of the value'),
|
|
123
|
+
}),
|
|
124
|
+
annotations: {
|
|
125
|
+
readOnlyHint: false,
|
|
126
|
+
destructiveHint: false,
|
|
127
|
+
},
|
|
128
|
+
}, withKnownIssues('ue_set_actor_property', async (args) => {
|
|
129
|
+
return sendOrDisconnect(_bridge, {
|
|
130
|
+
type: 'actor.setProperty',
|
|
131
|
+
payload: {
|
|
132
|
+
actor_label: args.actor_label,
|
|
133
|
+
property_name: args.property_name,
|
|
134
|
+
value: args.value,
|
|
135
|
+
value_type: args.value_type,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}));
|
|
139
|
+
// --------------------------------------------------------------------------
|
|
140
|
+
// ue_create_data_asset
|
|
141
|
+
// --------------------------------------------------------------------------
|
|
142
|
+
server.registerTool('ue_create_data_asset', {
|
|
143
|
+
title: 'Create Data Asset',
|
|
144
|
+
description: '[requires_plugin] Create a UPrimaryDataAsset (or subclass) at the given path with reflection-set properties.',
|
|
145
|
+
inputSchema: z.object({
|
|
146
|
+
asset_path: z.string().describe('UE asset path, e.g. /Game/Data/DA_ValveHandle'),
|
|
147
|
+
class_name: z.string().describe('Asset class name, e.g. ItemDefinition'),
|
|
148
|
+
properties: z.record(z.object({
|
|
149
|
+
value: z.union([z.string(), z.number(), z.boolean()]).describe('Property value'),
|
|
150
|
+
type: z.enum(['name', 'text', 'int', 'byte', 'string']).describe('Property type for reflection'),
|
|
151
|
+
})).optional().describe('Map of property_name → {value, type} to set after creation'),
|
|
152
|
+
}),
|
|
153
|
+
annotations: {
|
|
154
|
+
readOnlyHint: false,
|
|
155
|
+
destructiveHint: false,
|
|
156
|
+
},
|
|
157
|
+
}, withKnownIssues('ue_create_data_asset', async (args) => {
|
|
158
|
+
const payload = { asset_path: args.asset_path, class_name: args.class_name };
|
|
159
|
+
if (args.properties) {
|
|
160
|
+
payload['properties'] = args.properties;
|
|
161
|
+
}
|
|
162
|
+
return sendOrDisconnect(_bridge, { type: 'asset.createDataAsset', payload });
|
|
163
|
+
}));
|
|
164
|
+
// --------------------------------------------------------------------------
|
|
165
|
+
// ue_create_curve
|
|
166
|
+
// --------------------------------------------------------------------------
|
|
167
|
+
server.registerTool('ue_create_curve', {
|
|
168
|
+
title: 'Create Float Curve',
|
|
169
|
+
description: '[requires_plugin] Create a UCurveFloat asset with keyframes at the given path.',
|
|
170
|
+
inputSchema: z.object({
|
|
171
|
+
asset_path: z.string().describe('UE asset path, e.g. /Game/Curves/C_DoorSwing'),
|
|
172
|
+
keys: z.array(z.object({
|
|
173
|
+
time: z.number().describe('Key time in seconds'),
|
|
174
|
+
value: z.number().describe('Key value'),
|
|
175
|
+
interp: z.enum(['linear', 'cubic', 'constant']).optional().describe('Interpolation mode (default: cubic)'),
|
|
176
|
+
})).describe('Array of keyframes'),
|
|
177
|
+
}),
|
|
178
|
+
annotations: {
|
|
179
|
+
readOnlyHint: false,
|
|
180
|
+
destructiveHint: false,
|
|
181
|
+
},
|
|
182
|
+
}, withKnownIssues('ue_create_curve', async (args) => {
|
|
183
|
+
return sendOrDisconnect(_bridge, {
|
|
184
|
+
type: 'asset.createCurve',
|
|
185
|
+
payload: { asset_path: args.asset_path, keys: args.keys },
|
|
186
|
+
});
|
|
187
|
+
}));
|
|
188
|
+
// --------------------------------------------------------------------------
|
|
113
189
|
// ue_transform_actor (Phase 9 — adds scale field; renamed from Phase 1 stub)
|
|
114
190
|
// --------------------------------------------------------------------------
|
|
115
191
|
server.registerTool('ue_transform_actor', {
|
package/package.json
CHANGED
|
@@ -201,12 +201,11 @@ void RegisterActorCommands(FMCPCommandRouter& Router)
|
|
|
201
201
|
const FVector Location(X, Y, Z);
|
|
202
202
|
const FRotator Rotation(0.0, 0.0, 0.0);
|
|
203
203
|
|
|
204
|
-
// Optional label —
|
|
204
|
+
// Optional label — applied AFTER spawn via SetActorLabel().
|
|
205
|
+
// Do NOT set Params.Name — UE's name-uniqueness check can fatal-error
|
|
206
|
+
// when the requested FName collides with internal naming patterns.
|
|
205
207
|
FString Label;
|
|
206
|
-
|
|
207
|
-
{
|
|
208
|
-
Params.Name = FName(*Label);
|
|
209
|
-
}
|
|
208
|
+
Payload->TryGetStringField(TEXT("label"), Label);
|
|
210
209
|
|
|
211
210
|
AActor* Spawned = World->SpawnActor<AActor>(ActorClass, Location, Rotation, Params);
|
|
212
211
|
if (!Spawned)
|
|
@@ -425,4 +424,187 @@ void RegisterActorCommands(FMCPCommandRouter& Router)
|
|
|
425
424
|
|
|
426
425
|
SendResponse(BuildActorSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
427
426
|
});
|
|
427
|
+
|
|
428
|
+
// -----------------------------------------------------------------------
|
|
429
|
+
// actor.setProperty
|
|
430
|
+
// Sets a UPROPERTY on an actor by label. Supports:
|
|
431
|
+
// - Primitive types: bool, int, float, FString, FName, FText
|
|
432
|
+
// - Actor references: resolved by actor label in the current world
|
|
433
|
+
// - Asset references: resolved by UE asset path (e.g. /Game/Data/DA_Key)
|
|
434
|
+
// Payload: { actor_label, property_name, value, value_type }
|
|
435
|
+
// value_type: "bool"|"int"|"float"|"string"|"name"|"text"|"actor"|"asset"
|
|
436
|
+
// -----------------------------------------------------------------------
|
|
437
|
+
Router.RegisterHandler(TEXT("actor.setProperty"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
438
|
+
{
|
|
439
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
440
|
+
|
|
441
|
+
TSharedPtr<FJsonObject> Payload;
|
|
442
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
|
|
443
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
444
|
+
{
|
|
445
|
+
Payload = (*PayloadVal)->AsObject();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
FString ActorLabel, PropertyName, ValueType;
|
|
449
|
+
if (!Payload.IsValid()
|
|
450
|
+
|| !Payload->TryGetStringField(TEXT("actor_label"), ActorLabel) || ActorLabel.IsEmpty()
|
|
451
|
+
|| !Payload->TryGetStringField(TEXT("property_name"), PropertyName) || PropertyName.IsEmpty()
|
|
452
|
+
|| !Payload->TryGetStringField(TEXT("value_type"), ValueType) || ValueType.IsEmpty())
|
|
453
|
+
{
|
|
454
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("missing_required_fields")) + TEXT("\n"));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
|
|
459
|
+
if (!World)
|
|
460
|
+
{
|
|
461
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Find target actor by label.
|
|
466
|
+
AActor* TargetActor = nullptr;
|
|
467
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
468
|
+
{
|
|
469
|
+
if ((*It)->GetActorLabel() == ActorLabel)
|
|
470
|
+
{
|
|
471
|
+
TargetActor = *It;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (!TargetActor)
|
|
476
|
+
{
|
|
477
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Find the UPROPERTY by name on the actor's class.
|
|
482
|
+
FProperty* Prop = TargetActor->GetClass()->FindPropertyByName(FName(*PropertyName));
|
|
483
|
+
if (!Prop)
|
|
484
|
+
{
|
|
485
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("property_not_found")) + TEXT("\n"));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
TargetActor->Modify();
|
|
490
|
+
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(TargetActor);
|
|
491
|
+
|
|
492
|
+
if (ValueType == TEXT("bool"))
|
|
493
|
+
{
|
|
494
|
+
bool bVal = false;
|
|
495
|
+
Payload->TryGetBoolField(TEXT("value"), bVal);
|
|
496
|
+
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
|
|
497
|
+
{
|
|
498
|
+
BoolProp->SetPropertyValue(ValuePtr, bVal);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
else if (ValueType == TEXT("int"))
|
|
502
|
+
{
|
|
503
|
+
int32 IntVal = 0;
|
|
504
|
+
Payload->TryGetNumberField(TEXT("value"), *(double*)&IntVal);
|
|
505
|
+
double Dbl = 0;
|
|
506
|
+
Payload->TryGetNumberField(TEXT("value"), Dbl);
|
|
507
|
+
IntVal = static_cast<int32>(Dbl);
|
|
508
|
+
if (FIntProperty* IntProp = CastField<FIntProperty>(Prop))
|
|
509
|
+
{
|
|
510
|
+
IntProp->SetPropertyValue(ValuePtr, IntVal);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else if (ValueType == TEXT("float"))
|
|
514
|
+
{
|
|
515
|
+
double Dbl = 0.0;
|
|
516
|
+
Payload->TryGetNumberField(TEXT("value"), Dbl);
|
|
517
|
+
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop))
|
|
518
|
+
{
|
|
519
|
+
FloatProp->SetPropertyValue(ValuePtr, static_cast<float>(Dbl));
|
|
520
|
+
}
|
|
521
|
+
else if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop))
|
|
522
|
+
{
|
|
523
|
+
DoubleProp->SetPropertyValue(ValuePtr, Dbl);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else if (ValueType == TEXT("string"))
|
|
527
|
+
{
|
|
528
|
+
FString StrVal;
|
|
529
|
+
Payload->TryGetStringField(TEXT("value"), StrVal);
|
|
530
|
+
if (FStrProperty* StrProp = CastField<FStrProperty>(Prop))
|
|
531
|
+
{
|
|
532
|
+
StrProp->SetPropertyValue(ValuePtr, StrVal);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
else if (ValueType == TEXT("name"))
|
|
536
|
+
{
|
|
537
|
+
FString StrVal;
|
|
538
|
+
Payload->TryGetStringField(TEXT("value"), StrVal);
|
|
539
|
+
if (FNameProperty* NameProp = CastField<FNameProperty>(Prop))
|
|
540
|
+
{
|
|
541
|
+
NameProp->SetPropertyValue(ValuePtr, FName(*StrVal));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else if (ValueType == TEXT("text"))
|
|
545
|
+
{
|
|
546
|
+
FString StrVal;
|
|
547
|
+
Payload->TryGetStringField(TEXT("value"), StrVal);
|
|
548
|
+
if (FTextProperty* TextProp = CastField<FTextProperty>(Prop))
|
|
549
|
+
{
|
|
550
|
+
TextProp->SetPropertyValue(ValuePtr, FText::FromString(StrVal));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else if (ValueType == TEXT("actor"))
|
|
554
|
+
{
|
|
555
|
+
// value is the label of another actor in the world.
|
|
556
|
+
FString RefLabel;
|
|
557
|
+
Payload->TryGetStringField(TEXT("value"), RefLabel);
|
|
558
|
+
AActor* RefActor = nullptr;
|
|
559
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
560
|
+
{
|
|
561
|
+
if ((*It)->GetActorLabel() == RefLabel)
|
|
562
|
+
{
|
|
563
|
+
RefActor = *It;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
FObjectProperty* ObjProp = CastField<FObjectProperty>(Prop);
|
|
568
|
+
if (ObjProp)
|
|
569
|
+
{
|
|
570
|
+
ObjProp->SetObjectPropertyValue(ValuePtr, RefActor);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else if (ValueType == TEXT("asset"))
|
|
574
|
+
{
|
|
575
|
+
// value is an asset path, e.g. /Game/Data/DA_ValveHandle
|
|
576
|
+
FString AssetPath;
|
|
577
|
+
Payload->TryGetStringField(TEXT("value"), AssetPath);
|
|
578
|
+
UObject* Asset = LoadObject<UObject>(nullptr, *AssetPath);
|
|
579
|
+
if (!Asset)
|
|
580
|
+
{
|
|
581
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("asset_not_found")) + TEXT("\n"));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
FObjectProperty* ObjProp = CastField<FObjectProperty>(Prop);
|
|
585
|
+
if (ObjProp)
|
|
586
|
+
{
|
|
587
|
+
ObjProp->SetObjectPropertyValue(ValuePtr, Asset);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
else
|
|
591
|
+
{
|
|
592
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("unsupported_value_type")) + TEXT("\n"));
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Mark level dirty.
|
|
597
|
+
if (TargetActor->GetLevel())
|
|
598
|
+
{
|
|
599
|
+
TargetActor->GetLevel()->MarkPackageDirty();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
603
|
+
Data->SetStringField(TEXT("label"), ActorLabel);
|
|
604
|
+
Data->SetStringField(TEXT("property"), PropertyName);
|
|
605
|
+
Data->SetStringField(TEXT("value_type"), ValueType);
|
|
606
|
+
Data->SetBoolField(TEXT("applied"), true);
|
|
607
|
+
|
|
608
|
+
SendResponse(BuildActorSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
609
|
+
});
|
|
428
610
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// MCPActorCommands.h
|
|
2
2
|
// Declares the registration function for all actor-related MCP command handlers.
|
|
3
|
-
// Handlers: actor.list, actor.spawn, actor.transform, actor.delete
|
|
3
|
+
// Handlers: actor.list, actor.spawn, actor.transform, actor.delete, actor.setProperty
|
|
4
4
|
//
|
|
5
5
|
// Call RegisterActorCommands(*Router) in MCPBridgeSubsystem::Initialize()
|
|
6
6
|
// BEFORE the TCP server starts accepting connections.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
#include "MCPCommandRouter.h"
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Register all
|
|
13
|
+
* Register all actor command handlers into the given router.
|
|
14
14
|
* Must be called on the game thread before connections arrive.
|
|
15
15
|
*/
|
|
16
16
|
void RegisterActorCommands(FMCPCommandRouter& Router);
|
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
#include "Serialization/JsonWriter.h"
|
|
24
24
|
#include "Dom/JsonObject.h"
|
|
25
25
|
#include "Dom/JsonValue.h"
|
|
26
|
+
#include "AssetToolsModule.h"
|
|
27
|
+
#include "IAssetTools.h"
|
|
28
|
+
#include "Curves/CurveFloat.h"
|
|
29
|
+
#include "Curves/RichCurve.h"
|
|
30
|
+
#include "UObject/SavePackage.h"
|
|
31
|
+
#include "UObject/Package.h"
|
|
26
32
|
|
|
27
33
|
// ---------------------------------------------------------------------------
|
|
28
34
|
// File-scope JSON response helpers (identical pattern to MCPCommandRouter.cpp)
|
|
@@ -287,4 +293,245 @@ void RegisterAssetCommands(FMCPCommandRouter& Router)
|
|
|
287
293
|
|
|
288
294
|
SendResponse(BuildSuccessResponse(CorrId, Data));
|
|
289
295
|
});
|
|
296
|
+
|
|
297
|
+
// -----------------------------------------------------------------------
|
|
298
|
+
// asset.createDataAsset
|
|
299
|
+
// Creates a UPrimaryDataAsset (or subclass) in /Game/.
|
|
300
|
+
// Payload: { asset_path, class_name, properties: { name: {value, type}, ... } }
|
|
301
|
+
// Properties are set via UE reflection after creation.
|
|
302
|
+
// -----------------------------------------------------------------------
|
|
303
|
+
Router.RegisterHandler(TEXT("asset.createDataAsset"), [](TSharedPtr<FJsonObject> Command, FMCPResponseSender SendResponse)
|
|
304
|
+
{
|
|
305
|
+
const FString CorrId = Command.IsValid() ? Command->GetStringField(TEXT("correlationId")) : TEXT("");
|
|
306
|
+
|
|
307
|
+
TSharedPtr<FJsonObject> Payload;
|
|
308
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Command->Values.Find(TEXT("payload"));
|
|
309
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
310
|
+
{
|
|
311
|
+
Payload = (*PayloadVal)->AsObject();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
FString AssetPath, ClassName;
|
|
315
|
+
if (!Payload.IsValid()
|
|
316
|
+
|| !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty()
|
|
317
|
+
|| !Payload->TryGetStringField(TEXT("class_name"), ClassName) || ClassName.IsEmpty())
|
|
318
|
+
{
|
|
319
|
+
SendResponse(BuildErrorResponse(CorrId, TEXT("missing_asset_path_or_class")));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Security: only allow /Game/ paths.
|
|
324
|
+
if (!AssetPath.StartsWith(TEXT("/Game/")))
|
|
325
|
+
{
|
|
326
|
+
SendResponse(BuildErrorResponse(CorrId, TEXT("path_must_start_with_/Game/")));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Resolve class by name.
|
|
331
|
+
UClass* AssetClass = FindFirstObject<UClass>(*ClassName);
|
|
332
|
+
if (!AssetClass)
|
|
333
|
+
{
|
|
334
|
+
AssetClass = FindFirstObject<UClass>(*(TEXT("U") + ClassName));
|
|
335
|
+
}
|
|
336
|
+
if (!AssetClass)
|
|
337
|
+
{
|
|
338
|
+
SendResponse(BuildErrorResponse(CorrId, TEXT("class_not_found")));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Split AssetPath into package path and asset name.
|
|
343
|
+
FString PackagePath, AssetName;
|
|
344
|
+
AssetPath.Split(TEXT("/"), &PackagePath, &AssetName, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
|
|
345
|
+
if (PackagePath.IsEmpty() || AssetName.IsEmpty())
|
|
346
|
+
{
|
|
347
|
+
SendResponse(BuildErrorResponse(CorrId, TEXT("invalid_asset_path")));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Create package and the object inside it.
|
|
352
|
+
UPackage* Package = CreatePackage(*AssetPath);
|
|
353
|
+
if (!Package)
|
|
354
|
+
{
|
|
355
|
+
SendResponse(BuildErrorResponse(CorrId, TEXT("package_creation_failed")));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
UObject* NewAsset = NewObject<UObject>(Package, AssetClass, FName(*AssetName), RF_Public | RF_Standalone);
|
|
360
|
+
if (!NewAsset)
|
|
361
|
+
{
|
|
362
|
+
SendResponse(BuildErrorResponse(CorrId, TEXT("object_creation_failed")));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Apply properties via reflection.
|
|
367
|
+
const TSharedPtr<FJsonObject>* PropsObj;
|
|
368
|
+
if (Payload->TryGetObjectField(TEXT("properties"), PropsObj))
|
|
369
|
+
{
|
|
370
|
+
for (const auto& Pair : (*PropsObj)->Values)
|
|
371
|
+
{
|
|
372
|
+
FProperty* Prop = AssetClass->FindPropertyByName(FName(*Pair.Key));
|
|
373
|
+
if (!Prop) continue;
|
|
374
|
+
|
|
375
|
+
void* ValPtr = Prop->ContainerPtrToValuePtr<void>(NewAsset);
|
|
376
|
+
const TSharedPtr<FJsonObject>* PropDef;
|
|
377
|
+
if (!Pair.Value->TryGetObject(PropDef)) continue;
|
|
378
|
+
|
|
379
|
+
FString PropType;
|
|
380
|
+
(*PropDef)->TryGetStringField(TEXT("type"), PropType);
|
|
381
|
+
|
|
382
|
+
if (PropType == TEXT("name"))
|
|
383
|
+
{
|
|
384
|
+
FString Val; (*PropDef)->TryGetStringField(TEXT("value"), Val);
|
|
385
|
+
if (FNameProperty* NP = CastField<FNameProperty>(Prop)) NP->SetPropertyValue(ValPtr, FName(*Val));
|
|
386
|
+
}
|
|
387
|
+
else if (PropType == TEXT("text"))
|
|
388
|
+
{
|
|
389
|
+
FString Val; (*PropDef)->TryGetStringField(TEXT("value"), Val);
|
|
390
|
+
if (FTextProperty* TP = CastField<FTextProperty>(Prop)) TP->SetPropertyValue(ValPtr, FText::FromString(Val));
|
|
391
|
+
}
|
|
392
|
+
else if (PropType == TEXT("int"))
|
|
393
|
+
{
|
|
394
|
+
double Dbl = 0; (*PropDef)->TryGetNumberField(TEXT("value"), Dbl);
|
|
395
|
+
if (FIntProperty* IP = CastField<FIntProperty>(Prop)) IP->SetPropertyValue(ValPtr, static_cast<int32>(Dbl));
|
|
396
|
+
}
|
|
397
|
+
else if (PropType == TEXT("byte"))
|
|
398
|
+
{
|
|
399
|
+
// For UENUM(uint8) properties — set by numeric value.
|
|
400
|
+
double Dbl = 0; (*PropDef)->TryGetNumberField(TEXT("value"), Dbl);
|
|
401
|
+
if (FByteProperty* BP = CastField<FByteProperty>(Prop))
|
|
402
|
+
{
|
|
403
|
+
BP->SetPropertyValue(ValPtr, static_cast<uint8>(Dbl));
|
|
404
|
+
}
|
|
405
|
+
else if (FEnumProperty* EP = CastField<FEnumProperty>(Prop))
|
|
406
|
+
{
|
|
407
|
+
FNumericProperty* UnderlyingProp = EP->GetUnderlyingProperty();
|
|
408
|
+
if (UnderlyingProp)
|
|
409
|
+
{
|
|
410
|
+
UnderlyingProp->SetIntPropertyValue(ValPtr, static_cast<int64>(Dbl));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else if (PropType == TEXT("string"))
|
|
415
|
+
{
|
|
416
|
+
FString Val; (*PropDef)->TryGetStringField(TEXT("value"), Val);
|
|
417
|
+
if (FStrProperty* SP = CastField<FStrProperty>(Prop)) SP->SetPropertyValue(ValPtr, Val);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Notify asset registry and save.
|
|
423
|
+
FAssetRegistryModule::AssetCreated(NewAsset);
|
|
424
|
+
NewAsset->MarkPackageDirty();
|
|
425
|
+
Package->SetDirtyFlag(true);
|
|
426
|
+
|
|
427
|
+
// Auto-save so the asset persists.
|
|
428
|
+
FString FilePath = FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension());
|
|
429
|
+
FSavePackageArgs SaveArgs;
|
|
430
|
+
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
|
431
|
+
UPackage::SavePackage(Package, NewAsset, *FilePath, SaveArgs);
|
|
432
|
+
|
|
433
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
434
|
+
Data->SetStringField(TEXT("path"), AssetPath);
|
|
435
|
+
Data->SetStringField(TEXT("class"), ClassName);
|
|
436
|
+
Data->SetBoolField(TEXT("created"), true);
|
|
437
|
+
|
|
438
|
+
SendResponse(BuildSuccessResponse(CorrId, Data));
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// -----------------------------------------------------------------------
|
|
442
|
+
// asset.createCurve
|
|
443
|
+
// Creates a UCurveFloat asset with keyframes.
|
|
444
|
+
// Payload: { asset_path, keys: [ {time, value, interp?}, ... ] }
|
|
445
|
+
// interp: "linear"|"cubic"|"constant" (default: "cubic" for ease-in-out)
|
|
446
|
+
// -----------------------------------------------------------------------
|
|
447
|
+
Router.RegisterHandler(TEXT("asset.createCurve"), [](TSharedPtr<FJsonObject> Command, FMCPResponseSender SendResponse)
|
|
448
|
+
{
|
|
449
|
+
const FString CorrId = Command.IsValid() ? Command->GetStringField(TEXT("correlationId")) : TEXT("");
|
|
450
|
+
|
|
451
|
+
TSharedPtr<FJsonObject> Payload;
|
|
452
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Command->Values.Find(TEXT("payload"));
|
|
453
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
454
|
+
{
|
|
455
|
+
Payload = (*PayloadVal)->AsObject();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
FString AssetPath;
|
|
459
|
+
if (!Payload.IsValid() || !Payload->TryGetStringField(TEXT("asset_path"), AssetPath) || AssetPath.IsEmpty())
|
|
460
|
+
{
|
|
461
|
+
SendResponse(BuildErrorResponse(CorrId, TEXT("missing_asset_path")));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (!AssetPath.StartsWith(TEXT("/Game/")))
|
|
466
|
+
{
|
|
467
|
+
SendResponse(BuildErrorResponse(CorrId, TEXT("path_must_start_with_/Game/")));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Split path.
|
|
472
|
+
FString PackagePath, AssetName;
|
|
473
|
+
AssetPath.Split(TEXT("/"), &PackagePath, &AssetName, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
|
|
474
|
+
|
|
475
|
+
UPackage* Package = CreatePackage(*AssetPath);
|
|
476
|
+
if (!Package)
|
|
477
|
+
{
|
|
478
|
+
SendResponse(BuildErrorResponse(CorrId, TEXT("package_creation_failed")));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
UCurveFloat* Curve = NewObject<UCurveFloat>(Package, FName(*AssetName), RF_Public | RF_Standalone);
|
|
483
|
+
if (!Curve)
|
|
484
|
+
{
|
|
485
|
+
SendResponse(BuildErrorResponse(CorrId, TEXT("curve_creation_failed")));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Add keyframes from the "keys" array.
|
|
490
|
+
const TArray<TSharedPtr<FJsonValue>>* KeysArray;
|
|
491
|
+
if (Payload->TryGetArrayField(TEXT("keys"), KeysArray))
|
|
492
|
+
{
|
|
493
|
+
FRichCurve& RichCurve = Curve->FloatCurve;
|
|
494
|
+
for (const auto& KeyVal : *KeysArray)
|
|
495
|
+
{
|
|
496
|
+
const TSharedPtr<FJsonObject>* KeyObj;
|
|
497
|
+
if (!KeyVal->TryGetObject(KeyObj)) continue;
|
|
498
|
+
|
|
499
|
+
double Time = 0.0, Value = 0.0;
|
|
500
|
+
(*KeyObj)->TryGetNumberField(TEXT("time"), Time);
|
|
501
|
+
(*KeyObj)->TryGetNumberField(TEXT("value"), Value);
|
|
502
|
+
|
|
503
|
+
FString Interp = TEXT("cubic");
|
|
504
|
+
(*KeyObj)->TryGetStringField(TEXT("interp"), Interp);
|
|
505
|
+
|
|
506
|
+
ERichCurveInterpMode InterpMode = RCIM_Cubic;
|
|
507
|
+
if (Interp == TEXT("linear")) InterpMode = RCIM_Linear;
|
|
508
|
+
else if (Interp == TEXT("constant")) InterpMode = RCIM_Constant;
|
|
509
|
+
|
|
510
|
+
FKeyHandle Handle = RichCurve.AddKey(static_cast<float>(Time), static_cast<float>(Value));
|
|
511
|
+
RichCurve.SetKeyInterpMode(Handle, InterpMode);
|
|
512
|
+
|
|
513
|
+
if (InterpMode == RCIM_Cubic)
|
|
514
|
+
{
|
|
515
|
+
RichCurve.SetKeyTangentMode(Handle, RCTM_Auto);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
RichCurve.AutoSetTangents();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
FAssetRegistryModule::AssetCreated(Curve);
|
|
522
|
+
Curve->MarkPackageDirty();
|
|
523
|
+
Package->SetDirtyFlag(true);
|
|
524
|
+
|
|
525
|
+
FString FilePath = FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension());
|
|
526
|
+
FSavePackageArgs SaveArgs;
|
|
527
|
+
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
|
|
528
|
+
UPackage::SavePackage(Package, Curve, *FilePath, SaveArgs);
|
|
529
|
+
|
|
530
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
531
|
+
Data->SetStringField(TEXT("path"), AssetPath);
|
|
532
|
+
Data->SetNumberField(TEXT("key_count"), KeysArray ? KeysArray->Num() : 0);
|
|
533
|
+
Data->SetBoolField(TEXT("created"), true);
|
|
534
|
+
|
|
535
|
+
SendResponse(BuildSuccessResponse(CorrId, Data));
|
|
536
|
+
});
|
|
290
537
|
}
|