/* SPDX-FileCopyrightText: 2023 Blender Authors * * SPDX-License-Identifier: GPL-2.0-or-later */ /** \file * \ingroup bke * * High level `.blend` file read/write, * and functions for writing *partial* files (only selected data-blocks). */ #include #include #include #include "CLG_log.h" #include "MEM_guardedalloc.h" #include "DNA_brush_types.h" #include "DNA_scene_types.h" #include "DNA_screen_types.h" #include "DNA_space_types.h" #include "BLI_fileops.h" #include "BLI_function_ref.hh" #include "BLI_listbase.h" #include "BLI_path_utils.hh" #include "BLI_string.h" #include "BLI_system.h" #include "BLI_time.h" #include "BLI_utildefines.h" #include "BLI_vector.hh" #include "BLI_vector_set.hh" #include "BLT_translation.hh" #include "IMB_colormanagement.hh" #include "BKE_addon.h" #include "BKE_appdir.hh" #include "BKE_blender.hh" #include "BKE_blender_version.h" #include "BKE_blendfile.hh" #include "BKE_bpath.hh" #include "BKE_colorband.hh" #include "BKE_context.hh" #include "BKE_global.hh" #include "BKE_idtype.hh" #include "BKE_layer.hh" #include "BKE_lib_id.hh" #include "BKE_lib_override.hh" #include "BKE_lib_query.hh" #include "BKE_lib_remap.hh" #include "BKE_library.hh" #include "BKE_main.hh" #include "BKE_main_idmap.hh" #include "BKE_main_namemap.hh" #include "BKE_preferences.h" #include "BKE_report.hh" #include "BKE_scene.hh" #include "BKE_screen.hh" #include "BKE_studiolight.h" #include "BKE_undo_system.hh" #include "BKE_workspace.hh" #include "BLO_read_write.hh" #include "BLO_readfile.hh" #include "BLO_userdef_default.h" #include "BLO_writefile.hh" #include "RE_pipeline.h" #ifdef WITH_PYTHON # include "BPY_extern.hh" #endif using namespace blender::bke; /* -------------------------------------------------------------------- */ /** \name Blend/Library Paths * \{ */ bool BKE_blendfile_extension_check(const char *str) { const char *ext_test[4] = {".blend", ".ble", ".blend.gz", nullptr}; return BLI_path_extension_check_array(str, ext_test); } bool BKE_blendfile_library_path_explode(const char *path, char *r_dir, char **r_group, char **r_name) { /* We might get some data names with slashes, * so we have to go up in path until we find blend file itself, * then we know next path item is group, and everything else is data name. */ char *slash = nullptr, *prev_slash = nullptr, c = '\0'; r_dir[0] = '\0'; if (r_group) { *r_group = nullptr; } if (r_name) { *r_name = nullptr; } /* if path leads to an existing directory, we can be sure we're not (in) a library */ if (BLI_is_dir(path)) { return false; } BLI_strncpy(r_dir, path, FILE_MAX_LIBEXTRA); while ((slash = (char *)BLI_path_slash_rfind(r_dir))) { char tc = *slash; *slash = '\0'; if (BKE_blendfile_extension_check(r_dir) && BLI_is_file(r_dir)) { break; } if (STREQ(r_dir, BLO_EMBEDDED_STARTUP_BLEND)) { break; } if (prev_slash) { *prev_slash = c; } prev_slash = slash; c = tc; } if (!slash) { return false; } if (slash[1] != '\0') { BLI_assert(strlen(slash + 1) < BLO_GROUP_MAX); if (r_group) { *r_group = slash + 1; } } if (prev_slash && (prev_slash[1] != '\0')) { BLI_assert(strlen(prev_slash + 1) < MAX_ID_NAME - 2); if (r_name) { *r_name = prev_slash + 1; } } return true; } bool BKE_blendfile_is_readable(const char *path, ReportList *reports) { BlendFileReadReport readfile_reports; readfile_reports.reports = reports; BlendHandle *bh = BLO_blendhandle_from_file(path, &readfile_reports); if (bh != nullptr) { BLO_blendhandle_close(bh); return true; } return false; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Blend File IO (High Level) * \{ */ static bool foreach_path_clean_cb(BPathForeachPathData * /*bpath_data*/, char *path_dst, size_t path_dst_maxncpy, const char *path_src) { BLI_strncpy(path_dst, path_src, path_dst_maxncpy); BLI_path_slash_native(path_dst); return !STREQ(path_dst, path_src); } /* make sure path names are correct for OS */ static void clean_paths(Main *bmain) { BPathForeachPathData foreach_path_data{}; foreach_path_data.bmain = bmain; foreach_path_data.callback_function = foreach_path_clean_cb; foreach_path_data.flag = BKE_BPATH_FOREACH_PATH_SKIP_MULTIFILE; foreach_path_data.user_data = nullptr; BKE_bpath_foreach_path_main(&foreach_path_data); LISTBASE_FOREACH (Scene *, scene, &bmain->scenes) { BLI_path_slash_native(scene->r.pic); } } static bool wm_scene_is_visible(wmWindowManager *wm, Scene *scene) { LISTBASE_FOREACH (wmWindow *, win, &wm->windows) { if (win->scene == scene) { return true; } } return false; } static void setup_app_userdef(BlendFileData *bfd) { if (bfd->user) { /* only here free userdef themes... */ BKE_blender_userdef_data_set_and_free(bfd->user); bfd->user = nullptr; /* Security issue: any blend file could include a #BLO_CODE_USER block. * * Preferences are loaded from #BLENDER_STARTUP_FILE and later on load #BLENDER_USERPREF_FILE, * to load the preferences defined in the users home directory. * * This means we will never accidentally (or maliciously) * enable scripts auto-execution by loading a `.blend` file. */ U.flag |= USER_SCRIPT_AUTOEXEC_DISABLE; } } /** * Helper struct to manage IDs that are re-used across blend-file loading (i.e. moved from the old * Main the new one). * * NOTE: this is only used when actually loading a real `.blend` file, * loading of memfile undo steps does not need it. */ struct ReuseOldBMainData { Main *new_bmain; Main *old_bmain; /** Data generated and used by calling WM code to handle keeping WM and UI IDs as best as * possible across file reading. * * \note May be null in undo (memfile) case. */ BlendFileReadWMSetupData *wm_setup_data; /** Storage for all remapping rules (old_id -> new_id) required by the preservation of old IDs * into the new Main. */ id::IDRemapper *remapper; bool is_libraries_remapped; /** Used to find matching IDs by name/lib in new main, to remap ID usages of data ported over * from old main. */ IDNameLib_Map *id_map; }; /** * Search for all libraries in `old_bmain` that are also in `new_bmain` (i.e. different Library * IDs having the same absolute filepath), and create a remapping rule for these. * * NOTE: The case where the `old_bmain` would be a library in the newly read one is not handled * here, as it does not create explicit issues. The local data from `old_bmain` is either * discarded, or added to the `new_bmain` as local data as well. Worst case, there will be a * double of a linked data as a local one, without any known relationships between them. In * practice, this latter case is not expected to commonly happen. */ static id::IDRemapper &reuse_bmain_data_remapper_ensure(ReuseOldBMainData *reuse_data) { if (reuse_data->is_libraries_remapped) { return *reuse_data->remapper; } if (reuse_data->remapper == nullptr) { reuse_data->remapper = MEM_new(__func__); } Main *new_bmain = reuse_data->new_bmain; Main *old_bmain = reuse_data->old_bmain; id::IDRemapper &remapper = *reuse_data->remapper; LISTBASE_FOREACH (Library *, old_lib_iter, &old_bmain->libraries) { /* In case newly opened `new_bmain` is a library of the `old_bmain`, remap it to null, since a * file should never ever have linked data from itself. */ if (STREQ(old_lib_iter->runtime->filepath_abs, new_bmain->filepath)) { remapper.add(&old_lib_iter->id, nullptr); continue; } /* NOTE: Although this is quadratic complexity, it is not expected to be an issue in practice: * - Files using more than a few tens of libraries are extremely rare. * - This code is only executed once for every file reading (not on undos). */ LISTBASE_FOREACH (Library *, new_lib_iter, &new_bmain->libraries) { if (!STREQ(old_lib_iter->runtime->filepath_abs, new_lib_iter->runtime->filepath_abs)) { continue; } remapper.add(&old_lib_iter->id, &new_lib_iter->id); break; } } reuse_data->is_libraries_remapped = true; return *reuse_data->remapper; } static bool reuse_bmain_data_remapper_is_id_remapped(id::IDRemapper &remapper, ID *id) { IDRemapperApplyResult result = remapper.get_mapping_result(id, ID_REMAP_APPLY_DEFAULT, nullptr); if (ELEM(result, ID_REMAP_RESULT_SOURCE_REMAPPED, ID_REMAP_RESULT_SOURCE_UNASSIGNED)) { /* ID is already remapped to its matching ID in the new main, or explicitly remapped to null, * nothing else to do here. */ return true; } BLI_assert_msg(result != ID_REMAP_RESULT_SOURCE_NOT_MAPPABLE, "There should never be a non-mappable (i.e. null) input here."); BLI_assert(result == ID_REMAP_RESULT_SOURCE_UNAVAILABLE); return false; } static bool reuse_bmain_move_id(ReuseOldBMainData *reuse_data, ID *id, Library *lib, const bool reuse_existing) { id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data); /* Nothing to move for embedded ID. */ if (id->flag & ID_FLAG_EMBEDDED_DATA) { remapper.add(id, id); return true; } Main *new_bmain = reuse_data->new_bmain; Main *old_bmain = reuse_data->old_bmain; ListBase *new_lb = which_libbase(new_bmain, GS(id->name)); ListBase *old_lb = which_libbase(old_bmain, GS(id->name)); if (reuse_existing) { /* A 'new' version of the same data may already exist in new_bmain, in the rare case * that the same asset blend file was linked explicitly into the blend file we are loading. * Don't move the old linked ID, but remap its usages to the new one instead. */ LISTBASE_FOREACH_BACKWARD (ID *, id_iter, new_lb) { if (!ELEM(id_iter->lib, id->lib, lib)) { continue; } if (!STREQ(id_iter->name + 2, id->name + 2)) { continue; } remapper.add(id, id_iter); return false; } } /* If ID is already in the new_bmain, this should not have been called. */ BLI_assert(BLI_findindex(new_lb, id) < 0); BLI_assert(BLI_findindex(old_lb, id) >= 0); /* Move from one list to another, and ensure name is valid. */ BLI_remlink_safe(old_lb, id); /* In case the ID is linked and its library ID is re-used from the old Main, it is not possible * to handle name_map (and ensure name uniqueness). * This is because IDs are moved one by one from old Main's lists to new ones, while the re-used * library's name_map would be built only from IDs in the new list, leading to incomplete/invalid * states. * Currently, such name uniqueness checks should not be needed, as no new name would be expected * in the re-used library. Should this prove to be wrong at some point, the name check will have * to happen at the end of #reuse_editable_asset_bmain_data_for_blendfile, in a separate loop * over Main IDs. */ const bool handle_name_map_updates = !ID_IS_LINKED(id) || id->lib != lib; if (handle_name_map_updates) { BKE_main_namemap_remove_id(*old_bmain, *id); } id->lib = lib; BLI_addtail(new_lb, id); if (handle_name_map_updates) { BKE_id_new_name_validate( *new_bmain, *new_lb, *id, nullptr, IDNewNameMode::RenameExistingNever, true); } else { id_sort_by_name(new_lb, id, nullptr); } BKE_lib_libblock_session_uid_renew(id); /* Remap to itself, to avoid re-processing this ID again. */ remapper.add(id, id); return true; } static Library *reuse_bmain_data_dependencies_new_library_get(ReuseOldBMainData *reuse_data, Library *old_lib) { const id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data); Library *new_lib = old_lib; IDRemapperApplyResult result = remapper.apply(reinterpret_cast(&new_lib), ID_REMAP_APPLY_DEFAULT); switch (result) { case ID_REMAP_RESULT_SOURCE_UNAVAILABLE: { /* Move library to new bmain. * There should be no filepath conflicts, as #reuse_bmain_data_remapper_ensure has * already remapped existing libraries with matching filepath. */ reuse_bmain_move_id(reuse_data, &old_lib->id, nullptr, false); /* Clear the name_map of the library, as not all of its IDs are guaranteed reused. The name * map cannot be used/kept in valid state while some IDs are moved from old to new main. See * also #reuse_bmain_move_id code. */ BKE_main_namemap_destroy(&old_lib->runtime->name_map); return old_lib; } case ID_REMAP_RESULT_SOURCE_NOT_MAPPABLE: { BLI_assert_unreachable(); return nullptr; } case ID_REMAP_RESULT_SOURCE_REMAPPED: { /* Already in new bmain, only transfer flags. */ new_lib->runtime->tag |= old_lib->runtime->tag & (LIBRARY_ASSET_EDITABLE | LIBRARY_ASSET_FILE_WRITABLE); return new_lib; } case ID_REMAP_RESULT_SOURCE_UNASSIGNED: { /* Happens when the library is the newly opened blend file. */ return nullptr; } } BLI_assert_unreachable(); return nullptr; } static int reuse_editable_asset_bmain_data_dependencies_process_cb( LibraryIDLinkCallbackData *cb_data) { ID *id = *cb_data->id_pointer; if (id == nullptr) { return IDWALK_RET_NOP; } if (GS(id->name) == ID_LI) { /* Libraries are handled separately. */ return IDWALK_RET_STOP_RECURSION; } ReuseOldBMainData *reuse_data = static_cast(cb_data->user_data); /* First check if it has already been remapped. */ id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data); if (reuse_bmain_data_remapper_is_id_remapped(remapper, id)) { return IDWALK_RET_STOP_RECURSION; } if (id->lib == nullptr) { /* There should be no links to local datablocks from linked editable data. */ remapper.add(id, nullptr); BLI_assert_unreachable(); return IDWALK_RET_STOP_RECURSION; } /* Only preserve specific datablock types. */ if (!ID_TYPE_SUPPORTS_ASSET_EDITABLE(GS(id->name))) { remapper.add(id, nullptr); return IDWALK_RET_STOP_RECURSION; } /* There may be a new library pointer in new_bmain, matching a library in old_bmain, even * though pointer values are not the same. So we need to check new linked IDs in new_bmain * against both potential library pointers. */ Library *old_id_new_lib = reuse_bmain_data_dependencies_new_library_get(reuse_data, id->lib); /* Happens when the library is the newly opened blend file. */ if (old_id_new_lib == nullptr) { remapper.add(id, nullptr); return IDWALK_RET_STOP_RECURSION; } /* Move to new main database. */ return reuse_bmain_move_id(reuse_data, id, old_id_new_lib, true) ? IDWALK_RET_STOP_RECURSION : IDWALK_RET_NOP; } static bool reuse_editable_asset_needed(ReuseOldBMainData *reuse_data) { Main *old_bmain = reuse_data->old_bmain; LISTBASE_FOREACH (Library *, lib, &old_bmain->libraries) { if (lib->runtime->tag & LIBRARY_ASSET_EDITABLE) { return true; } } return false; } /** * Selectively 'import' data from old Main into new Main, provided it does not conflict with data * already present in the new Main (name-wise and library-wise). * * Dependencies from moved over old data are also imported into the new Main, (unless, in case of * linked data, a matching linked ID is already available in new Main). * * When a conflict is found, usages of the conflicted ID by the old data are stored in the * `remapper` of `ReuseOldBMainData` to be remapped to the matching data in the new Main later. * * NOTE: This function will never remove any original new data from the new Main, it only moves * (some of) the old data to the new Main. */ static void reuse_editable_asset_bmain_data_for_blendfile(ReuseOldBMainData *reuse_data, const short idcode) { Main *new_bmain = reuse_data->new_bmain; Main *old_bmain = reuse_data->old_bmain; id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data); ListBase *old_lb = which_libbase(old_bmain, idcode); ID *old_id_iter; FOREACH_MAIN_LISTBASE_ID_BEGIN (old_lb, old_id_iter) { /* Keep any datablocks from libraries marked as LIBRARY_ASSET_EDITABLE. */ if (!(ID_IS_LINKED(old_id_iter) && old_id_iter->lib->runtime->tag & LIBRARY_ASSET_EDITABLE)) { continue; } Library *old_id_new_lib = reuse_bmain_data_dependencies_new_library_get(reuse_data, old_id_iter->lib); /* Happens when the library is the newly opened blend file. */ if (old_id_new_lib == nullptr) { remapper.add(old_id_iter, nullptr); continue; } if (reuse_bmain_move_id(reuse_data, old_id_iter, old_id_new_lib, true)) { /* Port over dependencies of re-used ID, unless matching already existing ones in * new_bmain can be found. * * NOTE : No pointers are remapped here, this code only moves dependencies from old_bmain * to new_bmain if needed, and add necessary remapping rules to the reuse_data.remapper. */ BKE_library_foreach_ID_link(new_bmain, old_id_iter, reuse_editable_asset_bmain_data_dependencies_process_cb, reuse_data, IDWALK_RECURSE | IDWALK_DO_LIBRARY_POINTER); } } FOREACH_MAIN_LISTBASE_ID_END; } /** * Grease pencil brushes may have a material pinned that is from the current file. Moving local * scene data to a different #Main is tricky, so in that case, unpin the material. */ static void unpin_file_local_grease_pencil_brush_materials(const ReuseOldBMainData *reuse_data) { ID *old_id_iter; FOREACH_MAIN_LISTBASE_ID_BEGIN (&reuse_data->old_bmain->brushes, old_id_iter) { const Brush *brush = reinterpret_cast(old_id_iter); if (brush->gpencil_settings && brush->gpencil_settings->material && /* Don't unpin if this material is linked, then it can be preserved for the new file. */ !ID_IS_LINKED(brush->gpencil_settings->material)) { /* Unpin material and clear pointer. */ brush->gpencil_settings->flag &= ~GP_BRUSH_MATERIAL_PINNED; brush->gpencil_settings->material = nullptr; } } FOREACH_MAIN_LISTBASE_ID_END; } /** * Does a complete replacement of data in `new_bmain` by data from `old_bmain. Original new data * are moved to the `old_bmain`, and will be freed together with it. * * WARNING: Currently only expects to work on local data, won't work properly if some of the IDs of * given type are linked. * * NOTE: Unlike with #reuse_editable_asset_bmain_data_for_blendfile, there is no support at all for * potential dependencies of the IDs moved around. This is not expected to be necessary for the * current use cases (UI-related IDs). */ static void swap_old_bmain_data_for_blendfile(ReuseOldBMainData *reuse_data, const short id_code) { Main *new_bmain = reuse_data->new_bmain; Main *old_bmain = reuse_data->old_bmain; ListBase *new_lb = which_libbase(new_bmain, id_code); ListBase *old_lb = which_libbase(old_bmain, id_code); id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data); /* NOTE: Full swapping is only supported for ID types that are assumed to be only local * data-blocks (like UI-like ones). Otherwise, the swapping could fail in many funny ways. */ BLI_assert(BLI_listbase_is_empty(old_lb) || !ID_IS_LINKED(old_lb->last)); BLI_assert(BLI_listbase_is_empty(new_lb) || !ID_IS_LINKED(new_lb->last)); std::swap(*new_lb, *old_lb); /* TODO: Could add per-IDType control over name-maps clearing, if this becomes a performances * concern. */ BKE_main_namemap_clear(*old_bmain); BKE_main_namemap_clear(*new_bmain); /* Original 'new' IDs have been moved into the old listbase and will be discarded (deleted). * Original 'old' IDs have been moved into the new listbase and are being reused (kept). * The discarded ones need to be remapped to a matching reused one, based on their names, if * possible. * * Since both lists are ordered, and they are all local, we can do a smart parallel processing of * both lists here instead of doing complete full list searches. */ ID *discarded_id_iter = static_cast(old_lb->first); ID *reused_id_iter = static_cast(new_lb->first); while (!ELEM(nullptr, discarded_id_iter, reused_id_iter)) { const int strcmp_result = strcmp(discarded_id_iter->name + 2, reused_id_iter->name + 2); if (strcmp_result == 0) { /* Matching IDs, we can remap the discarded 'new' one to the re-used 'old' one. */ remapper.add(discarded_id_iter, reused_id_iter); discarded_id_iter = static_cast(discarded_id_iter->next); reused_id_iter = static_cast(reused_id_iter->next); } else if (strcmp_result < 0) { /* No matching reused 'old' ID for this discarded 'new' one. */ remapper.add(discarded_id_iter, nullptr); discarded_id_iter = static_cast(discarded_id_iter->next); } else { reused_id_iter = static_cast(reused_id_iter->next); } } /* Also remap all remaining non-compared discarded 'new' IDs to null. */ for (; discarded_id_iter != nullptr; discarded_id_iter = static_cast(discarded_id_iter->next)) { remapper.add(discarded_id_iter, nullptr); } FOREACH_MAIN_LISTBASE_ID_BEGIN (new_lb, reused_id_iter) { /* Necessary as all `session_uid` are renewed on blendfile loading. */ BKE_lib_libblock_session_uid_renew(reused_id_iter); /* Ensure that the reused ID is remapped to itself, since it is known to be in the `new_bmain`. */ remapper.add_overwrite(reused_id_iter, reused_id_iter); } FOREACH_MAIN_LISTBASE_ID_END; } /** * Similar to #swap_old_bmain_data_for_blendfile, but with special handling for WM ID. Tightly * related to further WM post-processing from calling WM code (see #WM_file_read and * #wm_homefile_read_ex). */ static void swap_wm_data_for_blendfile(ReuseOldBMainData *reuse_data, const bool load_ui) { Main *old_bmain = reuse_data->old_bmain; Main *new_bmain = reuse_data->new_bmain; ListBase *old_wm_list = &old_bmain->wm; ListBase *new_wm_list = &new_bmain->wm; /* Currently there should never be more than one WM in a main. */ BLI_assert(BLI_listbase_count_at_most(new_wm_list, 2) <= 1); BLI_assert(BLI_listbase_count_at_most(old_wm_list, 2) <= 1); wmWindowManager *old_wm = static_cast(old_wm_list->first); wmWindowManager *new_wm = static_cast(new_wm_list->first); if (old_wm == nullptr) { /* No current (old) WM. Either (new) WM from file is used, or if none, WM code is responsible * to add a new default WM. Nothing to do here. */ return; } /* Current (old) WM, and (new) WM in file, and loading UI: use WM from file, keep old WM around * for further processing in WM code. */ if (load_ui && new_wm != nullptr) { /* Support window-manager ID references being held between file load operations by keeping * #Main.wm.first memory address in-place, while swapping all of its contents. * * This is needed so items such as key-maps can be held by an add-on, * without it pointing to invalid memory, see: #86431. */ BLI_remlink(old_wm_list, old_wm); BLI_remlink(new_wm_list, new_wm); BKE_lib_id_swap_full(nullptr, &old_wm->id, &new_wm->id, true, (ID_REMAP_SKIP_NEVER_NULL_USAGE | ID_REMAP_SKIP_UPDATE_TAGGING | ID_REMAP_SKIP_USER_REFCOUNT | ID_REMAP_FORCE_UI_POINTERS)); /* Not strictly necessary, but helps for readability. */ std::swap(old_wm, new_wm); BLI_addhead(new_wm_list, new_wm); /* Do not add old WM back to `old_bmain`, so that it does not get freed when `old_bmain` is * freed. Calling WM code will need this old WM to restore some windows etc. data into the * new WM, and is responsible to free it properly. */ reuse_data->wm_setup_data->old_wm = old_wm; id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data); remapper.add(&old_wm->id, &new_wm->id); } /* Current (old) WM, but no (new) one in file (should only happen when reading pre 2.5 files, no * WM back then), or not loading UI: Keep current WM. */ else { swap_old_bmain_data_for_blendfile(reuse_data, ID_WM); old_wm->init_flag &= ~WM_INIT_FLAG_WINDOW; reuse_data->wm_setup_data->old_wm = old_wm; } } static int swap_old_bmain_data_for_blendfile_dependencies_process_cb( LibraryIDLinkCallbackData *cb_data) { ID *id = *cb_data->id_pointer; if (id == nullptr) { return IDWALK_RET_NOP; } ReuseOldBMainData *reuse_data = static_cast(cb_data->user_data); /* First check if it has already been remapped. */ id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data); if (reuse_bmain_data_remapper_is_id_remapped(remapper, id)) { return IDWALK_RET_NOP; } IDNameLib_Map *id_map = reuse_data->id_map; BLI_assert(id_map != nullptr); ID *id_new = BKE_main_idmap_lookup_id(id_map, id); remapper.add(id, id_new); return IDWALK_RET_NOP; } static void swap_old_bmain_data_dependencies_process(ReuseOldBMainData *reuse_data, const short id_code) { Main *new_bmain = reuse_data->new_bmain; ListBase *new_lb = which_libbase(new_bmain, id_code); BLI_assert(reuse_data->id_map != nullptr); ID *new_id_iter; FOREACH_MAIN_LISTBASE_ID_BEGIN (new_lb, new_id_iter) { /* Check all ID usages and find a matching new ID to remap them to in `new_bmain` if possible * (matching by names and libraries). * * Note that this call does not do any effective remapping, it only adds required remapping * operations to the remapper. */ BKE_library_foreach_ID_link(new_bmain, new_id_iter, swap_old_bmain_data_for_blendfile_dependencies_process_cb, reuse_data, IDWALK_READONLY | IDWALK_INCLUDE_UI | IDWALK_DO_LIBRARY_POINTER); } FOREACH_MAIN_LISTBASE_ID_END; } static int reuse_bmain_data_invalid_local_usages_fix_cb(LibraryIDLinkCallbackData *cb_data) { ID *id = *cb_data->id_pointer; if (id == nullptr) { return IDWALK_RET_NOP; } /* Embedded data cannot (yet) be fully trusted to have the same lib pointer as their owner ID, so * for now ignore them. This code should never have anything to fix for them anyway, otherwise * there is something extremely wrong going on. */ if ((cb_data->cb_flag & (IDWALK_CB_EMBEDDED | IDWALK_CB_EMBEDDED_NOT_OWNING)) != 0) { return IDWALK_RET_NOP; } if (!ID_IS_LINKED(id)) { ID *owner_id = cb_data->owner_id; /* Do not allow linked data to use local data. */ if (ID_IS_LINKED(owner_id)) { if (cb_data->cb_flag & IDWALK_CB_USER) { id_us_min(id); } *cb_data->id_pointer = nullptr; } /* Do not allow local liboverride data to use local data as reference. */ else if (ID_IS_OVERRIDE_LIBRARY_REAL(owner_id) && &owner_id->override_library->reference == cb_data->id_pointer) { if (cb_data->cb_flag & IDWALK_CB_USER) { id_us_min(id); } *cb_data->id_pointer = nullptr; } } return IDWALK_RET_NOP; } /** * Detect and fix invalid usages of locale IDs by linked ones (or as reference of liboverrides). */ static void reuse_bmain_data_invalid_local_usages_fix(ReuseOldBMainData *reuse_data) { Main *new_bmain = reuse_data->new_bmain; ID *id_iter; FOREACH_MAIN_ID_BEGIN (new_bmain, id_iter) { if (!ID_IS_LINKED(id_iter) && !ID_IS_OVERRIDE_LIBRARY_REAL(id_iter)) { continue; } ID *liboverride_reference = ID_IS_OVERRIDE_LIBRARY_REAL(id_iter) ? id_iter->override_library->reference : nullptr; BKE_library_foreach_ID_link( new_bmain, id_iter, reuse_bmain_data_invalid_local_usages_fix_cb, reuse_data, IDWALK_NOP); /* Liboverrides who lost their reference should not be liboverrides anymore, but regular IDs. */ if (ID_IS_OVERRIDE_LIBRARY_REAL(id_iter) && id_iter->override_library->reference != liboverride_reference) { BKE_lib_override_library_free(&id_iter->override_library, true); } } FOREACH_MAIN_ID_END; } /* Post-remapping helpers to ensure validity of the UI data. */ static void view3d_data_consistency_ensure(wmWindow *win, Scene *scene, ViewLayer *view_layer) { bScreen *screen = BKE_workspace_active_screen_get(win->workspace_hook); LISTBASE_FOREACH (ScrArea *, area, &screen->areabase) { LISTBASE_FOREACH (SpaceLink *, sl, &area->spacedata) { if (sl->spacetype != SPACE_VIEW3D) { continue; } View3D *v3d = reinterpret_cast(sl); if (v3d->camera == nullptr || v3d->scenelock) { v3d->camera = scene->camera; } if (v3d->localvd == nullptr) { continue; } if (v3d->localvd->camera == nullptr || v3d->scenelock) { v3d->localvd->camera = v3d->camera; } /* Local-view can become invalid during undo/redo steps, exit it when no valid object could * be found. */ Base *base; for (base = static_cast(view_layer->object_bases.first); base; base = base->next) { if (base->local_view_bits & v3d->local_view_uid) { break; } } if (base != nullptr) { /* The local view3D still has a valid object, nothing else to do. */ continue; } /* No valid object found for the local view3D, it has to be cleared off. */ MEM_freeN(v3d->localvd); v3d->localvd = nullptr; v3d->local_view_uid = 0; /* Region-base storage is different depending on whether the space is active or not. */ ListBase *regionbase = (sl == area->spacedata.first) ? &area->regionbase : &sl->regionbase; LISTBASE_FOREACH (ARegion *, region, regionbase) { if (region->regiontype != RGN_TYPE_WINDOW) { continue; } RegionView3D *rv3d = static_cast(region->regiondata); MEM_SAFE_FREE(rv3d->localvd); } } } } static void wm_data_consistency_ensure(wmWindowManager *curwm, Scene *cur_scene, ViewLayer *cur_view_layer) { /* There may not be any available WM (e.g. when reading `userpref.blend`). */ if (curwm == nullptr) { return; } LISTBASE_FOREACH (wmWindow *, win, &curwm->windows) { if (win->scene == nullptr) { win->scene = cur_scene; } if (BKE_view_layer_find(win->scene, win->view_layer_name) == nullptr) { STRNCPY(win->view_layer_name, cur_view_layer->name); } view3d_data_consistency_ensure(win, win->scene, cur_view_layer); } } /** * This function runs after loading blend-file data and is responsible * for setting up the context (the active view-layer, scene & work-space). * * When loading a blend-file without it's UI (#G_FILE_NO_UI), * the existing screen-data needs to point to the newly loaded blend-file data. * * \note this is called on undo so any slow conversion functions here * should be avoided or check `mode != LOAD_UNDO`. * * \param bfd: Blend file data, freed by this function on exit. */ static void setup_app_data(bContext *C, BlendFileData *bfd, const BlendFileReadParams *params, BlendFileReadWMSetupData *wm_setup_data, BlendFileReadReport *reports) { Main *bmain = G_MAIN; const bool recover = (G.fileflags & G_FILE_RECOVER_READ) != 0; enum { LOAD_UI = 1, LOAD_UI_OFF, LOAD_UNDO, } mode; if (params->undo_direction != STEP_INVALID) { BLI_assert(bfd->curscene != nullptr); mode = LOAD_UNDO; } else if (bfd->fileflags & G_FILE_ASSET_EDIT_FILE) { BKE_report(reports->reports, RPT_WARNING, "This file is managed by the asset system, you cannot overwrite it (using \"Save " "As\" is possible)"); /* From now on the file in memory is a normal file, further saving it will contain a * window-manager, scene, ... and potentially user created data. Use #Main.is_asset_edit_file * to detect if saving this file needs extra protections. */ bfd->fileflags &= ~G_FILE_ASSET_EDIT_FILE; BLI_assert(bfd->main->is_asset_edit_file); mode = LOAD_UI_OFF; } /* May happen with library files, loading undo-data should never have a null `curscene` * (but may have a null `curscreen`). */ else if (ELEM(nullptr, bfd->curscreen, bfd->curscene)) { BKE_report(reports->reports, RPT_WARNING, "Library file, loading empty scene"); mode = LOAD_UI_OFF; } else if (G.fileflags & G_FILE_NO_UI) { mode = LOAD_UI_OFF; } else { mode = LOAD_UI; } /* Free all render results and interactive compositor renders, without this stale data gets * displayed after loading files */ if (mode != LOAD_UNDO) { RE_FreeAllRenderResults(); RE_FreeInteractiveCompositorRenders(); } /* Only make file-paths compatible when loading for real (not undo). */ if (mode != LOAD_UNDO) { clean_paths(bfd->main); } BLI_assert(BKE_main_namemap_validate(*bfd->main)); /* Temporary data to handle swapping around IDs between old and new mains, * and accumulate the required remapping accordingly. */ ReuseOldBMainData reuse_data = {nullptr}; reuse_data.new_bmain = bfd->main; reuse_data.old_bmain = bmain; reuse_data.wm_setup_data = wm_setup_data; if (mode != LOAD_UNDO) { const short ui_id_codes[]{ID_WS, ID_SCR}; /* WM needs special complex handling, regardless of whether UI is kept or loaded from file. */ swap_wm_data_for_blendfile(&reuse_data, mode == LOAD_UI); if (mode != LOAD_UI) { /* Re-use UI data from `old_bmain` if keeping existing UI. */ for (auto id_code : ui_id_codes) { swap_old_bmain_data_for_blendfile(&reuse_data, id_code); } } /* Needs to happen after all data from `old_bmain` has been moved into new one. */ BLI_assert(reuse_data.id_map == nullptr); reuse_data.id_map = BKE_main_idmap_create( reuse_data.new_bmain, true, reuse_data.old_bmain, MAIN_IDMAP_TYPE_NAME); swap_old_bmain_data_dependencies_process(&reuse_data, ID_WM); if (mode != LOAD_UI) { for (auto id_code : ui_id_codes) { swap_old_bmain_data_dependencies_process(&reuse_data, id_code); } } BKE_main_idmap_destroy(reuse_data.id_map); if (!params->is_factory_settings && reuse_editable_asset_needed(&reuse_data)) { unpin_file_local_grease_pencil_brush_materials(&reuse_data); /* Keep linked brush asset data, similar to UI data. Only does a known * subset know. Could do everything, but that risks dragging along more * scene data than we want. */ for (short idtype_index = 0; idtype_index < INDEX_ID_MAX; idtype_index++) { const IDTypeInfo *idtype_info = BKE_idtype_get_info_from_idtype_index(idtype_index); if (ID_TYPE_SUPPORTS_ASSET_EDITABLE(idtype_info->id_code)) { reuse_editable_asset_bmain_data_for_blendfile(&reuse_data, idtype_info->id_code); } } } if (mode != LOAD_UI) { LISTBASE_FOREACH (bScreen *, screen, &bfd->main->screens) { BKE_screen_runtime_refresh_for_blendfile(screen); } } } /* Logic for 'track_undo_scene' is to keep using the scene which the active screen has, as long * as the scene associated with the undo operation is visible in one of the open windows. * * - 'curscreen->scene': Scene the user is currently looking at. * - 'bfd->curscene': Scene undo-step was created in. * * This means that users can have 2 or more windows open and undo in both without screens * switching. But if they close one of the screens, undo will ensure that the scene being * operated on will be activated (otherwise we'd be undoing on an off-screen scene which isn't * acceptable). See: #43424. */ bool track_undo_scene = false; /* Always use the Scene and ViewLayer pointers from new file, if possible. */ ViewLayer *cur_view_layer = bfd->cur_view_layer; Scene *curscene = bfd->curscene; wmWindow *win = nullptr; bScreen *curscreen = nullptr; /* Ensure that there is a valid scene and view-layer. */ if (curscene == nullptr) { curscene = static_cast(bfd->main->scenes.first); } /* Empty file, add a scene to make Blender work. */ if (curscene == nullptr) { curscene = BKE_scene_add(bfd->main, "Empty"); } if (cur_view_layer == nullptr) { /* Fallback to the active scene view layer. */ cur_view_layer = BKE_view_layer_default_view(curscene); } /* If UI is not loaded when opening actual `.blend` file, * and always in case of undo MEMFILE reading. */ if (mode != LOAD_UI) { /* Re-use current window and screen. */ win = CTX_wm_window(C); curscreen = CTX_wm_screen(C); track_undo_scene = (mode == LOAD_UNDO && curscreen && curscene && bfd->main->wm.first); if (track_undo_scene) { /* Keep the old (to-be-freed) scene, remapping below will ensure it's remapped to the * matching new scene if available, or null otherwise, in which case * #wm_data_consistency_ensure will define `curscene` as the active one. */ } /* Enforce `curscene` to be in current screen. */ else if (win) { /* The window may be null in background-mode. */ win->scene = curscene; } } BLI_assert(BKE_main_namemap_validate(*bfd->main)); /* Apply remapping of ID pointers caused by re-using part of the data from the 'old' main into * the new one. */ if (reuse_data.remapper != nullptr) { /* In undo case all "keeping old data" and remapping logic is now handled * in file reading code itself, so there should never be any remapping to do here. */ BLI_assert(mode != LOAD_UNDO); /* Handle all pending remapping from swapping old and new IDs around. */ BKE_libblock_remap_multiple_raw(bfd->main, *reuse_data.remapper, (ID_REMAP_FORCE_UI_POINTERS | ID_REMAP_SKIP_USER_REFCOUNT | ID_REMAP_SKIP_UPDATE_TAGGING | ID_REMAP_SKIP_USER_CLEAR)); /* Fix potential invalid usages of now-locale-data created by remapping above. Should never * be needed in undo case, this is to address cases like: * "opening a blend-file that was a library of the previous opened blend-file". */ reuse_bmain_data_invalid_local_usages_fix(&reuse_data); MEM_delete(reuse_data.remapper); reuse_data.remapper = nullptr; wm_data_consistency_ensure(CTX_wm_manager(C), curscene, cur_view_layer); } if (mode == LOAD_UNDO) { /* It's possible to undo into a time before the scene existed, in this case the window's scene * will be null. Since it doesn't make sense to remove the window, set it to the current scene. * * NOTE: Redo will restore the active scene to the window so a reasonably consistent state * is maintained. We could do better by keeping a window/scene map for each undo step. * * Another source of potential inconsistency is undoing into a step where the active camera * object does not exist (see e.g. #125636). */ wm_data_consistency_ensure(CTX_wm_manager(C), curscene, cur_view_layer); } BLI_assert(BKE_main_namemap_validate(*bfd->main)); if (mode != LOAD_UI) { if (win) { curscene = win->scene; } if (track_undo_scene) { wmWindowManager *wm = static_cast(bfd->main->wm.first); if (!wm_scene_is_visible(wm, bfd->curscene)) { curscene = bfd->curscene; if (win) { win->scene = curscene; } BKE_screen_view3d_scene_sync(curscreen, curscene); } } /* We need to tag this here because events may be handled immediately after. * only the current screen is important because we won't have to handle * events from multiple screens at once. */ if (curscreen) { BKE_screen_gizmo_tag_refresh(curscreen); } } CTX_data_scene_set(C, curscene); BLI_assert(BKE_main_namemap_validate(*bfd->main)); /* This frees the `old_bmain`. */ BKE_blender_globals_main_replace(bfd->main); bmain = G_MAIN; bfd->main = nullptr; CTX_data_main_set(C, bmain); BLI_assert(BKE_main_namemap_validate(*bmain)); /* These context data should remain valid if old UI is being re-used. */ if (mode == LOAD_UI) { /* Setting a window-manger clears all other windowing members (window, screen, area, etc). * So only do it when effectively loading a new #wmWindowManager * otherwise just assert that the WM from context is still the same as in `new_bmain`. */ CTX_wm_manager_set(C, static_cast(bmain->wm.first)); CTX_wm_screen_set(C, bfd->curscreen); CTX_wm_area_set(C, nullptr); CTX_wm_region_set(C, nullptr); CTX_wm_region_popup_set(C, nullptr); } BLI_assert(CTX_wm_manager(C) == static_cast(bmain->wm.first)); /* Keep state from preferences. */ const int fileflags_keep = G_FILE_FLAG_ALL_RUNTIME; G.fileflags = (G.fileflags & fileflags_keep) | (bfd->fileflags & ~fileflags_keep); /* Special cases, override any #G_FLAG_ALL_READFILE flags from the blend-file. */ if (G.f != bfd->globalf) { const int flags_keep = G_FLAG_ALL_RUNTIME; bfd->globalf &= G_FLAG_ALL_READFILE; bfd->globalf = (bfd->globalf & ~flags_keep) | (G.f & flags_keep); } G.f = bfd->globalf; #ifdef WITH_PYTHON /* let python know about new main */ if (CTX_py_init_get(C)) { BPY_context_update(C); } #endif if (mode != LOAD_UNDO) { /* Perform complex versioning that involves adding or removing IDs, * and/or needs to operate over the whole Main data-base * (versioning done in file reading code only operates on a per-library basis). */ BLO_read_do_version_after_setup(bmain, nullptr, reports); BLO_readfile_id_runtime_data_free_all(*bmain); } bmain->recovered = false; /* `startup.blend` or recovered startup. */ if (params->is_startup) { bmain->filepath[0] = '\0'; } else if (recover) { /* In case of auto-save or `quit.blend`, use original file-path instead * (see also #read_global in `readfile.cc`). */ bmain->recovered = true; STRNCPY(bmain->filepath, bfd->filepath); } /* Set the loaded .blend file path for crash recovery. */ STRNCPY(G.filepath_last_blend, bmain->filepath); /* Base-flags, groups, make depsgraph, etc. */ /* first handle case if other windows have different scenes visible. */ if (mode == LOAD_UI) { wmWindowManager *wm = static_cast(bmain->wm.first); if (wm) { LISTBASE_FOREACH (wmWindow *, win, &wm->windows) { if (win->scene && win->scene != curscene) { BKE_scene_set_background(bmain, win->scene); } } } } /* Setting scene might require having a dependency graph, with copy-on-eval * we need to make sure we ensure scene has correct color management before * constructing dependency graph. */ if (mode != LOAD_UNDO) { IMB_colormanagement_check_file_config(bmain); } BKE_scene_set_background(bmain, curscene); if (mode != LOAD_UNDO) { /* TODO(@sergey): Can this be also move above? */ RE_FreeAllPersistentData(); } /* Both undo and regular file loading can perform some fairly complex ID manipulation, simpler * and safer to fully redo reference-counting. This is a relatively cheap process anyway. */ BKE_main_id_refcount_recompute(bmain, false); BLI_assert(BKE_main_namemap_validate(*bmain)); if (mode != LOAD_UNDO && liboverride::is_auto_resync_enabled()) { reports->duration.lib_overrides_resync = BLI_time_now_seconds(); BKE_lib_override_library_main_resync( bmain, curscene, bfd->cur_view_layer ? bfd->cur_view_layer : BKE_view_layer_default_view(curscene), reports); reports->duration.lib_overrides_resync = BLI_time_now_seconds() - reports->duration.lib_overrides_resync; /* We need to rebuild some of the deleted override rules (for UI feedback purpose). */ BKE_lib_override_library_main_operations_create(bmain, true, nullptr); } /* Now that liboverrides have been resynced and 'irrelevant' missing linked IDs has been removed, * report actual missing linked data. */ if (mode != LOAD_UNDO) { ID *id_iter; int missing_linked_ids_num = 0; FOREACH_MAIN_ID_BEGIN (bmain, id_iter) { if (ID_IS_LINKED(id_iter) && (id_iter->tag & ID_TAG_MISSING)) { missing_linked_ids_num++; BLO_reportf_wrap(reports, RPT_INFO, RPT_("LIB: %s: '%s' missing from '%s', parent '%s'"), BKE_idtype_idcode_to_name(GS(id_iter->name)), id_iter->name + 2, id_iter->lib->runtime->filepath_abs, id_iter->lib->runtime->parent ? id_iter->lib->runtime->parent->runtime->filepath_abs : ""); } } FOREACH_MAIN_ID_END; reports->count.missing_linked_id = missing_linked_ids_num; } } static void setup_app_blend_file_data(bContext *C, BlendFileData *bfd, const BlendFileReadParams *params, BlendFileReadWMSetupData *wm_setup_data, BlendFileReadReport *reports) { if ((params->skip_flags & BLO_READ_SKIP_USERDEF) == 0) { setup_app_userdef(bfd); } if ((params->skip_flags & BLO_READ_SKIP_DATA) == 0) { setup_app_data(C, bfd, params, wm_setup_data, reports); } } static void handle_subversion_warning(Main *main, BlendFileReadReport *reports) { if (main->versionfile > BLENDER_FILE_VERSION || (main->versionfile == BLENDER_FILE_VERSION && main->subversionfile > BLENDER_FILE_SUBVERSION)) { BKE_reportf(reports->reports, RPT_WARNING, "File written by newer Blender binary (%d.%d), expect loss of data!", main->versionfile, main->subversionfile); } } void BKE_blendfile_read_setup_readfile(bContext *C, BlendFileData *bfd, const BlendFileReadParams *params, BlendFileReadWMSetupData *wm_setup_data, BlendFileReadReport *reports, /* Extra args. */ const bool startup_update_defaults, const char *startup_app_template) { if (bfd->main->is_read_invalid) { BKE_reports_prepend(reports->reports, "File could not be read, critical data corruption detected"); BLO_blendfiledata_free(bfd); return; } if (startup_update_defaults) { if ((params->skip_flags & BLO_READ_SKIP_DATA) == 0) { BLO_update_defaults_startup_blend(bfd->main, startup_app_template); } } setup_app_blend_file_data(C, bfd, params, wm_setup_data, reports); BLO_blendfiledata_free(bfd); } void BKE_blendfile_read_setup_undo(bContext *C, BlendFileData *bfd, const BlendFileReadParams *params, BlendFileReadReport *reports) { BKE_blendfile_read_setup_readfile(C, bfd, params, nullptr, reports, false, nullptr); } BlendFileData *BKE_blendfile_read(const char *filepath, const BlendFileReadParams *params, BlendFileReadReport *reports) { /* Don't print startup file loading. */ if (params->is_startup == false) { if (!G.quiet) { printf("Read blend: \"%s\"\n", filepath); } } BlendFileData *bfd = BLO_read_from_file(filepath, eBLOReadSkip(params->skip_flags), reports); if (bfd && bfd->main->is_read_invalid) { BLO_blendfiledata_free(bfd); bfd = nullptr; } if (bfd) { handle_subversion_warning(bfd->main, reports); } else { BKE_reports_prependf(reports->reports, "Loading \"%s\" failed: ", filepath); } return bfd; } BlendFileData *BKE_blendfile_read_from_memory(const void *file_buf, int file_buf_size, const BlendFileReadParams *params, ReportList *reports) { BlendFileData *bfd = BLO_read_from_memory( file_buf, file_buf_size, eBLOReadSkip(params->skip_flags), reports); if (bfd && bfd->main->is_read_invalid) { BLO_blendfiledata_free(bfd); bfd = nullptr; } if (bfd) { /* Pass. */ } else { BKE_reports_prepend(reports, "Loading failed: "); } return bfd; } BlendFileData *BKE_blendfile_read_from_memfile(Main *bmain, MemFile *memfile, const BlendFileReadParams *params, ReportList *reports) { BlendFileData *bfd = BLO_read_from_memfile( bmain, BKE_main_blendfile_path(bmain), memfile, params, reports); if (bfd && bfd->main->is_read_invalid) { BLO_blendfiledata_free(bfd); bfd = nullptr; } if (bfd == nullptr) { BKE_reports_prepend(reports, "Loading failed: "); } return bfd; } void BKE_blendfile_read_make_empty(bContext *C) { Main *bmain = CTX_data_main(C); ListBase *lb; ID *id; FOREACH_MAIN_LISTBASE_BEGIN (bmain, lb) { FOREACH_MAIN_LISTBASE_ID_BEGIN (lb, id) { if (ELEM(GS(id->name), ID_SCE, ID_SCR, ID_WM, ID_WS)) { break; } BKE_id_delete(bmain, id); } FOREACH_MAIN_LISTBASE_ID_END; } FOREACH_MAIN_LISTBASE_END; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Blend File IO (Preferences) * * Application Templates * ===================== * * When using app-templates, both regular & app-template preferences are used. * Note that "regular" preferences refers to the preferences used with no app-template is active. * * - Reading preferences is performed for both the app-template & regular preferences. * * The preferences are merged by using some from the app-template and other settings from the * regular preferences (add-ons from the app-template for example are used), * undo-memory uses the regular preferences (for e.g.). * * - Writing preferences is performed for both the app-template & regular preferences. * * Writing unmodified preference (#U) into the regular preferences * would loose any settings the app-template overrides. * To keep default settings the regular preferences is read, add-ons etc temporarily swapped * into #U for writing, then swapped back out so as not to change the run-time preferences. * * \note The function #BKE_blender_userdef_app_template_data_swap determines which settings * the app-template overrides. * \{ */ UserDef *BKE_blendfile_userdef_read(const char *filepath, ReportList *reports) { BlendFileData *bfd; UserDef *userdef = nullptr; BlendFileReadReport blend_file_read_reports{}; blend_file_read_reports.reports = reports; bfd = BLO_read_from_file( filepath, BLO_READ_SKIP_ALL & ~BLO_READ_SKIP_USERDEF, &blend_file_read_reports); if (bfd) { if (bfd->user) { userdef = bfd->user; } BKE_main_free(bfd->main); MEM_delete(bfd); } return userdef; } UserDef *BKE_blendfile_userdef_read_from_memory(const void *file_buf, int file_buf_size, ReportList *reports) { BlendFileData *bfd; UserDef *userdef = nullptr; bfd = BLO_read_from_memory( file_buf, file_buf_size, BLO_READ_SKIP_ALL & ~BLO_READ_SKIP_USERDEF, reports); if (bfd) { if (bfd->user) { userdef = bfd->user; } BKE_main_free(bfd->main); MEM_delete(bfd); } else { BKE_reports_prepend(reports, "Loading failed: "); } return userdef; } UserDef *BKE_blendfile_userdef_from_defaults() { UserDef *userdef = MEM_callocN(__func__); *userdef = blender::dna::shallow_copy(U_default); /* Add-ons. */ { const char *addons[] = { "io_anim_bvh", "io_curve_svg", "io_mesh_uv_layout", "io_scene_fbx", "io_scene_gltf2", "cycles", "pose_library", "bl_pkg", }; for (int i = 0; i < ARRAY_SIZE(addons); i++) { bAddon *addon = BKE_addon_new(); STRNCPY(addon->module, addons[i]); BLI_addtail(&userdef->addons, addon); } } /* Theme. */ { bTheme *btheme = MEM_mallocN(__func__); memcpy(btheme, &U_theme_default, sizeof(*btheme)); BLI_addtail(&userdef->themes, btheme); } #ifdef WITH_PYTHON_SECURITY /* use alternative setting for security nuts * otherwise we'd need to patch the binary blob - startup.blend.c */ userdef->flag |= USER_SCRIPT_AUTOEXEC_DISABLE; #else userdef->flag &= ~USER_SCRIPT_AUTOEXEC_DISABLE; #endif /* System-specific fonts directory. * NOTE: when not found, leaves as-is (`//` for the blend-file directory). */ if (BKE_appdir_font_folder_default(userdef->fontdir, sizeof(userdef->fontdir))) { /* Not actually needed, just a convention that directory selection * adds a trailing slash. */ BLI_path_slash_ensure(userdef->fontdir, sizeof(userdef->fontdir)); } userdef->memcachelimit = min_ii(BLI_system_memory_max_in_megabytes_int() / 2, userdef->memcachelimit); /* Init weight paint range. */ BKE_colorband_init(&userdef->coba_weight, true); /* Default studio light. */ BKE_studiolight_default(userdef->light_param, userdef->light_ambient); BKE_preferences_asset_library_default_add(userdef); BKE_preferences_extension_repo_add_defaults_all(userdef); { BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled( userdef, "VIEW3D_AST_brush_sculpt", "Brushes/Mesh Sculpt/General"); BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled( userdef, "VIEW3D_AST_brush_sculpt", "Brushes/Mesh Sculpt/Paint"); BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled( userdef, "VIEW3D_AST_brush_sculpt", "Brushes/Mesh Sculpt/Simulation"); BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled( userdef, "VIEW3D_AST_brush_gpencil_paint", "Brushes/Grease Pencil Draw/Draw"); BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled( userdef, "VIEW3D_AST_brush_gpencil_paint", "Brushes/Grease Pencil Draw/Erase"); BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled( userdef, "VIEW3D_AST_brush_gpencil_paint", "Brushes/Grease Pencil Draw/Utilities"); BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled( userdef, "VIEW3D_AST_brush_gpencil_sculpt", "Brushes/Grease Pencil Sculpt/Contrast"); BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled( userdef, "VIEW3D_AST_brush_gpencil_sculpt", "Brushes/Grease Pencil Sculpt/Transform"); BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled( userdef, "VIEW3D_AST_brush_gpencil_sculpt", "Brushes/Grease Pencil Sculpt/Utilities"); } return userdef; } bool BKE_blendfile_userdef_write(const char *filepath, ReportList *reports) { Main *mainb = MEM_callocN
("empty main"); bool ok = false; BlendFileWriteParams params{}; params.use_userdef = true; if (BLO_write_file(mainb, filepath, 0, ¶ms, reports)) { ok = true; } MEM_freeN(mainb); return ok; } bool BKE_blendfile_userdef_write_app_template(const char *filepath, ReportList *reports) { /* Checking that `filepath` exists is not essential, it just avoids printing a warning that * the file can't be found. In this case it's not an error - as the file is used if it exists, * falling back to the defaults. * If the preferences exists but file reading fails - the file can be assumed corrupt * so overwriting the file is OK. */ UserDef *userdef_default = BLI_exists(filepath) ? BKE_blendfile_userdef_read(filepath, nullptr) : nullptr; if (userdef_default == nullptr) { userdef_default = BKE_blendfile_userdef_from_defaults(); } BKE_blender_userdef_app_template_data_swap(&U, userdef_default); bool ok = BKE_blendfile_userdef_write(filepath, reports); BKE_blender_userdef_app_template_data_swap(&U, userdef_default); BKE_blender_userdef_data_free(userdef_default, false); MEM_freeN(userdef_default); return ok; } bool BKE_blendfile_userdef_write_all(ReportList *reports) { char filepath[FILE_MAX]; bool ok = true; const bool use_template_userpref = BKE_appdir_app_template_has_userpref(U.app_template); std::optional cfgdir = BKE_appdir_folder_id_create(BLENDER_USER_CONFIG, nullptr); if (cfgdir) { bool ok_write; BLI_path_join(filepath, sizeof(filepath), cfgdir->c_str(), BLENDER_USERPREF_FILE); if (!G.quiet) { printf("Writing userprefs: \"%s\" ", filepath); } if (use_template_userpref) { ok_write = BKE_blendfile_userdef_write_app_template(filepath, reports); } else { ok_write = BKE_blendfile_userdef_write(filepath, reports); } if (ok_write) { if (!G.quiet) { printf("ok\n"); } BKE_report(reports, RPT_INFO, "Preferences saved"); } else { if (!G.quiet) { printf("fail\n"); } ok = false; BKE_report(reports, RPT_ERROR, "Saving preferences failed"); } } else { BKE_report(reports, RPT_ERROR, "Unable to create userpref path"); } if (use_template_userpref) { cfgdir = BKE_appdir_folder_id_create(BLENDER_USER_CONFIG, U.app_template); if (cfgdir) { /* Also save app-template preferences. */ BLI_path_join(filepath, sizeof(filepath), cfgdir->c_str(), BLENDER_USERPREF_FILE); if (!G.quiet) { printf("Writing userprefs app-template: \"%s\" ", filepath); } if (BKE_blendfile_userdef_write(filepath, reports) != 0) { if (!G.quiet) { printf("ok\n"); } } else { if (!G.quiet) { printf("fail\n"); } ok = false; } } else { BKE_report(reports, RPT_ERROR, "Unable to create app-template userpref path"); ok = false; } } if (ok) { U.runtime.is_dirty = false; } return ok; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Blend File IO (WorkSpace) * \{ */ WorkspaceConfigFileData *BKE_blendfile_workspace_config_read(const char *filepath, const void *file_buf, int file_buf_size, ReportList *reports) { BlendFileData *bfd; WorkspaceConfigFileData *workspace_config = nullptr; if (filepath) { BlendFileReadReport blend_file_read_reports{}; blend_file_read_reports.reports = reports; bfd = BLO_read_from_file(filepath, BLO_READ_SKIP_USERDEF, &blend_file_read_reports); } else { bfd = BLO_read_from_memory(file_buf, file_buf_size, BLO_READ_SKIP_USERDEF, reports); } if (bfd) { workspace_config = MEM_callocN(__func__); workspace_config->main = bfd->main; /* Only 2.80+ files have actual workspaces, don't try to use screens * from older versions. */ if (bfd->main->versionfile >= 280) { workspace_config->workspaces = bfd->main->workspaces; } MEM_delete(bfd); } return workspace_config; } void BKE_blendfile_workspace_config_data_free(WorkspaceConfigFileData *workspace_config) { BKE_main_free(workspace_config->main); MEM_freeN(workspace_config); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Blend File Write (Partial) * \{ */ static CLG_LogRef LOG_PARTIALWRITE = {"bke.blendfile.partial_write"}; namespace blender::bke::blendfile { PartialWriteContext::PartialWriteContext(StringRefNull reference_root_filepath) : reference_root_filepath_(reference_root_filepath) { BKE_main_init(this->bmain); if (!reference_root_filepath_.empty()) { STRNCPY(this->bmain.filepath, reference_root_filepath_.c_str()); } /* Only for IDs matching existing data in current G_MAIN. */ matching_uid_map_ = BKE_main_idmap_create(&this->bmain, false, nullptr, MAIN_IDMAP_TYPE_UID); /* For all IDs existing in the context. */ this->bmain.id_map = BKE_main_idmap_create( &this->bmain, false, nullptr, MAIN_IDMAP_TYPE_UID | MAIN_IDMAP_TYPE_NAME); }; PartialWriteContext::~PartialWriteContext() { BKE_main_idmap_destroy(matching_uid_map_); BLI_assert(this->bmain.next == nullptr); BKE_main_destroy(this->bmain); }; void PartialWriteContext::preempt_session_uid(ID *ctx_id, uint session_uid) { /* If there is already an existing ID in the 'matching' set with that UID, it should be the same * as the given ctx_id. */ ID *matching_ctx_id = BKE_main_idmap_lookup_uid(matching_uid_map_, session_uid); if (matching_ctx_id == ctx_id) { /* That ID has already been added to the context, nothing to do. */ BLI_assert(matching_ctx_id->session_uid == session_uid); return; } if (matching_ctx_id != nullptr) { /* Another ID in the context, who has a matching ID in current G_MAIN, is sharing the same * session UID. This marks a critical corruption somewhere! */ CLOG_FATAL( &LOG_PARTIALWRITE, "Different matching IDs sharing the same session UID in the partial write context."); return; } /* No ID with this session UID in the context, who's matching a current ID in G_MAIN. Check if a * non-matching context ID is already using that UID, if yes, regenerate a new one for it, such * that given `ctx_id` can use the desired UID. */ /* NOTE: In theory, there should never be any session uid collision currently, since these are * generated session-wide, regardless of the type/source of the IDs. */ matching_ctx_id = BKE_main_idmap_lookup_uid(this->bmain.id_map, session_uid); BLI_assert(matching_ctx_id != ctx_id); if (matching_ctx_id) { CLOG_INFO(&LOG_PARTIALWRITE, 3, "Non-matching IDs sharing the same session UID in the partial write context."); BKE_main_idmap_remove_id(this->bmain.id_map, matching_ctx_id); /* FIXME: Allow #BKE_lib_libblock_session_uid_renew to work with temp IDs? */ matching_ctx_id->tag &= ~ID_TAG_TEMP_MAIN; BKE_lib_libblock_session_uid_renew(matching_ctx_id); matching_ctx_id->tag |= ID_TAG_TEMP_MAIN; BKE_main_idmap_insert_id(this->bmain.id_map, matching_ctx_id); BLI_assert(BKE_main_idmap_lookup_uid(this->bmain.id_map, session_uid) == nullptr); } ctx_id->session_uid = session_uid; } void PartialWriteContext::process_added_id(ID *ctx_id, const PartialWriteContext::IDAddOperations operations) { const bool set_fake_user = (operations & SET_FAKE_USER) != 0; const bool set_clipboard_mark = (operations & SET_CLIPBOARD_MARK) != 0; if (set_fake_user) { id_fake_user_set(ctx_id); } else { /* NOTE: Using this tag will ensure that this ID is written on disk in current state (current * context session). However, reloading the blendfile will clear this tag. */ id_us_ensure_real(ctx_id); } if (set_clipboard_mark) { ctx_id->flag |= ID_FLAG_CLIPBOARD_MARK; } } ID *PartialWriteContext::id_add_copy(const ID *id, const bool regenerate_session_uid) { ID *ctx_root_id = nullptr; BLI_assert(BKE_main_idmap_lookup_uid(matching_uid_map_, id->session_uid) == nullptr); const int copy_flags = (LIB_ID_CREATE_NO_MAIN | LIB_ID_CREATE_NO_USER_REFCOUNT | /* NOTE: Could make this an option if needed in the future */ LIB_ID_COPY_ASSET_METADATA); ctx_root_id = BKE_id_copy_in_lib(nullptr, id->lib, id, std::nullopt, nullptr, copy_flags); ctx_root_id->tag |= ID_TAG_TEMP_MAIN; /* Ensure that the newly copied ID has a library in temp local bmain if it was linked. * While this could be optimized out in case the ID is made local in the context, this adds * complexity as default ID management code like 'make local' code will create invalid bmain * namemap data. */ this->ensure_library(ctx_root_id); if (regenerate_session_uid) { /* Calling #BKE_lib_libblock_session_uid_renew is not needed here, copying already generated a * new one. */ BLI_assert(BKE_main_idmap_lookup_uid(matching_uid_map_, id->session_uid) == nullptr); } else { this->preempt_session_uid(ctx_root_id, id->session_uid); BKE_main_idmap_insert_id(matching_uid_map_, ctx_root_id); } BKE_main_idmap_insert_id(this->bmain.id_map, ctx_root_id); BKE_libblock_management_main_add(&this->bmain, ctx_root_id); /* Note: remapping of external file relative paths is done as part of the 'write' process. */ return ctx_root_id; } void PartialWriteContext::make_local(ID *ctx_id, const int make_local_flags) { /* Making an ID local typically resets its session UID, here we want to keep the same value. */ const uint ctx_id_session_uid = ctx_id->session_uid; BKE_main_idmap_remove_id(this->bmain.id_map, ctx_id); BKE_main_idmap_remove_id(matching_uid_map_, ctx_id); if (ID_IS_LINKED(ctx_id)) { BKE_lib_id_make_local(&this->bmain, ctx_id, make_local_flags); } /* NOTE: Cannot rely only on `ID_IS_OVERRIDE_LIBRARY` here, as the reference pointer to the * linked data may have already been cleared out by dependency management in code above that * call. */ else if ((ctx_id->override_library || ID_IS_OVERRIDE_LIBRARY(ctx_id)) && (make_local_flags & LIB_ID_MAKELOCAL_LIBOVERRIDE_CLEAR) != 0) { BKE_lib_override_library_make_local(&this->bmain, ctx_id); } this->preempt_session_uid(ctx_id, ctx_id_session_uid); BKE_main_idmap_insert_id(this->bmain.id_map, ctx_id); BKE_main_idmap_insert_id(matching_uid_map_, ctx_id); } Library *PartialWriteContext::ensure_library(ID *ctx_id) { if (!ID_IS_LINKED(ctx_id)) { return nullptr; } blender::StringRefNull lib_path = ctx_id->lib->runtime->filepath_abs; Library *ctx_lib = this->libraries_map_.lookup_default(lib_path, nullptr); if (!ctx_lib) { ctx_lib = reinterpret_cast(id_add_copy(&ctx_id->lib->id, true)); this->libraries_map_.add(lib_path, ctx_lib); } ctx_id->lib = ctx_lib; return ctx_lib; } Library *PartialWriteContext::ensure_library(blender::StringRefNull library_absolute_path) { Library *ctx_lib = this->libraries_map_.lookup_default(library_absolute_path, nullptr); if (!ctx_lib) { const char *library_name = BLI_path_basename(library_absolute_path.c_str()); ctx_lib = static_cast( BKE_id_new_in_lib(&this->bmain, nullptr, ID_LI, library_name)); ctx_lib->id.tag |= ID_TAG_TEMP_MAIN; id_us_min(&ctx_lib->id); this->libraries_map_.add(library_absolute_path, ctx_lib); } return ctx_lib; } ID *PartialWriteContext::id_add( const ID *id, PartialWriteContext::IDAddOptions options, blender::FunctionRef dependencies_filter_cb) { constexpr int make_local_flags = (LIB_ID_MAKELOCAL_INDIRECT | LIB_ID_MAKELOCAL_FORCE_LOCAL | LIB_ID_MAKELOCAL_LIBOVERRIDE_CLEAR); const bool add_dependencies = (options.operations & ADD_DEPENDENCIES) != 0; const bool clear_dependencies = (options.operations & CLEAR_DEPENDENCIES) != 0; const bool duplicate_dependencies = (options.operations & DUPLICATE_DEPENDENCIES) != 0; BLI_assert(clear_dependencies || add_dependencies || dependencies_filter_cb); BLI_assert(!clear_dependencies || !(add_dependencies || duplicate_dependencies)); UNUSED_VARS_NDEBUG(add_dependencies, clear_dependencies, duplicate_dependencies); /* Do not directly add an embedded ID. Add its owner instead. */ if (id->flag & ID_FLAG_EMBEDDED_DATA) { id = BKE_id_owner_get(const_cast(id), true); } /* The given ID may have already been added (either explicitly or as a dependency) before. */ ID *ctx_root_id = BKE_main_idmap_lookup_uid(matching_uid_map_, id->session_uid); if (ctx_root_id) { /* If the root orig ID is already in the context, assume all of its dependencies are as well. */ BLI_assert(ctx_root_id->session_uid == id->session_uid); this->process_added_id(ctx_root_id, options.operations); return ctx_root_id; } /* Local mapping, such that even in case dependencies are duplicated for this specific added ID, * once a dependency has been duplicated, it can be re-used for other ID usages within the * dependencies of the added ID. */ blender::Map local_ctx_id_map; /* A list of IDs to post-process. Only contains IDs that were actually added to the context (not * the ones that were already there and were re-used). The #IDAddOperations item of the pair * stores the returned value from the given #dependencies_filter_cb (or given global #options * parameter otherwise). */ blender::Vector> post_process_ids_todo; ctx_root_id = id_add_copy(id, false); BLI_assert(ctx_root_id->session_uid == id->session_uid); local_ctx_id_map.add(id, ctx_root_id); post_process_ids_todo.append({ctx_root_id, options.operations}); this->process_added_id(ctx_root_id, options.operations); blender::VectorSet ids_to_process{ctx_root_id}; auto dependencies_cb = [this, options, &local_ctx_id_map, &ids_to_process, &post_process_ids_todo, dependencies_filter_cb](LibraryIDLinkCallbackData *cb_data) -> int { ID **id_ptr = cb_data->id_pointer; const ID *orig_deps_id = *id_ptr; if (cb_data->cb_flag & (IDWALK_CB_EMBEDDED | IDWALK_CB_EMBEDDED_NOT_OWNING)) { return IDWALK_RET_NOP; } if (!orig_deps_id) { return IDWALK_RET_NOP; } if (cb_data->cb_flag & IDWALK_CB_INTERNAL) { /* Cleanup internal ID pointers. */ *id_ptr = nullptr; return IDWALK_RET_NOP; } PartialWriteContext::IDAddOperations operations_final = (options.operations & MASK_INHERITED); if (dependencies_filter_cb) { const PartialWriteContext::IDAddOperations operations_per_id = dependencies_filter_cb( cb_data, options); operations_final = ((operations_per_id & MASK_PER_ID_USAGE) | (operations_final & ~MASK_PER_ID_USAGE)); } const bool add_dependencies = (operations_final & ADD_DEPENDENCIES) != 0; const bool clear_dependencies = (operations_final & CLEAR_DEPENDENCIES) != 0; const bool duplicate_dependencies = (operations_final & DUPLICATE_DEPENDENCIES) != 0; BLI_assert(clear_dependencies || add_dependencies); BLI_assert(!clear_dependencies || !(add_dependencies || duplicate_dependencies)); UNUSED_VARS_NDEBUG(add_dependencies); if (clear_dependencies) { if (cb_data->cb_flag & IDWALK_CB_NEVER_NULL) { CLOG_WARN(&LOG_PARTIALWRITE, "Clearing a 'never null' ID usage of '%s' by '%s', this is likely not a " "desired action", (*id_ptr)->name, cb_data->owner_id->name); } /* Owner ID should be a 'context-main' duplicate of a real Main ID, as such there should be * no need to decrease ID usages refcount here. */ *id_ptr = nullptr; return IDWALK_RET_NOP; } /* else if (add_dependencies) */ /* The given ID may have already been added (either explicitly or as a dependency) before. */ ID *ctx_deps_id = nullptr; if (duplicate_dependencies) { ctx_deps_id = local_ctx_id_map.lookup(orig_deps_id); } else { ctx_deps_id = BKE_main_idmap_lookup_uid(matching_uid_map_, orig_deps_id->session_uid); } if (!ctx_deps_id) { if (cb_data->cb_flag & IDWALK_CB_LOOPBACK) { /* Do not follow 'loop back' pointers. */ /* NOTE: Not sure whether this should be considered an error or not. Typically hitting such * a case is bad practice. On the other hand, some of these pointers are present in * 'normal' IDs, like e.g. the parent collections ones. This implies that currently, all * attempt to adding a collection to a partial write context should make usage of a custom * `dependencies_filter_cb` function to explicitly clear these pointers. */ CLOG_ERROR(&LOG_PARTIALWRITE, "First dependency to ID '%s' found through a 'loopback' usage from ID '%s', " "this should never happen", (*id_ptr)->name, cb_data->owner_id->name); *id_ptr = nullptr; return IDWALK_RET_NOP; } ctx_deps_id = this->id_add_copy(orig_deps_id, duplicate_dependencies); local_ctx_id_map.add(orig_deps_id, ctx_deps_id); ids_to_process.add(ctx_deps_id); post_process_ids_todo.append({ctx_deps_id, operations_final}); } if (duplicate_dependencies) { BLI_assert(ctx_deps_id->session_uid != orig_deps_id->session_uid); } else { BLI_assert(ctx_deps_id->session_uid == orig_deps_id->session_uid); } this->process_added_id(ctx_deps_id, operations_final); /* In-place remapping. */ *id_ptr = ctx_deps_id; return IDWALK_RET_NOP; }; while (!ids_to_process.is_empty()) { ID *ctx_id = ids_to_process.pop(); BKE_library_foreach_ID_link( &this->bmain, ctx_id, dependencies_cb, &options, IDWALK_DO_INTERNAL_RUNTIME_POINTERS); } /* Post process all newly added IDs in the context: * - Make them local or ensure that their library reference is also in the context. */ for (auto [ctx_id, options_final] : post_process_ids_todo) { const bool do_make_local = (options_final & MAKE_LOCAL) != 0; if (do_make_local) { this->make_local(ctx_id, make_local_flags); } } return ctx_root_id; } ID *PartialWriteContext::id_create(const short id_type, const blender::StringRefNull id_name, Library *library, PartialWriteContext::IDAddOptions options) { Library *ctx_library = nullptr; if (library) { ctx_library = this->ensure_library(library->runtime->filepath_abs); } ID *ctx_id = static_cast( BKE_id_new_in_lib(&this->bmain, ctx_library, id_type, id_name.c_str())); ctx_id->tag |= ID_TAG_TEMP_MAIN; id_us_min(ctx_id); this->process_added_id(ctx_id, options.operations); /* See function doc about why handling of #matching_uid_map_ can be skipped here. */ BKE_main_idmap_insert_id(this->bmain.id_map, ctx_id); return ctx_id; } void PartialWriteContext::id_delete(const ID *id) { if (ID *ctx_id = BKE_main_idmap_lookup_uid(matching_uid_map_, id->session_uid)) { BKE_main_idmap_remove_id(matching_uid_map_, ctx_id); BKE_id_delete(&this->bmain, ctx_id); } } void PartialWriteContext::remove_unused(const bool clear_extra_user) { LibQueryUnusedIDsData parameters; parameters.do_local_ids = true; parameters.do_linked_ids = true; parameters.do_recursive = true; if (clear_extra_user) { ID *id_iter; FOREACH_MAIN_ID_BEGIN (&this->bmain, id_iter) { id_us_clear_real(id_iter); } FOREACH_MAIN_ID_END; } BKE_lib_query_unused_ids_tag(&this->bmain, ID_TAG_DOIT, parameters); CLOG_INFO(&LOG_PARTIALWRITE, 3, "Removing %d unused IDs from current partial write context", parameters.num_total[INDEX_ID_NULL]); ID *id_iter; FOREACH_MAIN_ID_BEGIN (&this->bmain, id_iter) { if ((id_iter->tag & ID_TAG_DOIT) != 0) { BKE_main_idmap_remove_id(matching_uid_map_, id_iter); } } FOREACH_MAIN_ID_END; BKE_id_multi_tagged_delete(&this->bmain); } void PartialWriteContext::clear() { BKE_main_idmap_clear(*matching_uid_map_); BKE_main_clear(this->bmain); } bool PartialWriteContext::is_valid() { blender::Set ids_in_context; blender::Set session_uids_in_context; bool is_valid = true; ID *id_iter; /* Fill `ids_in_context`, check uniqueness of session_uid's. */ FOREACH_MAIN_ID_BEGIN (&this->bmain, id_iter) { ids_in_context.add(id_iter); if (session_uids_in_context.contains(id_iter->session_uid)) { CLOG_ERROR(&LOG_PARTIALWRITE, "ID %s does not have a unique session_uid", id_iter->name); is_valid = false; } else { session_uids_in_context.add(id_iter->session_uid); } } FOREACH_MAIN_ID_END; /* Check that no ID uses IDs from outside this context. */ auto id_validate_dependencies_cb = [&ids_in_context, &is_valid](LibraryIDLinkCallbackData *cb_data) -> int { ID **id_p = cb_data->id_pointer; ID *owner_id = cb_data->owner_id; ID *self_id = cb_data->self_id; /* By definition, embedded IDs are not in Main, so they are not listed in this context either. */ if (cb_data->cb_flag & (IDWALK_CB_EMBEDDED | IDWALK_CB_EMBEDDED_NOT_OWNING)) { return IDWALK_RET_NOP; } if (*id_p && !ids_in_context.contains(*id_p)) { if (owner_id != self_id) { CLOG_ERROR( &LOG_PARTIALWRITE, "ID %s (used by ID '%s', embedded ID '%s') is not in current partial write context", (*id_p)->name, owner_id->name, self_id->name); } else { CLOG_ERROR(&LOG_PARTIALWRITE, "ID %s (used by ID '%s') is not in current partial write context", (*id_p)->name, owner_id->name); } is_valid = false; } return IDWALK_RET_NOP; }; FOREACH_MAIN_ID_BEGIN (&this->bmain, id_iter) { BKE_library_foreach_ID_link( &this->bmain, id_iter, id_validate_dependencies_cb, nullptr, IDWALK_READONLY); } FOREACH_MAIN_ID_END; return is_valid; } bool PartialWriteContext::write(const char *write_filepath, const int write_flags, const int remap_mode, ReportList &reports) { BLI_assert_msg(write_filepath != reference_root_filepath_, "A library blendfile should not overwrite currently edited blendfile"); /* In case the write path is the same as one of the libraries used by this context, make this * library local, and delete it (and all of its potentially remaining linked data). */ blender::Vector make_local_libs; LISTBASE_FOREACH (Library *, library, &this->bmain.libraries) { if (STREQ(write_filepath, library->runtime->filepath_abs)) { make_local_libs.append(library); } } /* Will likely change in the near future (embedded linked IDs, virtual libraries...), but * currently this should never happen. */ if (make_local_libs.size() > 1) { CLOG_WARN(&LOG_PARTIALWRITE, "%d libraries found using the same filepath as destination one ('%s'), should " "never happen.", int32_t(make_local_libs.size()), write_filepath); } for (Library *lib : make_local_libs) { BKE_library_make_local(&this->bmain, lib, nullptr, false, false, false); BKE_id_delete(&this->bmain, lib); } make_local_libs.clear(); BLI_assert(this->is_valid()); BlendFileWriteParams blend_file_write_params{}; blend_file_write_params.remap_mode = eBLO_WritePathRemap(remap_mode); return BLO_write_file( &this->bmain, write_filepath, write_flags, &blend_file_write_params, &reports); } bool PartialWriteContext::write(const char *write_filepath, ReportList &reports) { return this->write(write_filepath, 0, BLO_WRITE_PATH_REMAP_RELATIVE, reports); } } // namespace blender::bke::blendfile /** \} */