From 1f1cab0295ed5cba322c0a578e3cbeaaf202885c Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:44:25 -0700 Subject: [PATCH 01/10] Add option to use spaces instead of tabs for indenting GDScripts --- bytecode/bytecode_base.cpp | 22 +----- bytecode/bytecode_base.h | 6 ++ standalone/gdre_config_dialog.gd | 14 ++-- standalone/gdre_recover.gd | 2 +- utility/gdre_config.cpp | 121 +++++++++++++++++++++++++++++++ utility/gdre_config.h | 42 +++++++++++ utility/import_exporter.cpp | 13 ++-- 7 files changed, 188 insertions(+), 32 deletions(-) diff --git a/bytecode/bytecode_base.cpp b/bytecode/bytecode_base.cpp index 7c8e7eccd..c70cc8786 100644 --- a/bytecode/bytecode_base.cpp +++ b/bytecode/bytecode_base.cpp @@ -12,6 +12,7 @@ #include "compat/variant_decoder_compat.h" #include "compat/variant_writer_compat.h" #include "utility/common.h" +#include "utility/gdre_config.h" #include "utility/gdre_settings.h" #include "utility/godotver.h" @@ -623,24 +624,6 @@ Error GDScriptDecomp::decompile_buffer(Vector p_buffer) { Error err = get_script_state(p_buffer, s); ERR_FAIL_COND_V(err != OK, err); - int tab_size = 4; - - if (s.columns.size() > 0) { - Vector diffs; - int prev_column = 1; - for (auto &[key, value] : s.columns) { - int curr_column = value; - if (curr_column > prev_column) { - diffs.push_back(curr_column - prev_column); - } - prev_column = curr_column; - } - tab_size = gdre::get_most_popular_value(diffs); - if (tab_size <= 1) { - tab_size = 4; - } - } - Ref tokenizer = GDScriptTokenizerCompat::create_buffer_tokenizer(this, p_buffer); if (tokenizer.is_null()) { return ERR_INVALID_DATA; @@ -655,7 +638,8 @@ Error GDScriptDecomp::decompile_buffer(Vector p_buffer) { current = tokenizer->scan(); } - bool use_spaces = false; + bool use_spaces = (IndentType)GDREConfig::get_singleton()->get_setting("Script/Indent/type", 0) == INDENT_TYPE_SPACES; + int tab_size = GDREConfig::get_singleton()->get_setting("Script/Indent/size", 4).operator int(); bool first_line = true; int version = s.bytecode_version; int bytecode_version = get_bytecode_version(); diff --git a/bytecode/bytecode_base.h b/bytecode/bytecode_base.h index 8bc82592c..b0f5ee685 100644 --- a/bytecode/bytecode_base.h +++ b/bytecode/bytecode_base.h @@ -167,6 +167,12 @@ class GDScriptDecomp : public RefCounted { TOKEN_LINE_MASK = (1 << TOKEN_LINE_BITS) - 1, }; + enum IndentType { + INDENT_TYPE_TABS, + INDENT_TYPE_SPACES, + INDENT_TYPE_MAX, + }; + // bytecode_version, ids, constants, tokens, lines, columns struct ScriptState { int bytecode_version = -1; diff --git a/standalone/gdre_config_dialog.gd b/standalone/gdre_config_dialog.gd index 2629492e3..8aa0141cb 100644 --- a/standalone/gdre_config_dialog.gd +++ b/standalone/gdre_config_dialog.gd @@ -3,7 +3,7 @@ extends GDREWindow @export var show_ephemeral_settings: bool = false -signal config_changed(changed_settings: Dictionary[String, Variant]) +signal config_changed(changed_settings: Dictionary[String, Array]) func create_section_settings() -> LabelSettings: var label_settings: LabelSettings = LabelSettings.new() @@ -238,7 +238,9 @@ func create_setting_button(setting: GDREConfigSetting) -> Control: elif setting.get_type() == TYPE_INT: button = SpinBox.new() button.value = value - button.step = 1 + button.min_value = setting.get_min_value() + button.max_value = setting.get_max_value() + button.step = setting.get_step_value() var label: Label = make_button_label(setting.get_brief_description()) label.tooltip_text = setting.get_description() control = make_button_hbox(setting, button, label) @@ -246,7 +248,9 @@ func create_setting_button(setting: GDREConfigSetting) -> Control: elif setting.get_type() == TYPE_FLOAT: button = SpinBox.new() button.value = value - button.step = 0.1 + button.min_value = setting.get_min_value() + button.max_value = setting.get_max_value() + button.step = setting.get_step_value() var label: Label = make_button_label(setting.get_brief_description()) label.tooltip_text = setting.get_description() control = make_button_hbox(setting, button, label) @@ -325,10 +329,10 @@ func _render_settings(): func save_settings(): - var changed_settings: Dictionary[String, Variant] = {} + var changed_settings: Dictionary[String, Array] = {} for setting: GDREConfigSetting in setting_value_map.keys(): if setting.get_value() != setting_value_map[setting]: - changed_settings[setting.get_full_name()] = setting_value_map[setting] + changed_settings[setting.get_full_name()] = [setting.get_value(), setting_value_map[setting]] setting.set_value(setting_value_map[setting]) GDREConfig.save_config() if changed_settings.size() != 0 or force_change: diff --git a/standalone/gdre_recover.gd b/standalone/gdre_recover.gd index 8e7a12389..f8bdc5125 100644 --- a/standalone/gdre_recover.gd +++ b/standalone/gdre_recover.gd @@ -630,7 +630,7 @@ func _on_export_settings_button_pressed() -> void: %GDREConfigDialog.show() -func _on_gdre_config_dialog_config_changed(changed_settings: Dictionary[String, Variant]) -> void: +func _on_gdre_config_dialog_config_changed(changed_settings: Dictionary[String, Array]) -> void: GDRESettings.update_from_ephemeral_settings() diff --git a/utility/gdre_config.cpp b/utility/gdre_config.cpp index 57cbd4d13..50003a65c 100644 --- a/utility/gdre_config.cpp +++ b/utility/gdre_config.cpp @@ -277,6 +277,22 @@ HashMap> GDREConfig::_init_default_settings() { "Add imports to git repo", "Add .godot/imported/ (or .import/ for Godot 3) to the git repo.", false)), + memnew(GDREConfigSettingEnum( + "Script/Indent/type", + "Indent type", + "The type of indentation to use when decompiling and displaying scripts.", + 0, + "Tabs,Spaces", + false, + false)), + memnew(GDREConfigSettingRange( + "Script/Indent/size", + "Tab size", + "The number of spaces to use when decompiling and displaying scripts.", + 4, + "1,64,1", + false, + false)), memnew(GDREConfigSetting_BytecodeForceBytecodeRevision()), memnew(GDREConfigSetting_LoadCustomBytecode()), #if !GODOT_MONO_DECOMP_DISABLED @@ -594,6 +610,45 @@ void GDREConfigSetting::set_value(const Variant &p_value, bool p_force_ephemeral GDREConfig::get_singleton()->set_setting(full_name, p_value, p_force_ephemeral || ephemeral); } +Variant GDREConfigSetting::get_min_value() const { + switch (get_type()) { + case Variant::Type::INT: + return 0; + case Variant::Type::FLOAT: + return 0.0f; + case Variant::Type::BOOL: + return false; + default: + return Variant(); + } +} + +Variant GDREConfigSetting::get_max_value() const { + switch (get_type()) { + case Variant::Type::INT: + return INT_MAX; + case Variant::Type::FLOAT: + return 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0; + case Variant::Type::BOOL: + return true; + default: + return Variant(); + } +} + +Variant GDREConfigSetting::get_step_value() const { + switch (get_type()) { + case Variant::Type::INT: + return 1; + case Variant::Type::FLOAT: + return 0.01f; + case Variant::Type::BOOL: + return 1; + default: + return Variant(); + } +} + bool GDREConfigSetting::is_hidden() const { return hidden; } @@ -602,6 +657,68 @@ bool GDREConfigSetting::is_ephemeral() const { return ephemeral; } +GDREConfigSettingEnum::GDREConfigSettingEnum( + const String &p_full_name, + const String &p_brief, + const String &p_description, + const Variant &p_default_value, + const String &p_enum_values, + bool p_hidden, + bool p_ephemeral) : + GDREConfigSetting(p_full_name, p_brief, p_description, p_default_value, p_hidden, p_ephemeral) { + enum_values = p_enum_values.split(","); +} + +bool GDREConfigSettingEnum::has_special_value() const { + return true; +} + +Dictionary GDREConfigSettingEnum::get_list_of_possible_values() const { + Dictionary ret; + for (int i = 0; i < enum_values.size(); i++) { + ret[i] = enum_values[i]; + } + return ret; +} + +GDREConfigSettingRange::GDREConfigSettingRange( + const String &p_full_name, + const String &p_brief, + const String &p_description, + const Variant &p_default_value, + const String &p_range_string, bool p_hidden, bool p_ephemeral) : + GDREConfigSetting(p_full_name, p_brief, p_description, p_default_value, p_hidden, p_ephemeral) { + auto parts = p_range_string.split(","); + ERR_FAIL_COND_MSG(parts.size() != 3, "Invalid range string: " + p_range_string); + if (p_default_value.get_type() == Variant::Type::INT) { + min_value = parts[0].to_int(); + max_value = parts[1].to_int(); + step_value = parts[2].to_int(); + } else if (p_default_value.get_type() == Variant::Type::FLOAT) { + min_value = parts[0].to_float(); + max_value = parts[1].to_float(); + step_value = parts[2].to_float(); + } else { + ERR_FAIL_MSG("Invalid range string: " + p_range_string); + } +} + +Variant GDREConfigSettingRange::get_min_value() const { + return min_value; +} + +Variant GDREConfigSettingRange::get_max_value() const { + return max_value; +} + +Variant GDREConfigSettingRange::get_step_value() const { + return step_value; +} + +bool GDREConfigSettingRange::is_range_setting() const { + return true; +} + void GDREConfigSetting::_bind_methods() { ClassDB::bind_method(D_METHOD("reset"), &GDREConfigSetting::reset); ClassDB::bind_method(D_METHOD("set_value", "value", "force_ephemeral"), &GDREConfigSetting::set_value, DEFVAL(false)); @@ -617,6 +734,10 @@ void GDREConfigSetting::_bind_methods() { ClassDB::bind_method(D_METHOD("is_filepicker"), &GDREConfigSetting::is_filepicker); ClassDB::bind_method(D_METHOD("is_dirpicker"), &GDREConfigSetting::is_dirpicker); ClassDB::bind_method(D_METHOD("is_virtual_setting"), &GDREConfigSetting::is_virtual_setting); + ClassDB::bind_method(D_METHOD("is_range_setting"), &GDREConfigSetting::is_range_setting); + ClassDB::bind_method(D_METHOD("get_min_value"), &GDREConfigSetting::get_min_value); + ClassDB::bind_method(D_METHOD("get_max_value"), &GDREConfigSetting::get_max_value); + ClassDB::bind_method(D_METHOD("get_step_value"), &GDREConfigSetting::get_step_value); ClassDB::bind_method(D_METHOD("get_error_message"), &GDREConfigSetting::get_error_message); ClassDB::bind_method(D_METHOD("clear_error_message"), &GDREConfigSetting::clear_error_message); ClassDB::bind_method(D_METHOD("has_special_value"), &GDREConfigSetting::has_special_value); diff --git a/utility/gdre_config.h b/utility/gdre_config.h index a438c1c6a..05877a41f 100644 --- a/utility/gdre_config.h +++ b/utility/gdre_config.h @@ -32,6 +32,10 @@ class GDREConfigSetting : public RefCounted { bool is_hidden() const; Variant::Type get_type() const; bool is_ephemeral() const; + virtual bool is_range_setting() const { return false; } + virtual Variant get_min_value() const; + virtual Variant get_max_value() const; + virtual Variant get_step_value() const; virtual bool is_virtual_setting() const { return false; } virtual bool is_filepicker() const { return false; } virtual bool is_dirpicker() const { return false; } @@ -54,6 +58,44 @@ class GDREConfigSetting : public RefCounted { GDREConfigSetting() {} }; +class GDREConfigSettingEnum : public GDREConfigSetting { + GDSOFTCLASS(GDREConfigSettingEnum, GDREConfigSetting); + + Vector enum_values; + +public: + GDREConfigSettingEnum( + const String &p_full_name, + const String &p_brief, + const String &p_description, + const Variant &p_default_value, + const String &p_enum_values, + bool p_hidden = false, + bool p_ephemeral = false); + virtual bool has_special_value() const override; + virtual Dictionary get_list_of_possible_values() const override; +}; + +class GDREConfigSettingRange : public GDREConfigSetting { + GDSOFTCLASS(GDREConfigSettingRange, GDREConfigSetting); + + Variant min_value; + Variant max_value; + Variant step_value; + +public: + GDREConfigSettingRange( + const String &p_full_name, + const String &p_brief, + const String &p_description, + const Variant &p_default_value, + const String &p_range_string, bool p_hidden = false, bool p_ephemeral = false); + virtual Variant get_min_value() const override; + virtual Variant get_max_value() const override; + virtual Variant get_step_value() const override; + virtual bool is_range_setting() const override; +}; + class GDREConfig : public Object { GDCLASS(GDREConfig, Object); diff --git a/utility/import_exporter.cpp b/utility/import_exporter.cpp index 6aa1a1820..47332f440 100644 --- a/utility/import_exporter.cpp +++ b/utility/import_exporter.cpp @@ -1544,13 +1544,12 @@ Error ImportExporter::export_imports(const String &p_out_dir, const Vectoris_project_config_loaded()) { // some pcks do not have project configs write_project_metadata_cfg(output_dir); - // if constexpr (GDScriptDecomp::FORCE_SPACES_FOR_2_0) { - // // if we're at v4.5 or higher (<4.5 doesn't support editor_overrides), we want to set "editor_overrides/text_editor/behavior/indent/type" to "Spaces" - // // This avoids editor churn on the scripts when they're resaved by the editor - // if (get_ver_major() == 4 && get_ver_minor() >= 5 && get_ver_minor() < 7 && !get_settings()->has_project_setting("editor_overrides/text_editor/behavior/indent/type")) { - // get_settings()->set_project_setting("editor_overrides/text_editor/behavior/indent/type", 1); - // } - // } + if ((get_ver_major() > 4 || (get_ver_major() == 4 && get_ver_minor() >= 5))) { + // if we're at v4.5 or higher (<4.5 doesn't support editor_overrides), we want to set "editor_overrides/text_editor/behavior/indent/type" to the user defined value + // This avoids editor churn on the scripts when they're resaved by the editor + get_settings()->set_project_setting("editor_overrides/text_editor/behavior/indent/type", GDREConfig::get_singleton()->get_setting("Script/Indent/type", 0)); + get_settings()->set_project_setting("editor_overrides/text_editor/behavior/indent/size", GDREConfig::get_singleton()->get_setting("Script/Indent/size", 4)); + } if (get_settings()->save_project_config(output_dir) != OK) { print_line("ERROR: Failed to save project config!"); } else { From 1c5451724120c382ba4a8444d42b5c51983e2cde Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:13:11 -0700 Subject: [PATCH 02/10] refresh resource preview when settings change --- standalone/gdre_recover.gd | 1 + standalone/gdre_resource_preview.gd | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/standalone/gdre_recover.gd b/standalone/gdre_recover.gd index f8bdc5125..07799887d 100644 --- a/standalone/gdre_recover.gd +++ b/standalone/gdre_recover.gd @@ -632,6 +632,7 @@ func _on_export_settings_button_pressed() -> void: func _on_gdre_config_dialog_config_changed(changed_settings: Dictionary[String, Array]) -> void: GDRESettings.update_from_ephemeral_settings() + RESOURCE_PREVIEW.refresh() func _on_add_pcks_dialog_files_selected(paths: PackedStringArray) -> void: diff --git a/standalone/gdre_resource_preview.gd b/standalone/gdre_resource_preview.gd index 2529a72c6..e399af89e 100644 --- a/standalone/gdre_resource_preview.gd +++ b/standalone/gdre_resource_preview.gd @@ -313,6 +313,14 @@ func load_resource(path: String) -> void: if (%ResourceInfo.text == ""): pop_resource_info(path, info) +func refresh(): + print("Refreshing resource preview") + var current_view = get_currently_visible_view() + if current_view == %TextView and current_resource_path != "": + print("Loading text preview for ", current_resource_path) + %TextView.load_path(current_resource_path, %TextView.recognize(current_resource_path)) + # TODO: handle other views? Not currently necessary, config settings only affect the text view currently + func try_text_preview(path, type, res_type): if type == -1: From 1880fb7102c151405b8b92fe9dd72f25b99ae2ca Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:35:57 -0700 Subject: [PATCH 03/10] keep current view in text editor when reloading, respect user tab size --- standalone/gdre_resource_preview.gd | 4 +-- standalone/gdre_text_editor.gd | 50 ++++++++++++++++++----------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/standalone/gdre_resource_preview.gd b/standalone/gdre_resource_preview.gd index e399af89e..a8a5fd30c 100644 --- a/standalone/gdre_resource_preview.gd +++ b/standalone/gdre_resource_preview.gd @@ -314,11 +314,9 @@ func load_resource(path: String) -> void: pop_resource_info(path, info) func refresh(): - print("Refreshing resource preview") var current_view = get_currently_visible_view() if current_view == %TextView and current_resource_path != "": - print("Loading text preview for ", current_resource_path) - %TextView.load_path(current_resource_path, %TextView.recognize(current_resource_path)) + %TextView.reload_from_disk() # TODO: handle other views? Not currently necessary, config settings only affect the text view currently diff --git a/standalone/gdre_text_editor.gd b/standalone/gdre_text_editor.gd index 21a5e5fe5..42fb70632 100644 --- a/standalone/gdre_text_editor.gd +++ b/standalone/gdre_text_editor.gd @@ -59,6 +59,8 @@ enum HighlightType { XML } +var current_type: HighlightType = HighlightType.UNKNOWN + func _add_regions_to_gdscript_highlighter(): # Not working, if it ends in a newline, the color wraps to the next line gdscript_highlighter.add_color_region("@", " ", Color("#ffb373")) @@ -112,13 +114,6 @@ func _add_regions_to_gdscript_highlighter(): get: return xml_highlighter -@export var default_highlighter: HighlightType = HighlightType.GDSCRIPT: - set(val): - set_highlight_type(val) - default_highlighter = val - get: - return default_highlighter - @onready var font_size = get_theme_font_size("TextEdit") var code_opts_panel_button_icon = preload("res://gdre_icons/gdre_GuiTabMenuHl.svg") @@ -204,17 +199,30 @@ func _init(): code_opts_box.add_child(code_opts_panel_button) code_opts_box.add_child(CODE_VIWER_OPTIONS) add_child(code_opts_box) - set_highlight_type(default_highlighter) + set_text_viewer_props() func _ready(): - set_highlight_type(default_highlighter) + set_highlight_type(HighlightType.UNKNOWN) func reset(): current_path = "" set_viewer_text("") - set_text_viewer_props() + set_highlight_type(HighlightType.UNKNOWN) pass +func reload_from_disk(): + if current_path.is_empty(): + return + var h_scroll = CODE_VIEWER.scroll_horizontal + var v_scroll = CODE_VIEWER.scroll_vertical + var caret_line = CODE_VIEWER.get_caret_line() + var caret_column = CODE_VIEWER.get_caret_column() + load_path(current_path, current_type) + CODE_VIEWER.scroll_horizontal = h_scroll + CODE_VIEWER.scroll_vertical = v_scroll + CODE_VIEWER.set_caret_line(caret_line, false) + CODE_VIEWER.set_caret_column(caret_column, false) + func set_viewer_text(text: String): # This is a workaround for a bug in CodeEdit where setting the text property throws errors when wrapping is enabled if CODE_VIEWER.wrap_mode == TextEdit.LINE_WRAPPING_BOUNDARY: @@ -271,12 +279,12 @@ func load_code(path, override_bytecode_revision: int = 0) -> bool: if GDRESettings.has_loaded_dotnet_assembly(): var decompiler = GDRESettings.get_dotnet_decompiler() code_text = decompiler.decompile_individual_file(path) - set_csharp_viewer_props() + set_highlight_type(HighlightType.CSHARP) else: code_text = "Error loading script:\nNo .NET assembly loaded" - set_text_viewer_props() + set_highlight_type(HighlightType.TEXT) else: - set_csharp_viewer_props() + set_highlight_type(HighlightType.CSHARP) elif ext == "gde" or ext == "gdc": var script: FakeGDScript = FakeGDScript.new() if (override_bytecode_revision != 0): @@ -284,26 +292,26 @@ func load_code(path, override_bytecode_revision: int = 0) -> bool: script.load_source_code(path) if not script.get_error_message().is_empty(): code_text = "Error loading script:\n" + script.get_error_message() - set_text_viewer_props() + set_highlight_type(HighlightType.TEXT) else: code_text = script.get_source_code() - set_code_viewer_props() + set_highlight_type(HighlightType.GDSCRIPT) else: # ext == "gd" code_text = FileAccess.get_file_as_string(path) - set_code_viewer_props() + set_highlight_type(HighlightType.GDSCRIPT) set_viewer_text(code_text) return true func load_text_resource(path): current_path = path - set_resource_viewer_props() + set_highlight_type(HighlightType.GDRESOURCE) set_viewer_text(ResourceCompatLoader.resource_to_string(path)) return true func load_text_string(text): current_path = "" - set_text_viewer_props() + set_highlight_type(HighlightType.TEXT) set_viewer_text(text) return true @@ -377,7 +385,10 @@ func recognize(path): return HighlightType.UNKNOWN func set_highlight_type(type: HighlightType): - match type: + if current_type == type: + return + current_type = type + match current_type: HighlightType.GDSHADER: set_shader_viewer_props() HighlightType.GDSCRIPT: @@ -419,6 +430,7 @@ func set_common_code_viewer_props(is_code: bool): CODE_VIEWER.gutters_draw_fold_gutter = is_code CODE_VIEWER.gutters_draw_line_numbers = is_code CODE_VIEWER.auto_brace_completion_highlight_matching = is_code + CODE_VIEWER.set_tab_size(GDREConfig.get_setting("Script/Indent/size")) func set_shader_viewer_props(): From 83aa07acf7a477275ae24cc9c02d7b006a62a219 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:30:41 -0700 Subject: [PATCH 04/10] update fakemesh code to reflect 3.x fixes --- compat/fake_mesh.cpp | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/compat/fake_mesh.cpp b/compat/fake_mesh.cpp index 953f4ec49..1361b709a 100644 --- a/compat/fake_mesh.cpp +++ b/compat/fake_mesh.cpp @@ -196,19 +196,20 @@ void _fix_array_compatibility(const Vector &p_src, uint64_t p_old_forma if ((p_old_format & OLD_ARRAY_COMPRESS_NORMAL) && (p_old_format & OLD_ARRAY_FORMAT_TANGENT) && (p_old_format & OLD_ARRAY_COMPRESS_TANGENT)) { for (uint32_t i = 0; i < p_elements; i++) { const int8_t *src = (const int8_t *)&src_vertex_ptr[i * src_vertex_stride + src_offset]; - int16_t *dst = (int16_t *)&dst_vertex_ptr[i * dst_normal_tangent_stride + dst_offsets[Mesh::ARRAY_NORMAL]]; + uint16_t *dst = (uint16_t *)&dst_vertex_ptr[i * dst_normal_tangent_stride + dst_offsets[Mesh::ARRAY_NORMAL]]; - dst[0] = (int16_t)CLAMP(src[0] / 127.0f * 32767, -32768, 32767); - dst[1] = (int16_t)CLAMP(src[1] / 127.0f * 32767, -32768, 32767); + // 4.x requires biasing the octahedron components to a 0.0 <-> 1.0 range, whereas in 3.x they were stored in the -1.0 <-> 1.0 range + dst[0] = (uint16_t)CLAMP((src[0] / 127.0f * .5f + .5f) * 65535, 0, 65535); + dst[1] = (uint16_t)CLAMP((src[1] / 127.0f * .5f + .5f) * 65535, 0, 65535); } src_offset += sizeof(int8_t) * 2; } else { for (uint32_t i = 0; i < p_elements; i++) { const int16_t *src = (const int16_t *)&src_vertex_ptr[i * src_vertex_stride + src_offset]; - int16_t *dst = (int16_t *)&dst_vertex_ptr[i * dst_normal_tangent_stride + dst_offsets[Mesh::ARRAY_NORMAL]]; + uint16_t *dst = (uint16_t *)&dst_vertex_ptr[i * dst_normal_tangent_stride + dst_offsets[Mesh::ARRAY_NORMAL]]; - dst[0] = src[0]; - dst[1] = src[1]; + dst[0] = (uint16_t)CLAMP((src[0] / 32767.0f * .5f + .5f) * 65535, 0, 65535); + dst[1] = (uint16_t)CLAMP((src[1] / 32767.0f * .5f + .5f) * 65535, 0, 65535); } src_offset += sizeof(int16_t) * 2; } @@ -216,7 +217,7 @@ void _fix_array_compatibility(const Vector &p_src, uint64_t p_old_forma if (p_old_format & OLD_ARRAY_COMPRESS_NORMAL) { for (uint32_t i = 0; i < p_elements; i++) { const int8_t *src = (const int8_t *)&src_vertex_ptr[i * src_vertex_stride + src_offset]; - const Vector3 original_normal(src[0], src[1], src[2]); + const Vector3 original_normal(src[0] / 127.0f, src[1] / 127.0f, src[2] / 127.0f); Vector2 res = original_normal.octahedron_encode(); uint16_t *dst = (uint16_t *)&dst_vertex_ptr[i * dst_normal_tangent_stride + dst_offsets[Mesh::ARRAY_NORMAL]]; @@ -264,10 +265,10 @@ void _fix_array_compatibility(const Vector &p_src, uint64_t p_old_forma if (p_old_format & OLD_ARRAY_COMPRESS_TANGENT) { for (uint32_t i = 0; i < p_elements; i++) { const int8_t *src = (const int8_t *)&src_vertex_ptr[i * src_vertex_stride + src_offset]; - const Vector3 original_tangent(src[0], src[1], src[2]); - Vector2 res = original_tangent.octahedron_tangent_encode(src[3]); + const Vector3 original_tangent(src[0] / 127.0f, src[1] / 127.0f, src[2] / 127.0f); + Vector2 res = original_tangent.octahedron_tangent_encode(src[3] / 127.0f); - uint16_t *dst = (uint16_t *)&dst_vertex_ptr[i * dst_normal_tangent_stride + dst_offsets[Mesh::ARRAY_NORMAL]]; + uint16_t *dst = (uint16_t *)&dst_vertex_ptr[i * dst_normal_tangent_stride + dst_offsets[Mesh::ARRAY_TANGENT]]; dst[0] = (uint16_t)CLAMP(res.x * 65535, 0, 65535); dst[1] = (uint16_t)CLAMP(res.y * 65535, 0, 65535); if (dst[0] == 0 && dst[1] == 65535) { @@ -283,7 +284,7 @@ void _fix_array_compatibility(const Vector &p_src, uint64_t p_old_forma const Vector3 original_tangent(src[0], src[1], src[2]); Vector2 res = original_tangent.octahedron_tangent_encode(src[3]); - uint16_t *dst = (uint16_t *)&dst_vertex_ptr[i * dst_normal_tangent_stride + dst_offsets[Mesh::ARRAY_NORMAL]]; + uint16_t *dst = (uint16_t *)&dst_vertex_ptr[i * dst_normal_tangent_stride + dst_offsets[Mesh::ARRAY_TANGENT]]; dst[0] = (uint16_t)CLAMP(res.x * 65535, 0, 65535); dst[1] = (uint16_t)CLAMP(res.y * 65535, 0, 65535); if (dst[0] == 0 && dst[1] == 65535) { @@ -520,7 +521,7 @@ bool FakeMesh::_set(const StringName &p_name, const Variant &p_value) { new_format |= ARRAY_FORMAT_INDEX; } if (old_format & OLD_ARRAY_FLAG_USE_2D_VERTICES) { - new_format |= OLD_ARRAY_FLAG_USE_2D_VERTICES; + new_format |= ARRAY_FLAG_USE_2D_VERTICES; } Vector vertex_array; From d1b7814e9542268db05ac0ebfad0938a5db63e64 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:27:55 -0700 Subject: [PATCH 05/10] fix config dialog not showing centered --- standalone/gdre_recover.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standalone/gdre_recover.gd b/standalone/gdre_recover.gd index 07799887d..08f6b280c 100644 --- a/standalone/gdre_recover.gd +++ b/standalone/gdre_recover.gd @@ -627,7 +627,7 @@ func _on_assembly_focus_exited() -> void: func _on_export_settings_button_pressed() -> void: %GDREConfigDialog.clear() - %GDREConfigDialog.show() + %GDREConfigDialog.popup_centered() func _on_gdre_config_dialog_config_changed(changed_settings: Dictionary[String, Array]) -> void: From 4173042d208bb92a20d611360f8b00b67b9b1c2a Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:46:32 -0700 Subject: [PATCH 06/10] fix `set_real_from_missing_resource` not setting certain properties --- compat/resource_loader_compat.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/compat/resource_loader_compat.cpp b/compat/resource_loader_compat.cpp index 7abc67caf..88f9b0ff5 100644 --- a/compat/resource_loader_compat.cpp +++ b/compat/resource_loader_compat.cpp @@ -830,10 +830,7 @@ Ref ResourceCompatConverter::set_real_from_missing_resource(Refget_property_list(&property_info); for (auto &property : property_info) { bool is_storage = property.usage & PROPERTY_USAGE_STORAGE; - // if (property.usage & PROPERTY_USAGE_STORAGE) { - if (property.name == "resource_path") { - res->set_path_cache(mr->get(property.name)); - } else if (is_storage) { + if (is_storage) { Ref m_res = mr->get(property.name); const String &set_prop = prop_map.has(property.name) ? prop_map[property.name] : property.name; if (m_res.is_valid()) { @@ -841,12 +838,11 @@ Ref ResourceCompatConverter::set_real_from_missing_resource(Refset(set_prop, mr->get(property.name)); } - } else { - // WARN_PRINT("Property " + property.name + " is not storage"); } - // } } res->set_path_cache(mr->get_path()); + res->set_local_to_scene(mr->is_local_to_scene()); + res->set_scene_unique_id(mr->get_scene_unique_id()); return res; } From 5c21e9993865ae0a730eb41f5acfac25ae9f46a4 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:47:28 -0700 Subject: [PATCH 07/10] Add FakeMesh::load_from_array_mesh and FakeMesh::mesh_to_array_mesh --- compat/fake_mesh.cpp | 53 +++++++++++++++++++++++++++++++++++++++++--- compat/fake_mesh.h | 6 ++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/compat/fake_mesh.cpp b/compat/fake_mesh.cpp index 1361b709a..8fb4daae6 100644 --- a/compat/fake_mesh.cpp +++ b/compat/fake_mesh.cpp @@ -29,11 +29,10 @@ /**************************************************************************/ #include "fake_mesh.h" - -#include "core/math/convex_hull.h" #include "core/object/class_db.h" -#include "core/templates/pair.h" +#include "scene/resources/3d/importer_mesh.h" #include "scene/resources/surface_tool.h" +#include "servers/rendering/rendering_server.h" #ifndef PHYSICS_3D_DISABLED #include "scene/resources/3d/concave_polygon_shape_3d.h" @@ -1484,6 +1483,54 @@ Ref FakeMesh::get_shadow_mesh() const { return shadow_mesh; } +Ref FakeMesh::mesh_to_array_mesh(const Ref &p_mesh) { + if (p_mesh.is_null()) { + return Ref(); + } + Ref ret_mesh = p_mesh; + if (ret_mesh.is_valid()) { + return ret_mesh; + } + Ref importer_mesh = ImporterMesh::from_mesh(p_mesh); + + Ref fake_mesh = p_mesh; + if (fake_mesh.is_valid()) { + // Convert blend shape mode and names if any. + if (fake_mesh->get_blend_shape_count() > 0) { + importer_mesh->set_blend_shape_mode(fake_mesh->get_blend_shape_mode()); + } + for (int32_t surface_i = 0; surface_i < p_mesh->get_surface_count(); surface_i++) { + String surface_name = fake_mesh->surface_get_name(surface_i); + if (!surface_name.is_empty()) { + importer_mesh->set_surface_name(surface_i, surface_name); + } + } + } + ret_mesh = importer_mesh->get_mesh(); + + // Merge metadata. + ret_mesh->merge_meta_from(*p_mesh); + ret_mesh->set_name(p_mesh->get_name()); + ret_mesh->set_local_to_scene(p_mesh->is_local_to_scene()); + ret_mesh->set_scene_unique_id(p_mesh->get_scene_unique_id()); + ret_mesh->set_path_cache(p_mesh->get_path()); + + return ret_mesh; +} + +Ref FakeMesh::load_from_array_mesh(const String &p_path) { + Error err; + Ref mr = ResourceCompatLoader::fake_load(p_path, "", &err); + ERR_FAIL_COND_V_MSG(err != OK || !mr.is_valid(), Ref(), "Failed to load mesh: " + p_path); + ERR_FAIL_COND_V_MSG(mr->get_original_class() != "ArrayMesh", Ref(), "Mesh is not an array mesh: " + p_path); + + Ref mesh; + mesh.instantiate(); + mesh->load_type = ResourceCompatLoader::get_default_load_type(); + ResourceCompatConverter::set_real_from_missing_resource(mr, mesh, mesh->load_type); + return mesh; +} + void FakeMesh::_bind_methods() { ClassDB::bind_method(D_METHOD("add_blend_shape", "name"), &FakeMesh::add_blend_shape); ClassDB::bind_method(D_METHOD("get_blend_shape_count"), &FakeMesh::get_blend_shape_count); diff --git a/compat/fake_mesh.h b/compat/fake_mesh.h index c094a7855..6caed3963 100644 --- a/compat/fake_mesh.h +++ b/compat/fake_mesh.h @@ -30,10 +30,7 @@ #pragma once #include "core/io/resource.h" -#include "core/math/face3.h" -#include "core/math/triangle_mesh.h" #include "scene/resources/material.h" -#include "servers/rendering/rendering_server.h" #include "utility/resource_info.h" @@ -45,6 +42,7 @@ class ConvexPolygonShape3D; class Shape3D; #endif // PHYSICS_3D_DISABLED class MeshConvexDecompositionSettings; +class ImporterMesh; #include "scene/resources/mesh.h" @@ -155,6 +153,8 @@ class FakeMesh : public Mesh { void set_shadow_mesh(const Ref &p_mesh); Ref get_shadow_mesh() const; + static Ref mesh_to_array_mesh(const Ref &p_mesh); + static Ref load_from_array_mesh(const String &p_path); FakeMesh(); From a61a5459bf7eb6e14db7e593020fe0209d9ba48f Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:48:10 -0700 Subject: [PATCH 08/10] improve ObjExporter loading, add `export_file_with_options` --- exporters/obj_exporter.cpp | 127 ++++++++++++++++++------------------- exporters/obj_exporter.h | 3 + 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/exporters/obj_exporter.cpp b/exporters/obj_exporter.cpp index 98080fdc1..e352466d6 100644 --- a/exporters/obj_exporter.cpp +++ b/exporters/obj_exporter.cpp @@ -4,6 +4,7 @@ #include "core/error/error_list.h" #include "core/io/file_access.h" #include "core/templates/hashfuncs.h" +#include "exporters/export_report.h" #include "scene/resources/material.h" #include "scene/resources/mesh.h" #include "utility/common.h" @@ -450,12 +451,65 @@ Error ObjExporter::write_meshes_to_obj(const Vector> &p_meshes, const return _write_meshes_to_obj(p_meshes, p_path, "", mesh_info); } +Ref real_load(const String &p_path, Error *r_error) { + if (!ResourceCompatLoader::is_globally_available() || ResourceCompatLoader::get_default_load_type() != ResourceInfo::LoadType::GLTF_LOAD) { + return ResourceCompatLoader::custom_load(p_path, "", ResourceInfo::LoadType::GLTF_LOAD, r_error, false, ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP); + } else { + return ResourceCompatLoader::load_with_real_resource_loader(p_path, "", r_error, false, ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP); + } +} + +Vector> ObjExporter::load_meshes_as_fake_meshes(const HashSet &p_paths) { + Vector> meshes; + for (auto &path : p_paths) { + Error err = OK; + Ref res; + if (ResourceCompatLoader::get_resource_type(path) == "ArrayMesh") { + res = FakeMesh::load_from_array_mesh(path); + } else { + res = real_load(path, &err); + } + ERR_FAIL_COND_V_MSG(err != OK || res.is_null(), Vector>(), "Failed to load mesh: " + path); + meshes.push_back(res); + } + return meshes; +} + +Vector> ObjExporter::load_meshes(const HashSet &p_paths, int ver_major, bool p_for_scene_export) { + Vector> meshes; + bool use_fake_meshes = !p_for_scene_export || ver_major <= 3; + if (use_fake_meshes) { + meshes = load_meshes_as_fake_meshes(p_paths); + } else { + Error err = OK; + for (auto &path : p_paths) { + Ref mesh = real_load(path, &err); + if (mesh.is_null()) { + ERR_FAIL_V_MSG(Vector>(), "Not a valid mesh resource: " + path); + } + meshes.push_back(mesh); + } + } + if (use_fake_meshes && p_for_scene_export) { + for (int i = 0; i < meshes.size(); i++) { + meshes.write[i] = FakeMesh::mesh_to_array_mesh(meshes[i]); + if (meshes[i].is_null()) { + ERR_FAIL_V_MSG(Vector>(), "Failed to convert mesh to array mesh: " + *p_paths.begin()); + } + } + } + return meshes; +} + Error ObjExporter::export_file(const String &p_out_path, const String &p_source_path) { - Error err; - Ref mesh = ResourceCompatLoader::custom_load(p_source_path, "ArrayMesh", ResourceInfo::LoadType::GLTF_LOAD, &err, false, ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP); - ERR_FAIL_COND_V_MSG(mesh.is_null(), ERR_FILE_UNRECOGNIZED, "Not a valid mesh resource: " + p_source_path); + return ObjExporter::export_file_with_options(p_out_path, p_source_path, {}); +} - return write_meshes_to_obj({ mesh }, p_out_path); +Error ObjExporter::export_file_with_options(const String &p_out_path, const String &p_source_path, const Dictionary &p_options) { + ERR_FAIL_COND_V_MSG(!p_out_path.has_extension("obj"), ERR_INVALID_PARAMETER, "Output path must have .obj extension"); + Vector> meshes = load_meshes({ p_source_path }, 0, false); + ERR_FAIL_COND_V_MSG(meshes.is_empty(), ERR_FILE_UNRECOGNIZED, "Error loading mesh: " + p_source_path); + return write_meshes_to_obj(meshes, p_out_path); } void ObjExporter::get_handled_types(List *r_types) const { @@ -469,7 +523,7 @@ void ObjExporter::get_handled_importers(List *r_importers) const { } void ObjExporter::_bind_methods() { - // No methods to bind in this implementation + ClassDB::bind_static_method(get_class_static(), D_METHOD("export_file_with_options", "out_path", "source_path", "options"), &ObjExporter::export_file_with_options); } void ObjExporter::rewrite_import_params(Ref p_import_info, const MeshInfo &p_mesh_info) { @@ -505,42 +559,6 @@ void ObjExporter::rewrite_import_params(Ref p_import_info, const Mes // 2.x doesn't require this } -Vector> load_meshes_as_fake_meshes(const HashSet &p_paths, Ref report) { - Vector> meshes; - for (auto &path : p_paths) { - Error err; - Ref mr = ResourceCompatLoader::fake_load(path, "", &err); - if (err != OK) { - report->set_error(err); - report->set_message("Failed to load mesh: " + path); - return Vector>(); - } - - Ref mesh; - mesh.instantiate(); - mesh->load_type = ResourceCompatLoader::get_default_load_type(); - List property_info; - mr->get_property_list(&property_info); - for (auto &property : property_info) { - bool is_storage = property.usage & PROPERTY_USAGE_STORAGE; - // if (property.usage & PROPERTY_USAGE_STORAGE) { - if (property.name == "resource_path") { - mesh->set_path_cache(mr->get(property.name)); - } else if (is_storage) { - mesh->set(property.name, mr->get(property.name)); - } else { - // WARN_PRINT("Property " + property.name + " is not storage"); - } - // } - } - mesh->set_path_cache(mr->get_path()); - mesh->merge_meta_from(mr.ptr()); - - meshes.push_back(mesh); - } - return meshes; -} - Ref ObjExporter::export_resource(const String &p_output_dir, Ref p_import_info) { // TODO: This may fail on Godot 2.x objs due to "blend_shapes" property being named "morph_targets" in 2.x, // but I can't find any to test... @@ -563,37 +581,16 @@ Ref ObjExporter::export_resource(const String &p_output_dir, Ref> meshes; // deduplicate - size_t errors_before = supports_multithread() ? GDRELogger::get_thread_error_count() : GDRELogger::get_error_count(); auto dest_files = gdre::vector_to_hashset(p_import_info->get_dest_files()); // always force this if we're using multithreading, or if we're <= 3.x // if we support multithreading, loading real ArrayMesh objects on the task thread will often cause segfaults // if we're recovering a mesh from 3.x or lower, they supported meshes with > 256 surfaces, loading those will fail - bool use_fake_meshes = supports_multithread() || p_import_info->get_ver_major() <= 3; - if (use_fake_meshes) { - meshes = load_meshes_as_fake_meshes(dest_files, report); - } else { - for (auto &path : dest_files) { - Ref mesh; - if (!ResourceCompatLoader::is_globally_available() || ResourceCompatLoader::get_default_load_type() != ResourceInfo::LoadType::GLTF_LOAD) { - mesh = ResourceCompatLoader::custom_load(path, "ArrayMesh", ResourceInfo::LoadType::GLTF_LOAD, &err, false, ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP); - } else { - mesh = ResourceCompatLoader::load_with_real_resource_loader(path, "ArrayMesh", &err, false, ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP); - // mesh = ResourceLoader::load(path, "ArrayMesh", ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP, &err); - } - if (mesh.is_null()) { - report->set_error(ERR_FILE_UNRECOGNIZED); - report->set_message("Not a valid mesh resource: " + path); - return report; - } - meshes.push_back(mesh); - } - } - size_t errors_after = supports_multithread() ? GDRELogger::get_thread_error_count() : GDRELogger::get_error_count(); - if (errors_after > errors_before) { + meshes = load_meshes(dest_files, p_import_info->get_ver_major(), false); + if (meshes.is_empty()) { #if DEBUG_ENABLED // save it to a text resource so that we can see the resource errors for (auto &path : dest_files) { - ResourceCompatLoader::to_text(path, p_output_dir.path_join((path.get_basename() + ".tres").trim_prefix("res://")), err); + ResourceCompatLoader::to_text(path, p_output_dir.path_join((path.get_basename() + ".tres").trim_prefix("res://")), 0); } #endif report->set_error(ERR_INVALID_DATA); diff --git a/exporters/obj_exporter.h b/exporters/obj_exporter.h index 2b13dcbd7..79769c7c7 100644 --- a/exporters/obj_exporter.h +++ b/exporters/obj_exporter.h @@ -8,6 +8,7 @@ class ObjExporter : public ResourceExporter { private: static Error write_materials_to_mtl(const HashMap> &p_materials, const String &p_path, const String &p_output_dir, bool force_single_precision); + static Vector> load_meshes_as_fake_meshes(const HashSet &p_paths); protected: static void _bind_methods(); @@ -27,6 +28,8 @@ class ObjExporter : public ResourceExporter { String path; }; + static Vector> load_meshes(const HashSet &p_paths, int ver_major, bool p_for_scene_export); + static Error export_file_with_options(const String &p_out_path, const String &p_source_path, const Dictionary &p_options); static constexpr const char *const EXPORTER_NAME = "Wavefront OBJ"; virtual String get_name() const override; static Error _write_meshes_to_obj(const Vector> &p_meshes, const String &p_path, const String &p_output_dir, MeshInfo &r_mesh_info); From 4c800d68ed9576929ea428723bc5d66db51eac16 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:50:34 -0700 Subject: [PATCH 09/10] scene: don't run export_file_with_options on task thread if we're already on one --- exporters/scene_exporter.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/exporters/scene_exporter.cpp b/exporters/scene_exporter.cpp index 9708c9e34..a586cdf36 100644 --- a/exporters/scene_exporter.cpp +++ b/exporters/scene_exporter.cpp @@ -3534,7 +3534,13 @@ Ref SceneExporter::export_file_with_options(const String &out_path } return token->report; } - Error err = TaskManager::get_singleton()->run_task(token, nullptr, "Exporting scene " + res_path, -1, true, true, true); + Error err; + if (Thread::is_main_thread()) { + err = TaskManager::get_singleton()->run_task(token, nullptr, "Exporting scene " + res_path, -1, true, true, true); + } else { // we're on a task thread, run it here + token->batch_export_instanced_scene(); + err = token->err; + } token->post_export(err); if (remove_physics_bodies) { register_physics_extension(); From 9c051194219716b4247369db588639826c619ef8 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:14:06 -0700 Subject: [PATCH 10/10] fix fakemesh creation --- compat/fake_mesh.cpp | 33 ++++++++++++++++++--------------- compat/fake_mesh.h | 2 ++ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/compat/fake_mesh.cpp b/compat/fake_mesh.cpp index 8fb4daae6..af6e067ab 100644 --- a/compat/fake_mesh.cpp +++ b/compat/fake_mesh.cpp @@ -1458,17 +1458,7 @@ void FakeMesh::set_shadow_mesh(const Ref &p_mesh) { ERR_FAIL_COND_MSG(p_mesh == this, "Cannot set a mesh as its own shadow mesh."); Ref mr = p_mesh; if (mr.is_valid()) { - Ref m_res = mr; - if (ResourceCompatConverter::is_external_resource(mr)) { - Error err; - m_res = ResourceCompatLoader::fake_load(mr->get_path(), "", &err); - ERR_FAIL_COND_MSG(err != OK || !m_res.is_valid(), "Failed to load material: " + mr->get_path()); - } - Ref fake_mesh; - fake_mesh.instantiate(); - fake_mesh->load_type = load_type; - - shadow_mesh = ResourceCompatConverter::set_real_from_missing_resource(m_res, fake_mesh, load_type); + shadow_mesh = create_from_missing_resource(mr, load_type); } else { shadow_mesh = p_mesh; } @@ -1521,13 +1511,26 @@ Ref FakeMesh::mesh_to_array_mesh(const Ref &p_mesh) { Ref FakeMesh::load_from_array_mesh(const String &p_path) { Error err; Ref mr = ResourceCompatLoader::fake_load(p_path, "", &err); - ERR_FAIL_COND_V_MSG(err != OK || !mr.is_valid(), Ref(), "Failed to load mesh: " + p_path); - ERR_FAIL_COND_V_MSG(mr->get_original_class() != "ArrayMesh", Ref(), "Mesh is not an array mesh: " + p_path); + return create_from_missing_resource(mr, ResourceCompatLoader::get_default_load_type()); +} +Ref FakeMesh::create_from_missing_resource(const Ref &p_mr, ResourceInfo::LoadType p_load_type) { + ERR_FAIL_COND_V_MSG(!p_mr.is_valid(), Ref(), "Missing resource is not valid"); + ERR_FAIL_COND_V_MSG(p_mr->get_original_class() != "ArrayMesh", Ref(), "Missing resource is not an array mesh"); Ref mesh; mesh.instantiate(); - mesh->load_type = ResourceCompatLoader::get_default_load_type(); - ResourceCompatConverter::set_real_from_missing_resource(mr, mesh, mesh->load_type); + mesh->load_type = p_load_type; + // Not using ResourceCompatConverter::set_real_from_missing_resource because we have to convert any embedded ArrayMeshes to FakeMeshes + List property_info; + p_mr->get_property_list(&property_info); + for (auto &property : property_info) { + if (property.usage & PROPERTY_USAGE_STORAGE) { + mesh->set(property.name, p_mr->get(property.name)); + } + } + mesh->set_path_cache(p_mr->get_path()); + mesh->set_local_to_scene(p_mr->is_local_to_scene()); + mesh->set_scene_unique_id(p_mr->get_scene_unique_id()); return mesh; } diff --git a/compat/fake_mesh.h b/compat/fake_mesh.h index 6caed3963..ac6f6080d 100644 --- a/compat/fake_mesh.h +++ b/compat/fake_mesh.h @@ -43,6 +43,7 @@ class Shape3D; #endif // PHYSICS_3D_DISABLED class MeshConvexDecompositionSettings; class ImporterMesh; +class MissingResource; #include "scene/resources/mesh.h" @@ -154,6 +155,7 @@ class FakeMesh : public Mesh { void set_shadow_mesh(const Ref &p_mesh); Ref get_shadow_mesh() const; static Ref mesh_to_array_mesh(const Ref &p_mesh); + static Ref create_from_missing_resource(const Ref &p_mr, ResourceInfo::LoadType p_load_type); static Ref load_from_array_mesh(const String &p_path); FakeMesh();