@createlex/createlexgenai 1.0.4 → 1.0.6
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 +7 -7
- package/package.json +1 -1
- package/src/core/remote-execution.js +202 -106
- package/src/core/unreal-connection.js +130 -30
- package/src/core/web-remote-control.js +10 -4
package/README.md
CHANGED
|
@@ -63,22 +63,22 @@ You need **at least one** of these enabled in your Unreal project. Both are buil
|
|
|
63
63
|
|
|
64
64
|
1. Open your UE project
|
|
65
65
|
2. Go to **Edit > Plugins**
|
|
66
|
-
3.
|
|
66
|
+
3. Enable **"Remote Control API"** and **"Remote Control Web Interface"**
|
|
67
67
|
4. Restart the editor
|
|
68
68
|
5. Go to **Edit > Project Settings > Plugins > Remote Control**
|
|
69
|
-
6.
|
|
70
|
-
7. Set
|
|
71
|
-
8.
|
|
69
|
+
6. Under **Remote Control Web Server**, check **"Auto Start Web Server"**
|
|
70
|
+
7. Set **"Remote Control HTTP Server Port"** to **30010** (default)
|
|
71
|
+
8. Under **Remote Control > Security**, check **"Enable Remote Python Execution"**
|
|
72
72
|
|
|
73
73
|
#### Option B: Python Remote Execution
|
|
74
74
|
|
|
75
75
|
1. Open your UE project
|
|
76
76
|
2. Go to **Edit > Plugins**
|
|
77
|
-
3.
|
|
77
|
+
3. Enable **"Python Editor Script Plugin"**
|
|
78
78
|
4. Restart the editor
|
|
79
79
|
5. Go to **Edit > Project Settings > Plugins > Python**
|
|
80
|
-
6.
|
|
81
|
-
7. Multicast
|
|
80
|
+
6. Under **Python Remote Execution**, check **"Enable Remote Execution?"**
|
|
81
|
+
7. Multicast Group Endpoint: `239.0.0.1:6766`, Multicast Bind Address: `127.0.0.1` (defaults)
|
|
82
82
|
|
|
83
83
|
> **Tip:** Enable both for maximum reliability. If one backend is unavailable, the CLI automatically falls back to the other.
|
|
84
84
|
|
package/package.json
CHANGED
|
@@ -4,21 +4,58 @@ const dgram = require('dgram');
|
|
|
4
4
|
const net = require('net');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
|
|
7
|
-
// UE Python Remote Execution protocol constants
|
|
7
|
+
// UE Python Remote Execution protocol constants (from remote_execution.py)
|
|
8
8
|
const PROTOCOL_MAGIC = 'ue_py';
|
|
9
9
|
const PROTOCOL_VERSION = 1;
|
|
10
10
|
|
|
11
11
|
const MULTICAST_GROUP = '239.0.0.1';
|
|
12
12
|
const MULTICAST_PORT = 6766;
|
|
13
|
-
const
|
|
13
|
+
const MULTICAST_BIND_ADDRESS = '127.0.0.1';
|
|
14
|
+
const MULTICAST_TTL = 0;
|
|
15
|
+
|
|
16
|
+
const DEFAULT_COMMAND_ENDPOINT = ['127.0.0.1', 6776];
|
|
17
|
+
const DEFAULT_RECEIVE_BUFFER_SIZE = 8192;
|
|
14
18
|
|
|
15
19
|
const DISCOVERY_TIMEOUT = 3000;
|
|
16
|
-
const CONNECT_TIMEOUT = 10000;
|
|
17
20
|
const COMMAND_TIMEOUT = 30000;
|
|
18
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Build a protocol message matching UE's _RemoteExecutionMessage.to_json() format.
|
|
24
|
+
*/
|
|
25
|
+
function buildMessage(type, source, dest, data) {
|
|
26
|
+
const msg = {
|
|
27
|
+
version: PROTOCOL_VERSION,
|
|
28
|
+
magic: PROTOCOL_MAGIC,
|
|
29
|
+
type: type,
|
|
30
|
+
source: source
|
|
31
|
+
};
|
|
32
|
+
if (dest) {
|
|
33
|
+
msg.dest = dest;
|
|
34
|
+
}
|
|
35
|
+
if (data) {
|
|
36
|
+
msg.data = data;
|
|
37
|
+
}
|
|
38
|
+
return JSON.stringify(msg);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse and validate a received protocol message.
|
|
43
|
+
*/
|
|
44
|
+
function parseMessage(buf) {
|
|
45
|
+
try {
|
|
46
|
+
const msg = JSON.parse(buf.toString('utf-8'));
|
|
47
|
+
if (msg.version !== PROTOCOL_VERSION) return null;
|
|
48
|
+
if (msg.magic !== PROTOCOL_MAGIC) return null;
|
|
49
|
+
if (!msg.type || !msg.source) return null;
|
|
50
|
+
return msg;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
19
56
|
/**
|
|
20
57
|
* Discover running Unreal Engine instances via UDP multicast ping.
|
|
21
|
-
*
|
|
58
|
+
* Matches UE's _RemoteExecutionBroadcastConnection protocol exactly.
|
|
22
59
|
*/
|
|
23
60
|
function discover(timeout = DISCOVERY_TIMEOUT) {
|
|
24
61
|
return new Promise((resolve) => {
|
|
@@ -28,56 +65,62 @@ function discover(timeout = DISCOVERY_TIMEOUT) {
|
|
|
28
65
|
const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
29
66
|
|
|
30
67
|
const timer = setTimeout(() => {
|
|
31
|
-
socket.close();
|
|
68
|
+
try { socket.close(); } catch {}
|
|
32
69
|
resolve(nodes);
|
|
33
70
|
}, timeout);
|
|
34
71
|
|
|
35
72
|
socket.on('message', (msg) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// Ignore non-JSON or malformed messages
|
|
73
|
+
const message = parseMessage(msg);
|
|
74
|
+
if (!message) return;
|
|
75
|
+
|
|
76
|
+
// Filter: must not be from self, must be a pong
|
|
77
|
+
if (message.source === nodeId) return;
|
|
78
|
+
if (message.dest && message.dest !== nodeId) return;
|
|
79
|
+
if (message.type !== 'pong') return;
|
|
80
|
+
|
|
81
|
+
// Avoid duplicates
|
|
82
|
+
if (!nodes.find(n => n.node_id === message.source)) {
|
|
83
|
+
nodes.push({
|
|
84
|
+
node_id: message.source,
|
|
85
|
+
...(message.data || {})
|
|
86
|
+
});
|
|
51
87
|
}
|
|
52
88
|
});
|
|
53
89
|
|
|
54
90
|
socket.on('error', () => {
|
|
55
91
|
clearTimeout(timer);
|
|
56
|
-
socket.close();
|
|
92
|
+
try { socket.close(); } catch {}
|
|
57
93
|
resolve(nodes);
|
|
58
94
|
});
|
|
59
95
|
|
|
60
|
-
|
|
96
|
+
// Bind to the same port and address as UE expects
|
|
97
|
+
socket.bind(MULTICAST_PORT, MULTICAST_BIND_ADDRESS, () => {
|
|
61
98
|
try {
|
|
62
|
-
socket
|
|
99
|
+
// Match UE's socket options
|
|
100
|
+
socket.setMulticastLoopback(true);
|
|
101
|
+
socket.setMulticastTTL(MULTICAST_TTL);
|
|
102
|
+
socket.setMulticastInterface(MULTICAST_BIND_ADDRESS);
|
|
103
|
+
socket.addMembership(MULTICAST_GROUP, MULTICAST_BIND_ADDRESS);
|
|
63
104
|
} catch {
|
|
64
105
|
// Multicast may not be available
|
|
65
106
|
}
|
|
66
107
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
version: PROTOCOL_VERSION,
|
|
70
|
-
type: 'ping',
|
|
71
|
-
source: nodeId
|
|
72
|
-
});
|
|
73
|
-
|
|
108
|
+
// Send ping (matches UE's _broadcast_ping)
|
|
109
|
+
const ping = buildMessage('ping', nodeId);
|
|
74
110
|
socket.send(ping, 0, ping.length, MULTICAST_PORT, MULTICAST_GROUP, (err) => {
|
|
75
111
|
if (err) {
|
|
76
112
|
clearTimeout(timer);
|
|
77
|
-
socket.close();
|
|
113
|
+
try { socket.close(); } catch {}
|
|
78
114
|
resolve(nodes);
|
|
79
115
|
}
|
|
80
116
|
});
|
|
117
|
+
|
|
118
|
+
// Send a second ping after 1 second (UE pings every _NODE_PING_SECONDS = 1)
|
|
119
|
+
setTimeout(() => {
|
|
120
|
+
try {
|
|
121
|
+
socket.send(ping, 0, ping.length, MULTICAST_PORT, MULTICAST_GROUP);
|
|
122
|
+
} catch {}
|
|
123
|
+
}, 1000);
|
|
81
124
|
});
|
|
82
125
|
});
|
|
83
126
|
}
|
|
@@ -85,22 +128,18 @@ function discover(timeout = DISCOVERY_TIMEOUT) {
|
|
|
85
128
|
/**
|
|
86
129
|
* Execute a Python command on a discovered UE instance via the Remote Execution protocol.
|
|
87
130
|
*
|
|
88
|
-
*
|
|
89
|
-
* 1.
|
|
90
|
-
* 2.
|
|
91
|
-
* 3.
|
|
92
|
-
* 4.
|
|
93
|
-
*
|
|
94
|
-
* @param {string} command - Python code to execute
|
|
95
|
-
* @param {object} options
|
|
96
|
-
* @param {string} options.nodeId - Target node ID from discovery (optional, broadcasts if omitted)
|
|
97
|
-
* @param {string} options.execMode - 'ExecuteStatement', 'EvaluateStatement', or 'ExecuteFile'
|
|
98
|
-
* @param {number} options.timeout - Command timeout in ms
|
|
131
|
+
* Matches UE's _RemoteExecutionCommandConnection flow:
|
|
132
|
+
* 1. Start TCP listen server on command_endpoint
|
|
133
|
+
* 2. Send open_connection via UDP multicast targeting the node
|
|
134
|
+
* 3. Accept TCP connection from UE
|
|
135
|
+
* 4. Send command message over TCP
|
|
136
|
+
* 5. Receive command_result over TCP
|
|
99
137
|
*/
|
|
100
138
|
function executeCommand(command, options = {}) {
|
|
101
139
|
const {
|
|
102
140
|
nodeId = null,
|
|
103
|
-
execMode = '
|
|
141
|
+
execMode = 'ExecuteFile',
|
|
142
|
+
unattended = true,
|
|
104
143
|
timeout = COMMAND_TIMEOUT
|
|
105
144
|
} = options;
|
|
106
145
|
|
|
@@ -108,104 +147,162 @@ function executeCommand(command, options = {}) {
|
|
|
108
147
|
|
|
109
148
|
return new Promise((resolve, reject) => {
|
|
110
149
|
const timer = setTimeout(() => {
|
|
111
|
-
|
|
150
|
+
cleanup();
|
|
151
|
+
reject(new Error('Remote execution timed out. Ensure "Enable Remote Execution" is checked in UE Project Settings > Python.'));
|
|
112
152
|
}, timeout);
|
|
113
153
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
let responseBuffer = '';
|
|
154
|
+
let tcpServer = null;
|
|
155
|
+
let udpSocket = null;
|
|
117
156
|
|
|
157
|
+
function cleanup() {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
try { if (tcpServer) tcpServer.close(); } catch {}
|
|
160
|
+
try { if (udpSocket) udpSocket.close(); } catch {}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Step 1: Create TCP server to accept connection from UE
|
|
164
|
+
tcpServer = net.createServer((socket) => {
|
|
165
|
+
socket.setNoDelay(true);
|
|
166
|
+
let responseBuffer = Buffer.alloc(0);
|
|
167
|
+
|
|
168
|
+
// Step 4: Send command once UE connects
|
|
169
|
+
const commandMsg = buildMessage('command', clientId, nodeId || '', {
|
|
170
|
+
command: command,
|
|
171
|
+
unattended: unattended,
|
|
172
|
+
exec_mode: execMode
|
|
173
|
+
});
|
|
174
|
+
socket.write(commandMsg, 'utf-8');
|
|
175
|
+
|
|
176
|
+
// Step 5: Receive command_result
|
|
118
177
|
socket.on('data', (data) => {
|
|
119
|
-
responseBuffer
|
|
178
|
+
responseBuffer = Buffer.concat([responseBuffer, data]);
|
|
120
179
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
clearTimeout(timer);
|
|
125
|
-
tcpServer.close();
|
|
180
|
+
const message = parseMessage(responseBuffer);
|
|
181
|
+
if (message && message.type === 'command_result') {
|
|
182
|
+
cleanup();
|
|
126
183
|
socket.destroy();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
outputLog: result.output_log || []
|
|
134
|
-
});
|
|
135
|
-
} else {
|
|
136
|
-
resolve({ success: true, raw: result });
|
|
137
|
-
}
|
|
138
|
-
} catch {
|
|
139
|
-
// Keep buffering
|
|
184
|
+
resolve({
|
|
185
|
+
success: message.data ? message.data.success !== false : true,
|
|
186
|
+
output: message.data?.output || '',
|
|
187
|
+
result: message.data?.result || null,
|
|
188
|
+
outputLog: message.data?.output_log || []
|
|
189
|
+
});
|
|
140
190
|
}
|
|
141
191
|
});
|
|
142
192
|
|
|
143
193
|
socket.on('error', (err) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
reject(new Error(`TCP socket error: ${err.message}`));
|
|
194
|
+
cleanup();
|
|
195
|
+
reject(new Error(`TCP command socket error: ${err.message}`));
|
|
147
196
|
});
|
|
148
|
-
|
|
149
|
-
// Step 3: Once UE connects, send the command
|
|
150
|
-
const commandMsg = JSON.stringify({
|
|
151
|
-
magic: PROTOCOL_MAGIC,
|
|
152
|
-
version: PROTOCOL_VERSION,
|
|
153
|
-
type: 'command',
|
|
154
|
-
source: clientId,
|
|
155
|
-
command: command,
|
|
156
|
-
unattended: false,
|
|
157
|
-
exec_mode: execMode
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
socket.write(commandMsg);
|
|
161
197
|
});
|
|
162
198
|
|
|
163
199
|
tcpServer.on('error', (err) => {
|
|
164
|
-
|
|
200
|
+
cleanup();
|
|
165
201
|
reject(new Error(`TCP server error: ${err.message}`));
|
|
166
202
|
});
|
|
167
203
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
204
|
+
// Listen on the command endpoint
|
|
205
|
+
const commandPort = DEFAULT_COMMAND_ENDPOINT[1];
|
|
206
|
+
const commandHost = DEFAULT_COMMAND_ENDPOINT[0];
|
|
171
207
|
|
|
172
|
-
|
|
208
|
+
tcpServer.listen(commandPort, commandHost, () => {
|
|
209
|
+
// Step 2: Send open_connection via UDP multicast
|
|
210
|
+
udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
173
211
|
|
|
174
|
-
udpSocket.bind(0,
|
|
212
|
+
udpSocket.bind(0, MULTICAST_BIND_ADDRESS, () => {
|
|
175
213
|
try {
|
|
176
|
-
udpSocket.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
214
|
+
udpSocket.setMulticastLoopback(true);
|
|
215
|
+
udpSocket.setMulticastTTL(MULTICAST_TTL);
|
|
216
|
+
udpSocket.setMulticastInterface(MULTICAST_BIND_ADDRESS);
|
|
217
|
+
udpSocket.addMembership(MULTICAST_GROUP, MULTICAST_BIND_ADDRESS);
|
|
218
|
+
} catch {}
|
|
180
219
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
source: clientId,
|
|
186
|
-
dest: nodeId || '',
|
|
187
|
-
command_ip: '127.0.0.1',
|
|
188
|
-
command_port: tcpPort
|
|
220
|
+
// Send open_connection (UE retries 6 times with 5s waits)
|
|
221
|
+
const openMsg = buildMessage('open_connection', clientId, nodeId || '', {
|
|
222
|
+
command_ip: commandHost,
|
|
223
|
+
command_port: commandPort
|
|
189
224
|
});
|
|
190
225
|
|
|
191
226
|
udpSocket.send(openMsg, 0, openMsg.length, MULTICAST_PORT, MULTICAST_GROUP, () => {
|
|
192
|
-
udpSocket.close();
|
|
227
|
+
try { udpSocket.close(); } catch {}
|
|
228
|
+
udpSocket = null;
|
|
193
229
|
});
|
|
194
230
|
});
|
|
195
231
|
});
|
|
232
|
+
|
|
233
|
+
// If command port is in use, try a random port
|
|
234
|
+
tcpServer.on('error', (err) => {
|
|
235
|
+
if (err.code === 'EADDRINUSE') {
|
|
236
|
+
tcpServer = net.createServer((socket) => {
|
|
237
|
+
socket.setNoDelay(true);
|
|
238
|
+
let responseBuffer = Buffer.alloc(0);
|
|
239
|
+
|
|
240
|
+
const commandMsg = buildMessage('command', clientId, nodeId || '', {
|
|
241
|
+
command: command,
|
|
242
|
+
unattended: unattended,
|
|
243
|
+
exec_mode: execMode
|
|
244
|
+
});
|
|
245
|
+
socket.write(commandMsg, 'utf-8');
|
|
246
|
+
|
|
247
|
+
socket.on('data', (data) => {
|
|
248
|
+
responseBuffer = Buffer.concat([responseBuffer, data]);
|
|
249
|
+
const message = parseMessage(responseBuffer);
|
|
250
|
+
if (message && message.type === 'command_result') {
|
|
251
|
+
cleanup();
|
|
252
|
+
socket.destroy();
|
|
253
|
+
resolve({
|
|
254
|
+
success: message.data ? message.data.success !== false : true,
|
|
255
|
+
output: message.data?.output || '',
|
|
256
|
+
result: message.data?.result || null,
|
|
257
|
+
outputLog: message.data?.output_log || []
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
socket.on('error', (err2) => {
|
|
263
|
+
cleanup();
|
|
264
|
+
reject(new Error(`TCP command socket error: ${err2.message}`));
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
tcpServer.listen(0, commandHost, () => {
|
|
269
|
+
const actualPort = tcpServer.address().port;
|
|
270
|
+
udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
271
|
+
|
|
272
|
+
udpSocket.bind(0, MULTICAST_BIND_ADDRESS, () => {
|
|
273
|
+
try {
|
|
274
|
+
udpSocket.setMulticastLoopback(true);
|
|
275
|
+
udpSocket.setMulticastTTL(MULTICAST_TTL);
|
|
276
|
+
udpSocket.setMulticastInterface(MULTICAST_BIND_ADDRESS);
|
|
277
|
+
udpSocket.addMembership(MULTICAST_GROUP, MULTICAST_BIND_ADDRESS);
|
|
278
|
+
} catch {}
|
|
279
|
+
|
|
280
|
+
const openMsg = buildMessage('open_connection', clientId, nodeId || '', {
|
|
281
|
+
command_ip: commandHost,
|
|
282
|
+
command_port: actualPort
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
udpSocket.send(openMsg, 0, openMsg.length, MULTICAST_PORT, MULTICAST_GROUP, () => {
|
|
286
|
+
try { udpSocket.close(); } catch {}
|
|
287
|
+
udpSocket = null;
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
});
|
|
196
293
|
});
|
|
197
294
|
}
|
|
198
295
|
|
|
199
296
|
/**
|
|
200
|
-
* Quick test: discover
|
|
297
|
+
* Quick test: discover remote nodes.
|
|
201
298
|
*/
|
|
202
299
|
async function testConnection() {
|
|
203
300
|
try {
|
|
204
|
-
const nodes = await discover(
|
|
301
|
+
const nodes = await discover(3000);
|
|
205
302
|
if (nodes.length === 0) {
|
|
206
303
|
return {
|
|
207
304
|
available: false,
|
|
208
|
-
error: 'No UE instances found via Remote Execution. Enable "Python Editor Script Plugin" and "
|
|
305
|
+
error: 'No UE instances found via Remote Execution. Enable "Python Editor Script Plugin" and check "Enable Remote Execution" in Project Settings > Python.'
|
|
209
306
|
};
|
|
210
307
|
}
|
|
211
308
|
return {
|
|
@@ -223,6 +320,5 @@ module.exports = {
|
|
|
223
320
|
executeCommand,
|
|
224
321
|
testConnection,
|
|
225
322
|
MULTICAST_GROUP,
|
|
226
|
-
MULTICAST_PORT
|
|
227
|
-
COMMAND_PORT
|
|
323
|
+
MULTICAST_PORT
|
|
228
324
|
};
|
|
@@ -171,14 +171,18 @@ async function executeWithBackend(toolName, params = {}, options = {}) {
|
|
|
171
171
|
|
|
172
172
|
if (backend === BACKEND.WEB_REMOTE) {
|
|
173
173
|
// Web Remote Control — translate tool calls to HTTP API calls
|
|
174
|
+
// Don't pass plugin port to web remote; let it use its own default (30010)
|
|
174
175
|
const webRemote = require('./web-remote-control');
|
|
175
|
-
|
|
176
|
+
const webOpts = { ...options, port: options.webRemotePort || undefined };
|
|
177
|
+
return executeViaWebRemote(webRemote, toolName, params, webOpts);
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
if (backend === BACKEND.REMOTE_EXEC) {
|
|
179
181
|
// Python Remote Execution — generate Python code and execute
|
|
182
|
+
// Don't pass plugin port to remote exec; it uses UDP multicast
|
|
180
183
|
const remoteExec = require('./remote-execution');
|
|
181
|
-
|
|
184
|
+
const execOpts = { ...options, port: undefined };
|
|
185
|
+
return executeViaRemoteExec(remoteExec, toolName, params, execOpts);
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
// Auto-detect
|
|
@@ -196,9 +200,6 @@ async function executeWithBackend(toolName, params = {}, options = {}) {
|
|
|
196
200
|
async function executeViaWebRemote(webRemote, toolName, params, options) {
|
|
197
201
|
// Map common tool names to Web Remote Control API calls
|
|
198
202
|
switch (toolName) {
|
|
199
|
-
case 'get_all_scene_objects':
|
|
200
|
-
return webRemote.getAllLevelActors(options);
|
|
201
|
-
|
|
202
203
|
case 'execute_python_script':
|
|
203
204
|
return webRemote.executePythonScript(params.script, options);
|
|
204
205
|
|
|
@@ -206,12 +207,10 @@ async function executeViaWebRemote(webRemote, toolName, params, options) {
|
|
|
206
207
|
return webRemote.executeConsoleCommand(params.command, options);
|
|
207
208
|
|
|
208
209
|
default: {
|
|
209
|
-
// For tools
|
|
210
|
+
// For all tools (including get_all_scene_objects, spawn_object, etc.),
|
|
211
|
+
// generate standalone UE Python and execute via Python subsystem
|
|
210
212
|
const script = buildPythonForTool(toolName, params);
|
|
211
|
-
|
|
212
|
-
return webRemote.executePythonScript(script, options);
|
|
213
|
-
}
|
|
214
|
-
throw new Error(`Tool '${toolName}' is not supported via Web Remote Control. Use the CreatelexGenAI plugin for full tool access.`);
|
|
213
|
+
return webRemote.executePythonScript(script, options);
|
|
215
214
|
}
|
|
216
215
|
}
|
|
217
216
|
}
|
|
@@ -238,7 +237,7 @@ async function executeViaRemoteExec(remoteExec, toolName, params, options) {
|
|
|
238
237
|
case 'get_all_scene_objects': {
|
|
239
238
|
const pyCode = `
|
|
240
239
|
import unreal, json
|
|
241
|
-
actors = unreal.
|
|
240
|
+
actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).get_all_level_actors()
|
|
242
241
|
result = [{"name": a.get_actor_label(), "class": a.get_class().get_name(), "location": [a.get_actor_location().x, a.get_actor_location().y, a.get_actor_location().z]} for a in actors]
|
|
243
242
|
print(json.dumps(result))
|
|
244
243
|
`;
|
|
@@ -257,7 +256,7 @@ import unreal, json
|
|
|
257
256
|
loc = ${loc}
|
|
258
257
|
rot = ${rot}
|
|
259
258
|
scale = ${scale}
|
|
260
|
-
actor = unreal.
|
|
259
|
+
actor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).spawn_actor_from_class(unreal.EditorAssetLibrary.load_asset('/Script/Engine.${params.actor_class}').get_class(), unreal.Vector(*loc), unreal.Rotator(*rot))
|
|
261
260
|
if actor:
|
|
262
261
|
actor.set_actor_scale3d(unreal.Vector(*scale))
|
|
263
262
|
${params.actor_label ? `actor.set_actor_label("${params.actor_label}")` : ''}
|
|
@@ -285,26 +284,127 @@ else:
|
|
|
285
284
|
}
|
|
286
285
|
|
|
287
286
|
/**
|
|
288
|
-
* Build a
|
|
289
|
-
*
|
|
287
|
+
* Build a standalone UE Python script to execute a tool by name.
|
|
288
|
+
* These scripts use only the `unreal` module available inside UE's Python interpreter.
|
|
290
289
|
*/
|
|
291
290
|
function buildPythonForTool(toolName, params) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
291
|
+
const paramsJson = JSON.stringify(params);
|
|
292
|
+
|
|
293
|
+
switch (toolName) {
|
|
294
|
+
case 'spawn_object': {
|
|
295
|
+
const actorClass = params.actor_class || 'StaticMeshActor';
|
|
296
|
+
const loc = params.location || '0,0,0';
|
|
297
|
+
const rot = params.rotation || '0,0,0';
|
|
298
|
+
const scale = params.scale || '1,1,1';
|
|
299
|
+
const label = params.actor_label || '';
|
|
300
|
+
const locParts = loc.split(',').map(s => s.trim());
|
|
301
|
+
const rotParts = rot.split(',').map(s => s.trim());
|
|
302
|
+
const scaleParts = scale.split(',').map(s => s.trim());
|
|
303
|
+
return `import unreal, json
|
|
304
|
+
actor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).spawn_actor_from_class(unreal.${actorClass}, unreal.Vector(${locParts.join(',')}), unreal.Rotator(${rotParts.join(',')}))
|
|
305
|
+
if actor:
|
|
306
|
+
actor.set_actor_scale3d(unreal.Vector(${scaleParts.join(',')}))
|
|
307
|
+
${label ? `actor.set_actor_label("${label}")` : ''}
|
|
308
|
+
mesh_path = '/Engine/BasicShapes/Cube'
|
|
309
|
+
if hasattr(actor, 'static_mesh_component'):
|
|
310
|
+
mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
|
|
311
|
+
if mesh:
|
|
312
|
+
actor.static_mesh_component.set_static_mesh(mesh)
|
|
313
|
+
print(json.dumps({"success": True, "actor": actor.get_actor_label(), "class": "${actorClass}"}))
|
|
314
|
+
else:
|
|
315
|
+
print(json.dumps({"success": False, "error": "Failed to spawn ${actorClass}"}))`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
case 'delete_actor': {
|
|
319
|
+
const name = params.actor_name || params.name || '';
|
|
320
|
+
return `import unreal, json
|
|
321
|
+
actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).get_all_level_actors()
|
|
322
|
+
deleted = []
|
|
323
|
+
for a in actors:
|
|
324
|
+
if a.get_actor_label() == "${name}":
|
|
325
|
+
a.destroy_actor()
|
|
326
|
+
deleted.append("${name}")
|
|
327
|
+
print(json.dumps({"success": len(deleted) > 0, "deleted": deleted}))`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
case 'find_actors_by_name': {
|
|
331
|
+
const pattern = params.pattern || params.name || '';
|
|
332
|
+
return `import unreal, json
|
|
333
|
+
actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).get_all_level_actors()
|
|
334
|
+
found = []
|
|
335
|
+
for a in actors:
|
|
336
|
+
label = a.get_actor_label()
|
|
337
|
+
if "${pattern}".lower() in label.lower():
|
|
338
|
+
loc = a.get_actor_location()
|
|
339
|
+
found.append({"name": label, "class": a.get_class().get_name(), "location": [loc.x, loc.y, loc.z]})
|
|
340
|
+
print(json.dumps({"success": True, "actors": found, "count": len(found)}))`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
case 'set_actor_transform': {
|
|
344
|
+
const name = params.actor_name || params.name || '';
|
|
345
|
+
const loc = params.location || '';
|
|
346
|
+
const rot = params.rotation || '';
|
|
347
|
+
const scale = params.scale || '';
|
|
348
|
+
return `import unreal, json
|
|
349
|
+
actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).get_all_level_actors()
|
|
350
|
+
target = None
|
|
351
|
+
for a in actors:
|
|
352
|
+
if a.get_actor_label() == "${name}":
|
|
353
|
+
target = a
|
|
354
|
+
break
|
|
355
|
+
if target:
|
|
356
|
+
${loc ? `target.set_actor_location(unreal.Vector(${loc}), False, False)` : '# no location change'}
|
|
357
|
+
${rot ? `target.set_actor_rotation(unreal.Rotator(${rot}), False, False)` : '# no rotation change'}
|
|
358
|
+
${scale ? `target.set_actor_scale3d(unreal.Vector(${scale}))` : '# no scale change'}
|
|
359
|
+
print(json.dumps({"success": True, "actor": "${name}"}))
|
|
360
|
+
else:
|
|
361
|
+
print(json.dumps({"success": False, "error": "Actor '${name}' not found"}))`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
case 'create_blueprint': {
|
|
365
|
+
const name = params.name || 'NewBlueprint';
|
|
366
|
+
const parentClass = params.parent_class || 'Actor';
|
|
367
|
+
const path = params.path || '/Game/Blueprints';
|
|
368
|
+
return `import unreal, json
|
|
369
|
+
factory = unreal.BlueprintFactory()
|
|
370
|
+
factory.set_editor_property("parent_class", unreal.${parentClass})
|
|
371
|
+
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
372
|
+
bp = asset_tools.create_asset("${name}", "${path}", unreal.Blueprint, factory)
|
|
373
|
+
if bp:
|
|
374
|
+
print(json.dumps({"success": True, "name": "${name}", "path": "${path}/${name}"}))
|
|
375
|
+
else:
|
|
376
|
+
print(json.dumps({"success": False, "error": "Failed to create blueprint"}))`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
case 'create_material': {
|
|
380
|
+
const name = params.name || 'NewMaterial';
|
|
381
|
+
const path = params.path || '/Game/Materials';
|
|
382
|
+
return `import unreal, json
|
|
383
|
+
factory = unreal.MaterialFactoryNew()
|
|
384
|
+
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
385
|
+
mat = asset_tools.create_asset("${name}", "${path}", unreal.Material, factory)
|
|
386
|
+
if mat:
|
|
387
|
+
print(json.dumps({"success": True, "name": "${name}", "path": "${path}/${name}"}))
|
|
388
|
+
else:
|
|
389
|
+
print(json.dumps({"success": False, "error": "Failed to create material"}))`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
case 'get_all_scene_objects':
|
|
393
|
+
return `import unreal, json
|
|
394
|
+
subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
395
|
+
actors = subsystem.get_all_level_actors()
|
|
396
|
+
result = []
|
|
397
|
+
for a in actors:
|
|
398
|
+
loc = a.get_actor_location()
|
|
399
|
+
result.append({"name": a.get_actor_label(), "class": a.get_class().get_name(), "location": [loc.x, loc.y, loc.z]})
|
|
400
|
+
print(json.dumps(result))`;
|
|
401
|
+
|
|
402
|
+
default:
|
|
403
|
+
// Generic: pass tool name and params as JSON, let a generic handler try
|
|
404
|
+
return `import unreal, json
|
|
405
|
+
params = json.loads('${paramsJson.replace(/'/g, "\\'")}')
|
|
406
|
+
print(json.dumps({"success": False, "error": "Tool '${toolName}' is not directly supported in no-plugin mode via CLI exec. Use 'createlex serve' with an AI tool for full 71-tool support."}))`;
|
|
407
|
+
}
|
|
308
408
|
}
|
|
309
409
|
|
|
310
410
|
module.exports = {
|
|
@@ -15,12 +15,18 @@ function request(method, path, body = null, options = {}) {
|
|
|
15
15
|
const timeout = options.timeout || REQUEST_TIMEOUT;
|
|
16
16
|
|
|
17
17
|
return new Promise((resolve, reject) => {
|
|
18
|
+
const bodyStr = body ? JSON.stringify(body) : null;
|
|
19
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
20
|
+
if (bodyStr) {
|
|
21
|
+
headers['Content-Length'] = Buffer.byteLength(bodyStr);
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
const reqOptions = {
|
|
19
25
|
hostname: host,
|
|
20
26
|
port,
|
|
21
27
|
path,
|
|
22
28
|
method,
|
|
23
|
-
headers
|
|
29
|
+
headers,
|
|
24
30
|
timeout
|
|
25
31
|
};
|
|
26
32
|
|
|
@@ -49,8 +55,8 @@ function request(method, path, body = null, options = {}) {
|
|
|
49
55
|
}
|
|
50
56
|
});
|
|
51
57
|
|
|
52
|
-
if (
|
|
53
|
-
req.write(
|
|
58
|
+
if (bodyStr) {
|
|
59
|
+
req.write(bodyStr);
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
req.end();
|
|
@@ -184,7 +190,7 @@ async function executePythonScript(script, options = {}) {
|
|
|
184
190
|
'ExecutePythonCommandEx',
|
|
185
191
|
{
|
|
186
192
|
PythonCommand: script,
|
|
187
|
-
ExecutionMode: '
|
|
193
|
+
ExecutionMode: 'ExecuteFile',
|
|
188
194
|
FileExecutionScope: 'Public'
|
|
189
195
|
},
|
|
190
196
|
options
|