1/**************************************************************************/
2/* editor_import_blend_runner.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "editor_import_blend_runner.h"
32
33#ifdef TOOLS_ENABLED
34
35#include "core/io/http_client.h"
36#include "editor/editor_file_system.h"
37#include "editor/editor_node.h"
38#include "editor/editor_settings.h"
39
40static constexpr char PYTHON_SCRIPT_RPC[] = R"(
41import bpy, sys, threading
42from xmlrpc.server import SimpleXMLRPCServer
43req = threading.Condition()
44res = threading.Condition()
45info = None
46def xmlrpc_server():
47 server = SimpleXMLRPCServer(('127.0.0.1', %d))
48 server.register_function(export_gltf)
49 server.serve_forever()
50def export_gltf(opts):
51 with req:
52 global info
53 info = ('export_gltf', opts)
54 req.notify()
55 with res:
56 res.wait()
57if bpy.app.version < (3, 0, 0):
58 print('Blender 3.0 or higher is required.', file=sys.stderr)
59threading.Thread(target=xmlrpc_server).start()
60while True:
61 with req:
62 while info is None:
63 req.wait()
64 method, opts = info
65 if method == 'export_gltf':
66 try:
67 bpy.ops.wm.open_mainfile(filepath=opts['path'])
68 if opts['unpack_all']:
69 bpy.ops.file.unpack_all(method='USE_LOCAL')
70 bpy.ops.export_scene.gltf(**opts['gltf_options'])
71 except:
72 pass
73 info = None
74 with res:
75 res.notify()
76)";
77
78static constexpr char PYTHON_SCRIPT_DIRECT[] = R"(
79import bpy, sys
80opts = %s
81if bpy.app.version < (3, 0, 0):
82 print('Blender 3.0 or higher is required.', file=sys.stderr)
83bpy.ops.wm.open_mainfile(filepath=opts['path'])
84if opts['unpack_all']:
85 bpy.ops.file.unpack_all(method='USE_LOCAL')
86bpy.ops.export_scene.gltf(**opts['gltf_options'])
87)";
88
89String dict_to_python(const Dictionary &p_dict) {
90 String entries;
91 Array dict_keys = p_dict.keys();
92 for (int i = 0; i < dict_keys.size(); i++) {
93 const String key = dict_keys[i];
94 String value;
95 Variant raw_value = p_dict[key];
96
97 switch (raw_value.get_type()) {
98 case Variant::Type::BOOL: {
99 value = raw_value ? "True" : "False";
100 break;
101 }
102 case Variant::Type::STRING:
103 case Variant::Type::STRING_NAME: {
104 value = raw_value;
105 value = vformat("'%s'", value.c_escape());
106 break;
107 }
108 case Variant::Type::DICTIONARY: {
109 value = dict_to_python(raw_value);
110 break;
111 }
112 default: {
113 ERR_FAIL_V_MSG("", vformat("Unhandled Variant type %s for python dictionary", Variant::get_type_name(raw_value.get_type())));
114 }
115 }
116
117 entries += vformat("'%s': %s,", key, value);
118 }
119 return vformat("{%s}", entries);
120}
121
122String dict_to_xmlrpc(const Dictionary &p_dict) {
123 String members;
124 Array dict_keys = p_dict.keys();
125 for (int i = 0; i < dict_keys.size(); i++) {
126 const String key = dict_keys[i];
127 String value;
128 Variant raw_value = p_dict[key];
129
130 switch (raw_value.get_type()) {
131 case Variant::Type::BOOL: {
132 value = vformat("<boolean>%d</boolean>", raw_value ? 1 : 0);
133 break;
134 }
135 case Variant::Type::STRING:
136 case Variant::Type::STRING_NAME: {
137 value = raw_value;
138 value = vformat("<string>%s</string>", value.xml_escape());
139 break;
140 }
141 case Variant::Type::DICTIONARY: {
142 value = dict_to_xmlrpc(raw_value);
143 break;
144 }
145 default: {
146 ERR_FAIL_V_MSG("", vformat("Unhandled Variant type %s for XMLRPC", Variant::get_type_name(raw_value.get_type())));
147 }
148 }
149
150 members += vformat("<member><name>%s</name><value>%s</value></member>", key, value);
151 }
152 return vformat("<struct>%s</struct>", members);
153}
154
155Error EditorImportBlendRunner::start_blender(const String &p_python_script, bool p_blocking) {
156 String blender_path = EDITOR_GET("filesystem/import/blender/blender3_path");
157
158#ifdef WINDOWS_ENABLED
159 blender_path = blender_path.path_join("blender.exe");
160#else
161 blender_path = blender_path.path_join("blender");
162#endif
163
164 List<String> args;
165 args.push_back("--background");
166 args.push_back("--python-expr");
167 args.push_back(p_python_script);
168
169 Error err;
170 if (p_blocking) {
171 int exitcode = 0;
172 err = OS::get_singleton()->execute(blender_path, args, nullptr, &exitcode);
173 if (exitcode != 0) {
174 return FAILED;
175 }
176 } else {
177 err = OS::get_singleton()->create_process(blender_path, args, &blender_pid);
178 }
179 return err;
180}
181
182Error EditorImportBlendRunner::do_import(const Dictionary &p_options) {
183 if (is_using_rpc()) {
184 Error err = do_import_rpc(p_options);
185 if (err != OK) {
186 // Retry without using RPC (slow, but better than the import failing completely).
187 if (err == ERR_CONNECTION_ERROR) {
188 // Disable RPC if the connection could not be established.
189 print_error(vformat("Failed to connect to Blender via RPC, switching to direct imports of .blend files. Check your proxy and firewall settings, then RPC can be re-enabled by changing the editor setting `filesystem/import/blender/rpc_port` to %d.", rpc_port));
190 EditorSettings::get_singleton()->set_manually("filesystem/import/blender/rpc_port", 0);
191 rpc_port = 0;
192 }
193 err = do_import_direct(p_options);
194 }
195 return err;
196 } else {
197 return do_import_direct(p_options);
198 }
199}
200
201Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) {
202 kill_timer->stop();
203
204 // Start Blender if not already running.
205 if (!is_running()) {
206 // Start an XML RPC server on the given port.
207 String python = vformat(PYTHON_SCRIPT_RPC, rpc_port);
208 Error err = start_blender(python, false);
209 if (err != OK || blender_pid == 0) {
210 return FAILED;
211 }
212 }
213
214 // Convert options to XML body.
215 String xml_options = dict_to_xmlrpc(p_options);
216 String xml_body = vformat("<?xml version=\"1.0\"?><methodCall><methodName>export_gltf</methodName><params><param><value>%s</value></param></params></methodCall>", xml_options);
217
218 // Connect to RPC server.
219 Ref<HTTPClient> client = HTTPClient::create();
220 client->connect_to_host("127.0.0.1", rpc_port);
221
222 bool done = false;
223 while (!done) {
224 HTTPClient::Status status = client->get_status();
225 switch (status) {
226 case HTTPClient::STATUS_RESOLVING:
227 case HTTPClient::STATUS_CONNECTING: {
228 client->poll();
229 break;
230 }
231 case HTTPClient::STATUS_CONNECTED: {
232 done = true;
233 break;
234 }
235 default: {
236 ERR_FAIL_V_MSG(ERR_CONNECTION_ERROR, vformat("Unexpected status during RPC connection: %d", status));
237 }
238 }
239 }
240
241 // Send XML request.
242 PackedByteArray xml_buffer = xml_body.to_utf8_buffer();
243 Error err = client->request(HTTPClient::METHOD_POST, "/", Vector<String>(), xml_buffer.ptr(), xml_buffer.size());
244 if (err != OK) {
245 ERR_FAIL_V_MSG(err, vformat("Unable to send RPC request: %d", err));
246 }
247
248 // Wait for response.
249 done = false;
250 while (!done) {
251 HTTPClient::Status status = client->get_status();
252 switch (status) {
253 case HTTPClient::STATUS_REQUESTING: {
254 client->poll();
255 break;
256 }
257 case HTTPClient::STATUS_BODY: {
258 client->poll();
259 // Parse response here if needed. For now we can just ignore it.
260 done = true;
261 break;
262 }
263 default: {
264 ERR_FAIL_V_MSG(ERR_CONNECTION_ERROR, vformat("Unexpected status during RPC response: %d", status));
265 }
266 }
267 }
268
269 return OK;
270}
271
272Error EditorImportBlendRunner::do_import_direct(const Dictionary &p_options) {
273 // Export glTF directly.
274 String python = vformat(PYTHON_SCRIPT_DIRECT, dict_to_python(p_options));
275 Error err = start_blender(python, true);
276 if (err != OK) {
277 return err;
278 }
279
280 return OK;
281}
282
283void EditorImportBlendRunner::_resources_reimported(const PackedStringArray &p_files) {
284 if (is_running()) {
285 // After a batch of imports is done, wait a few seconds before trying to kill blender,
286 // in case of having multiple imports trigger in quick succession.
287 kill_timer->start();
288 }
289}
290
291void EditorImportBlendRunner::_kill_blender() {
292 kill_timer->stop();
293 if (is_running()) {
294 OS::get_singleton()->kill(blender_pid);
295 }
296 blender_pid = 0;
297}
298
299void EditorImportBlendRunner::_notification(int p_what) {
300 switch (p_what) {
301 case NOTIFICATION_PREDELETE: {
302 _kill_blender();
303 break;
304 }
305 }
306}
307
308EditorImportBlendRunner *EditorImportBlendRunner::singleton = nullptr;
309
310EditorImportBlendRunner::EditorImportBlendRunner() {
311 ERR_FAIL_COND_MSG(singleton != nullptr, "EditorImportBlendRunner already created.");
312 singleton = this;
313
314 rpc_port = EDITOR_GET("filesystem/import/blender/rpc_port");
315
316 kill_timer = memnew(Timer);
317 add_child(kill_timer);
318 kill_timer->set_one_shot(true);
319 kill_timer->set_wait_time(EDITOR_GET("filesystem/import/blender/rpc_server_uptime"));
320 kill_timer->connect("timeout", callable_mp(this, &EditorImportBlendRunner::_kill_blender));
321
322 EditorFileSystem::get_singleton()->connect("resources_reimported", callable_mp(this, &EditorImportBlendRunner::_resources_reimported));
323}
324
325#endif // TOOLS_ENABLED
326