From 044a514211c0ba2b97d0b904375d6eb3d47823cf Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:45:43 -0700 Subject: [PATCH 1/9] add option to export mesh as a GLB/GLTF file in the single resource export dialogue --- exporters/scene_exporter.cpp | 52 ++++++++++++++++++++------ exporters/scene_exporter.h | 4 +- standalone/gdre_recover.gd | 72 +++++++++++++++++++++++++++++++++++- 3 files changed, 113 insertions(+), 15 deletions(-) diff --git a/exporters/scene_exporter.cpp b/exporters/scene_exporter.cpp index a586cdf36..30a07335f 100644 --- a/exporters/scene_exporter.cpp +++ b/exporters/scene_exporter.cpp @@ -2849,7 +2849,7 @@ Node *GLBExporterInstance::_instantiate_scene(Ref scene) { return root; } -Error GLBExporterInstance::_load_scene_and_deps(Ref &r_scene) { +Error GLBExporterInstance::_load_scene_and_deps(Ref &r_scene) { MeshInstance3D::upgrading_skeleton_compat = true; err = _load_deps(); if (err != OK) { @@ -2858,7 +2858,7 @@ Error GLBExporterInstance::_load_scene_and_deps(Ref &r_scene) { return _load_scene(r_scene); } -Error GLBExporterInstance::_load_scene(Ref &r_scene) { +Error GLBExporterInstance::_load_scene(Ref &r_scene) { auto mode_type = ResourceCompatLoader::get_default_load_type(); // loading older scenes will spam warnings about deprecated features #ifndef DEBUG_ENABLED @@ -2866,15 +2866,15 @@ Error GLBExporterInstance::_load_scene(Ref &r_scene) { _silence_errors(true); } #endif - std::optional> result; + std::optional> result; // For some reason, scenes with meshes fail to load without the load done by ResourceLoader::load, possibly due to notification shenanigans. if (ResourceCompatLoader::is_globally_available()) { - result = TaskManager::get_singleton()->dispatch_to_main_thread((std::function()>)[&]() -> Ref { - return ResourceLoader::load(source_path, "PackedScene", ResourceFormatLoader::CACHE_MODE_REUSE, &err); + result = TaskManager::get_singleton()->dispatch_to_main_thread((std::function()>)[&]() -> Ref { + return ResourceLoader::load(source_path, "", ResourceFormatLoader::CACHE_MODE_REUSE, &err); }); } else { - result = TaskManager::get_singleton()->dispatch_to_main_thread((std::function()>)[&]() -> Ref { - return ResourceCompatLoader::custom_load(source_path, "PackedScene", mode_type, &err, using_threaded_load(), ResourceFormatLoader::CACHE_MODE_REUSE); + result = TaskManager::get_singleton()->dispatch_to_main_thread((std::function()>)[&]() -> Ref { + return ResourceCompatLoader::custom_load(source_path, "", mode_type, &err, using_threaded_load(), ResourceFormatLoader::CACHE_MODE_REUSE); }); } if (!result.has_value()) { @@ -3294,6 +3294,18 @@ struct BatchExportToken : public TaskRunnerStruct { return _check_unsupported(ver_major, is_text_output()); } + Error create_packed_scene_from_mesh(const Ref &mesh, Ref &scene) { + ERR_FAIL_COND_V_MSG(mesh.is_null(), ERR_INVALID_PARAMETER, "Mesh is null"); + return TaskManager::get_singleton()->dispatch_to_main_thread((std::function)[&]() -> Error { + MeshInstance3D *root = memnew(MeshInstance3D); + root->set_mesh(mesh); + scene = Ref(memnew(PackedScene)); + scene->pack(root); + return OK; + }) + .value_or(ERR_SKIP); + } + // scene loading and scene instancing has to be done on the main thread to avoid deadlocks and crashes bool batch_preload() { GDRELogger::clear_error_queues(); @@ -3318,13 +3330,31 @@ struct BatchExportToken : public TaskRunnerStruct { err = gdre::ensure_dir(p_dest_path.get_base_dir()); report->set_error(err); - ERR_FAIL_COND_V_MSG(err, false, "Failed to ensure directory " + p_dest_path.get_base_dir()); + if (err) { + after_preload(); + ERR_FAIL_V_MSG(false, "Failed to ensure directory " + p_dest_path.get_base_dir()); + } { - Ref scene; - err = instance._load_scene_and_deps(scene); - if (scene.is_null() && err == OK) { + String resource_type = report->get_import_info()->get_type(); + bool is_mesh = false; + if (resource_type != "PackedScene") { + if (resource_type != "Mesh" && !ClassDB::is_parent_class(resource_type, "Mesh")) { + after_preload(); + ERR_FAIL_V_MSG(false, "Unsupported resource type: " + resource_type); + } + is_mesh = true; + } + Ref resource; + err = instance._load_scene_and_deps(resource); + if (resource.is_null() && err == OK) { err = ERR_CANT_ACQUIRE_RESOURCE; } + + Ref scene = resource; + if (err == OK && scene.is_null() && is_mesh) { + err = create_packed_scene_from_mesh(resource, scene); + } + if (err != OK) { report->set_error(err); after_preload(); diff --git a/exporters/scene_exporter.h b/exporters/scene_exporter.h index 811861c29..51cebb4fc 100644 --- a/exporters/scene_exporter.h +++ b/exporters/scene_exporter.h @@ -174,8 +174,8 @@ class GLBExporterInstance { Dictionary _get_default_subresource_options(); Error _check_model_can_load(const String &p_dest_path); Error _load_deps(); - Error _load_scene_and_deps(Ref &r_scene); - Error _load_scene(Ref &r_scene); + Error _load_scene_and_deps(Ref &r_scene); + Error _load_scene(Ref &r_scene); void recompute_animation_tracks_for_library(AnimationPlayer *p_player, const Ref &p_anim_lib, const LocalVector &p_anim_names); void convert_animation_tracks_to_v4_for_player(AnimationPlayer *p_player); diff --git a/standalone/gdre_recover.gd b/standalone/gdre_recover.gd index 08f6b280c..a45b5afff 100644 --- a/standalone/gdre_recover.gd +++ b/standalone/gdre_recover.gd @@ -82,6 +82,7 @@ func _get_all_files(files: PackedStringArray) -> PackedStringArray: const DIR_STRUCTURE_OPTION_NAME = "Directory Structure" const EXPORT_SCENE_OPTION_NAME = "Export Scenes as" +const EXPORT_MESH_OPTION_NAME = "Export Meshes as" enum DirStructure { FLAT, @@ -96,6 +97,14 @@ enum ExportSceneType { GLTF } +enum ExportMeshType { + AUTO, + TRES, + OBJ, + GLB, + GLTF +} + const DIR_STRUCTURE_NAMES: PackedStringArray = [ "Flat", "Relative Hierarchical", @@ -109,6 +118,14 @@ const EXPORT_SCENE_TYPE_NAMES: PackedStringArray = [ "GLTF", ] +const EXPORT_MESH_TYPE_NAMES: PackedStringArray = [ + "Auto", + "tres", + "OBJ", + "GLB", + "GLTF", +] + func get_output_file_name(src: String, output_folder: String, dir_structure_option: DirStructure, new_ext: String = "", rel_base: String = "") -> String: var new_name = "" if dir_structure_option == DirStructure.FLAT: @@ -150,6 +167,44 @@ func _export_scene(file: String, output_dir: String, dir_structure: DirStructure report.error = OK return report +# TODO: A more generic way to export resources, stop copying all this code around +func _export_mesh(file: String, output_dir: String, dir_structure: DirStructure, rel_base: String, export_type: ExportMeshType) -> ExportReport: + var source_file = file + var iinfo = GDRESettings.get_import_info_by_dest(file) + if iinfo: + source_file = iinfo.source_file + + var ext = source_file.get_extension().to_lower() + + if export_type == ExportMeshType.GLB: + ext = "glb" + elif export_type == ExportMeshType.GLTF: + ext = "gltf" + elif export_type == ExportMeshType.OBJ: + ext = "obj" + elif export_type == ExportMeshType.TRES: + ext = "tres" + else: # AUTO + if not is_instance_valid(iinfo): + ext = "tres" + + var report: ExportReport = ExportReport.new() + var export_dest = get_output_file_name(source_file, output_dir, dir_structure, ext, rel_base) + if ext == "tres": + # just use bin to text + report.error = ResourceCompatLoader.to_text(file, export_dest) + return report + + if export_type == ExportMeshType.OBJ: + report.error = ObjExporter.export_file_with_options(export_dest, file, {}) + else: + report = SceneExporter.export_file_with_options(export_dest, file, { + "Exporter/Scene/GLTF/replace_shader_materials": true, + }) + if (report.error == ERR_BUG or report.error == ERR_PRINTER_ON_FIRE or report.error == ERR_DATABASE_CANT_READ): + report.error = OK + return report + func get_log_error_string(errs: PackedStringArray) -> String: return "\n".join(GDRECommon.filter_error_backtraces(errs)) @@ -167,7 +222,7 @@ func convert_pcfg_to_text(path: String, output_dir: String) -> Array: return [err, text_file] return [loader.save_cfb(output_dir, ver_major, ver_minor), text_file] -func _export_files(files: PackedStringArray, output_dir: String, dir_structure: DirStructure, rel_base: String, export_glb: ExportSceneType) -> PackedStringArray: +func _export_files(files: PackedStringArray, output_dir: String, dir_structure: DirStructure, rel_base: String, export_glb: ExportSceneType, export_mesh: ExportMeshType) -> PackedStringArray: var errs: PackedStringArray = [] files = _get_all_files(files) @@ -201,6 +256,12 @@ func _export_files(files: PackedStringArray, output_dir: String, dir_structure: errs.append("Exporting cancelled: " + file + "\n" + report.message + "\n" + get_log_error_string(report.get_error_messages())) break errs.append("Failed to export resource: " + file + "\n" + report.message + "\n" + get_log_error_string(report.get_error_messages())) + if file_ext == "mesh" or (_ret and _ret.get_compat_type().contains("Mesh")) and export_mesh != ExportMeshType.AUTO: + var report: ExportReport = _export_mesh(file, output_dir, dir_structure, rel_base, export_mesh) + if not report: + errs.append("Failed to export resource: " + file + get_log_error_string(GDRESettings.get_errors())) + elif report.error != OK and report.error != ERR_PRINTER_ON_FIRE: + errs.append("Failed to export resource: " + file + "\n" + report.message + "\n" + get_log_error_string(report.get_error_messages())) elif _ret: var iinfo: ImportInfo = ImportInfo.copy(_ret) iinfo.export_dest = get_output_file_name(iinfo.source_file, "res://", dir_structure, iinfo.source_file.get_extension().to_lower(), rel_base) @@ -263,8 +324,9 @@ func _do_export(output_dir: String, export_preview_visible: bool): var options = %ExportResDirDialog.get_selected_options() var dir_structure = options.get(DIR_STRUCTURE_OPTION_NAME, DirStructure.RELATIVE_HIERARCHICAL) var export_glb: ExportSceneType = options.get(EXPORT_SCENE_OPTION_NAME, int(ExportSceneType.AUTO)) + var export_mesh: ExportMeshType = options.get(EXPORT_MESH_OPTION_NAME, int(ExportMeshType.AUTO)) - errs = _export_files(files, output_dir, dir_structure, rel_base, export_glb) + errs = _export_files(files, output_dir, dir_structure, rel_base, export_glb, export_mesh) if export_preview_visible: %GdreResourcePreview.set_main_view_visible(true) @@ -371,6 +433,12 @@ func _set_file_dialog_options(file_dialog: FileDialog, default_dir_structure: Di if not include_scene: return var include_glb = GDRESettings.get_ver_major() >= SceneExporter.get_minimum_godot_ver_supported() + var mesh_default = options.get(EXPORT_MESH_OPTION_NAME, int(ExportMeshType.AUTO)) + var mesh_opts = EXPORT_MESH_TYPE_NAMES.duplicate() + if not include_glb: + mesh_opts.remove_at(int(ExportMeshType.GLTF)) + mesh_opts.remove_at(int(ExportMeshType.GLB)) + file_dialog.add_option(EXPORT_MESH_OPTION_NAME, mesh_opts, mesh_default) var scene_default = options.get(EXPORT_SCENE_OPTION_NAME, int(ExportSceneType.AUTO)) #file_dialog.set_option_default(0, int(default_dir_structure)) var glb_opts = EXPORT_SCENE_TYPE_NAMES.duplicate() From f6a0d0e28e44418a7f62f01a7c8b6962e1a726dd Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:45:47 -0700 Subject: [PATCH 2/9] add `gltf-mutex-all-document-extensions` to rebase --- .scripts/rebase_godot.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.scripts/rebase_godot.sh b/.scripts/rebase_godot.sh index b6bb178b2..1fafe9d66 100755 --- a/.scripts/rebase_godot.sh +++ b/.scripts/rebase_godot.sh @@ -41,6 +41,7 @@ BRANCHES_TO_MERGE=( fix-v3-meshes fix-clearcoat-gloss fix-blend-export + gltf-mutex-all-document-extensions ) # set fail on error From 3a9d67e9e9d7276d9a41bed035b8ea6a5628a5e7 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:45:47 -0700 Subject: [PATCH 3/9] rebase on master @ f964fa714f5 --- .github/workflows/all_builds.yml | 2 +- .scripts/rebase_godot.sh | 2 -- README.md | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/all_builds.yml b/.github/workflows/all_builds.yml index d2668fb0c..dad45209e 100644 --- a/.github/workflows/all_builds.yml +++ b/.github/workflows/all_builds.yml @@ -28,7 +28,7 @@ env: # TODO: change this back to godotengine/godot and target master when #109685 and #109475 are merged GODOT_REPOSITORY: nikitalita/godot # Change the README too - GODOT_MAIN_SYNC_REF: gdre-wb-df6235838b6 + GODOT_MAIN_SYNC_REF: gdre-wb-f964fa714f5 SCONSFLAGS: verbose=yes warnings=all werror=no module_text_server_fb_enabled=yes minizip=yes deprecated=yes angle=yes accesskit=no SCONSFLAGS_TEMPLATE: disable_path_overrides=no no_editor_splash=yes module_camera_enabled=no module_mobile_vr_enabled=no module_upnp_enabled=no module_websocket_enabled=no module_csg_enabled=yes module_gridmap_enabled=yes use_static_cpp=yes builtin_freetype=yes builtin_libpng=yes builtin_zlib=yes builtin_libwebp=yes builtin_libvorbis=yes builtin_libogg=yes disable_3d=no SCONS_CACHE_MSVC_CONFIG: true diff --git a/.scripts/rebase_godot.sh b/.scripts/rebase_godot.sh index 1fafe9d66..168bca487 100755 --- a/.scripts/rebase_godot.sh +++ b/.scripts/rebase_godot.sh @@ -37,10 +37,8 @@ BRANCHES_TO_MERGE=( gltf-fix-skeleton-bone gltf-fix-double-precision gltf-fix-vertex-colors - ensure-bptc-textures fix-v3-meshes fix-clearcoat-gloss - fix-blend-export gltf-mutex-all-document-extensions ) diff --git a/README.md b/README.md index 028063ddb..856a6b340 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ During SCons configure, the module auto-applies the patches under `modules/gdsde ### Requirements -[Our fork of godot](https://github.com/nikitalita/godot) @ branch `gdre-wb-df6235838b6` +[Our fork of godot](https://github.com/nikitalita/godot) @ branch `gdre-wb-f964fa714f5` - Support for building on 3.x has been dropped and no new features are being pushed - Godot RE Tools still retains the ability to decompile 3.x and 2.x projects, however. From e096e3d992eb46316c2e64ab2620a03eab0ad7d5 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:12:22 -0700 Subject: [PATCH 4/9] fix custom_magic not being passed from `seek_offset_from_exe` --- utility/gdre_packed_source.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utility/gdre_packed_source.cpp b/utility/gdre_packed_source.cpp index 35d419d0e..b2f5833df 100644 --- a/utility/gdre_packed_source.cpp +++ b/utility/gdre_packed_source.cpp @@ -242,7 +242,7 @@ bool GDREPackedSource::_get_exe_embedded_pck_info(Ref f, const Strin bool GDREPackedSource::seek_offset_from_exe(Ref f, const String &p_path, uint64_t &r_pck_size, const PackedByteArray &custom_magic) { EXEPCKInfo info; - auto ret = _get_exe_embedded_pck_info(f, p_path, info); + auto ret = _get_exe_embedded_pck_info(f, p_path, info, custom_magic); #ifdef DEBUG_ENABLED if (ret) { if (info.pck_section_header_pos == 0) { From 73b35ff2d1b35b33ffebd7db681ccbcc47355039 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 21 Jun 2026 13:40:16 -0700 Subject: [PATCH 5/9] enforce type-safety on config value setting --- standalone/gdre_config_dialog.gd | 4 ++++ utility/gdre_config.cpp | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/standalone/gdre_config_dialog.gd b/standalone/gdre_config_dialog.gd index 8aa0141cb..6849dce8b 100644 --- a/standalone/gdre_config_dialog.gd +++ b/standalone/gdre_config_dialog.gd @@ -94,6 +94,10 @@ func make_button_label(text: String) -> Label: func set_setting_value(setting: GDREConfigSetting, value: Variant): + if setting.get_type() == TYPE_INT: + value = int(value) + elif setting.get_type() == TYPE_FLOAT: + value = float(value) setting_value_map[setting] = value func setting_callback(setting: GDREConfigSetting, value: Variant, control: Control): diff --git a/utility/gdre_config.cpp b/utility/gdre_config.cpp index 50003a65c..1233a0779 100644 --- a/utility/gdre_config.cpp +++ b/utility/gdre_config.cpp @@ -7,6 +7,7 @@ #include "core/object/class_db.h" #include "core/object/worker_thread_pool.h" #include "core/os/os.h" +#include "core/variant/variant_utility.h" #include "gdre_logger.h" #include "gdre_settings.h" #include "gdre_version.gen.h" @@ -486,8 +487,12 @@ String GDREConfig::get_config_path() { void GDREConfig::save_config() { auto cfg_path = get_config_path(); Ref config = memnew(ConfigFile); - for (const auto &[key, value] : settings) { + for (const auto &[key, p_value] : settings) { String name = get_name_from_key(key); + Variant value = p_value; + if (default_settings.has(key) && default_settings[key]->get_type() != Variant::Type::NIL) { + value = VariantUtilityFunctions::type_convert(p_value, default_settings[key]->get_type()); + } if (!default_settings.has(key) || get_default_value(key) != value) { config->set_value(get_section_from_key(key), name, value); } @@ -508,11 +513,16 @@ String get_full_name(const String &p_setting) { } void GDREConfig::set_setting(const String &p_setting, const Variant &p_value, bool p_force_ephemeral) { - if (p_force_ephemeral || ephemeral_settings.contains(get_full_name(p_setting))) { - ephemeral_settings.try_emplace_l(get_full_name(p_setting), [=](auto &v) { v.second = p_value; }, p_value); + String full_name = get_full_name(p_setting); + Variant value = p_value; + if (default_settings.has(full_name) && default_settings[full_name]->get_type() != Variant::Type::NIL) { + value = VariantUtilityFunctions::type_convert(p_value, default_settings[full_name]->get_type()); + } + if (p_force_ephemeral || ephemeral_settings.contains(full_name)) { + ephemeral_settings.try_emplace_l(full_name, [=](auto &v) { v.second = value; }, value); return; } - settings.try_emplace_l(get_full_name(p_setting), [=](auto &v) { v.second = p_value; }, p_value); + settings.try_emplace_l(full_name, [=](auto &v) { v.second = value; }, value); } bool GDREConfig::has_setting(const String &p_setting) const { From 285bc3a16940ed1b538490b83acae6f88b8cf538 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:20:41 -0700 Subject: [PATCH 6/9] improve choosing preferred import path logic in import info --- compat/config_file_compat.cpp | 16 ++++ compat/config_file_compat.h | 1 + utility/import_info.cpp | 154 ++++++++++++++++++++++++++++++---- utility/import_info.h | 3 + 4 files changed, 157 insertions(+), 17 deletions(-) diff --git a/compat/config_file_compat.cpp b/compat/config_file_compat.cpp index e3b746ffa..98dfaf5e9 100644 --- a/compat/config_file_compat.cpp +++ b/compat/config_file_compat.cpp @@ -107,6 +107,22 @@ Vector ConfigFileCompat::get_section_keys(const String &p_section) const return keys; } +Vector> ConfigFileCompat::get_section_keys_with_values_beginning_with(const String &p_section, const String &p_prefix) const { + Vector> keys; + ERR_FAIL_COND_V_MSG(!values.has(p_section), keys, vformat("Cannot get keys from nonexistent section \"%s\".", p_section)); + + const HashMap &keys_map = values[p_section]; + keys.reserve(keys_map.size()); + + for (const KeyValue &E : keys_map) { + if (E.key.begins_with(p_prefix)) { + keys.push_back({ E.key, E.value }); + } + } + + return keys; +} + void ConfigFileCompat::erase_section(const String &p_section) { ERR_FAIL_COND_MSG(!values.has(p_section), vformat("Cannot erase nonexistent section \"%s\".", p_section)); values.erase(p_section); diff --git a/compat/config_file_compat.h b/compat/config_file_compat.h index fa834491a..c4aefd053 100644 --- a/compat/config_file_compat.h +++ b/compat/config_file_compat.h @@ -60,6 +60,7 @@ class ConfigFileCompat : public RefCounted { Vector get_sections() const; Vector get_section_keys(const String &p_section) const; + Vector> get_section_keys_with_values_beginning_with(const String &p_section, const String &p_key_prefix) const; void erase_section(const String &p_section); void erase_section_key(const String &p_section, const String &p_key); diff --git a/utility/import_info.cpp b/utility/import_info.cpp index adb6a7e9b..ca4a67432 100644 --- a/utility/import_info.cpp +++ b/utility/import_info.cpp @@ -418,19 +418,85 @@ Vector ImportInfoModern::get_dest_files() const { return cf->get_value("deps", "dest_files", Vector()); } namespace { -Vector get_remap_paths(const Ref &cf) { +struct RemapPathSorter { + bool operator()(const Pair &a, const Pair &b) const { + String feature = a.first.get_slicec('.', 1); + return a.first < b.first; + } +}; + +void _insert_remap_paths(const String &key, const String &value, int &decomp_paths_found, Vector &remap_paths, Vector &candidates) { + if (key.begins_with("path.")) { + String feature = key.get_slicec('.', 1); + if (OS::get_singleton()->has_feature(feature)) { + remap_paths.push_back(value); + } else if (Image::can_decompress(feature)) { // When loading, check for decompressable formats and use first one found if nothing else is supported. + candidates.insert(decomp_paths_found, value); + decomp_paths_found++; + } else { + candidates.push_back(value); + } + } else if (key == "path") { + remap_paths.push_back(value); + } +} + +Pair> get_remap_paths_from_cf(const Ref &cf) { Vector remap_paths; - Vector remap_keys = cf->get_section_keys("remap"); + Vector candidates; + Vector ret; + int decomp_paths_found = 0; + Vector> remap_keys = cf->get_section_keys_with_values_beginning_with("remap", "path"); + if (remap_keys.is_empty()) { + return {}; + } else if (remap_keys.size() == 1) { + return { remap_keys[0].second.operator String(), { remap_keys[0].second.operator String() } }; + } // iterate over keys in remap section - for (int64_t i = 0; i < remap_keys.size(); i++) { - // if we find a path key, we have a match - if (remap_keys[i].begins_with("path.") || remap_keys[i] == "path") { - String try_path = cf->get_value("remap", remap_keys[i], ""); - remap_paths.push_back(try_path); + for (auto &E : remap_keys) { + ret.push_back(E.second.operator String()); + _insert_remap_paths(E.first, E.second.operator String(), decomp_paths_found, remap_paths, candidates); + } + remap_paths.append_array(candidates); + for (auto &E : remap_paths) { + if (FileAccess::exists(E)) { + return { E, ret }; + } + } + return { remap_keys[0].second.operator String(), ret }; +} + +Vector _parse_remap_paths_from_import_file(const Ref &p_f) { + Vector remap_paths; + Error err; + + VariantParser::StreamFile stream; + stream.f = p_f; + String assign, error_text; + Variant value; + VariantParser::Tag next_tag; + int lines = 0; + int decomp_paths_found = 0; + Vector candidates; + while (true) { + assign = Variant(); + next_tag.fields.clear(); + next_tag.name = String(); + + err = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, nullptr, true); + if (err != OK) { + break; + } + if (!assign.is_empty()) { + _insert_remap_paths(assign, value, decomp_paths_found, remap_paths, candidates); + } else if (next_tag.name != "remap") { + break; } } + remap_paths.append_array(candidates); return remap_paths; } + Array vec_to_array(const Vector &vec) { Array arr; for (int64_t i = 0; i < vec.size(); i++) { @@ -553,7 +619,10 @@ Error ImportInfoModern::_load(const String &p_path) { cf->set_value("deps", "dest_files", vec_to_array({ preferred_import_path })); } else { // this is a multi-path import, get all the "path.*" key values - dest_files = get_remap_paths(cf); + auto [p, d] = get_remap_paths_from_cf(cf); + preferred_import_path = p; + dest_files = d; + // No path values at all; may be a translation file if (dest_files.is_empty()) { String importer = cf->get_value("remap", "importer", ""); @@ -608,20 +677,20 @@ Error ImportInfoModern::_load(const String &p_path) { if (preferred_import_path.is_empty()) { //check destination files if (dest_files.size() == 0) { - dest_files = get_remap_paths(cf); - // Reverse the order; we want to get the s3tc textures first if they exist. - dest_files.reverse(); + auto [p, d] = get_remap_paths_from_cf(cf); + preferred_import_path = p; + dest_files = d; } if (dest_files.size() == 0) { dest_files = get_dest_files(); - } - ERR_FAIL_COND_V_MSG(dest_files.size() == 0, ERR_FILE_CORRUPT, p_path + ": no destination files found in import data"); - for (int64_t i = 0; i < dest_files.size(); i++) { - if (FileAccess::exists(dest_files[i])) { - preferred_import_path = dest_files[i]; - break; + for (int64_t i = 0; i < dest_files.size(); i++) { + if (FileAccess::exists(dest_files[i])) { + preferred_import_path = dest_files[i]; + break; + } } } + ERR_FAIL_COND_V_MSG(dest_files.size() == 0, ERR_FILE_CORRUPT, p_path + ": no destination files found in import data"); if (preferred_import_path.is_empty()) { // just set it to the first one preferred_import_path = dest_files[0]; @@ -644,6 +713,28 @@ Error ImportInfoModern::_load(const String &p_path) { return OK; } +String ImportInfoModern::get_remap_path_from_file(const String &p_path) { + if (FileAccess::exists(p_path)) { + Ref f = FileAccess::open(p_path + ".import", FileAccess::READ); + if (f.is_null()) { + return ""; + } + Vector remap_paths = _parse_remap_paths_from_import_file(f); + if (remap_paths.is_empty()) { + return ""; + } else if (remap_paths.size() == 1) { + return remap_paths[0]; + } + for (int i = 0; i < remap_paths.size(); i++) { + if (FileAccess::exists(remap_paths[i])) { + return remap_paths[i]; + } + } + return remap_paths[0]; + } + return ""; +} + Error ImportInfoDummy::_load(const String &p_path) { Error err; Ref res_info; @@ -699,6 +790,35 @@ Ref ImportInfoDummy::create_dummy(const String &p_path) { return iinfo; } +String ImportInfoRemap::get_remap_path_from_file(const String &p_path) { + Error err; + Ref f = FileAccess::open(p_path, FileAccess::READ, &err); + if (f.is_valid()) { + VariantParser::StreamFile stream; + stream.f = f; + String assign, error_text; + Variant value; + VariantParser::Tag next_tag; + int lines = 0; + while (true) { + assign.clear(); + next_tag.fields.clear(); + next_tag.name.clear(); + err = VariantParserCompat::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, nullptr, true); + if (err) { + break; + } + + if (assign == "path") { + return value; + } else if (next_tag.name != "remap") { + break; + } + } + } + return ""; +} + Error ImportInfoRemap::_load(const String &p_path) { Ref cf; cf.instantiate(); diff --git a/utility/import_info.h b/utility/import_info.h index e840f4a45..33362fe47 100644 --- a/utility/import_info.h +++ b/utility/import_info.h @@ -206,6 +206,8 @@ class ImportInfoModern : public ImportInfo { virtual void _set_from_json(const Dictionary &p_json) override; public: + static String get_remap_path_from_file(const String &p_import_file_path); + // Gets the Godot resource type (e.g. "StreamTexture") virtual String get_type() const override; virtual void set_type(const String &p_type) override; @@ -373,6 +375,7 @@ class ImportInfoRemap : public ImportInfoDummy { virtual Error _load(const String &p_path) override; public: + static String get_remap_path_from_file(const String &p_remap_file_path); ImportInfoRemap(); }; From 916bd468b09dc6d1338e8cb684df7fb5cfb15c9f Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:22:27 -0700 Subject: [PATCH 7/9] GDRESettings: fix mapping path logic, fix uid cache conflict resolution logic --- utility/gdre_settings.cpp | 113 +++++++++++++++++++------------------- utility/gdre_settings.h | 2 +- 2 files changed, 56 insertions(+), 59 deletions(-) diff --git a/utility/gdre_settings.cpp b/utility/gdre_settings.cpp index 19f05b270..be8e792d0 100644 --- a/utility/gdre_settings.cpp +++ b/utility/gdre_settings.cpp @@ -1656,8 +1656,7 @@ Dictionary GDRESettings::get_remaps(bool include_imports) const { } } if (include_imports) { - for (int i = 0; i < import_files.size(); i++) { - Ref iinfo = import_files[i]; + for (auto &[path, iinfo] : import_files) { ret[iinfo->get_source_file()] = iinfo->get_path(); } } @@ -1665,6 +1664,7 @@ Dictionary GDRESettings::get_remaps(bool include_imports) const { return ret; } +namespace { bool has_old_remap(const Vector &remaps, const String &src, const String &dst) { int idx = remaps.find(src); if (idx != -1 && idx % 2 == 0) { @@ -1676,6 +1676,35 @@ bool has_old_remap(const Vector &remaps, const String &src, const String return false; } +String get_mapped_path_unloaded(const String &p_path) { + String src = p_path; + if (src.begins_with("uid://")) { + auto id = ResourceUID::get_singleton()->text_to_id(src); + if (ResourceUID::get_singleton()->has_id(id)) { + src = ResourceUID::get_singleton()->get_id_path(id); + } else { + return ""; + } + } + + String iinfo_path = src + ".remap"; + if (FileAccess::exists(iinfo_path)) { + String new_path = ImportInfoRemap::get_remap_path_from_file(iinfo_path); + if (!new_path.is_empty()) { + return new_path; + } + } + iinfo_path = src + ".import"; + if (FileAccess::exists(iinfo_path)) { + String new_path = ImportInfoModern::get_remap_path_from_file(iinfo_path); + if (!new_path.is_empty()) { + return new_path; + } + } + return src; +} +} //namespace + String GDRESettings::get_remapped_source_path(const String &p_dst) const { if (is_pack_loaded()) { if (get_ver_major() >= 3) { @@ -1698,6 +1727,9 @@ String GDRESettings::get_remapped_source_path(const String &p_dst) const { } String GDRESettings::get_mapped_path(const String &p_src) const { + if (!is_pack_loaded()) { + return get_mapped_path_unloaded(p_src); + } String src = p_src; if (src.begins_with("uid://")) { auto id = ResourceUID::get_singleton()->text_to_id(src); @@ -1707,43 +1739,19 @@ String GDRESettings::get_mapped_path(const String &p_src) const { return ""; } } - if (is_pack_loaded()) { - String local_src = localize_path(src); - String remapped_path = get_remap(local_src); - if (!remapped_path.is_empty()) { - return remapped_path; - } + String local_src = localize_path(src); + String remapped_path = get_remap(local_src); + if (!remapped_path.is_empty()) { + return remapped_path; + } + String import_path = local_src + ".import"; + if (import_files.has(import_path)) { + return import_files[import_path]->get_path(); + } - for (int i = 0; i < import_files.size(); i++) { - Ref iinfo = import_files[i]; - if (iinfo->get_source_file().nocasecmp_to(local_src) == 0) { - return iinfo->get_path(); - } - } - } else { - Ref iinfo; - String iinfo_path = src + ".import"; - String dep_path; - if (FileAccess::exists(iinfo_path)) { - iinfo = ImportInfo::load_from_file(iinfo_path, 0, 0); - if (iinfo.is_valid()) { - if (FileAccess::exists(iinfo->get_path())) { - return iinfo->get_path(); - } - auto dests = iinfo->get_dest_files(); - for (int i = 0; i < dests.size(); i++) { - if (FileAccess::exists(dests[i])) { - return dests[i]; - } - } - } - } - iinfo_path = src + ".remap"; - if (FileAccess::exists(iinfo_path)) { - iinfo = ImportInfo::load_from_file(iinfo_path, 0, 0); - if (iinfo.is_valid() && FileAccess::exists(iinfo->get_path())) { - return iinfo->get_path(); - } + for (auto &[path, iinfo] : import_files) { + if (iinfo->get_source_file().nocasecmp_to(local_src) == 0) { + return iinfo->get_path(); } } return src; @@ -1984,12 +1992,9 @@ Error GDRESettings::close_log_file() { } Array GDRESettings::get_import_files(bool copy) { - if (!copy) { - return import_files; - } Array ifiles; - for (int i = 0; i < import_files.size(); i++) { - ifiles.push_back(ImportInfo::copy(import_files[i])); + for (auto &[path, iinfo] : import_files) { + ifiles.push_back(copy ? ImportInfo::copy(iinfo) : iinfo); } return ifiles; } @@ -2063,13 +2068,13 @@ Error GDRESettings::load_pack_uid_cache(bool p_reset) { if (old_path.simplify_path() == new_path.simplify_path()) { // Sometimes uid caches have duplicate paths when paths were not simplified before saving; this is a workaround new_path = new_path.simplify_path(); - } else if (has_path_loaded(old_path)) { - if (!has_path_loaded(new_path)) { // had old path, but not new path + } else if (has_path_loaded(get_mapped_path_unloaded(old_path))) { + if (!has_path_loaded(get_mapped_path_unloaded(new_path))) { // had old path, but not new path continue; // skip } // has both dupes.push_back(ResourceUID::get_singleton()->id_to_text(E.second) + " -> " + old_path + "\n Replacing with: " + new_path); - } else if (!has_path_loaded(new_path)) { // has neither + } else if (!has_path_loaded(get_mapped_path_unloaded(new_path))) { // has neither dupes.push_back(ResourceUID::get_singleton()->id_to_text(E.second) + " -> " + old_path + "\n Replacing with: " + new_path); } // else we have the new_path but not the old path } @@ -2428,7 +2433,7 @@ Error GDRESettings::load_import_files() { remap_iinfo.insert(tokens[i].path, tokens[i].info); } } - import_files.push_back(tokens[i].info); + import_files.insert(tokens[i].path, tokens[i].info); } return OK; } @@ -2437,7 +2442,7 @@ Error GDRESettings::load_import_file(const String &p_path) { Ref i_info = ImportInfo::load_from_file(p_path, get_ver_major(), get_ver_minor()); ERR_FAIL_COND_V_MSG(i_info.is_null(), ERR_FILE_CANT_OPEN, "Failed to load import file " + p_path); - import_files.push_back(i_info); + import_files.insert(p_path, i_info); if (i_info->get_iitype() == ImportInfo::REMAP) { if (!FileAccess::exists(i_info->get_path())) { print_line(vformat("Remapped path does not exist: %s -> %s", i_info->get_source_file(), i_info->get_path())); @@ -2450,25 +2455,17 @@ Error GDRESettings::load_import_file(const String &p_path) { Ref GDRESettings::get_import_info_by_source(const String &p_path) { Ref iinfo; - for (int i = 0; i < import_files.size(); i++) { - iinfo = import_files[i]; + for (const auto &[path, iinfo] : import_files) { if (iinfo->get_source_file() == p_path) { return iinfo; } } - // not found return Ref(); } Ref GDRESettings::get_import_info_by_dest(const String &p_path) const { Ref iinfo; - for (int i = 0; i < import_files.size(); i++) { - iinfo = import_files[i]; - // for (auto &dest : iinfo->get_dest_files()) { - // if (dest.to_lower() == p_path.to_lower()) { - // return iinfo; - // } - // } + for (auto &[path, iinfo] : import_files) { if (iinfo->get_dest_files().has(p_path)) { return iinfo; } diff --git a/utility/gdre_settings.h b/utility/gdre_settings.h index 612a1e011..05117cc44 100644 --- a/utility/gdre_settings.h +++ b/utility/gdre_settings.h @@ -53,7 +53,7 @@ class GDRESettings : public Object { Ref current_project; Ref version_override; GDRELogger *logger; - Array import_files; + HashMap> import_files; HashMap> remap_iinfo; String gdre_resource_path = ""; String v2_remap_setting = "remap/all"; From f25a40dba7d0c3dd57d75de67ff161dc05c803a4 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:22:55 -0700 Subject: [PATCH 8/9] Don't init shaders if rendering server doesn't exist --- register_types.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/register_types.cpp b/register_types.cpp index ff35871f8..85bf2d3a2 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -16,6 +16,7 @@ #include "gui/texture_previewer.h" #include "modules/regex/regex.h" #include "modules/register_module_types.h" +#include "servers/rendering/rendering_server.h" #include "utility/app_version_getter.h" #include "utility/file_access_gdre.h" #include "utility/file_access_patched_gdre.h" @@ -618,16 +619,20 @@ void initialize_gdsdecomp_module(ModuleInitializationLevel p_level) { // Register ICO image loader ico_loader.instantiate(); ImageLoader::add_image_format_loader(ico_loader); - TextureLayeredPreviewer::init_shaders(); - TexturePreviewer::init_shaders(); + if (RenderingServer::get_singleton()) { + TextureLayeredPreviewer::init_shaders(); + TexturePreviewer::init_shaders(); + } } void uninitialize_gdsdecomp_module(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; } - TextureLayeredPreviewer::finish_shaders(); - TexturePreviewer::finish_shaders(); + if (RenderingServer::get_singleton()) { + TextureLayeredPreviewer::finish_shaders(); + TexturePreviewer::finish_shaders(); + } if (ico_loader.is_valid()) { ImageLoader::remove_image_format_loader(ico_loader); ico_loader.unref(); From f7480c310fc08c45b0e5a9b373b73292ce68e88e Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:24:14 -0700 Subject: [PATCH 9/9] fix gdre_custom_pack_source documentation --- docs/gdre_custom_pack_source.gd | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/gdre_custom_pack_source.gd b/docs/gdre_custom_pack_source.gd index 78158be3d..1930a669e 100644 --- a/docs/gdre_custom_pack_source.gd +++ b/docs/gdre_custom_pack_source.gd @@ -1,9 +1,12 @@ -class_name GDRECustomPackSource +class_name GDREStandardPackSource extends PackSourceCustom +# This script re-implements the standard pack source logic for Godot PCK files. +# Use this as a base for your own custom pack source logic. -# Godot's packed file magic header ("GDPC" in ASCII, little-endian). +# Godot's packed file magic header ("GDPC" in ASCII, stored as little-endian in the file). const PACK_HEADER_MAGIC: int = 0x43504447 +# 'G', 'D', 'P', 'C' in ASCII. var PACK_HEADER_MAGIC_BYTES: PackedByteArray = PackedByteArray([0x47, 0x44, 0x50, 0x43]) const PACK_FORMAT_VERSION_V2: int = 2 const PACK_FORMAT_VERSION_V3: int = 3 @@ -26,7 +29,7 @@ func open_encrypted_file(base: FileAccess, key: PackedByteArray) -> FileAccess: return FileAccessEncryptedCustom.create_and_parse_non_custom(base, key, FileAccessEncryptedCustom.MODE_READ, false) func _try_open_pack(pck_path: String, p_replace_files: bool, p_offset: int, p_decryption_key: PackedByteArray) -> bool: - var ext: String = p_path.get_extension().to_lower() + var ext: String = pck_path.get_extension().to_lower() if ext == "apk" or ext == "zip": return false