This repository has been archived on 2023-10-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
blender-archive/source/blender/asset_system/intern/asset_catalog.cc
Julian Eisel 35e54b52e6 Assets: "All" asset library
Adds a new built-in asset library that contains all other asset
libraries visible in the asset library selector menu. This also means
all their asset catalogs will be displayed as a single merged tree. The
asset catalogs are not editable, since this would require support for
writing multiple catalog definition files, which isn't there yet.

Often it's not relevant where an asset comes from. Users just want to be
able to get an asset quickly, comparable to how people use a search
engine to browse images or the web itself, instead of first going to a
dedicated platform. They don't want to bother with first choosing where
they want the result to come from.
This especially is needed for the Asset Shelf (T102879) that is being
developed for the brush assets project (T101895). With this, users will
have access to all their brushes efficiently from the 3D view, without
much browsing.

Did an informal review of the asset system bits with Sybren.
2023-01-10 15:41:58 +01:00

1011 lines
33 KiB
C++

/* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup asset_system
*/
#include <fstream>
#include <set>
#include "AS_asset_catalog.hh"
#include "AS_asset_catalog_tree.hh"
#include "AS_asset_library.hh"
#include "BLI_fileops.hh"
#include "BLI_path_util.h"
/* For S_ISREG() and S_ISDIR() on Windows. */
#ifdef WIN32
# include "BLI_winstuff.h"
#endif
#include "CLG_log.h"
static CLG_LogRef LOG = {"asset_system.asset_catalog_service"};
namespace blender::asset_system {
const CatalogFilePath AssetCatalogService::DEFAULT_CATALOG_FILENAME = "blender_assets.cats.txt";
const int AssetCatalogDefinitionFile::SUPPORTED_VERSION = 1;
const std::string AssetCatalogDefinitionFile::VERSION_MARKER = "VERSION ";
const std::string AssetCatalogDefinitionFile::HEADER =
"# This is an Asset Catalog Definition file for Blender.\n"
"#\n"
"# Empty lines and lines starting with `#` will be ignored.\n"
"# The first non-ignored line should be the version indicator.\n"
"# Other lines are of the format \"UUID:catalog/path/for/assets:simple catalog name\"\n";
AssetCatalogService::AssetCatalogService()
: catalog_collection_(std::make_unique<AssetCatalogCollection>()),
catalog_tree_(std::make_unique<AssetCatalogTree>())
{
}
AssetCatalogService::AssetCatalogService(read_only_tag) : AssetCatalogService()
{
const_cast<bool &>(is_read_only_) = true;
}
AssetCatalogService::AssetCatalogService(const CatalogFilePath &asset_library_root)
: AssetCatalogService()
{
asset_library_root_ = asset_library_root;
}
void AssetCatalogService::tag_has_unsaved_changes(AssetCatalog *edited_catalog)
{
BLI_assert(!is_read_only_);
if (edited_catalog) {
edited_catalog->flags.has_unsaved_changes = true;
}
BLI_assert(catalog_collection_);
catalog_collection_->has_unsaved_changes_ = true;
}
void AssetCatalogService::untag_has_unsaved_changes()
{
BLI_assert(catalog_collection_);
catalog_collection_->has_unsaved_changes_ = false;
/* TODO(Sybren): refactor; this is more like "post-write cleanup" than "remove a tag" code. */
/* Forget about any deleted catalogs. */
if (catalog_collection_->catalog_definition_file_) {
for (CatalogID catalog_id : catalog_collection_->deleted_catalogs_.keys()) {
catalog_collection_->catalog_definition_file_->forget(catalog_id);
}
}
catalog_collection_->deleted_catalogs_.clear();
/* Mark all remaining catalogs as "without unsaved changes". */
for (auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
catalog_uptr->flags.has_unsaved_changes = false;
}
}
bool AssetCatalogService::has_unsaved_changes() const
{
BLI_assert(catalog_collection_);
return catalog_collection_->has_unsaved_changes_;
}
bool AssetCatalogService::is_read_only() const
{
return is_read_only_;
}
void AssetCatalogService::tag_all_catalogs_as_unsaved_changes()
{
for (auto &catalog : catalog_collection_->catalogs_.values()) {
catalog->flags.has_unsaved_changes = true;
}
catalog_collection_->has_unsaved_changes_ = true;
}
bool AssetCatalogService::is_empty() const
{
BLI_assert(catalog_collection_);
return catalog_collection_->catalogs_.is_empty();
}
OwningAssetCatalogMap &AssetCatalogService::get_catalogs()
{
return catalog_collection_->catalogs_;
}
OwningAssetCatalogMap &AssetCatalogService::get_deleted_catalogs()
{
return catalog_collection_->deleted_catalogs_;
}
AssetCatalogDefinitionFile *AssetCatalogService::get_catalog_definition_file()
{
return catalog_collection_->catalog_definition_file_.get();
}
AssetCatalog *AssetCatalogService::find_catalog(CatalogID catalog_id) const
{
const std::unique_ptr<AssetCatalog> *catalog_uptr_ptr =
catalog_collection_->catalogs_.lookup_ptr(catalog_id);
if (catalog_uptr_ptr == nullptr) {
return nullptr;
}
return catalog_uptr_ptr->get();
}
AssetCatalog *AssetCatalogService::find_catalog_by_path(const AssetCatalogPath &path) const
{
/* Use an AssetCatalogOrderedSet to find the 'best' catalog for this path. This will be the first
* one loaded from disk, or if that does not exist the one with the lowest UUID. This ensures
* stable, predictable results. */
MutableAssetCatalogOrderedSet ordered_catalogs;
for (const auto &catalog : catalog_collection_->catalogs_.values()) {
if (catalog->path == path) {
ordered_catalogs.insert(catalog.get());
}
}
if (ordered_catalogs.empty()) {
return nullptr;
}
MutableAssetCatalogOrderedSet::iterator best_choice_it = ordered_catalogs.begin();
return *best_choice_it;
}
bool AssetCatalogService::is_catalog_known(CatalogID catalog_id) const
{
BLI_assert(catalog_collection_);
return catalog_collection_->catalogs_.contains(catalog_id);
}
AssetCatalogFilter AssetCatalogService::create_catalog_filter(
const CatalogID active_catalog_id) const
{
Set<CatalogID> matching_catalog_ids;
Set<CatalogID> known_catalog_ids;
matching_catalog_ids.add(active_catalog_id);
const AssetCatalog *active_catalog = find_catalog(active_catalog_id);
/* This cannot just iterate over tree items to get all the required data, because tree items only
* represent single UUIDs. It could be used to get the main UUIDs of the children, though, and
* then only do an exact match on the path (instead of the more complex `is_contained_in()`
* call). Without an extra indexed-by-path acceleration structure, this is still going to require
* a linear search, though. */
for (const auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
if (active_catalog && catalog_uptr->path.is_contained_in(active_catalog->path)) {
matching_catalog_ids.add(catalog_uptr->catalog_id);
}
known_catalog_ids.add(catalog_uptr->catalog_id);
}
return AssetCatalogFilter(std::move(matching_catalog_ids), std::move(known_catalog_ids));
}
void AssetCatalogService::delete_catalog_by_id_soft(const CatalogID catalog_id)
{
std::unique_ptr<AssetCatalog> *catalog_uptr_ptr = catalog_collection_->catalogs_.lookup_ptr(
catalog_id);
if (catalog_uptr_ptr == nullptr) {
/* Catalog cannot be found, which is fine. */
return;
}
/* Mark the catalog as deleted. */
AssetCatalog *catalog = catalog_uptr_ptr->get();
catalog->flags.is_deleted = true;
/* Move ownership from catalog_collection_->catalogs_ to catalog_collection_->deleted_catalogs_.
*/
catalog_collection_->deleted_catalogs_.add(catalog_id, std::move(*catalog_uptr_ptr));
/* The catalog can now be removed from the map without freeing the actual AssetCatalog. */
catalog_collection_->catalogs_.remove(catalog_id);
}
void AssetCatalogService::delete_catalog_by_id_hard(CatalogID catalog_id)
{
catalog_collection_->catalogs_.remove(catalog_id);
catalog_collection_->deleted_catalogs_.remove(catalog_id);
/* TODO(@sybren): adjust this when supporting multiple CDFs. */
catalog_collection_->catalog_definition_file_->forget(catalog_id);
}
void AssetCatalogService::prune_catalogs_by_path(const AssetCatalogPath &path)
{
/* Build a collection of catalog IDs to delete. */
Set<CatalogID> catalogs_to_delete;
for (const auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
const AssetCatalog *cat = catalog_uptr.get();
if (cat->path.is_contained_in(path)) {
catalogs_to_delete.add(cat->catalog_id);
}
}
/* Delete the catalogs. */
for (const CatalogID cat_id : catalogs_to_delete) {
this->delete_catalog_by_id_soft(cat_id);
}
this->rebuild_tree();
}
void AssetCatalogService::prune_catalogs_by_id(const CatalogID catalog_id)
{
const AssetCatalog *catalog = find_catalog(catalog_id);
BLI_assert_msg(catalog, "trying to prune asset catalogs by the path of a non-existent catalog");
if (!catalog) {
return;
}
this->prune_catalogs_by_path(catalog->path);
}
void AssetCatalogService::update_catalog_path(const CatalogID catalog_id,
const AssetCatalogPath &new_catalog_path)
{
AssetCatalog *renamed_cat = this->find_catalog(catalog_id);
const AssetCatalogPath old_cat_path = renamed_cat->path;
for (auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
AssetCatalog *cat = catalog_uptr.get();
const AssetCatalogPath new_path = cat->path.rebase(old_cat_path, new_catalog_path);
if (!new_path) {
continue;
}
cat->path = new_path;
cat->simple_name_refresh();
/* TODO(Sybren): go over all assets that are assigned to this catalog, defined in the current
* blend file, and update the catalog simple name stored there. */
}
this->rebuild_tree();
}
AssetCatalog *AssetCatalogService::create_catalog(const AssetCatalogPath &catalog_path)
{
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path(catalog_path);
catalog->flags.has_unsaved_changes = true;
/* So we can std::move(catalog) and still use the non-owning pointer: */
AssetCatalog *const catalog_ptr = catalog.get();
/* TODO(@sybren): move the `AssetCatalog::from_path()` function to another place, that can reuse
* catalogs when a catalog with the given path is already known, and avoid duplicate catalog IDs.
*/
BLI_assert_msg(!catalog_collection_->catalogs_.contains(catalog->catalog_id),
"duplicate catalog ID not supported");
catalog_collection_->catalogs_.add_new(catalog->catalog_id, std::move(catalog));
if (catalog_collection_->catalog_definition_file_) {
/* Ensure the new catalog gets written to disk at some point. If there is no CDF in memory yet,
* it's enough to have the catalog known to the service as it'll be saved to a new file. */
catalog_collection_->catalog_definition_file_->add_new(catalog_ptr);
}
BLI_assert_msg(catalog_tree_, "An Asset Catalog tree should always exist.");
catalog_tree_->insert_item(*catalog_ptr);
return catalog_ptr;
}
static std::string asset_definition_default_file_path_from_dir(StringRef asset_library_root)
{
char file_path[PATH_MAX];
BLI_path_join(file_path,
sizeof(file_path),
asset_library_root.data(),
AssetCatalogService::DEFAULT_CATALOG_FILENAME.data());
return file_path;
}
void AssetCatalogService::load_from_disk()
{
load_from_disk(asset_library_root_);
}
void AssetCatalogService::load_from_disk(const CatalogFilePath &file_or_directory_path)
{
BLI_stat_t status;
if (BLI_stat(file_or_directory_path.data(), &status) == -1) {
/* TODO(@sybren): throw an appropriate exception. */
CLOG_WARN(&LOG, "path not found: %s", file_or_directory_path.data());
return;
}
if (S_ISREG(status.st_mode)) {
load_single_file(file_or_directory_path);
}
else if (S_ISDIR(status.st_mode)) {
load_directory_recursive(file_or_directory_path);
}
else {
/* TODO(@sybren): throw an appropriate exception. */
}
/* TODO: Should there be a sanitize step? E.g. to remove catalogs with identical paths? */
rebuild_tree();
}
void AssetCatalogService::add_from_existing(const AssetCatalogService &other_service)
{
catalog_collection_->add_catalogs_from_existing(*other_service.catalog_collection_);
}
void AssetCatalogService::load_directory_recursive(const CatalogFilePath &directory_path)
{
/* TODO(@sybren): implement proper multi-file support. For now, just load
* the default file if it is there. */
CatalogFilePath file_path = asset_definition_default_file_path_from_dir(directory_path);
if (!BLI_exists(file_path.data())) {
/* No file to be loaded is perfectly fine. */
CLOG_INFO(&LOG, 2, "path not found: %s", file_path.data());
return;
}
this->load_single_file(file_path);
}
void AssetCatalogService::load_single_file(const CatalogFilePath &catalog_definition_file_path)
{
/* TODO(@sybren): check that #catalog_definition_file_path is contained in #asset_library_root_,
* otherwise some assumptions may fail. */
std::unique_ptr<AssetCatalogDefinitionFile> cdf = parse_catalog_file(
catalog_definition_file_path);
BLI_assert_msg(!catalog_collection_->catalog_definition_file_,
"Only loading of a single catalog definition file is supported.");
catalog_collection_->catalog_definition_file_ = std::move(cdf);
}
std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::parse_catalog_file(
const CatalogFilePath &catalog_definition_file_path)
{
auto cdf = std::make_unique<AssetCatalogDefinitionFile>();
cdf->file_path = catalog_definition_file_path;
/* TODO(Sybren): this might have to move to a higher level when supporting multiple CDFs. */
Set<AssetCatalogPath> seen_paths;
auto catalog_parsed_callback = [this, catalog_definition_file_path, &seen_paths](
std::unique_ptr<AssetCatalog> catalog) {
if (catalog_collection_->catalogs_.contains(catalog->catalog_id)) {
/* TODO(@sybren): apparently another CDF was already loaded. This is not supported yet. */
std::cerr << catalog_definition_file_path << ": multiple definitions of catalog "
<< catalog->catalog_id << " in multiple files, ignoring this one." << std::endl;
/* Don't store 'catalog'; unique_ptr will free its memory. */
return false;
}
catalog->flags.is_first_loaded = seen_paths.add(catalog->path);
/* The AssetCatalog pointer is now owned by the AssetCatalogService. */
catalog_collection_->catalogs_.add_new(catalog->catalog_id, std::move(catalog));
return true;
};
cdf->parse_catalog_file(cdf->file_path, catalog_parsed_callback);
return cdf;
}
void AssetCatalogService::reload_catalogs()
{
/* TODO(Sybren): expand to support multiple CDFs. */
AssetCatalogDefinitionFile *const cdf = catalog_collection_->catalog_definition_file_.get();
if (!cdf || cdf->file_path.empty() || !BLI_is_file(cdf->file_path.c_str())) {
return;
}
/* Keeps track of the catalog IDs that are seen in the CDF, so that we also know what was deleted
* from the file on disk. */
Set<CatalogID> cats_in_file;
auto catalog_parsed_callback = [this, &cats_in_file](std::unique_ptr<AssetCatalog> catalog) {
const CatalogID catalog_id = catalog->catalog_id;
cats_in_file.add(catalog_id);
const bool should_skip = is_catalog_known_with_unsaved_changes(catalog_id);
if (should_skip) {
/* Do not overwrite unsaved local changes. */
return false;
}
/* This is either a new catalog, or we can just replace the in-memory one with the newly loaded
* one. */
catalog_collection_->catalogs_.add_overwrite(catalog_id, std::move(catalog));
return true;
};
cdf->parse_catalog_file(cdf->file_path, catalog_parsed_callback);
this->purge_catalogs_not_listed(cats_in_file);
this->rebuild_tree();
}
void AssetCatalogService::purge_catalogs_not_listed(const Set<CatalogID> &catalogs_to_keep)
{
Set<CatalogID> cats_to_remove;
for (CatalogID cat_id : this->catalog_collection_->catalogs_.keys()) {
if (catalogs_to_keep.contains(cat_id)) {
continue;
}
if (is_catalog_known_with_unsaved_changes(cat_id)) {
continue;
}
/* This catalog is not on disk, but also not modified, so get rid of it. */
cats_to_remove.add(cat_id);
}
for (CatalogID cat_id : cats_to_remove) {
delete_catalog_by_id_hard(cat_id);
}
}
bool AssetCatalogService::is_catalog_known_with_unsaved_changes(const CatalogID catalog_id) const
{
if (catalog_collection_->deleted_catalogs_.contains(catalog_id)) {
/* Deleted catalogs are always considered modified, by definition. */
return true;
}
const std::unique_ptr<AssetCatalog> *catalog_uptr_ptr =
catalog_collection_->catalogs_.lookup_ptr(catalog_id);
if (!catalog_uptr_ptr) {
/* Catalog is unknown. */
return false;
}
const bool has_unsaved_changes = (*catalog_uptr_ptr)->flags.has_unsaved_changes;
return has_unsaved_changes;
}
bool AssetCatalogService::write_to_disk(const CatalogFilePath &blend_file_path)
{
BLI_assert(!is_read_only_);
if (!write_to_disk_ex(blend_file_path)) {
return false;
}
untag_has_unsaved_changes();
rebuild_tree();
return true;
}
bool AssetCatalogService::write_to_disk_ex(const CatalogFilePath &blend_file_path)
{
/* TODO(Sybren): expand to support multiple CDFs. */
/* - Already loaded a CDF from disk? -> Always write to that file. */
if (catalog_collection_->catalog_definition_file_) {
reload_catalogs();
return catalog_collection_->catalog_definition_file_->write_to_disk();
}
if (catalog_collection_->catalogs_.is_empty() &&
catalog_collection_->deleted_catalogs_.is_empty()) {
/* Avoid saving anything, when there is nothing to save. */
return true; /* Writing nothing when there is nothing to write is still a success. */
}
const CatalogFilePath cdf_path_to_write = find_suitable_cdf_path_for_writing(blend_file_path);
catalog_collection_->catalog_definition_file_ = construct_cdf_in_memory(cdf_path_to_write);
reload_catalogs();
return catalog_collection_->catalog_definition_file_->write_to_disk();
}
void AssetCatalogService::prepare_to_merge_on_write()
{
/* TODO(Sybren): expand to support multiple CDFs. */
if (!catalog_collection_->catalog_definition_file_) {
/* There is no CDF connected, so it's a no-op. */
return;
}
/* Remove any association with the CDF, so that a new location will be chosen
* when the blend file is saved. */
catalog_collection_->catalog_definition_file_.reset();
/* Mark all in-memory catalogs as "dirty", to force them to be kept around on
* the next "load-merge-write" cycle. */
tag_all_catalogs_as_unsaved_changes();
}
CatalogFilePath AssetCatalogService::find_suitable_cdf_path_for_writing(
const CatalogFilePath &blend_file_path)
{
BLI_assert_msg(!blend_file_path.empty(),
"A non-empty .blend file path is required to be able to determine where the "
"catalog definition file should be put");
/* Ask the asset library API for an appropriate location. */
const std::string suitable_root_path = AS_asset_library_find_suitable_root_path_from_path(
blend_file_path);
if (!suitable_root_path.empty()) {
char asset_lib_cdf_path[PATH_MAX];
BLI_path_join(asset_lib_cdf_path,
sizeof(asset_lib_cdf_path),
suitable_root_path.c_str(),
DEFAULT_CATALOG_FILENAME.c_str());
return asset_lib_cdf_path;
}
/* Determine the default CDF path in the same directory of the blend file. */
char blend_dir_path[PATH_MAX];
BLI_split_dir_part(blend_file_path.c_str(), blend_dir_path, sizeof(blend_dir_path));
const CatalogFilePath cdf_path_next_to_blend = asset_definition_default_file_path_from_dir(
blend_dir_path);
return cdf_path_next_to_blend;
}
std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::construct_cdf_in_memory(
const CatalogFilePath &file_path)
{
auto cdf = std::make_unique<AssetCatalogDefinitionFile>();
cdf->file_path = file_path;
for (auto &catalog : catalog_collection_->catalogs_.values()) {
cdf->add_new(catalog.get());
}
return cdf;
}
AssetCatalogTree *AssetCatalogService::get_catalog_tree()
{
return catalog_tree_.get();
}
std::unique_ptr<AssetCatalogTree> AssetCatalogService::read_into_tree()
{
auto tree = std::make_unique<AssetCatalogTree>();
/* Go through the catalogs, insert each path component into the tree where needed. */
for (auto &catalog : catalog_collection_->catalogs_.values()) {
tree->insert_item(*catalog);
}
return tree;
}
void AssetCatalogService::rebuild_tree()
{
create_missing_catalogs();
this->catalog_tree_ = read_into_tree();
}
void AssetCatalogService::create_missing_catalogs()
{
/* Construct an ordered set of paths to check, so that parents are ordered before children. */
std::set<AssetCatalogPath> paths_to_check;
for (auto &catalog : catalog_collection_->catalogs_.values()) {
paths_to_check.insert(catalog->path);
}
std::set<AssetCatalogPath> seen_paths;
/* The empty parent should never be created, so always be considered "seen". */
seen_paths.insert(AssetCatalogPath(""));
/* Find and create missing direct parents (so ignoring parents-of-parents). */
while (!paths_to_check.empty()) {
/* Pop the first path of the queue. */
const AssetCatalogPath path = *paths_to_check.begin();
paths_to_check.erase(paths_to_check.begin());
if (seen_paths.find(path) != seen_paths.end()) {
/* This path has been seen already, so it can be ignored. */
continue;
}
seen_paths.insert(path);
const AssetCatalogPath parent_path = path.parent();
if (seen_paths.find(parent_path) != seen_paths.end()) {
/* The parent exists, continue to the next path. */
continue;
}
/* The parent doesn't exist, so create it and queue it up for checking its parent. */
AssetCatalog *parent_catalog = create_catalog(parent_path);
parent_catalog->flags.has_unsaved_changes = true;
paths_to_check.insert(parent_path);
}
/* TODO(Sybren): bind the newly created catalogs to a CDF, if we know about it. */
}
bool AssetCatalogService::is_undo_possbile() const
{
return !undo_snapshots_.is_empty();
}
bool AssetCatalogService::is_redo_possbile() const
{
return !redo_snapshots_.is_empty();
}
void AssetCatalogService::undo()
{
BLI_assert_msg(is_undo_possbile(), "Undo stack is empty");
redo_snapshots_.append(std::move(catalog_collection_));
catalog_collection_ = undo_snapshots_.pop_last();
rebuild_tree();
}
void AssetCatalogService::redo()
{
BLI_assert(!is_read_only_);
BLI_assert_msg(is_redo_possbile(), "Redo stack is empty");
undo_snapshots_.append(std::move(catalog_collection_));
catalog_collection_ = redo_snapshots_.pop_last();
rebuild_tree();
}
void AssetCatalogService::undo_push()
{
BLI_assert(!is_read_only_);
std::unique_ptr<AssetCatalogCollection> snapshot = catalog_collection_->deep_copy();
undo_snapshots_.append(std::move(snapshot));
redo_snapshots_.clear();
}
/* ---------------------------------------------------------------------- */
std::unique_ptr<AssetCatalogCollection> AssetCatalogCollection::deep_copy() const
{
auto copy = std::make_unique<AssetCatalogCollection>();
copy->has_unsaved_changes_ = this->has_unsaved_changes_;
copy->catalogs_ = copy_catalog_map(this->catalogs_);
copy->deleted_catalogs_ = copy_catalog_map(this->deleted_catalogs_);
if (catalog_definition_file_) {
copy->catalog_definition_file_ = catalog_definition_file_->copy_and_remap(
copy->catalogs_, copy->deleted_catalogs_);
}
return copy;
}
static void copy_catalog_map_into_existing(const OwningAssetCatalogMap &source,
OwningAssetCatalogMap &dest)
{
for (const auto &orig_catalog_uptr : source.values()) {
auto copy_catalog_uptr = std::make_unique<AssetCatalog>(*orig_catalog_uptr);
dest.add_new(copy_catalog_uptr->catalog_id, std::move(copy_catalog_uptr));
}
}
void AssetCatalogCollection::add_catalogs_from_existing(const AssetCatalogCollection &other)
{
copy_catalog_map_into_existing(other.catalogs_, catalogs_);
}
OwningAssetCatalogMap AssetCatalogCollection::copy_catalog_map(const OwningAssetCatalogMap &orig)
{
OwningAssetCatalogMap copy;
copy_catalog_map_into_existing(orig, copy);
return copy;
}
/* ---------------------------------------------------------------------- */
bool AssetCatalogDefinitionFile::contains(const CatalogID catalog_id) const
{
return catalogs_.contains(catalog_id);
}
void AssetCatalogDefinitionFile::add_new(AssetCatalog *catalog)
{
catalogs_.add_new(catalog->catalog_id, catalog);
}
void AssetCatalogDefinitionFile::add_overwrite(AssetCatalog *catalog)
{
catalogs_.add_overwrite(catalog->catalog_id, catalog);
}
void AssetCatalogDefinitionFile::forget(CatalogID catalog_id)
{
catalogs_.remove(catalog_id);
}
void AssetCatalogDefinitionFile::parse_catalog_file(
const CatalogFilePath &catalog_definition_file_path,
AssetCatalogParsedFn catalog_loaded_callback)
{
fstream infile(catalog_definition_file_path, std::ios::in);
if (!infile.is_open()) {
CLOG_ERROR(&LOG, "%s: unable to open file", catalog_definition_file_path.c_str());
return;
}
bool seen_version_number = false;
std::string line;
while (std::getline(infile, line)) {
const StringRef trimmed_line = StringRef(line).trim();
if (trimmed_line.is_empty() || trimmed_line[0] == '#') {
continue;
}
if (!seen_version_number) {
/* The very first non-ignored line should be the version declaration. */
const bool is_valid_version = this->parse_version_line(trimmed_line);
if (!is_valid_version) {
std::cerr << catalog_definition_file_path
<< ": first line should be version declaration; ignoring file." << std::endl;
break;
}
seen_version_number = true;
continue;
}
std::unique_ptr<AssetCatalog> catalog = this->parse_catalog_line(trimmed_line);
if (!catalog) {
continue;
}
AssetCatalog *non_owning_ptr = catalog.get();
const bool keep_catalog = catalog_loaded_callback(std::move(catalog));
if (!keep_catalog) {
continue;
}
/* The AssetDefinitionFile should include this catalog when writing it back to disk. */
this->add_overwrite(non_owning_ptr);
}
}
bool AssetCatalogDefinitionFile::parse_version_line(const StringRef line)
{
if (!line.startswith(VERSION_MARKER)) {
return false;
}
const std::string version_string = line.substr(VERSION_MARKER.length());
const int file_version = std::atoi(version_string.c_str());
/* No versioning, just a blunt check whether it's the right one. */
return file_version == SUPPORTED_VERSION;
}
std::unique_ptr<AssetCatalog> AssetCatalogDefinitionFile::parse_catalog_line(const StringRef line)
{
const char delim = ':';
const int64_t first_delim = line.find_first_of(delim);
if (first_delim == StringRef::not_found) {
std::cerr << "Invalid catalog line in " << this->file_path << ": " << line << std::endl;
return std::unique_ptr<AssetCatalog>(nullptr);
}
/* Parse the catalog ID. */
const std::string id_as_string = line.substr(0, first_delim).trim();
bUUID catalog_id;
const bool uuid_parsed_ok = BLI_uuid_parse_string(&catalog_id, id_as_string.c_str());
if (!uuid_parsed_ok) {
std::cerr << "Invalid UUID in " << this->file_path << ": " << line << std::endl;
return std::unique_ptr<AssetCatalog>(nullptr);
}
/* Parse the path and simple name. */
const StringRef path_and_simple_name = line.substr(first_delim + 1);
const int64_t second_delim = path_and_simple_name.find_first_of(delim);
std::string path_in_file;
std::string simple_name;
if (second_delim == 0) {
/* Delimiter as first character means there is no path. These lines are to be ignored. */
return std::unique_ptr<AssetCatalog>(nullptr);
}
if (second_delim == StringRef::not_found) {
/* No delimiter means no simple name, just treat it as all "path". */
path_in_file = path_and_simple_name;
simple_name = "";
}
else {
path_in_file = path_and_simple_name.substr(0, second_delim);
simple_name = path_and_simple_name.substr(second_delim + 1).trim();
}
AssetCatalogPath catalog_path = path_in_file;
return std::make_unique<AssetCatalog>(catalog_id, catalog_path.cleanup(), simple_name);
}
bool AssetCatalogDefinitionFile::write_to_disk() const
{
BLI_assert_msg(!this->file_path.empty(), "Writing to CDF requires its file path to be known");
return this->write_to_disk(this->file_path);
}
bool AssetCatalogDefinitionFile::write_to_disk(const CatalogFilePath &dest_file_path) const
{
const CatalogFilePath writable_path = dest_file_path + ".writing";
const CatalogFilePath backup_path = dest_file_path + "~";
if (!this->write_to_disk_unsafe(writable_path)) {
/* TODO: communicate what went wrong. */
return false;
}
if (BLI_exists(dest_file_path.c_str())) {
if (BLI_rename(dest_file_path.c_str(), backup_path.c_str())) {
/* TODO: communicate what went wrong. */
return false;
}
}
if (BLI_rename(writable_path.c_str(), dest_file_path.c_str())) {
/* TODO: communicate what went wrong. */
return false;
}
return true;
}
bool AssetCatalogDefinitionFile::write_to_disk_unsafe(const CatalogFilePath &dest_file_path) const
{
char directory[PATH_MAX];
BLI_split_dir_part(dest_file_path.c_str(), directory, sizeof(directory));
if (!ensure_directory_exists(directory)) {
/* TODO(Sybren): pass errors to the UI somehow. */
return false;
}
fstream output(dest_file_path, std::ios::out);
/* TODO(@sybren): remember the line ending style that was originally read, then use that to write
* the file again. */
/* Write the header. */
output << HEADER;
output << "" << std::endl;
output << VERSION_MARKER << SUPPORTED_VERSION << std::endl;
output << "" << std::endl;
/* Write the catalogs, ordered by path (primary) and UUID (secondary). */
AssetCatalogOrderedSet catalogs_by_path;
for (const AssetCatalog *catalog : catalogs_.values()) {
if (catalog->flags.is_deleted) {
continue;
}
catalogs_by_path.insert(catalog);
}
for (const AssetCatalog *catalog : catalogs_by_path) {
output << catalog->catalog_id << ":" << catalog->path << ":" << catalog->simple_name
<< std::endl;
}
output.close();
return !output.bad();
}
bool AssetCatalogDefinitionFile::ensure_directory_exists(
const CatalogFilePath directory_path) const
{
/* TODO(@sybren): design a way to get such errors presented to users (or ensure that they never
* occur). */
if (directory_path.empty()) {
std::cerr
<< "AssetCatalogService: no asset library root configured, unable to ensure it exists."
<< std::endl;
return false;
}
if (BLI_exists(directory_path.data())) {
if (!BLI_is_dir(directory_path.data())) {
std::cerr << "AssetCatalogService: " << directory_path
<< " exists but is not a directory, this is not a supported situation."
<< std::endl;
return false;
}
/* Root directory exists, work is done. */
return true;
}
/* Ensure the root directory exists. */
std::error_code err_code;
if (!BLI_dir_create_recursive(directory_path.data())) {
std::cerr << "AssetCatalogService: error creating directory " << directory_path << ": "
<< err_code << std::endl;
return false;
}
/* Root directory has been created, work is done. */
return true;
}
std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogDefinitionFile::copy_and_remap(
const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const
{
auto copy = std::make_unique<AssetCatalogDefinitionFile>(*this);
copy->catalogs_.clear();
/* Remap pointers of the copy from the original AssetCatalogCollection to the given one. */
for (CatalogID catalog_id : catalogs_.keys()) {
/* The catalog can be in the regular or the deleted map. */
const std::unique_ptr<AssetCatalog> *remapped_catalog_uptr_ptr = catalogs.lookup_ptr(
catalog_id);
if (remapped_catalog_uptr_ptr) {
copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get());
continue;
}
remapped_catalog_uptr_ptr = deleted_catalogs.lookup_ptr(catalog_id);
if (remapped_catalog_uptr_ptr) {
copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get());
continue;
}
BLI_assert(!"A CDF should only reference known catalogs.");
}
return copy;
}
AssetCatalog::AssetCatalog(const CatalogID catalog_id,
const AssetCatalogPath &path,
const std::string &simple_name)
: catalog_id(catalog_id), path(path), simple_name(simple_name)
{
}
std::unique_ptr<AssetCatalog> AssetCatalog::from_path(const AssetCatalogPath &path)
{
const AssetCatalogPath clean_path = path.cleanup();
const CatalogID cat_id = BLI_uuid_generate_random();
const std::string simple_name = sensible_simple_name_for_path(clean_path);
auto catalog = std::make_unique<AssetCatalog>(cat_id, clean_path, simple_name);
return catalog;
}
void AssetCatalog::simple_name_refresh()
{
this->simple_name = sensible_simple_name_for_path(this->path);
}
std::string AssetCatalog::sensible_simple_name_for_path(const AssetCatalogPath &path)
{
std::string name = path.str();
std::replace(name.begin(), name.end(), AssetCatalogPath::SEPARATOR, '-');
if (name.length() < MAX_NAME - 1) {
return name;
}
/* Trim off the start of the path, as that's the most generic part and thus contains the least
* information. */
return "..." + name.substr(name.length() - 60);
}
AssetCatalogFilter::AssetCatalogFilter(Set<CatalogID> &&matching_catalog_ids,
Set<CatalogID> &&known_catalog_ids)
: matching_catalog_ids_(std::move(matching_catalog_ids)),
known_catalog_ids_(std::move(known_catalog_ids))
{
}
bool AssetCatalogFilter::contains(const CatalogID asset_catalog_id) const
{
return matching_catalog_ids_.contains(asset_catalog_id);
}
bool AssetCatalogFilter::is_known(const CatalogID asset_catalog_id) const
{
if (BLI_uuid_is_nil(asset_catalog_id)) {
return false;
}
return known_catalog_ids_.contains(asset_catalog_id);
}
} // namespace blender::asset_system