ultimate-unreal-engine-mcp 0.1.8 → 0.1.10
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 +82 -1
- package/package.json +1 -1
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +196 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +2 -2
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +247 -0
|
@@ -93,15 +93,96 @@ export function registerEditorTools(server, bridge) {
|
|
|
93
93
|
y: z.number().describe('Y coordinate in Unreal units (cm)'),
|
|
94
94
|
z: z.number().describe('Z coordinate in Unreal units (cm)'),
|
|
95
95
|
}).describe('World-space location to spawn the actor at'),
|
|
96
|
+
label: z.string().optional().describe('Optional editor display label for the spawned actor'),
|
|
96
97
|
}),
|
|
97
98
|
annotations: {
|
|
98
99
|
readOnlyHint: false,
|
|
99
100
|
destructiveHint: false,
|
|
100
101
|
},
|
|
101
102
|
}, withKnownIssues('ue_spawn_actor', async (args) => {
|
|
103
|
+
const payload = { class_name: args.class_name, location: args.location };
|
|
104
|
+
if (args.label) {
|
|
105
|
+
payload['label'] = args.label;
|
|
106
|
+
}
|
|
102
107
|
return sendOrDisconnect(_bridge, {
|
|
103
108
|
type: 'actor.spawn',
|
|
104
|
-
payload
|
|
109
|
+
payload,
|
|
110
|
+
});
|
|
111
|
+
}));
|
|
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 },
|
|
105
186
|
});
|
|
106
187
|
}));
|
|
107
188
|
// --------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -201,6 +201,13 @@ 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 — sets the editor display name after spawn.
|
|
205
|
+
FString Label;
|
|
206
|
+
if (Payload->TryGetStringField(TEXT("label"), Label) && !Label.IsEmpty())
|
|
207
|
+
{
|
|
208
|
+
Params.Name = FName(*Label);
|
|
209
|
+
}
|
|
210
|
+
|
|
204
211
|
AActor* Spawned = World->SpawnActor<AActor>(ActorClass, Location, Rotation, Params);
|
|
205
212
|
if (!Spawned)
|
|
206
213
|
{
|
|
@@ -208,6 +215,12 @@ void RegisterActorCommands(FMCPCommandRouter& Router)
|
|
|
208
215
|
return;
|
|
209
216
|
}
|
|
210
217
|
|
|
218
|
+
// Apply explicit label if provided (overrides auto-generated label).
|
|
219
|
+
if (!Label.IsEmpty())
|
|
220
|
+
{
|
|
221
|
+
Spawned->SetActorLabel(Label);
|
|
222
|
+
}
|
|
223
|
+
|
|
211
224
|
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
212
225
|
Data->SetStringField(TEXT("label"), Spawned->GetActorLabel());
|
|
213
226
|
Data->SetStringField(TEXT("id"), Spawned->GetName());
|
|
@@ -412,4 +425,187 @@ void RegisterActorCommands(FMCPCommandRouter& Router)
|
|
|
412
425
|
|
|
413
426
|
SendResponse(BuildActorSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
414
427
|
});
|
|
428
|
+
|
|
429
|
+
// -----------------------------------------------------------------------
|
|
430
|
+
// actor.setProperty
|
|
431
|
+
// Sets a UPROPERTY on an actor by label. Supports:
|
|
432
|
+
// - Primitive types: bool, int, float, FString, FName, FText
|
|
433
|
+
// - Actor references: resolved by actor label in the current world
|
|
434
|
+
// - Asset references: resolved by UE asset path (e.g. /Game/Data/DA_Key)
|
|
435
|
+
// Payload: { actor_label, property_name, value, value_type }
|
|
436
|
+
// value_type: "bool"|"int"|"float"|"string"|"name"|"text"|"actor"|"asset"
|
|
437
|
+
// -----------------------------------------------------------------------
|
|
438
|
+
Router.RegisterHandler(TEXT("actor.setProperty"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
|
|
439
|
+
{
|
|
440
|
+
const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
|
|
441
|
+
|
|
442
|
+
TSharedPtr<FJsonObject> Payload;
|
|
443
|
+
const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
|
|
444
|
+
if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
|
|
445
|
+
{
|
|
446
|
+
Payload = (*PayloadVal)->AsObject();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
FString ActorLabel, PropertyName, ValueType;
|
|
450
|
+
if (!Payload.IsValid()
|
|
451
|
+
|| !Payload->TryGetStringField(TEXT("actor_label"), ActorLabel) || ActorLabel.IsEmpty()
|
|
452
|
+
|| !Payload->TryGetStringField(TEXT("property_name"), PropertyName) || PropertyName.IsEmpty()
|
|
453
|
+
|| !Payload->TryGetStringField(TEXT("value_type"), ValueType) || ValueType.IsEmpty())
|
|
454
|
+
{
|
|
455
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("missing_required_fields")) + TEXT("\n"));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
|
|
460
|
+
if (!World)
|
|
461
|
+
{
|
|
462
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Find target actor by label.
|
|
467
|
+
AActor* TargetActor = nullptr;
|
|
468
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
469
|
+
{
|
|
470
|
+
if ((*It)->GetActorLabel() == ActorLabel)
|
|
471
|
+
{
|
|
472
|
+
TargetActor = *It;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (!TargetActor)
|
|
477
|
+
{
|
|
478
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Find the UPROPERTY by name on the actor's class.
|
|
483
|
+
FProperty* Prop = TargetActor->GetClass()->FindPropertyByName(FName(*PropertyName));
|
|
484
|
+
if (!Prop)
|
|
485
|
+
{
|
|
486
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("property_not_found")) + TEXT("\n"));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
TargetActor->Modify();
|
|
491
|
+
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(TargetActor);
|
|
492
|
+
|
|
493
|
+
if (ValueType == TEXT("bool"))
|
|
494
|
+
{
|
|
495
|
+
bool bVal = false;
|
|
496
|
+
Payload->TryGetBoolField(TEXT("value"), bVal);
|
|
497
|
+
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
|
|
498
|
+
{
|
|
499
|
+
BoolProp->SetPropertyValue(ValuePtr, bVal);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
else if (ValueType == TEXT("int"))
|
|
503
|
+
{
|
|
504
|
+
int32 IntVal = 0;
|
|
505
|
+
Payload->TryGetNumberField(TEXT("value"), *(double*)&IntVal);
|
|
506
|
+
double Dbl = 0;
|
|
507
|
+
Payload->TryGetNumberField(TEXT("value"), Dbl);
|
|
508
|
+
IntVal = static_cast<int32>(Dbl);
|
|
509
|
+
if (FIntProperty* IntProp = CastField<FIntProperty>(Prop))
|
|
510
|
+
{
|
|
511
|
+
IntProp->SetPropertyValue(ValuePtr, IntVal);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
else if (ValueType == TEXT("float"))
|
|
515
|
+
{
|
|
516
|
+
double Dbl = 0.0;
|
|
517
|
+
Payload->TryGetNumberField(TEXT("value"), Dbl);
|
|
518
|
+
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop))
|
|
519
|
+
{
|
|
520
|
+
FloatProp->SetPropertyValue(ValuePtr, static_cast<float>(Dbl));
|
|
521
|
+
}
|
|
522
|
+
else if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop))
|
|
523
|
+
{
|
|
524
|
+
DoubleProp->SetPropertyValue(ValuePtr, Dbl);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
else if (ValueType == TEXT("string"))
|
|
528
|
+
{
|
|
529
|
+
FString StrVal;
|
|
530
|
+
Payload->TryGetStringField(TEXT("value"), StrVal);
|
|
531
|
+
if (FStrProperty* StrProp = CastField<FStrProperty>(Prop))
|
|
532
|
+
{
|
|
533
|
+
StrProp->SetPropertyValue(ValuePtr, StrVal);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else if (ValueType == TEXT("name"))
|
|
537
|
+
{
|
|
538
|
+
FString StrVal;
|
|
539
|
+
Payload->TryGetStringField(TEXT("value"), StrVal);
|
|
540
|
+
if (FNameProperty* NameProp = CastField<FNameProperty>(Prop))
|
|
541
|
+
{
|
|
542
|
+
NameProp->SetPropertyValue(ValuePtr, FName(*StrVal));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else if (ValueType == TEXT("text"))
|
|
546
|
+
{
|
|
547
|
+
FString StrVal;
|
|
548
|
+
Payload->TryGetStringField(TEXT("value"), StrVal);
|
|
549
|
+
if (FTextProperty* TextProp = CastField<FTextProperty>(Prop))
|
|
550
|
+
{
|
|
551
|
+
TextProp->SetPropertyValue(ValuePtr, FText::FromString(StrVal));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else if (ValueType == TEXT("actor"))
|
|
555
|
+
{
|
|
556
|
+
// value is the label of another actor in the world.
|
|
557
|
+
FString RefLabel;
|
|
558
|
+
Payload->TryGetStringField(TEXT("value"), RefLabel);
|
|
559
|
+
AActor* RefActor = nullptr;
|
|
560
|
+
for (TActorIterator<AActor> It(World); It; ++It)
|
|
561
|
+
{
|
|
562
|
+
if ((*It)->GetActorLabel() == RefLabel)
|
|
563
|
+
{
|
|
564
|
+
RefActor = *It;
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
FObjectProperty* ObjProp = CastField<FObjectProperty>(Prop);
|
|
569
|
+
if (ObjProp)
|
|
570
|
+
{
|
|
571
|
+
ObjProp->SetObjectPropertyValue(ValuePtr, RefActor);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else if (ValueType == TEXT("asset"))
|
|
575
|
+
{
|
|
576
|
+
// value is an asset path, e.g. /Game/Data/DA_ValveHandle
|
|
577
|
+
FString AssetPath;
|
|
578
|
+
Payload->TryGetStringField(TEXT("value"), AssetPath);
|
|
579
|
+
UObject* Asset = LoadObject<UObject>(nullptr, *AssetPath);
|
|
580
|
+
if (!Asset)
|
|
581
|
+
{
|
|
582
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("asset_not_found")) + TEXT("\n"));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
FObjectProperty* ObjProp = CastField<FObjectProperty>(Prop);
|
|
586
|
+
if (ObjProp)
|
|
587
|
+
{
|
|
588
|
+
ObjProp->SetObjectPropertyValue(ValuePtr, Asset);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
else
|
|
592
|
+
{
|
|
593
|
+
SendResponse(BuildActorErrorResponse(CorrId, TEXT("unsupported_value_type")) + TEXT("\n"));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Mark level dirty.
|
|
598
|
+
if (TargetActor->GetLevel())
|
|
599
|
+
{
|
|
600
|
+
TargetActor->GetLevel()->MarkPackageDirty();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
604
|
+
Data->SetStringField(TEXT("label"), ActorLabel);
|
|
605
|
+
Data->SetStringField(TEXT("property"), PropertyName);
|
|
606
|
+
Data->SetStringField(TEXT("value_type"), ValueType);
|
|
607
|
+
Data->SetBoolField(TEXT("applied"), true);
|
|
608
|
+
|
|
609
|
+
SendResponse(BuildActorSuccessResponse(CorrId, Data) + TEXT("\n"));
|
|
610
|
+
});
|
|
415
611
|
}
|
|
@@ -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
|
}
|