1 | /**************************************************************************/ |
2 | /* resource_format_text.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 "resource_format_text.h" |
32 | |
33 | #include "core/config/project_settings.h" |
34 | #include "core/io/dir_access.h" |
35 | #include "core/io/missing_resource.h" |
36 | #include "core/io/resource_format_binary.h" |
37 | #include "core/object/script_language.h" |
38 | #include "core/version.h" |
39 | |
40 | // Version 2: changed names for Basis, AABB, Vectors, etc. |
41 | // Version 3: new string ID for ext/subresources, breaks forward compat. |
42 | #define FORMAT_VERSION 3 |
43 | |
44 | #define BINARY_FORMAT_VERSION 4 |
45 | |
46 | #include "core/io/dir_access.h" |
47 | #include "core/version.h" |
48 | |
49 | #define _printerr() ERR_PRINT(String(res_path + ":" + itos(lines) + " - Parse Error: " + error_text).utf8().get_data()); |
50 | |
51 | /// |
52 | |
53 | Ref<Resource> ResourceLoaderText::get_resource() { |
54 | return resource; |
55 | } |
56 | |
57 | Error ResourceLoaderText::_parse_sub_resource_dummy(DummyReadData *p_data, VariantParser::Stream *p_stream, Ref<Resource> &r_res, int &line, String &r_err_str) { |
58 | VariantParser::Token token; |
59 | VariantParser::get_token(p_stream, token, line, r_err_str); |
60 | if (token.type != VariantParser::TK_NUMBER && token.type != VariantParser::TK_STRING) { |
61 | r_err_str = "Expected number (old style) or string (sub-resource index)" ; |
62 | return ERR_PARSE_ERROR; |
63 | } |
64 | |
65 | if (p_data->no_placeholders) { |
66 | r_res.unref(); |
67 | } else { |
68 | String unique_id = token.value; |
69 | |
70 | if (!p_data->resource_map.has(unique_id)) { |
71 | r_err_str = "Found unique_id reference before mapping, sub-resources stored out of order in resource file" ; |
72 | return ERR_PARSE_ERROR; |
73 | } |
74 | |
75 | r_res = p_data->resource_map[unique_id]; |
76 | } |
77 | |
78 | VariantParser::get_token(p_stream, token, line, r_err_str); |
79 | if (token.type != VariantParser::TK_PARENTHESIS_CLOSE) { |
80 | r_err_str = "Expected ')'" ; |
81 | return ERR_PARSE_ERROR; |
82 | } |
83 | |
84 | return OK; |
85 | } |
86 | |
87 | Error ResourceLoaderText::_parse_ext_resource_dummy(DummyReadData *p_data, VariantParser::Stream *p_stream, Ref<Resource> &r_res, int &line, String &r_err_str) { |
88 | VariantParser::Token token; |
89 | VariantParser::get_token(p_stream, token, line, r_err_str); |
90 | if (token.type != VariantParser::TK_NUMBER && token.type != VariantParser::TK_STRING) { |
91 | r_err_str = "Expected number (old style sub-resource index) or String (ext-resource ID)" ; |
92 | return ERR_PARSE_ERROR; |
93 | } |
94 | |
95 | if (p_data->no_placeholders) { |
96 | r_res.unref(); |
97 | } else { |
98 | String id = token.value; |
99 | |
100 | ERR_FAIL_COND_V(!p_data->rev_external_resources.has(id), ERR_PARSE_ERROR); |
101 | |
102 | r_res = p_data->rev_external_resources[id]; |
103 | } |
104 | |
105 | VariantParser::get_token(p_stream, token, line, r_err_str); |
106 | if (token.type != VariantParser::TK_PARENTHESIS_CLOSE) { |
107 | r_err_str = "Expected ')'" ; |
108 | return ERR_PARSE_ERROR; |
109 | } |
110 | |
111 | return OK; |
112 | } |
113 | |
114 | Error ResourceLoaderText::_parse_sub_resource(VariantParser::Stream *p_stream, Ref<Resource> &r_res, int &line, String &r_err_str) { |
115 | VariantParser::Token token; |
116 | VariantParser::get_token(p_stream, token, line, r_err_str); |
117 | if (token.type != VariantParser::TK_NUMBER && token.type != VariantParser::TK_STRING) { |
118 | r_err_str = "Expected number (old style sub-resource index) or string" ; |
119 | return ERR_PARSE_ERROR; |
120 | } |
121 | |
122 | String id = token.value; |
123 | ERR_FAIL_COND_V(!int_resources.has(id), ERR_INVALID_PARAMETER); |
124 | r_res = int_resources[id]; |
125 | |
126 | VariantParser::get_token(p_stream, token, line, r_err_str); |
127 | if (token.type != VariantParser::TK_PARENTHESIS_CLOSE) { |
128 | r_err_str = "Expected ')'" ; |
129 | return ERR_PARSE_ERROR; |
130 | } |
131 | |
132 | return OK; |
133 | } |
134 | |
135 | Error ResourceLoaderText::_parse_ext_resource(VariantParser::Stream *p_stream, Ref<Resource> &r_res, int &line, String &r_err_str) { |
136 | VariantParser::Token token; |
137 | VariantParser::get_token(p_stream, token, line, r_err_str); |
138 | if (token.type != VariantParser::TK_NUMBER && token.type != VariantParser::TK_STRING) { |
139 | r_err_str = "Expected number (old style sub-resource index) or String (ext-resource ID)" ; |
140 | return ERR_PARSE_ERROR; |
141 | } |
142 | |
143 | String id = token.value; |
144 | Error err = OK; |
145 | |
146 | if (!ignore_resource_parsing) { |
147 | if (!ext_resources.has(id)) { |
148 | r_err_str = "Can't load cached ext-resource id: " + id; |
149 | return ERR_PARSE_ERROR; |
150 | } |
151 | |
152 | String path = ext_resources[id].path; |
153 | String type = ext_resources[id].type; |
154 | Ref<ResourceLoader::LoadToken> &load_token = ext_resources[id].load_token; |
155 | |
156 | if (load_token.is_valid()) { // If not valid, it's OK since then we know this load accepts broken dependencies. |
157 | Ref<Resource> res = ResourceLoader::_load_complete(*load_token.ptr(), &err); |
158 | if (res.is_null()) { |
159 | if (!ResourceLoader::is_cleaning_tasks()) { |
160 | if (ResourceLoader::get_abort_on_missing_resources()) { |
161 | error = ERR_FILE_MISSING_DEPENDENCIES; |
162 | error_text = "[ext_resource] referenced non-existent resource at: " + path; |
163 | _printerr(); |
164 | err = error; |
165 | } else { |
166 | ResourceLoader::notify_dependency_error(local_path, path, type); |
167 | } |
168 | } |
169 | } else { |
170 | #ifdef TOOLS_ENABLED |
171 | //remember ID for saving |
172 | res->set_id_for_path(local_path, id); |
173 | #endif |
174 | r_res = res; |
175 | } |
176 | } else { |
177 | r_res = Ref<Resource>(); |
178 | } |
179 | } |
180 | |
181 | VariantParser::get_token(p_stream, token, line, r_err_str); |
182 | if (token.type != VariantParser::TK_PARENTHESIS_CLOSE) { |
183 | r_err_str = "Expected ')'" ; |
184 | return ERR_PARSE_ERROR; |
185 | } |
186 | |
187 | return err; |
188 | } |
189 | |
190 | Ref<PackedScene> ResourceLoaderText::_parse_node_tag(VariantParser::ResourceParser &parser) { |
191 | Ref<PackedScene> packed_scene; |
192 | packed_scene.instantiate(); |
193 | |
194 | while (true) { |
195 | if (next_tag.name == "node" ) { |
196 | int parent = -1; |
197 | int owner = -1; |
198 | int type = -1; |
199 | int name = -1; |
200 | int instance = -1; |
201 | int index = -1; |
202 | //int base_scene=-1; |
203 | |
204 | if (next_tag.fields.has("name" )) { |
205 | name = packed_scene->get_state()->add_name(next_tag.fields["name" ]); |
206 | } |
207 | |
208 | if (next_tag.fields.has("parent" )) { |
209 | NodePath np = next_tag.fields["parent" ]; |
210 | np.prepend_period(); //compatible to how it manages paths internally |
211 | parent = packed_scene->get_state()->add_node_path(np); |
212 | } |
213 | |
214 | if (next_tag.fields.has("type" )) { |
215 | type = packed_scene->get_state()->add_name(next_tag.fields["type" ]); |
216 | } else { |
217 | type = SceneState::TYPE_INSTANTIATED; //no type? assume this was instantiated |
218 | } |
219 | |
220 | HashSet<StringName> path_properties; |
221 | |
222 | if (next_tag.fields.has("node_paths" )) { |
223 | Vector<String> paths = next_tag.fields["node_paths" ]; |
224 | for (int i = 0; i < paths.size(); i++) { |
225 | path_properties.insert(paths[i]); |
226 | } |
227 | } |
228 | |
229 | if (next_tag.fields.has("instance" )) { |
230 | instance = packed_scene->get_state()->add_value(next_tag.fields["instance" ]); |
231 | |
232 | if (packed_scene->get_state()->get_node_count() == 0 && parent == -1) { |
233 | packed_scene->get_state()->set_base_scene(instance); |
234 | instance = -1; |
235 | } |
236 | } |
237 | |
238 | if (next_tag.fields.has("instance_placeholder" )) { |
239 | String path = next_tag.fields["instance_placeholder" ]; |
240 | |
241 | int path_v = packed_scene->get_state()->add_value(path); |
242 | |
243 | if (packed_scene->get_state()->get_node_count() == 0) { |
244 | error = ERR_FILE_CORRUPT; |
245 | error_text = "Instance Placeholder can't be used for inheritance." ; |
246 | _printerr(); |
247 | return Ref<PackedScene>(); |
248 | } |
249 | |
250 | instance = path_v | SceneState::FLAG_INSTANCE_IS_PLACEHOLDER; |
251 | } |
252 | |
253 | if (next_tag.fields.has("owner" )) { |
254 | owner = packed_scene->get_state()->add_node_path(next_tag.fields["owner" ]); |
255 | } else { |
256 | if (parent != -1 && !(type == SceneState::TYPE_INSTANTIATED && instance == -1)) { |
257 | owner = 0; //if no owner, owner is root |
258 | } |
259 | } |
260 | |
261 | if (next_tag.fields.has("index" )) { |
262 | index = next_tag.fields["index" ]; |
263 | } |
264 | |
265 | int node_id = packed_scene->get_state()->add_node(parent, owner, type, name, instance, index); |
266 | |
267 | if (next_tag.fields.has("groups" )) { |
268 | Array groups = next_tag.fields["groups" ]; |
269 | for (int i = 0; i < groups.size(); i++) { |
270 | packed_scene->get_state()->add_node_group(node_id, packed_scene->get_state()->add_name(groups[i])); |
271 | } |
272 | } |
273 | |
274 | while (true) { |
275 | String assign; |
276 | Variant value; |
277 | |
278 | error = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, &parser); |
279 | |
280 | if (error) { |
281 | if (error == ERR_FILE_MISSING_DEPENDENCIES) { |
282 | // Resource loading error, just skip it. |
283 | } else if (error != ERR_FILE_EOF) { |
284 | _printerr(); |
285 | return Ref<PackedScene>(); |
286 | } else { |
287 | error = OK; |
288 | return packed_scene; |
289 | } |
290 | } |
291 | |
292 | if (!assign.is_empty()) { |
293 | StringName assign_name = assign; |
294 | int nameidx = packed_scene->get_state()->add_name(assign_name); |
295 | int valueidx = packed_scene->get_state()->add_value(value); |
296 | packed_scene->get_state()->add_node_property(node_id, nameidx, valueidx, path_properties.has(assign_name)); |
297 | //it's assignment |
298 | } else if (!next_tag.name.is_empty()) { |
299 | break; |
300 | } |
301 | } |
302 | } else if (next_tag.name == "connection" ) { |
303 | if (!next_tag.fields.has("from" )) { |
304 | error = ERR_FILE_CORRUPT; |
305 | error_text = "missing 'from' field from connection tag" ; |
306 | return Ref<PackedScene>(); |
307 | } |
308 | |
309 | if (!next_tag.fields.has("to" )) { |
310 | error = ERR_FILE_CORRUPT; |
311 | error_text = "missing 'to' field from connection tag" ; |
312 | return Ref<PackedScene>(); |
313 | } |
314 | |
315 | if (!next_tag.fields.has("signal" )) { |
316 | error = ERR_FILE_CORRUPT; |
317 | error_text = "missing 'signal' field from connection tag" ; |
318 | return Ref<PackedScene>(); |
319 | } |
320 | |
321 | if (!next_tag.fields.has("method" )) { |
322 | error = ERR_FILE_CORRUPT; |
323 | error_text = "missing 'method' field from connection tag" ; |
324 | return Ref<PackedScene>(); |
325 | } |
326 | |
327 | NodePath from = next_tag.fields["from" ]; |
328 | NodePath to = next_tag.fields["to" ]; |
329 | StringName method = next_tag.fields["method" ]; |
330 | StringName signal = next_tag.fields["signal" ]; |
331 | int flags = Object::CONNECT_PERSIST; |
332 | int unbinds = 0; |
333 | Array binds; |
334 | |
335 | if (next_tag.fields.has("flags" )) { |
336 | flags = next_tag.fields["flags" ]; |
337 | } |
338 | |
339 | if (next_tag.fields.has("binds" )) { |
340 | binds = next_tag.fields["binds" ]; |
341 | } |
342 | |
343 | if (next_tag.fields.has("unbinds" )) { |
344 | unbinds = next_tag.fields["unbinds" ]; |
345 | } |
346 | |
347 | Vector<int> bind_ints; |
348 | for (int i = 0; i < binds.size(); i++) { |
349 | bind_ints.push_back(packed_scene->get_state()->add_value(binds[i])); |
350 | } |
351 | |
352 | packed_scene->get_state()->add_connection( |
353 | packed_scene->get_state()->add_node_path(from.simplified()), |
354 | packed_scene->get_state()->add_node_path(to.simplified()), |
355 | packed_scene->get_state()->add_name(signal), |
356 | packed_scene->get_state()->add_name(method), |
357 | flags, |
358 | unbinds, |
359 | bind_ints); |
360 | |
361 | error = VariantParser::parse_tag(&stream, lines, error_text, next_tag, &parser); |
362 | |
363 | if (error) { |
364 | if (error != ERR_FILE_EOF) { |
365 | _printerr(); |
366 | return Ref<PackedScene>(); |
367 | } else { |
368 | error = OK; |
369 | return packed_scene; |
370 | } |
371 | } |
372 | } else if (next_tag.name == "editable" ) { |
373 | if (!next_tag.fields.has("path" )) { |
374 | error = ERR_FILE_CORRUPT; |
375 | error_text = "missing 'path' field from editable tag" ; |
376 | _printerr(); |
377 | return Ref<PackedScene>(); |
378 | } |
379 | |
380 | NodePath path = next_tag.fields["path" ]; |
381 | |
382 | packed_scene->get_state()->add_editable_instance(path.simplified()); |
383 | |
384 | error = VariantParser::parse_tag(&stream, lines, error_text, next_tag, &parser); |
385 | |
386 | if (error) { |
387 | if (error != ERR_FILE_EOF) { |
388 | _printerr(); |
389 | return Ref<PackedScene>(); |
390 | } else { |
391 | error = OK; |
392 | return packed_scene; |
393 | } |
394 | } |
395 | } else { |
396 | error = ERR_FILE_CORRUPT; |
397 | _printerr(); |
398 | return Ref<PackedScene>(); |
399 | } |
400 | } |
401 | } |
402 | |
403 | Error ResourceLoaderText::load() { |
404 | if (error != OK) { |
405 | return error; |
406 | } |
407 | |
408 | while (true) { |
409 | if (next_tag.name != "ext_resource" ) { |
410 | break; |
411 | } |
412 | |
413 | if (!next_tag.fields.has("path" )) { |
414 | error = ERR_FILE_CORRUPT; |
415 | error_text = "Missing 'path' in external resource tag" ; |
416 | _printerr(); |
417 | return error; |
418 | } |
419 | |
420 | if (!next_tag.fields.has("type" )) { |
421 | error = ERR_FILE_CORRUPT; |
422 | error_text = "Missing 'type' in external resource tag" ; |
423 | _printerr(); |
424 | return error; |
425 | } |
426 | |
427 | if (!next_tag.fields.has("id" )) { |
428 | error = ERR_FILE_CORRUPT; |
429 | error_text = "Missing 'id' in external resource tag" ; |
430 | _printerr(); |
431 | return error; |
432 | } |
433 | |
434 | String path = next_tag.fields["path" ]; |
435 | String type = next_tag.fields["type" ]; |
436 | String id = next_tag.fields["id" ]; |
437 | |
438 | if (next_tag.fields.has("uid" )) { |
439 | String uidt = next_tag.fields["uid" ]; |
440 | ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(uidt); |
441 | if (uid != ResourceUID::INVALID_ID && ResourceUID::get_singleton()->has_id(uid)) { |
442 | // If a UID is found and the path is valid, it will be used, otherwise, it falls back to the path. |
443 | path = ResourceUID::get_singleton()->get_id_path(uid); |
444 | } else { |
445 | #ifdef TOOLS_ENABLED |
446 | // Silence a warning that can happen during the initial filesystem scan due to cache being regenerated. |
447 | if (ResourceLoader::get_resource_uid(path) != uid) { |
448 | WARN_PRINT(String(res_path + ":" + itos(lines) + " - ext_resource, invalid UID: " + uidt + " - using text path instead: " + path).utf8().get_data()); |
449 | } |
450 | #else |
451 | WARN_PRINT(String(res_path + ":" + itos(lines) + " - ext_resource, invalid UID: " + uidt + " - using text path instead: " + path).utf8().get_data()); |
452 | #endif |
453 | } |
454 | } |
455 | |
456 | if (!path.contains("://" ) && path.is_relative_path()) { |
457 | // path is relative to file being loaded, so convert to a resource path |
458 | path = ProjectSettings::get_singleton()->localize_path(local_path.get_base_dir().path_join(path)); |
459 | } |
460 | |
461 | if (remaps.has(path)) { |
462 | path = remaps[path]; |
463 | } |
464 | |
465 | ext_resources[id].path = path; |
466 | ext_resources[id].type = type; |
467 | ext_resources[id].load_token = ResourceLoader::_load_start(path, type, use_sub_threads ? ResourceLoader::LOAD_THREAD_DISTRIBUTE : ResourceLoader::LOAD_THREAD_FROM_CURRENT, ResourceFormatLoader::CACHE_MODE_REUSE); |
468 | if (!ext_resources[id].load_token.is_valid()) { |
469 | if (ResourceLoader::get_abort_on_missing_resources()) { |
470 | error = ERR_FILE_CORRUPT; |
471 | error_text = "[ext_resource] referenced non-existent resource at: " + path; |
472 | _printerr(); |
473 | return error; |
474 | } else { |
475 | ResourceLoader::notify_dependency_error(local_path, path, type); |
476 | } |
477 | } |
478 | |
479 | error = VariantParser::parse_tag(&stream, lines, error_text, next_tag, &rp); |
480 | |
481 | if (error) { |
482 | _printerr(); |
483 | return error; |
484 | } |
485 | |
486 | resource_current++; |
487 | } |
488 | |
489 | //these are the ones that count |
490 | resources_total -= resource_current; |
491 | resource_current = 0; |
492 | |
493 | while (true) { |
494 | if (next_tag.name != "sub_resource" ) { |
495 | break; |
496 | } |
497 | |
498 | if (!next_tag.fields.has("type" )) { |
499 | error = ERR_FILE_CORRUPT; |
500 | error_text = "Missing 'type' in external resource tag" ; |
501 | _printerr(); |
502 | return error; |
503 | } |
504 | |
505 | if (!next_tag.fields.has("id" )) { |
506 | error = ERR_FILE_CORRUPT; |
507 | error_text = "Missing 'id' in external resource tag" ; |
508 | _printerr(); |
509 | return error; |
510 | } |
511 | |
512 | String type = next_tag.fields["type" ]; |
513 | String id = next_tag.fields["id" ]; |
514 | |
515 | String path = local_path + "::" + id; |
516 | |
517 | //bool exists=ResourceCache::has(path); |
518 | |
519 | Ref<Resource> res; |
520 | bool do_assign = false; |
521 | |
522 | if (cache_mode == ResourceFormatLoader::CACHE_MODE_REPLACE && ResourceCache::has(path)) { |
523 | //reuse existing |
524 | Ref<Resource> cache = ResourceCache::get_ref(path); |
525 | if (cache.is_valid() && cache->get_class() == type) { |
526 | res = cache; |
527 | res->reset_state(); |
528 | do_assign = true; |
529 | } |
530 | } |
531 | |
532 | MissingResource *missing_resource = nullptr; |
533 | |
534 | if (res.is_null()) { //not reuse |
535 | Ref<Resource> cache = ResourceCache::get_ref(path); |
536 | if (cache_mode != ResourceFormatLoader::CACHE_MODE_IGNORE && cache.is_valid()) { //only if it doesn't exist |
537 | //cached, do not assign |
538 | res = cache; |
539 | } else { |
540 | //create |
541 | |
542 | Object *obj = ClassDB::instantiate(type); |
543 | if (!obj) { |
544 | if (ResourceLoader::is_creating_missing_resources_if_class_unavailable_enabled()) { |
545 | missing_resource = memnew(MissingResource); |
546 | missing_resource->set_original_class(type); |
547 | missing_resource->set_recording_properties(true); |
548 | obj = missing_resource; |
549 | } else { |
550 | error_text += "Can't create sub resource of type: " + type; |
551 | _printerr(); |
552 | error = ERR_FILE_CORRUPT; |
553 | return error; |
554 | } |
555 | } |
556 | |
557 | Resource *r = Object::cast_to<Resource>(obj); |
558 | if (!r) { |
559 | error_text += "Can't create sub resource of type, because not a resource: " + type; |
560 | _printerr(); |
561 | error = ERR_FILE_CORRUPT; |
562 | return error; |
563 | } |
564 | |
565 | res = Ref<Resource>(r); |
566 | do_assign = true; |
567 | } |
568 | } |
569 | |
570 | resource_current++; |
571 | |
572 | if (progress && resources_total > 0) { |
573 | *progress = resource_current / float(resources_total); |
574 | } |
575 | |
576 | int_resources[id] = res; // Always assign int resources. |
577 | if (do_assign) { |
578 | if (cache_mode != ResourceFormatLoader::CACHE_MODE_IGNORE) { |
579 | res->set_path(path, cache_mode == ResourceFormatLoader::CACHE_MODE_REPLACE); |
580 | } |
581 | res->set_scene_unique_id(id); |
582 | } |
583 | |
584 | Dictionary missing_resource_properties; |
585 | |
586 | while (true) { |
587 | String assign; |
588 | Variant value; |
589 | |
590 | error = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, &rp); |
591 | |
592 | if (error) { |
593 | _printerr(); |
594 | return error; |
595 | } |
596 | |
597 | if (!assign.is_empty()) { |
598 | if (do_assign) { |
599 | bool set_valid = true; |
600 | |
601 | if (value.get_type() == Variant::OBJECT && missing_resource != nullptr) { |
602 | // If the property being set is a missing resource (and the parent is not), |
603 | // then setting it will most likely not work. |
604 | // Instead, save it as metadata. |
605 | |
606 | Ref<MissingResource> mr = value; |
607 | if (mr.is_valid()) { |
608 | missing_resource_properties[assign] = mr; |
609 | set_valid = false; |
610 | } |
611 | } |
612 | |
613 | if (value.get_type() == Variant::ARRAY) { |
614 | Array set_array = value; |
615 | bool is_get_valid = false; |
616 | Variant get_value = res->get(assign, &is_get_valid); |
617 | if (is_get_valid && get_value.get_type() == Variant::ARRAY) { |
618 | Array get_array = get_value; |
619 | if (!set_array.is_same_typed(get_array)) { |
620 | value = Array(set_array, get_array.get_typed_builtin(), get_array.get_typed_class_name(), get_array.get_typed_script()); |
621 | } |
622 | } |
623 | } |
624 | |
625 | if (set_valid) { |
626 | res->set(assign, value); |
627 | } |
628 | } |
629 | //it's assignment |
630 | } else if (!next_tag.name.is_empty()) { |
631 | error = OK; |
632 | break; |
633 | } else { |
634 | error = ERR_FILE_CORRUPT; |
635 | error_text = "Premature end of file while parsing [sub_resource]" ; |
636 | _printerr(); |
637 | return error; |
638 | } |
639 | } |
640 | |
641 | if (missing_resource) { |
642 | missing_resource->set_recording_properties(false); |
643 | } |
644 | |
645 | if (!missing_resource_properties.is_empty()) { |
646 | res->set_meta(META_MISSING_RESOURCES, missing_resource_properties); |
647 | } |
648 | } |
649 | |
650 | while (true) { |
651 | if (next_tag.name != "resource" ) { |
652 | break; |
653 | } |
654 | |
655 | if (is_scene) { |
656 | error_text += "found the 'resource' tag on a scene file!" ; |
657 | _printerr(); |
658 | error = ERR_FILE_CORRUPT; |
659 | return error; |
660 | } |
661 | |
662 | Ref<Resource> cache = ResourceCache::get_ref(local_path); |
663 | if (cache_mode == ResourceFormatLoader::CACHE_MODE_REPLACE && cache.is_valid() && cache->get_class() == res_type) { |
664 | cache->reset_state(); |
665 | resource = cache; |
666 | } |
667 | |
668 | MissingResource *missing_resource = nullptr; |
669 | |
670 | if (!resource.is_valid()) { |
671 | Object *obj = ClassDB::instantiate(res_type); |
672 | if (!obj) { |
673 | if (ResourceLoader::is_creating_missing_resources_if_class_unavailable_enabled()) { |
674 | missing_resource = memnew(MissingResource); |
675 | missing_resource->set_original_class(res_type); |
676 | missing_resource->set_recording_properties(true); |
677 | obj = missing_resource; |
678 | } else { |
679 | error_text += "Can't create sub resource of type: " + res_type; |
680 | _printerr(); |
681 | error = ERR_FILE_CORRUPT; |
682 | return error; |
683 | } |
684 | } |
685 | |
686 | Resource *r = Object::cast_to<Resource>(obj); |
687 | if (!r) { |
688 | error_text += "Can't create sub resource of type, because not a resource: " + res_type; |
689 | _printerr(); |
690 | error = ERR_FILE_CORRUPT; |
691 | return error; |
692 | } |
693 | |
694 | resource = Ref<Resource>(r); |
695 | } |
696 | |
697 | Dictionary missing_resource_properties; |
698 | |
699 | while (true) { |
700 | String assign; |
701 | Variant value; |
702 | |
703 | error = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, &rp); |
704 | |
705 | if (error) { |
706 | if (error != ERR_FILE_EOF) { |
707 | _printerr(); |
708 | } else { |
709 | error = OK; |
710 | if (cache_mode != ResourceFormatLoader::CACHE_MODE_IGNORE) { |
711 | if (!ResourceCache::has(res_path)) { |
712 | resource->set_path(res_path); |
713 | } |
714 | resource->set_as_translation_remapped(translation_remapped); |
715 | } |
716 | } |
717 | return error; |
718 | } |
719 | |
720 | if (!assign.is_empty()) { |
721 | bool set_valid = true; |
722 | |
723 | if (value.get_type() == Variant::OBJECT && missing_resource != nullptr) { |
724 | // If the property being set is a missing resource (and the parent is not), |
725 | // then setting it will most likely not work. |
726 | // Instead, save it as metadata. |
727 | |
728 | Ref<MissingResource> mr = value; |
729 | if (mr.is_valid()) { |
730 | missing_resource_properties[assign] = mr; |
731 | set_valid = false; |
732 | } |
733 | } |
734 | |
735 | if (value.get_type() == Variant::ARRAY) { |
736 | Array set_array = value; |
737 | bool is_get_valid = false; |
738 | Variant get_value = resource->get(assign, &is_get_valid); |
739 | if (is_get_valid && get_value.get_type() == Variant::ARRAY) { |
740 | Array get_array = get_value; |
741 | if (!set_array.is_same_typed(get_array)) { |
742 | value = Array(set_array, get_array.get_typed_builtin(), get_array.get_typed_class_name(), get_array.get_typed_script()); |
743 | } |
744 | } |
745 | } |
746 | |
747 | if (set_valid) { |
748 | resource->set(assign, value); |
749 | } |
750 | //it's assignment |
751 | } else if (!next_tag.name.is_empty()) { |
752 | error = ERR_FILE_CORRUPT; |
753 | error_text = "Extra tag found when parsing main resource file" ; |
754 | _printerr(); |
755 | return error; |
756 | } else { |
757 | break; |
758 | } |
759 | } |
760 | |
761 | resource_current++; |
762 | |
763 | if (progress && resources_total > 0) { |
764 | *progress = resource_current / float(resources_total); |
765 | } |
766 | |
767 | if (missing_resource) { |
768 | missing_resource->set_recording_properties(false); |
769 | } |
770 | |
771 | if (!missing_resource_properties.is_empty()) { |
772 | resource->set_meta(META_MISSING_RESOURCES, missing_resource_properties); |
773 | } |
774 | |
775 | error = OK; |
776 | |
777 | return error; |
778 | } |
779 | |
780 | //for scene files |
781 | |
782 | if (next_tag.name == "node" ) { |
783 | if (!is_scene) { |
784 | error_text += "found the 'node' tag on a resource file!" ; |
785 | _printerr(); |
786 | error = ERR_FILE_CORRUPT; |
787 | return error; |
788 | } |
789 | |
790 | Ref<PackedScene> packed_scene = _parse_node_tag(rp); |
791 | |
792 | if (!packed_scene.is_valid()) { |
793 | return error; |
794 | } |
795 | |
796 | error = OK; |
797 | //get it here |
798 | resource = packed_scene; |
799 | if (cache_mode != ResourceFormatLoader::CACHE_MODE_IGNORE && !ResourceCache::has(res_path)) { |
800 | packed_scene->set_path(res_path); |
801 | } |
802 | |
803 | resource_current++; |
804 | |
805 | if (progress && resources_total > 0) { |
806 | *progress = resource_current / float(resources_total); |
807 | } |
808 | |
809 | return error; |
810 | } else { |
811 | error_text += "Unknown tag in file: " + next_tag.name; |
812 | _printerr(); |
813 | error = ERR_FILE_CORRUPT; |
814 | return error; |
815 | } |
816 | } |
817 | |
818 | int ResourceLoaderText::get_stage() const { |
819 | return resource_current; |
820 | } |
821 | |
822 | int ResourceLoaderText::get_stage_count() const { |
823 | return resources_total; //+ext_resources; |
824 | } |
825 | |
826 | void ResourceLoaderText::set_translation_remapped(bool p_remapped) { |
827 | translation_remapped = p_remapped; |
828 | } |
829 | |
830 | ResourceLoaderText::ResourceLoaderText() : |
831 | stream(false) {} |
832 | |
833 | void ResourceLoaderText::get_dependencies(Ref<FileAccess> p_f, List<String> *p_dependencies, bool p_add_types) { |
834 | open(p_f); |
835 | ignore_resource_parsing = true; |
836 | ERR_FAIL_COND(error != OK); |
837 | |
838 | while (next_tag.name == "ext_resource" ) { |
839 | if (!next_tag.fields.has("type" )) { |
840 | error = ERR_FILE_CORRUPT; |
841 | error_text = "Missing 'type' in external resource tag" ; |
842 | _printerr(); |
843 | return; |
844 | } |
845 | |
846 | if (!next_tag.fields.has("id" )) { |
847 | error = ERR_FILE_CORRUPT; |
848 | error_text = "Missing 'id' in external resource tag" ; |
849 | _printerr(); |
850 | return; |
851 | } |
852 | |
853 | String path = next_tag.fields["path" ]; |
854 | String type = next_tag.fields["type" ]; |
855 | String fallback_path; |
856 | |
857 | bool using_uid = false; |
858 | if (next_tag.fields.has("uid" )) { |
859 | // If uid exists, return uid in text format, not the path. |
860 | String uidt = next_tag.fields["uid" ]; |
861 | ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(uidt); |
862 | if (uid != ResourceUID::INVALID_ID) { |
863 | fallback_path = path; // Used by Dependency Editor, in case uid path fails. |
864 | path = ResourceUID::get_singleton()->id_to_text(uid); |
865 | using_uid = true; |
866 | } |
867 | } |
868 | |
869 | if (!using_uid && !path.contains("://" ) && path.is_relative_path()) { |
870 | // Path is relative to file being loaded, so convert to a resource path. |
871 | path = ProjectSettings::get_singleton()->localize_path(local_path.get_base_dir().path_join(path)); |
872 | } |
873 | |
874 | if (p_add_types) { |
875 | path += "::" + type; |
876 | } |
877 | if (!fallback_path.is_empty()) { |
878 | if (!p_add_types) { |
879 | path += "::" ; // Ensure that path comes third, even if there is no type. |
880 | } |
881 | path += "::" + fallback_path; |
882 | } |
883 | |
884 | p_dependencies->push_back(path); |
885 | |
886 | Error err = VariantParser::parse_tag(&stream, lines, error_text, next_tag, &rp); |
887 | |
888 | if (err) { |
889 | print_line(error_text + " - " + itos(lines)); |
890 | error_text = "Unexpected end of file" ; |
891 | _printerr(); |
892 | error = ERR_FILE_CORRUPT; |
893 | return; |
894 | } |
895 | } |
896 | } |
897 | |
898 | Error ResourceLoaderText::rename_dependencies(Ref<FileAccess> p_f, const String &p_path, const HashMap<String, String> &p_map) { |
899 | open(p_f, true); |
900 | ERR_FAIL_COND_V(error != OK, error); |
901 | ignore_resource_parsing = true; |
902 | //FileAccess |
903 | |
904 | Ref<FileAccess> fw; |
905 | |
906 | String base_path = local_path.get_base_dir(); |
907 | |
908 | uint64_t tag_end = f->get_position(); |
909 | |
910 | while (true) { |
911 | Error err = VariantParser::parse_tag(&stream, lines, error_text, next_tag, &rp); |
912 | |
913 | if (err != OK) { |
914 | error = ERR_FILE_CORRUPT; |
915 | ERR_FAIL_V(error); |
916 | } |
917 | |
918 | if (next_tag.name != "ext_resource" ) { |
919 | //nothing was done |
920 | if (fw.is_null()) { |
921 | return OK; |
922 | } |
923 | |
924 | break; |
925 | |
926 | } else { |
927 | if (fw.is_null()) { |
928 | fw = FileAccess::open(p_path + ".depren" , FileAccess::WRITE); |
929 | |
930 | if (res_uid == ResourceUID::INVALID_ID) { |
931 | res_uid = ResourceSaver::get_resource_id_for_path(p_path); |
932 | } |
933 | |
934 | String uid_text = "" ; |
935 | if (res_uid != ResourceUID::INVALID_ID) { |
936 | uid_text = " uid=\"" + ResourceUID::get_singleton()->id_to_text(res_uid) + "\"" ; |
937 | } |
938 | |
939 | if (is_scene) { |
940 | fw->store_line("[gd_scene load_steps=" + itos(resources_total) + " format=" + itos(FORMAT_VERSION) + uid_text + "]\n" ); |
941 | } else { |
942 | String script_res_text; |
943 | if (!script_class.is_empty()) { |
944 | script_res_text = "script_class=\"" + script_class + "\" " ; |
945 | } |
946 | fw->store_line("[gd_resource type=\"" + res_type + "\" " + script_res_text + "load_steps=" + itos(resources_total) + " format=" + itos(FORMAT_VERSION) + uid_text + "]\n" ); |
947 | } |
948 | } |
949 | |
950 | if (!next_tag.fields.has("path" ) || !next_tag.fields.has("id" ) || !next_tag.fields.has("type" )) { |
951 | error = ERR_FILE_CORRUPT; |
952 | ERR_FAIL_V(error); |
953 | } |
954 | |
955 | String path = next_tag.fields["path" ]; |
956 | String id = next_tag.fields["id" ]; |
957 | String type = next_tag.fields["type" ]; |
958 | |
959 | if (next_tag.fields.has("uid" )) { |
960 | String uidt = next_tag.fields["uid" ]; |
961 | ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(uidt); |
962 | if (uid != ResourceUID::INVALID_ID && ResourceUID::get_singleton()->has_id(uid)) { |
963 | // If a UID is found and the path is valid, it will be used, otherwise, it falls back to the path. |
964 | path = ResourceUID::get_singleton()->get_id_path(uid); |
965 | } |
966 | } |
967 | bool relative = false; |
968 | if (!path.begins_with("res://" )) { |
969 | path = base_path.path_join(path).simplify_path(); |
970 | relative = true; |
971 | } |
972 | |
973 | if (p_map.has(path)) { |
974 | String np = p_map[path]; |
975 | path = np; |
976 | } |
977 | |
978 | if (relative) { |
979 | //restore relative |
980 | path = base_path.path_to_file(path); |
981 | } |
982 | |
983 | String s = "[ext_resource type=\"" + type + "\"" ; |
984 | |
985 | ResourceUID::ID uid = ResourceSaver::get_resource_id_for_path(path); |
986 | if (uid != ResourceUID::INVALID_ID) { |
987 | s += " uid=\"" + ResourceUID::get_singleton()->id_to_text(uid) + "\"" ; |
988 | } |
989 | s += " path=\"" + path + "\" id=\"" + id + "\"]" ; |
990 | fw->store_line(s); // Bundled. |
991 | |
992 | tag_end = f->get_position(); |
993 | } |
994 | } |
995 | |
996 | f->seek(tag_end); |
997 | |
998 | const uint32_t buffer_size = 2048; |
999 | uint8_t *buffer = (uint8_t *)alloca(buffer_size); |
1000 | uint32_t num_read; |
1001 | |
1002 | num_read = f->get_buffer(buffer, buffer_size); |
1003 | ERR_FAIL_COND_V_MSG(num_read == UINT32_MAX, ERR_CANT_CREATE, "Failed to allocate memory for buffer." ); |
1004 | ERR_FAIL_COND_V(num_read == 0, ERR_FILE_CORRUPT); |
1005 | |
1006 | if (*buffer == '\n') { |
1007 | // Skip first newline character since we added one. |
1008 | if (num_read > 1) { |
1009 | fw->store_buffer(buffer + 1, num_read - 1); |
1010 | } |
1011 | } else { |
1012 | fw->store_buffer(buffer, num_read); |
1013 | } |
1014 | |
1015 | while (!f->eof_reached()) { |
1016 | num_read = f->get_buffer(buffer, buffer_size); |
1017 | fw->store_buffer(buffer, num_read); |
1018 | } |
1019 | |
1020 | bool all_ok = fw->get_error() == OK; |
1021 | |
1022 | if (!all_ok) { |
1023 | return ERR_CANT_CREATE; |
1024 | } |
1025 | |
1026 | return OK; |
1027 | } |
1028 | |
1029 | void ResourceLoaderText::open(Ref<FileAccess> p_f, bool p_skip_first_tag) { |
1030 | error = OK; |
1031 | |
1032 | lines = 1; |
1033 | f = p_f; |
1034 | |
1035 | stream.f = f; |
1036 | is_scene = false; |
1037 | ignore_resource_parsing = false; |
1038 | resource_current = 0; |
1039 | |
1040 | VariantParser::Tag tag; |
1041 | Error err = VariantParser::parse_tag(&stream, lines, error_text, tag); |
1042 | |
1043 | if (err) { |
1044 | error = err; |
1045 | _printerr(); |
1046 | return; |
1047 | } |
1048 | |
1049 | if (tag.fields.has("format" )) { |
1050 | int fmt = tag.fields["format" ]; |
1051 | if (fmt > FORMAT_VERSION) { |
1052 | error_text = "Saved with newer format version" ; |
1053 | _printerr(); |
1054 | error = ERR_PARSE_ERROR; |
1055 | return; |
1056 | } |
1057 | } |
1058 | |
1059 | if (tag.name == "gd_scene" ) { |
1060 | is_scene = true; |
1061 | |
1062 | } else if (tag.name == "gd_resource" ) { |
1063 | if (!tag.fields.has("type" )) { |
1064 | error_text = "Missing 'type' field in 'gd_resource' tag" ; |
1065 | _printerr(); |
1066 | error = ERR_PARSE_ERROR; |
1067 | return; |
1068 | } |
1069 | |
1070 | if (tag.fields.has("script_class" )) { |
1071 | script_class = tag.fields["script_class" ]; |
1072 | } |
1073 | |
1074 | res_type = tag.fields["type" ]; |
1075 | |
1076 | } else { |
1077 | error_text = "Unrecognized file type: " + tag.name; |
1078 | _printerr(); |
1079 | error = ERR_PARSE_ERROR; |
1080 | return; |
1081 | } |
1082 | |
1083 | if (tag.fields.has("uid" )) { |
1084 | res_uid = ResourceUID::get_singleton()->text_to_id(tag.fields["uid" ]); |
1085 | } else { |
1086 | res_uid = ResourceUID::INVALID_ID; |
1087 | } |
1088 | |
1089 | if (tag.fields.has("load_steps" )) { |
1090 | resources_total = tag.fields["load_steps" ]; |
1091 | } else { |
1092 | resources_total = 0; |
1093 | } |
1094 | |
1095 | if (!p_skip_first_tag) { |
1096 | err = VariantParser::parse_tag(&stream, lines, error_text, next_tag, &rp); |
1097 | |
1098 | if (err) { |
1099 | error_text = "Unexpected end of file" ; |
1100 | _printerr(); |
1101 | error = ERR_FILE_CORRUPT; |
1102 | } |
1103 | } |
1104 | |
1105 | rp.ext_func = _parse_ext_resources; |
1106 | rp.sub_func = _parse_sub_resources; |
1107 | rp.userdata = this; |
1108 | } |
1109 | |
1110 | static void bs_save_unicode_string(Ref<FileAccess> p_f, const String &p_string, bool p_bit_on_len = false) { |
1111 | CharString utf8 = p_string.utf8(); |
1112 | if (p_bit_on_len) { |
1113 | p_f->store_32((utf8.length() + 1) | 0x80000000); |
1114 | } else { |
1115 | p_f->store_32(utf8.length() + 1); |
1116 | } |
1117 | p_f->store_buffer((const uint8_t *)utf8.get_data(), utf8.length() + 1); |
1118 | } |
1119 | |
1120 | Error ResourceLoaderText::save_as_binary(const String &p_path) { |
1121 | if (error) { |
1122 | return error; |
1123 | } |
1124 | |
1125 | Ref<FileAccess> wf = FileAccess::open(p_path, FileAccess::WRITE); |
1126 | if (wf.is_null()) { |
1127 | return ERR_CANT_OPEN; |
1128 | } |
1129 | |
1130 | //save header compressed |
1131 | static const uint8_t [4] = { 'R', 'S', 'R', 'C' }; |
1132 | wf->store_buffer(header, 4); |
1133 | |
1134 | wf->store_32(0); //endianness, little endian |
1135 | wf->store_32(0); //64 bits file, false for now |
1136 | wf->store_32(VERSION_MAJOR); |
1137 | wf->store_32(VERSION_MINOR); |
1138 | static const int save_format_version = BINARY_FORMAT_VERSION; |
1139 | wf->store_32(save_format_version); |
1140 | |
1141 | bs_save_unicode_string(wf, is_scene ? "PackedScene" : resource_type); |
1142 | wf->store_64(0); //offset to import metadata, this is no longer used |
1143 | |
1144 | wf->store_32(ResourceFormatSaverBinaryInstance::FORMAT_FLAG_NAMED_SCENE_IDS | ResourceFormatSaverBinaryInstance::FORMAT_FLAG_UIDS); |
1145 | |
1146 | wf->store_64(res_uid); |
1147 | |
1148 | for (int i = 0; i < ResourceFormatSaverBinaryInstance::RESERVED_FIELDS; i++) { |
1149 | wf->store_32(0); // reserved |
1150 | } |
1151 | |
1152 | wf->store_32(0); //string table size, will not be in use |
1153 | uint64_t ext_res_count_pos = wf->get_position(); |
1154 | |
1155 | wf->store_32(0); //zero ext resources, still parsing them |
1156 | |
1157 | //go with external resources |
1158 | |
1159 | DummyReadData dummy_read; |
1160 | VariantParser::ResourceParser rp_new; |
1161 | rp_new.ext_func = _parse_ext_resource_dummys; |
1162 | rp_new.sub_func = _parse_sub_resource_dummys; |
1163 | rp_new.userdata = &dummy_read; |
1164 | |
1165 | while (next_tag.name == "ext_resource" ) { |
1166 | if (!next_tag.fields.has("path" )) { |
1167 | error = ERR_FILE_CORRUPT; |
1168 | error_text = "Missing 'path' in external resource tag" ; |
1169 | _printerr(); |
1170 | return error; |
1171 | } |
1172 | |
1173 | if (!next_tag.fields.has("type" )) { |
1174 | error = ERR_FILE_CORRUPT; |
1175 | error_text = "Missing 'type' in external resource tag" ; |
1176 | _printerr(); |
1177 | return error; |
1178 | } |
1179 | |
1180 | if (!next_tag.fields.has("id" )) { |
1181 | error = ERR_FILE_CORRUPT; |
1182 | error_text = "Missing 'id' in external resource tag" ; |
1183 | _printerr(); |
1184 | return error; |
1185 | } |
1186 | |
1187 | String path = next_tag.fields["path" ]; |
1188 | String type = next_tag.fields["type" ]; |
1189 | String id = next_tag.fields["id" ]; |
1190 | ResourceUID::ID uid = ResourceUID::INVALID_ID; |
1191 | if (next_tag.fields.has("uid" )) { |
1192 | String uidt = next_tag.fields["uid" ]; |
1193 | uid = ResourceUID::get_singleton()->text_to_id(uidt); |
1194 | } |
1195 | |
1196 | bs_save_unicode_string(wf, type); |
1197 | bs_save_unicode_string(wf, path); |
1198 | wf->store_64(uid); |
1199 | |
1200 | int lindex = dummy_read.external_resources.size(); |
1201 | Ref<DummyResource> dr; |
1202 | dr.instantiate(); |
1203 | dr->set_path("res://dummy" + itos(lindex)); //anything is good to detect it for saving as external |
1204 | dummy_read.external_resources[dr] = lindex; |
1205 | dummy_read.rev_external_resources[id] = dr; |
1206 | |
1207 | error = VariantParser::parse_tag(&stream, lines, error_text, next_tag, &rp_new); |
1208 | |
1209 | if (error) { |
1210 | _printerr(); |
1211 | return error; |
1212 | } |
1213 | } |
1214 | |
1215 | // save external resource table |
1216 | wf->seek(ext_res_count_pos); |
1217 | wf->store_32(dummy_read.external_resources.size()); |
1218 | wf->seek_end(); |
1219 | |
1220 | //now, save resources to a separate file, for now |
1221 | |
1222 | uint64_t sub_res_count_pos = wf->get_position(); |
1223 | wf->store_32(0); //zero sub resources, still parsing them |
1224 | |
1225 | String temp_file = p_path + ".temp" ; |
1226 | Vector<uint64_t> local_offsets; |
1227 | Vector<uint64_t> local_pointers_pos; |
1228 | { |
1229 | Ref<FileAccess> wf2 = FileAccess::open(temp_file, FileAccess::WRITE); |
1230 | if (wf2.is_null()) { |
1231 | return ERR_CANT_OPEN; |
1232 | } |
1233 | |
1234 | while (next_tag.name == "sub_resource" || next_tag.name == "resource" ) { |
1235 | String type; |
1236 | String id; |
1237 | bool main_res; |
1238 | |
1239 | if (next_tag.name == "sub_resource" ) { |
1240 | if (!next_tag.fields.has("type" )) { |
1241 | error = ERR_FILE_CORRUPT; |
1242 | error_text = "Missing 'type' in external resource tag" ; |
1243 | _printerr(); |
1244 | return error; |
1245 | } |
1246 | |
1247 | if (!next_tag.fields.has("id" )) { |
1248 | error = ERR_FILE_CORRUPT; |
1249 | error_text = "Missing 'id' in external resource tag" ; |
1250 | _printerr(); |
1251 | return error; |
1252 | } |
1253 | |
1254 | type = next_tag.fields["type" ]; |
1255 | id = next_tag.fields["id" ]; |
1256 | main_res = false; |
1257 | |
1258 | if (!dummy_read.resource_map.has(id)) { |
1259 | Ref<DummyResource> dr; |
1260 | dr.instantiate(); |
1261 | dr->set_scene_unique_id(id); |
1262 | dummy_read.resource_map[id] = dr; |
1263 | uint32_t im_size = dummy_read.resource_index_map.size(); |
1264 | dummy_read.resource_index_map.insert(dr, im_size); |
1265 | } |
1266 | |
1267 | } else { |
1268 | type = res_type; |
1269 | String uid_text = ResourceUID::get_singleton()->id_to_text(res_uid); |
1270 | id = type + "_" + uid_text.replace("uid://" , "" ).replace("<invalid>" , "0" ); |
1271 | main_res = true; |
1272 | } |
1273 | |
1274 | local_offsets.push_back(wf2->get_position()); |
1275 | |
1276 | bs_save_unicode_string(wf, "local://" + id); |
1277 | local_pointers_pos.push_back(wf->get_position()); |
1278 | wf->store_64(0); //temp local offset |
1279 | |
1280 | bs_save_unicode_string(wf2, type); |
1281 | uint64_t propcount_ofs = wf2->get_position(); |
1282 | wf2->store_32(0); |
1283 | |
1284 | int prop_count = 0; |
1285 | |
1286 | while (true) { |
1287 | String assign; |
1288 | Variant value; |
1289 | |
1290 | error = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, &rp_new); |
1291 | |
1292 | if (error) { |
1293 | if (main_res && error == ERR_FILE_EOF) { |
1294 | next_tag.name = "" ; //exit |
1295 | break; |
1296 | } |
1297 | |
1298 | _printerr(); |
1299 | return error; |
1300 | } |
1301 | |
1302 | if (!assign.is_empty()) { |
1303 | HashMap<StringName, int> empty_string_map; //unused |
1304 | bs_save_unicode_string(wf2, assign, true); |
1305 | ResourceFormatSaverBinaryInstance::write_variant(wf2, value, dummy_read.resource_index_map, dummy_read.external_resources, empty_string_map); |
1306 | prop_count++; |
1307 | |
1308 | } else if (!next_tag.name.is_empty()) { |
1309 | error = OK; |
1310 | break; |
1311 | } else { |
1312 | error = ERR_FILE_CORRUPT; |
1313 | error_text = "Premature end of file while parsing [sub_resource]" ; |
1314 | _printerr(); |
1315 | return error; |
1316 | } |
1317 | } |
1318 | |
1319 | wf2->seek(propcount_ofs); |
1320 | wf2->store_32(prop_count); |
1321 | wf2->seek_end(); |
1322 | } |
1323 | |
1324 | if (next_tag.name == "node" ) { |
1325 | // This is a node, must save one more! |
1326 | |
1327 | if (!is_scene) { |
1328 | error_text += "found the 'node' tag on a resource file!" ; |
1329 | _printerr(); |
1330 | error = ERR_FILE_CORRUPT; |
1331 | return error; |
1332 | } |
1333 | |
1334 | Ref<PackedScene> packed_scene = _parse_node_tag(rp_new); |
1335 | |
1336 | if (!packed_scene.is_valid()) { |
1337 | return error; |
1338 | } |
1339 | |
1340 | error = OK; |
1341 | //get it here |
1342 | List<PropertyInfo> props; |
1343 | packed_scene->get_property_list(&props); |
1344 | |
1345 | String id = "PackedScene_" + ResourceUID::get_singleton()->id_to_text(res_uid).replace("uid://" , "" ).replace("<invalid>" , "0" ); |
1346 | bs_save_unicode_string(wf, "local://" + id); |
1347 | local_pointers_pos.push_back(wf->get_position()); |
1348 | wf->store_64(0); //temp local offset |
1349 | |
1350 | local_offsets.push_back(wf2->get_position()); |
1351 | bs_save_unicode_string(wf2, "PackedScene" ); |
1352 | uint64_t propcount_ofs = wf2->get_position(); |
1353 | wf2->store_32(0); |
1354 | |
1355 | int prop_count = 0; |
1356 | |
1357 | for (const PropertyInfo &E : props) { |
1358 | if (!(E.usage & PROPERTY_USAGE_STORAGE)) { |
1359 | continue; |
1360 | } |
1361 | |
1362 | String name = E.name; |
1363 | Variant value = packed_scene->get(name); |
1364 | |
1365 | HashMap<StringName, int> empty_string_map; //unused |
1366 | bs_save_unicode_string(wf2, name, true); |
1367 | ResourceFormatSaverBinaryInstance::write_variant(wf2, value, dummy_read.resource_index_map, dummy_read.external_resources, empty_string_map); |
1368 | prop_count++; |
1369 | } |
1370 | |
1371 | wf2->seek(propcount_ofs); |
1372 | wf2->store_32(prop_count); |
1373 | wf2->seek_end(); |
1374 | } |
1375 | } |
1376 | |
1377 | uint64_t offset_from = wf->get_position(); |
1378 | wf->seek(sub_res_count_pos); //plus one because the saved one |
1379 | wf->store_32(local_offsets.size()); |
1380 | |
1381 | for (int i = 0; i < local_offsets.size(); i++) { |
1382 | wf->seek(local_pointers_pos[i]); |
1383 | wf->store_64(local_offsets[i] + offset_from); |
1384 | } |
1385 | |
1386 | wf->seek_end(); |
1387 | |
1388 | Vector<uint8_t> data = FileAccess::get_file_as_bytes(temp_file); |
1389 | wf->store_buffer(data.ptr(), data.size()); |
1390 | { |
1391 | Ref<DirAccess> dar = DirAccess::open(temp_file.get_base_dir()); |
1392 | dar->remove(temp_file); |
1393 | } |
1394 | |
1395 | wf->store_buffer((const uint8_t *)"RSRC" , 4); //magic at end |
1396 | |
1397 | return OK; |
1398 | } |
1399 | |
1400 | Error ResourceLoaderText::get_classes_used(HashSet<StringName> *r_classes) { |
1401 | if (error) { |
1402 | return error; |
1403 | } |
1404 | |
1405 | ignore_resource_parsing = true; |
1406 | |
1407 | DummyReadData dummy_read; |
1408 | dummy_read.no_placeholders = true; |
1409 | VariantParser::ResourceParser rp_new; |
1410 | rp_new.ext_func = _parse_ext_resource_dummys; |
1411 | rp_new.sub_func = _parse_sub_resource_dummys; |
1412 | rp_new.userdata = &dummy_read; |
1413 | |
1414 | while (next_tag.name == "ext_resource" ) { |
1415 | error = VariantParser::parse_tag(&stream, lines, error_text, next_tag, &rp_new); |
1416 | |
1417 | if (error) { |
1418 | _printerr(); |
1419 | return error; |
1420 | } |
1421 | } |
1422 | |
1423 | while (next_tag.name == "sub_resource" || next_tag.name == "resource" ) { |
1424 | if (next_tag.name == "sub_resource" ) { |
1425 | if (!next_tag.fields.has("type" )) { |
1426 | error = ERR_FILE_CORRUPT; |
1427 | error_text = "Missing 'type' in external resource tag" ; |
1428 | _printerr(); |
1429 | return error; |
1430 | } |
1431 | |
1432 | r_classes->insert(next_tag.fields["type" ]); |
1433 | |
1434 | } else { |
1435 | r_classes->insert(next_tag.fields["res_type" ]); |
1436 | } |
1437 | |
1438 | while (true) { |
1439 | String assign; |
1440 | Variant value; |
1441 | |
1442 | error = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, &rp_new); |
1443 | |
1444 | if (error) { |
1445 | if (error == ERR_FILE_EOF) { |
1446 | return OK; |
1447 | } |
1448 | |
1449 | _printerr(); |
1450 | return error; |
1451 | } |
1452 | |
1453 | if (!assign.is_empty()) { |
1454 | continue; |
1455 | } else if (!next_tag.name.is_empty()) { |
1456 | error = OK; |
1457 | break; |
1458 | } else { |
1459 | error = ERR_FILE_CORRUPT; |
1460 | error_text = "Premature end of file while parsing [sub_resource]" ; |
1461 | _printerr(); |
1462 | return error; |
1463 | } |
1464 | } |
1465 | } |
1466 | |
1467 | while (next_tag.name == "node" ) { |
1468 | // This is a node, must save one more! |
1469 | |
1470 | if (!is_scene) { |
1471 | error_text += "found the 'node' tag on a resource file!" ; |
1472 | _printerr(); |
1473 | error = ERR_FILE_CORRUPT; |
1474 | return error; |
1475 | } |
1476 | |
1477 | if (!next_tag.fields.has("type" )) { |
1478 | error = ERR_FILE_CORRUPT; |
1479 | error_text = "Missing 'type' in external resource tag" ; |
1480 | _printerr(); |
1481 | return error; |
1482 | } |
1483 | |
1484 | r_classes->insert(next_tag.fields["type" ]); |
1485 | |
1486 | while (true) { |
1487 | String assign; |
1488 | Variant value; |
1489 | |
1490 | error = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, &rp_new); |
1491 | |
1492 | if (error) { |
1493 | if (error == ERR_FILE_MISSING_DEPENDENCIES) { |
1494 | // Resource loading error, just skip it. |
1495 | } else if (error != ERR_FILE_EOF) { |
1496 | _printerr(); |
1497 | return error; |
1498 | } else { |
1499 | return OK; |
1500 | } |
1501 | } |
1502 | |
1503 | if (!assign.is_empty()) { |
1504 | continue; |
1505 | } else if (!next_tag.name.is_empty()) { |
1506 | error = OK; |
1507 | break; |
1508 | } else { |
1509 | error = ERR_FILE_CORRUPT; |
1510 | error_text = "Premature end of file while parsing [sub_resource]" ; |
1511 | _printerr(); |
1512 | return error; |
1513 | } |
1514 | } |
1515 | } |
1516 | |
1517 | return OK; |
1518 | } |
1519 | |
1520 | String ResourceLoaderText::recognize_script_class(Ref<FileAccess> p_f) { |
1521 | error = OK; |
1522 | |
1523 | lines = 1; |
1524 | f = p_f; |
1525 | |
1526 | stream.f = f; |
1527 | |
1528 | ignore_resource_parsing = true; |
1529 | |
1530 | VariantParser::Tag tag; |
1531 | Error err = VariantParser::parse_tag(&stream, lines, error_text, tag); |
1532 | |
1533 | if (err) { |
1534 | _printerr(); |
1535 | return "" ; |
1536 | } |
1537 | |
1538 | if (tag.fields.has("format" )) { |
1539 | int fmt = tag.fields["format" ]; |
1540 | if (fmt > FORMAT_VERSION) { |
1541 | error_text = "Saved with newer format version" ; |
1542 | _printerr(); |
1543 | return "" ; |
1544 | } |
1545 | } |
1546 | |
1547 | if (tag.name != "gd_resource" ) { |
1548 | return "" ; |
1549 | } |
1550 | |
1551 | if (tag.fields.has("script_class" )) { |
1552 | return tag.fields["script_class" ]; |
1553 | } |
1554 | |
1555 | return "" ; |
1556 | } |
1557 | |
1558 | String ResourceLoaderText::recognize(Ref<FileAccess> p_f) { |
1559 | error = OK; |
1560 | |
1561 | lines = 1; |
1562 | f = p_f; |
1563 | |
1564 | stream.f = f; |
1565 | |
1566 | ignore_resource_parsing = true; |
1567 | |
1568 | VariantParser::Tag tag; |
1569 | Error err = VariantParser::parse_tag(&stream, lines, error_text, tag); |
1570 | |
1571 | if (err) { |
1572 | _printerr(); |
1573 | return "" ; |
1574 | } |
1575 | |
1576 | if (tag.fields.has("format" )) { |
1577 | int fmt = tag.fields["format" ]; |
1578 | if (fmt > FORMAT_VERSION) { |
1579 | error_text = "Saved with newer format version" ; |
1580 | _printerr(); |
1581 | return "" ; |
1582 | } |
1583 | } |
1584 | |
1585 | if (tag.name == "gd_scene" ) { |
1586 | return "PackedScene" ; |
1587 | } |
1588 | |
1589 | if (tag.name != "gd_resource" ) { |
1590 | return "" ; |
1591 | } |
1592 | |
1593 | if (!tag.fields.has("type" )) { |
1594 | error_text = "Missing 'type' field in 'gd_resource' tag" ; |
1595 | _printerr(); |
1596 | return "" ; |
1597 | } |
1598 | |
1599 | return tag.fields["type" ]; |
1600 | } |
1601 | |
1602 | ResourceUID::ID ResourceLoaderText::get_uid(Ref<FileAccess> p_f) { |
1603 | error = OK; |
1604 | |
1605 | lines = 1; |
1606 | f = p_f; |
1607 | |
1608 | stream.f = f; |
1609 | |
1610 | ignore_resource_parsing = true; |
1611 | |
1612 | VariantParser::Tag tag; |
1613 | Error err = VariantParser::parse_tag(&stream, lines, error_text, tag); |
1614 | |
1615 | if (err) { |
1616 | _printerr(); |
1617 | return ResourceUID::INVALID_ID; |
1618 | } |
1619 | |
1620 | if (tag.fields.has("uid" )) { //field is optional |
1621 | String uidt = tag.fields["uid" ]; |
1622 | return ResourceUID::get_singleton()->text_to_id(uidt); |
1623 | } |
1624 | |
1625 | return ResourceUID::INVALID_ID; |
1626 | } |
1627 | |
1628 | ///////////////////// |
1629 | |
1630 | Ref<Resource> ResourceFormatLoaderText::load(const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode) { |
1631 | if (r_error) { |
1632 | *r_error = ERR_CANT_OPEN; |
1633 | } |
1634 | |
1635 | Error err; |
1636 | |
1637 | Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ, &err); |
1638 | |
1639 | ERR_FAIL_COND_V_MSG(err != OK, Ref<Resource>(), "Cannot open file '" + p_path + "'." ); |
1640 | |
1641 | ResourceLoaderText loader; |
1642 | String path = !p_original_path.is_empty() ? p_original_path : p_path; |
1643 | loader.cache_mode = p_cache_mode; |
1644 | loader.use_sub_threads = p_use_sub_threads; |
1645 | loader.local_path = ProjectSettings::get_singleton()->localize_path(path); |
1646 | loader.progress = r_progress; |
1647 | loader.res_path = loader.local_path; |
1648 | loader.open(f); |
1649 | err = loader.load(); |
1650 | if (r_error) { |
1651 | *r_error = err; |
1652 | } |
1653 | if (err == OK) { |
1654 | return loader.get_resource(); |
1655 | } else { |
1656 | return Ref<Resource>(); |
1657 | } |
1658 | } |
1659 | |
1660 | void ResourceFormatLoaderText::get_recognized_extensions_for_type(const String &p_type, List<String> *p_extensions) const { |
1661 | if (p_type.is_empty()) { |
1662 | get_recognized_extensions(p_extensions); |
1663 | return; |
1664 | } |
1665 | |
1666 | if (ClassDB::is_parent_class("PackedScene" , p_type)) { |
1667 | p_extensions->push_back("tscn" ); |
1668 | } |
1669 | |
1670 | // Don't allow .tres for PackedScenes. |
1671 | if (p_type != "PackedScene" ) { |
1672 | p_extensions->push_back("tres" ); |
1673 | } |
1674 | } |
1675 | |
1676 | void ResourceFormatLoaderText::get_recognized_extensions(List<String> *p_extensions) const { |
1677 | p_extensions->push_back("tscn" ); |
1678 | p_extensions->push_back("tres" ); |
1679 | } |
1680 | |
1681 | bool ResourceFormatLoaderText::handles_type(const String &p_type) const { |
1682 | return true; |
1683 | } |
1684 | |
1685 | void ResourceFormatLoaderText::get_classes_used(const String &p_path, HashSet<StringName> *r_classes) { |
1686 | String ext = p_path.get_extension().to_lower(); |
1687 | if (ext == "tscn" ) { |
1688 | r_classes->insert("PackedScene" ); |
1689 | } |
1690 | |
1691 | // ...for anything else must test... |
1692 | |
1693 | Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); |
1694 | if (f.is_null()) { |
1695 | return; // Could not read. |
1696 | } |
1697 | |
1698 | ResourceLoaderText loader; |
1699 | loader.local_path = ProjectSettings::get_singleton()->localize_path(p_path); |
1700 | loader.res_path = loader.local_path; |
1701 | loader.open(f); |
1702 | loader.get_classes_used(r_classes); |
1703 | } |
1704 | |
1705 | String ResourceFormatLoaderText::get_resource_type(const String &p_path) const { |
1706 | String ext = p_path.get_extension().to_lower(); |
1707 | if (ext == "tscn" ) { |
1708 | return "PackedScene" ; |
1709 | } else if (ext != "tres" ) { |
1710 | return String(); |
1711 | } |
1712 | |
1713 | // ...for anything else must test... |
1714 | |
1715 | Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); |
1716 | if (f.is_null()) { |
1717 | return "" ; //could not read |
1718 | } |
1719 | |
1720 | ResourceLoaderText loader; |
1721 | loader.local_path = ProjectSettings::get_singleton()->localize_path(p_path); |
1722 | loader.res_path = loader.local_path; |
1723 | String r = loader.recognize(f); |
1724 | return ClassDB::get_compatibility_remapped_class(r); |
1725 | } |
1726 | |
1727 | String ResourceFormatLoaderText::get_resource_script_class(const String &p_path) const { |
1728 | String ext = p_path.get_extension().to_lower(); |
1729 | if (ext != "tres" ) { |
1730 | return String(); |
1731 | } |
1732 | |
1733 | // ...for anything else must test... |
1734 | |
1735 | Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); |
1736 | if (f.is_null()) { |
1737 | return "" ; //could not read |
1738 | } |
1739 | |
1740 | ResourceLoaderText loader; |
1741 | loader.local_path = ProjectSettings::get_singleton()->localize_path(p_path); |
1742 | loader.res_path = loader.local_path; |
1743 | return loader.recognize_script_class(f); |
1744 | } |
1745 | |
1746 | ResourceUID::ID ResourceFormatLoaderText::get_resource_uid(const String &p_path) const { |
1747 | String ext = p_path.get_extension().to_lower(); |
1748 | |
1749 | if (ext != "tscn" && ext != "tres" ) { |
1750 | return ResourceUID::INVALID_ID; |
1751 | } |
1752 | |
1753 | Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); |
1754 | if (f.is_null()) { |
1755 | return ResourceUID::INVALID_ID; //could not read |
1756 | } |
1757 | |
1758 | ResourceLoaderText loader; |
1759 | loader.local_path = ProjectSettings::get_singleton()->localize_path(p_path); |
1760 | loader.res_path = loader.local_path; |
1761 | return loader.get_uid(f); |
1762 | } |
1763 | |
1764 | void ResourceFormatLoaderText::get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types) { |
1765 | Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); |
1766 | if (f.is_null()) { |
1767 | ERR_FAIL(); |
1768 | } |
1769 | |
1770 | ResourceLoaderText loader; |
1771 | loader.local_path = ProjectSettings::get_singleton()->localize_path(p_path); |
1772 | loader.res_path = loader.local_path; |
1773 | loader.get_dependencies(f, p_dependencies, p_add_types); |
1774 | } |
1775 | |
1776 | Error ResourceFormatLoaderText::rename_dependencies(const String &p_path, const HashMap<String, String> &p_map) { |
1777 | Error err = OK; |
1778 | { |
1779 | Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); |
1780 | if (f.is_null()) { |
1781 | ERR_FAIL_V(ERR_CANT_OPEN); |
1782 | } |
1783 | |
1784 | ResourceLoaderText loader; |
1785 | loader.local_path = ProjectSettings::get_singleton()->localize_path(p_path); |
1786 | loader.res_path = loader.local_path; |
1787 | err = loader.rename_dependencies(f, p_path, p_map); |
1788 | } |
1789 | |
1790 | if (err == OK) { |
1791 | Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES); |
1792 | da->remove(p_path); |
1793 | da->rename(p_path + ".depren" , p_path); |
1794 | } |
1795 | |
1796 | return err; |
1797 | } |
1798 | |
1799 | ResourceFormatLoaderText *ResourceFormatLoaderText::singleton = nullptr; |
1800 | |
1801 | Error ResourceFormatLoaderText::convert_file_to_binary(const String &p_src_path, const String &p_dst_path) { |
1802 | Error err; |
1803 | Ref<FileAccess> f = FileAccess::open(p_src_path, FileAccess::READ, &err); |
1804 | |
1805 | ERR_FAIL_COND_V_MSG(err != OK, ERR_CANT_OPEN, "Cannot open file '" + p_src_path + "'." ); |
1806 | |
1807 | ResourceLoaderText loader; |
1808 | const String &path = p_src_path; |
1809 | loader.local_path = ProjectSettings::get_singleton()->localize_path(path); |
1810 | loader.res_path = loader.local_path; |
1811 | loader.open(f); |
1812 | return loader.save_as_binary(p_dst_path); |
1813 | } |
1814 | |
1815 | /*****************************************************************************************************/ |
1816 | /*****************************************************************************************************/ |
1817 | /*****************************************************************************************************/ |
1818 | /*****************************************************************************************************/ |
1819 | /*****************************************************************************************************/ |
1820 | /*****************************************************************************************************/ |
1821 | /*****************************************************************************************************/ |
1822 | /*****************************************************************************************************/ |
1823 | /*****************************************************************************************************/ |
1824 | /*****************************************************************************************************/ |
1825 | |
1826 | String ResourceFormatSaverTextInstance::_write_resources(void *ud, const Ref<Resource> &p_resource) { |
1827 | ResourceFormatSaverTextInstance *rsi = static_cast<ResourceFormatSaverTextInstance *>(ud); |
1828 | return rsi->_write_resource(p_resource); |
1829 | } |
1830 | |
1831 | String ResourceFormatSaverTextInstance::_write_resource(const Ref<Resource> &res) { |
1832 | if (res->get_meta(SNAME("_skip_save_" ), false)) { |
1833 | return "null" ; |
1834 | } |
1835 | |
1836 | if (external_resources.has(res)) { |
1837 | return "ExtResource(\"" + external_resources[res] + "\")" ; |
1838 | } else { |
1839 | if (internal_resources.has(res)) { |
1840 | return "SubResource(\"" + internal_resources[res] + "\")" ; |
1841 | } else if (!res->is_built_in()) { |
1842 | if (res->get_path() == local_path) { //circular reference attempt |
1843 | return "null" ; |
1844 | } |
1845 | //external resource |
1846 | String path = relative_paths ? local_path.path_to_file(res->get_path()) : res->get_path(); |
1847 | return "Resource(\"" + path + "\")" ; |
1848 | } else { |
1849 | ERR_FAIL_V_MSG("null" , "Resource was not pre cached for the resource section, bug?" ); |
1850 | //internal resource |
1851 | } |
1852 | } |
1853 | } |
1854 | |
1855 | void ResourceFormatSaverTextInstance::_find_resources(const Variant &p_variant, bool p_main) { |
1856 | switch (p_variant.get_type()) { |
1857 | case Variant::OBJECT: { |
1858 | Ref<Resource> res = p_variant; |
1859 | |
1860 | if (res.is_null() || external_resources.has(res) || res->get_meta(SNAME("_skip_save_" ), false)) { |
1861 | return; |
1862 | } |
1863 | |
1864 | if (!p_main && (!bundle_resources) && !res->is_built_in()) { |
1865 | if (res->get_path() == local_path) { |
1866 | ERR_PRINT("Circular reference to resource being saved found: '" + local_path + "' will be null next time it's loaded." ); |
1867 | return; |
1868 | } |
1869 | |
1870 | // Use a numeric ID as a base, because they are sorted in natural order before saving. |
1871 | // This increases the chances of thread loading to fetch them first. |
1872 | String id = itos(external_resources.size() + 1) + "_" + Resource::generate_scene_unique_id(); |
1873 | external_resources[res] = id; |
1874 | return; |
1875 | } |
1876 | |
1877 | if (resource_set.has(res)) { |
1878 | return; |
1879 | } |
1880 | |
1881 | resource_set.insert(res); |
1882 | |
1883 | List<PropertyInfo> property_list; |
1884 | |
1885 | res->get_property_list(&property_list); |
1886 | property_list.sort(); |
1887 | |
1888 | List<PropertyInfo>::Element *I = property_list.front(); |
1889 | |
1890 | while (I) { |
1891 | PropertyInfo pi = I->get(); |
1892 | |
1893 | if (pi.usage & PROPERTY_USAGE_STORAGE) { |
1894 | Variant v = res->get(I->get().name); |
1895 | |
1896 | if (pi.usage & PROPERTY_USAGE_RESOURCE_NOT_PERSISTENT) { |
1897 | NonPersistentKey npk; |
1898 | npk.base = res; |
1899 | npk.property = pi.name; |
1900 | non_persistent_map[npk] = v; |
1901 | |
1902 | Ref<Resource> sres = v; |
1903 | if (sres.is_valid()) { |
1904 | resource_set.insert(sres); |
1905 | saved_resources.push_back(sres); |
1906 | } else { |
1907 | _find_resources(v); |
1908 | } |
1909 | } else { |
1910 | _find_resources(v); |
1911 | } |
1912 | } |
1913 | |
1914 | I = I->next(); |
1915 | } |
1916 | |
1917 | saved_resources.push_back(res); // Saved after, so the children it needs are available when loaded |
1918 | |
1919 | } break; |
1920 | case Variant::ARRAY: { |
1921 | Array varray = p_variant; |
1922 | int len = varray.size(); |
1923 | for (int i = 0; i < len; i++) { |
1924 | const Variant &v = varray.get(i); |
1925 | _find_resources(v); |
1926 | } |
1927 | |
1928 | } break; |
1929 | case Variant::DICTIONARY: { |
1930 | Dictionary d = p_variant; |
1931 | List<Variant> keys; |
1932 | d.get_key_list(&keys); |
1933 | for (const Variant &E : keys) { |
1934 | // Of course keys should also be cached, after all we can't prevent users from using resources as keys, right? |
1935 | // See also ResourceFormatSaverBinaryInstance::_find_resources (when p_variant is of type Variant::DICTIONARY) |
1936 | _find_resources(E); |
1937 | Variant v = d[E]; |
1938 | _find_resources(v); |
1939 | } |
1940 | } break; |
1941 | default: { |
1942 | } |
1943 | } |
1944 | } |
1945 | |
1946 | static String _resource_get_class(Ref<Resource> p_resource) { |
1947 | Ref<MissingResource> missing_resource = p_resource; |
1948 | if (missing_resource.is_valid()) { |
1949 | return missing_resource->get_original_class(); |
1950 | } else { |
1951 | return p_resource->get_class(); |
1952 | } |
1953 | } |
1954 | |
1955 | Error ResourceFormatSaverTextInstance::save(const String &p_path, const Ref<Resource> &p_resource, uint32_t p_flags) { |
1956 | if (p_path.ends_with(".tscn" )) { |
1957 | packed_scene = p_resource; |
1958 | } |
1959 | |
1960 | Error err; |
1961 | Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE, &err); |
1962 | ERR_FAIL_COND_V_MSG(err, ERR_CANT_OPEN, "Cannot save file '" + p_path + "'." ); |
1963 | Ref<FileAccess> _fref(f); |
1964 | |
1965 | local_path = ProjectSettings::get_singleton()->localize_path(p_path); |
1966 | |
1967 | relative_paths = p_flags & ResourceSaver::FLAG_RELATIVE_PATHS; |
1968 | skip_editor = p_flags & ResourceSaver::FLAG_OMIT_EDITOR_PROPERTIES; |
1969 | bundle_resources = p_flags & ResourceSaver::FLAG_BUNDLE_RESOURCES; |
1970 | takeover_paths = p_flags & ResourceSaver::FLAG_REPLACE_SUBRESOURCE_PATHS; |
1971 | if (!p_path.begins_with("res://" )) { |
1972 | takeover_paths = false; |
1973 | } |
1974 | |
1975 | // Save resources. |
1976 | _find_resources(p_resource, true); |
1977 | |
1978 | if (packed_scene.is_valid()) { |
1979 | // Add instances to external resources if saving a packed scene. |
1980 | for (int i = 0; i < packed_scene->get_state()->get_node_count(); i++) { |
1981 | if (packed_scene->get_state()->is_node_instance_placeholder(i)) { |
1982 | continue; |
1983 | } |
1984 | |
1985 | Ref<PackedScene> instance = packed_scene->get_state()->get_node_instance(i); |
1986 | if (instance.is_valid() && !external_resources.has(instance)) { |
1987 | int index = external_resources.size() + 1; |
1988 | external_resources[instance] = itos(index) + "_" + Resource::generate_scene_unique_id(); // Keep the order for improved thread loading performance. |
1989 | } |
1990 | } |
1991 | } |
1992 | |
1993 | { |
1994 | String title = packed_scene.is_valid() ? "[gd_scene " : "[gd_resource " ; |
1995 | if (packed_scene.is_null()) { |
1996 | title += "type=\"" + _resource_get_class(p_resource) + "\" " ; |
1997 | Ref<Script> script = p_resource->get_script(); |
1998 | if (script.is_valid() && script->get_global_name()) { |
1999 | title += "script_class=\"" + String(script->get_global_name()) + "\" " ; |
2000 | } |
2001 | } |
2002 | |
2003 | int load_steps = saved_resources.size() + external_resources.size(); |
2004 | |
2005 | if (load_steps > 1) { |
2006 | title += "load_steps=" + itos(load_steps) + " " ; |
2007 | } |
2008 | title += "format=" + itos(FORMAT_VERSION) + "" ; |
2009 | |
2010 | ResourceUID::ID uid = ResourceSaver::get_resource_id_for_path(local_path, true); |
2011 | |
2012 | if (uid != ResourceUID::INVALID_ID) { |
2013 | title += " uid=\"" + ResourceUID::get_singleton()->id_to_text(uid) + "\"" ; |
2014 | } |
2015 | |
2016 | f->store_string(title); |
2017 | f->store_line("]\n" ); // One empty line. |
2018 | } |
2019 | |
2020 | #ifdef TOOLS_ENABLED |
2021 | // Keep order from cached ids. |
2022 | HashSet<String> cached_ids_found; |
2023 | for (KeyValue<Ref<Resource>, String> &E : external_resources) { |
2024 | String cached_id = E.key->get_id_for_path(local_path); |
2025 | if (cached_id.is_empty() || cached_ids_found.has(cached_id)) { |
2026 | int sep_pos = E.value.find("_" ); |
2027 | if (sep_pos != -1) { |
2028 | E.value = E.value.substr(0, sep_pos + 1); // Keep the order found, for improved thread loading performance. |
2029 | } else { |
2030 | E.value = "" ; |
2031 | } |
2032 | |
2033 | } else { |
2034 | E.value = cached_id; |
2035 | cached_ids_found.insert(cached_id); |
2036 | } |
2037 | } |
2038 | // Create IDs for non cached resources. |
2039 | for (KeyValue<Ref<Resource>, String> &E : external_resources) { |
2040 | if (cached_ids_found.has(E.value)) { // Already cached, go on. |
2041 | continue; |
2042 | } |
2043 | |
2044 | String attempt; |
2045 | while (true) { |
2046 | attempt = E.value + Resource::generate_scene_unique_id(); |
2047 | if (!cached_ids_found.has(attempt)) { |
2048 | break; |
2049 | } |
2050 | } |
2051 | |
2052 | cached_ids_found.insert(attempt); |
2053 | E.value = attempt; |
2054 | // Update also in resource. |
2055 | Ref<Resource> res = E.key; |
2056 | res->set_id_for_path(local_path, attempt); |
2057 | } |
2058 | #else |
2059 | // Make sure to start from one, as it makes format more readable. |
2060 | int counter = 1; |
2061 | for (KeyValue<Ref<Resource>, String> &E : external_resources) { |
2062 | E.value = itos(counter++); |
2063 | } |
2064 | #endif |
2065 | |
2066 | Vector<ResourceSort> sorted_er; |
2067 | |
2068 | for (const KeyValue<Ref<Resource>, String> &E : external_resources) { |
2069 | ResourceSort rs; |
2070 | rs.resource = E.key; |
2071 | rs.id = E.value; |
2072 | sorted_er.push_back(rs); |
2073 | } |
2074 | |
2075 | sorted_er.sort(); |
2076 | |
2077 | for (int i = 0; i < sorted_er.size(); i++) { |
2078 | String p = sorted_er[i].resource->get_path(); |
2079 | |
2080 | String s = "[ext_resource type=\"" + sorted_er[i].resource->get_save_class() + "\"" ; |
2081 | |
2082 | ResourceUID::ID uid = ResourceSaver::get_resource_id_for_path(p, false); |
2083 | if (uid != ResourceUID::INVALID_ID) { |
2084 | s += " uid=\"" + ResourceUID::get_singleton()->id_to_text(uid) + "\"" ; |
2085 | } |
2086 | s += " path=\"" + p + "\" id=\"" + sorted_er[i].id + "\"]\n" ; |
2087 | f->store_string(s); // Bundled. |
2088 | } |
2089 | |
2090 | if (external_resources.size()) { |
2091 | f->store_line(String()); // Separate. |
2092 | } |
2093 | |
2094 | HashSet<String> used_unique_ids; |
2095 | |
2096 | for (List<Ref<Resource>>::Element *E = saved_resources.front(); E; E = E->next()) { |
2097 | Ref<Resource> res = E->get(); |
2098 | if (E->next() && res->is_built_in()) { |
2099 | if (!res->get_scene_unique_id().is_empty()) { |
2100 | if (used_unique_ids.has(res->get_scene_unique_id())) { |
2101 | res->set_scene_unique_id("" ); // Repeated. |
2102 | } else { |
2103 | used_unique_ids.insert(res->get_scene_unique_id()); |
2104 | } |
2105 | } |
2106 | } |
2107 | } |
2108 | |
2109 | for (List<Ref<Resource>>::Element *E = saved_resources.front(); E; E = E->next()) { |
2110 | Ref<Resource> res = E->get(); |
2111 | ERR_CONTINUE(!resource_set.has(res)); |
2112 | bool main = (E->next() == nullptr); |
2113 | |
2114 | if (main && packed_scene.is_valid()) { |
2115 | break; // Save as a scene. |
2116 | } |
2117 | |
2118 | if (main) { |
2119 | f->store_line("[resource]" ); |
2120 | } else { |
2121 | String line = "[sub_resource " ; |
2122 | if (res->get_scene_unique_id().is_empty()) { |
2123 | String new_id; |
2124 | while (true) { |
2125 | new_id = _resource_get_class(res) + "_" + Resource::generate_scene_unique_id(); |
2126 | |
2127 | if (!used_unique_ids.has(new_id)) { |
2128 | break; |
2129 | } |
2130 | } |
2131 | |
2132 | res->set_scene_unique_id(new_id); |
2133 | used_unique_ids.insert(new_id); |
2134 | } |
2135 | |
2136 | String id = res->get_scene_unique_id(); |
2137 | line += "type=\"" + _resource_get_class(res) + "\" id=\"" + id; |
2138 | f->store_line(line + "\"]" ); |
2139 | if (takeover_paths) { |
2140 | res->set_path(p_path + "::" + id, true); |
2141 | } |
2142 | |
2143 | internal_resources[res] = id; |
2144 | #ifdef TOOLS_ENABLED |
2145 | res->set_edited(false); |
2146 | #endif |
2147 | } |
2148 | |
2149 | Dictionary missing_resource_properties = p_resource->get_meta(META_MISSING_RESOURCES, Dictionary()); |
2150 | |
2151 | List<PropertyInfo> property_list; |
2152 | res->get_property_list(&property_list); |
2153 | for (List<PropertyInfo>::Element *PE = property_list.front(); PE; PE = PE->next()) { |
2154 | if (skip_editor && PE->get().name.begins_with("__editor" )) { |
2155 | continue; |
2156 | } |
2157 | if (PE->get().name == META_PROPERTY_MISSING_RESOURCES) { |
2158 | continue; |
2159 | } |
2160 | |
2161 | if (PE->get().usage & PROPERTY_USAGE_STORAGE) { |
2162 | String name = PE->get().name; |
2163 | Variant value; |
2164 | if (PE->get().usage & PROPERTY_USAGE_RESOURCE_NOT_PERSISTENT) { |
2165 | NonPersistentKey npk; |
2166 | npk.base = res; |
2167 | npk.property = name; |
2168 | if (non_persistent_map.has(npk)) { |
2169 | value = non_persistent_map[npk]; |
2170 | } |
2171 | } else { |
2172 | value = res->get(name); |
2173 | } |
2174 | |
2175 | if (PE->get().type == Variant::OBJECT && missing_resource_properties.has(PE->get().name)) { |
2176 | // Was this missing resource overridden? If so do not save the old value. |
2177 | Ref<Resource> ures = value; |
2178 | if (ures.is_null()) { |
2179 | value = missing_resource_properties[PE->get().name]; |
2180 | } |
2181 | } |
2182 | |
2183 | Variant default_value = ClassDB::class_get_default_property_value(res->get_class(), name); |
2184 | |
2185 | if (default_value.get_type() != Variant::NIL && bool(Variant::evaluate(Variant::OP_EQUAL, value, default_value))) { |
2186 | continue; |
2187 | } |
2188 | |
2189 | if (PE->get().type == Variant::OBJECT && value.is_zero() && !(PE->get().usage & PROPERTY_USAGE_STORE_IF_NULL)) { |
2190 | continue; |
2191 | } |
2192 | |
2193 | String vars; |
2194 | VariantWriter::write_to_string(value, vars, _write_resources, this); |
2195 | f->store_string(name.property_name_encode() + " = " + vars + "\n" ); |
2196 | } |
2197 | } |
2198 | |
2199 | if (E->next()) { |
2200 | f->store_line(String()); |
2201 | } |
2202 | } |
2203 | |
2204 | if (packed_scene.is_valid()) { |
2205 | // If this is a scene, save nodes and connections! |
2206 | Ref<SceneState> state = packed_scene->get_state(); |
2207 | for (int i = 0; i < state->get_node_count(); i++) { |
2208 | StringName type = state->get_node_type(i); |
2209 | StringName name = state->get_node_name(i); |
2210 | int index = state->get_node_index(i); |
2211 | NodePath path = state->get_node_path(i, true); |
2212 | NodePath owner = state->get_node_owner_path(i); |
2213 | Ref<PackedScene> instance = state->get_node_instance(i); |
2214 | String instance_placeholder = state->get_node_instance_placeholder(i); |
2215 | Vector<StringName> groups = state->get_node_groups(i); |
2216 | Vector<String> deferred_node_paths = state->get_node_deferred_nodepath_properties(i); |
2217 | |
2218 | String = "[node" ; |
2219 | header += " name=\"" + String(name).c_escape() + "\"" ; |
2220 | if (type != StringName()) { |
2221 | header += " type=\"" + String(type) + "\"" ; |
2222 | } |
2223 | if (path != NodePath()) { |
2224 | header += " parent=\"" + String(path.simplified()).c_escape() + "\"" ; |
2225 | } |
2226 | if (owner != NodePath() && owner != NodePath("." )) { |
2227 | header += " owner=\"" + String(owner.simplified()).c_escape() + "\"" ; |
2228 | } |
2229 | if (index >= 0) { |
2230 | header += " index=\"" + itos(index) + "\"" ; |
2231 | } |
2232 | |
2233 | if (deferred_node_paths.size()) { |
2234 | header += " node_paths=" + Variant(deferred_node_paths).get_construct_string(); |
2235 | } |
2236 | |
2237 | if (groups.size()) { |
2238 | // Write all groups on the same line as they're part of a section header. |
2239 | // This improves readability while not impacting VCS friendliness too much, |
2240 | // since it's rare to have more than 5 groups assigned to a single node. |
2241 | groups.sort_custom<StringName::AlphCompare>(); |
2242 | String sgroups = " groups=[" ; |
2243 | for (int j = 0; j < groups.size(); j++) { |
2244 | sgroups += "\"" + String(groups[j]).c_escape() + "\"" ; |
2245 | if (j < groups.size() - 1) { |
2246 | sgroups += ", " ; |
2247 | } |
2248 | } |
2249 | sgroups += "]" ; |
2250 | header += sgroups; |
2251 | } |
2252 | |
2253 | f->store_string(header); |
2254 | |
2255 | if (!instance_placeholder.is_empty()) { |
2256 | String vars; |
2257 | f->store_string(" instance_placeholder=" ); |
2258 | VariantWriter::write_to_string(instance_placeholder, vars, _write_resources, this); |
2259 | f->store_string(vars); |
2260 | } |
2261 | |
2262 | if (instance.is_valid()) { |
2263 | String vars; |
2264 | f->store_string(" instance=" ); |
2265 | VariantWriter::write_to_string(instance, vars, _write_resources, this); |
2266 | f->store_string(vars); |
2267 | } |
2268 | |
2269 | f->store_line("]" ); |
2270 | |
2271 | for (int j = 0; j < state->get_node_property_count(i); j++) { |
2272 | String vars; |
2273 | VariantWriter::write_to_string(state->get_node_property_value(i, j), vars, _write_resources, this); |
2274 | |
2275 | f->store_string(String(state->get_node_property_name(i, j)).property_name_encode() + " = " + vars + "\n" ); |
2276 | } |
2277 | |
2278 | if (i < state->get_node_count() - 1) { |
2279 | f->store_line(String()); |
2280 | } |
2281 | } |
2282 | |
2283 | for (int i = 0; i < state->get_connection_count(); i++) { |
2284 | if (i == 0) { |
2285 | f->store_line("" ); |
2286 | } |
2287 | |
2288 | String connstr = "[connection" ; |
2289 | connstr += " signal=\"" + String(state->get_connection_signal(i)) + "\"" ; |
2290 | connstr += " from=\"" + String(state->get_connection_source(i).simplified()) + "\"" ; |
2291 | connstr += " to=\"" + String(state->get_connection_target(i).simplified()) + "\"" ; |
2292 | connstr += " method=\"" + String(state->get_connection_method(i)) + "\"" ; |
2293 | int flags = state->get_connection_flags(i); |
2294 | if (flags != Object::CONNECT_PERSIST) { |
2295 | connstr += " flags=" + itos(flags); |
2296 | } |
2297 | |
2298 | int unbinds = state->get_connection_unbinds(i); |
2299 | if (unbinds > 0) { |
2300 | connstr += " unbinds=" + itos(unbinds); |
2301 | } |
2302 | |
2303 | Array binds = state->get_connection_binds(i); |
2304 | f->store_string(connstr); |
2305 | if (binds.size()) { |
2306 | String vars; |
2307 | VariantWriter::write_to_string(binds, vars, _write_resources, this); |
2308 | f->store_string(" binds= " + vars); |
2309 | } |
2310 | |
2311 | f->store_line("]" ); |
2312 | } |
2313 | |
2314 | Vector<NodePath> editable_instances = state->get_editable_instances(); |
2315 | for (int i = 0; i < editable_instances.size(); i++) { |
2316 | if (i == 0) { |
2317 | f->store_line("" ); |
2318 | } |
2319 | f->store_line("[editable path=\"" + editable_instances[i].operator String() + "\"]" ); |
2320 | } |
2321 | } |
2322 | |
2323 | if (f->get_error() != OK && f->get_error() != ERR_FILE_EOF) { |
2324 | return ERR_CANT_CREATE; |
2325 | } |
2326 | |
2327 | return OK; |
2328 | } |
2329 | |
2330 | Error ResourceLoaderText::set_uid(Ref<FileAccess> p_f, ResourceUID::ID p_uid) { |
2331 | open(p_f, true); |
2332 | ERR_FAIL_COND_V(error != OK, error); |
2333 | ignore_resource_parsing = true; |
2334 | |
2335 | Ref<FileAccess> fw; |
2336 | |
2337 | fw = FileAccess::open(local_path + ".uidren" , FileAccess::WRITE); |
2338 | if (is_scene) { |
2339 | fw->store_string("[gd_scene load_steps=" + itos(resources_total) + " format=" + itos(FORMAT_VERSION) + " uid=\"" + ResourceUID::get_singleton()->id_to_text(p_uid) + "\"]" ); |
2340 | } else { |
2341 | String script_res_text; |
2342 | if (!script_class.is_empty()) { |
2343 | script_res_text = "script_class=\"" + script_class + "\" " ; |
2344 | } |
2345 | |
2346 | fw->store_string("[gd_resource type=\"" + res_type + "\" " + script_res_text + "load_steps=" + itos(resources_total) + " format=" + itos(FORMAT_VERSION) + " uid=\"" + ResourceUID::get_singleton()->id_to_text(p_uid) + "\"]" ); |
2347 | } |
2348 | |
2349 | uint8_t c = f->get_8(); |
2350 | while (!f->eof_reached()) { |
2351 | fw->store_8(c); |
2352 | c = f->get_8(); |
2353 | } |
2354 | |
2355 | bool all_ok = fw->get_error() == OK; |
2356 | |
2357 | if (!all_ok) { |
2358 | return ERR_CANT_CREATE; |
2359 | } |
2360 | |
2361 | return OK; |
2362 | } |
2363 | |
2364 | Error ResourceFormatSaverText::save(const Ref<Resource> &p_resource, const String &p_path, uint32_t p_flags) { |
2365 | if (p_path.ends_with(".tscn" ) && !Ref<PackedScene>(p_resource).is_valid()) { |
2366 | return ERR_FILE_UNRECOGNIZED; |
2367 | } |
2368 | |
2369 | ResourceFormatSaverTextInstance saver; |
2370 | return saver.save(p_path, p_resource, p_flags); |
2371 | } |
2372 | |
2373 | Error ResourceFormatSaverText::set_uid(const String &p_path, ResourceUID::ID p_uid) { |
2374 | String lc = p_path.to_lower(); |
2375 | if (!lc.ends_with(".tscn" ) && !lc.ends_with(".tres" )) { |
2376 | return ERR_FILE_UNRECOGNIZED; |
2377 | } |
2378 | |
2379 | String local_path = ProjectSettings::get_singleton()->localize_path(p_path); |
2380 | Error err = OK; |
2381 | { |
2382 | Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::READ); |
2383 | if (file.is_null()) { |
2384 | ERR_FAIL_V(ERR_CANT_OPEN); |
2385 | } |
2386 | |
2387 | ResourceLoaderText loader; |
2388 | loader.local_path = local_path; |
2389 | loader.res_path = loader.local_path; |
2390 | err = loader.set_uid(file, p_uid); |
2391 | } |
2392 | |
2393 | if (err == OK) { |
2394 | Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_RESOURCES); |
2395 | da->remove(local_path); |
2396 | da->rename(local_path + ".uidren" , local_path); |
2397 | } |
2398 | |
2399 | return err; |
2400 | } |
2401 | |
2402 | bool ResourceFormatSaverText::recognize(const Ref<Resource> &p_resource) const { |
2403 | return true; // All resources recognized! |
2404 | } |
2405 | |
2406 | void ResourceFormatSaverText::get_recognized_extensions(const Ref<Resource> &p_resource, List<String> *p_extensions) const { |
2407 | if (Ref<PackedScene>(p_resource).is_valid()) { |
2408 | p_extensions->push_back("tscn" ); // Text scene. |
2409 | } else { |
2410 | p_extensions->push_back("tres" ); // Text resource. |
2411 | } |
2412 | } |
2413 | |
2414 | ResourceFormatSaverText *ResourceFormatSaverText::singleton = nullptr; |
2415 | ResourceFormatSaverText::ResourceFormatSaverText() { |
2416 | singleton = this; |
2417 | } |
2418 | |