// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. #include "Content.h" #include "JsonAsset.h" #include "Cache/AssetsCache.h" #include "Storage/ContentStorageManager.h" #include "Storage/JsonStorageProxy.h" #include "Factories/IAssetFactory.h" #include "Engine/Core/Log.h" #include "Engine/Core/Types/String.h" #include "Engine/Core/ObjectsRemovalService.h" #include "Engine/Engine/EngineService.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Threading/Threading.h" #include "Engine/Graphics/Graphics.h" #include "Engine/Engine/Time.h" #include "Engine/Engine/Globals.h" #include "Engine/Level/Types.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Scripting/ManagedCLR/MClass.h" #include "Engine/Scripting/ManagedCLR/MUtils.h" #include "Engine/Scripting/Scripting.h" #include "Engine/Utilities/StringConverter.h" #if USE_EDITOR #include "Editor/Editor.h" #include "Editor/ProjectInfo.h" #endif #if ENABLE_ASSETS_DISCOVERY #include "Engine/Core/Collections/HashSet.h" #endif TimeSpan Content::AssetsUpdateInterval = TimeSpan::FromMilliseconds(500); TimeSpan Content::AssetsUnloadInterval = TimeSpan::FromSeconds(10); Delegate Content::AssetDisposing; Delegate Content::AssetReloading; String AssetInfo::ToString() const { return String::Format(TEXT("ID: {0}, TypeName: {1}, Path: \'{2}\'"), ID, TypeName, Path); } namespace { // Assets CriticalSection AssetsLocker; Dictionary Assets(2048); CriticalSection LoadCallAssetsLocker; Array LoadCallAssets(64); CriticalSection LoadedAssetsToInvokeLocker; Array LoadedAssetsToInvoke(64); Array ToUnload; // Assets Registry Stuff AssetsCache Cache; // Unloading assets Dictionary UnloadQueue; TimeSpan LastUnloadCheckTime(0); bool IsExiting = false; #if ENABLE_ASSETS_DISCOVERY DateTime LastWorkspaceDiscovery; CriticalSection WorkspaceDiscoveryLocker; #endif } #if ENABLE_ASSETS_DISCOVERY bool findAsset(const Guid& id, const String& directory, Array& tmpCache, AssetInfo& info); #endif class ContentService : public EngineService { public: ContentService() : EngineService(TEXT("Content"), -600) { } bool Init() override; void Update() override; void LateUpdate() override; void Dispose() override; }; ContentService ContentServiceInstance; bool ContentService::Init() { // Load assets registry Cache.Init(); return false; } void ContentService::Update() { PROFILE_CPU(); ScopeLock lock(LoadedAssetsToInvokeLocker); // Broadcast `OnLoaded` events while (LoadedAssetsToInvoke.HasItems()) { auto asset = LoadedAssetsToInvoke.Dequeue(); asset->onLoaded_MainThread(); } } void ContentService::LateUpdate() { PROFILE_CPU(); // Check if need to perform an update of unloading assets const TimeSpan timeNow = Time::Update.UnscaledTime; if (timeNow - LastUnloadCheckTime < Content::AssetsUpdateInterval) return; LastUnloadCheckTime = timeNow; Asset* asset; ScopeLock lock(AssetsLocker); // TODO: maybe it would be better to link for asset remove ref event and cache only assets with no references - test it with millions of assets? // Verify all assets for (auto i = Assets.Begin(); i.IsNotEnd(); ++i) { asset = i->Value; // Check if has no references and is not during unloading if (asset->GetReferencesCount() <= 0 && !UnloadQueue.ContainsKey(asset)) { // Add to removes UnloadQueue.Add(asset, timeNow); } } // Find assets to unload in unload queue ToUnload.Clear(); for (auto i = UnloadQueue.Begin(); i != UnloadQueue.End(); ++i) { // Check if asset gain any new reference or if need to unload it if (i->Key->GetReferencesCount() > 0 || timeNow - i->Value >= Content::AssetsUnloadInterval) { ToUnload.Add(i->Key); } } // Unload marked assets for (int32 i = 0; i < ToUnload.Count(); i++) { asset = ToUnload[i]; // Check if has no references if (asset->GetReferencesCount() <= 0) { Content::UnloadAsset(asset); } // Remove from unload queue UnloadQueue.Remove(asset); } // Update cache (for longer sessions it will help to reduce cache misses) Cache.Save(); } void ContentService::Dispose() { IsExiting = true; // Save assets registry before engine closing Cache.Save(); // Flush objects (some asset-related objects/references may be pending to delete) ObjectsRemovalService::Flush(); // Unload all remaining assets { ScopeLock lock(AssetsLocker); for (auto i = Assets.Begin(); i.IsNotEnd(); ++i) { i->Value->DeleteObject(); } } // Flush objects (some assets may be pending to delete) ObjectsRemovalService::Flush(); // NOW dispose graphics device - where there is no loaded assets at all Graphics::DisposeDevice(); } AssetsCache* Content::GetRegistry() { return &Cache; } #if USE_EDITOR bool FindAssets(const ProjectInfo* project, HashSet& projects, const Guid& id, Array& tmpCache, AssetInfo& info) { if (projects.Contains(project)) return false; projects.Add(project); bool found = findAsset(id, project->ProjectFolderPath / TEXT("Content"), tmpCache, info); for (const auto& reference : project->References) { if (reference.Project) found |= FindAssets(reference.Project, projects, id, tmpCache, info); } return found; } #endif bool Content::GetAssetInfo(const Guid& id, AssetInfo& info) { if (!id.IsValid()) return false; #if ENABLE_ASSETS_DISCOVERY // Find asset in registry if (Cache.FindAsset(id, info)) return true; // Locking injects some stalls but we need to make it safe (only one thread can pass though it at once) ScopeLock lock(WorkspaceDiscoveryLocker); // Check if we can search workspace // Note: we want to limit searching frequency due to high I/O usage and thread stall // We also perform full workspace discovery so all new assets will be found auto now = DateTime::NowUTC(); auto diff = now - LastWorkspaceDiscovery; if (diff <= TimeSpan::FromSeconds(5)) { //LOG(Warning, "Cannot perform workspace scan for '{1}'. Too often call. Time diff: {0} ms", static_cast(diff.GetTotalMilliseconds()), id); return false; } LastWorkspaceDiscovery = now; // Try to find an asset within the project, engine, plugins workspace folders DateTime startTime = now; int32 startCount = Cache.Size(); Array tmpCache(1024); #if USE_EDITOR HashSet projects; bool found = FindAssets(Editor::Project, projects, id, tmpCache, info); #else bool found = findAsset(id, Globals::ProjectContentFolder, tmpCache, info); #endif if (found) { LOG(Info, "Workspace searching time: {0} ms, new assets found: {1}", static_cast((DateTime::NowUTC() - startTime).GetTotalMilliseconds()), Cache.Size() - startCount); return true; } //LOG(Warning, "Cannot find {0}.", id); return false; #else // Find asset in registry return Cache.FindAsset(id, info); #endif } bool Content::GetAssetInfo(const StringView& path, AssetInfo& info) { #if ENABLE_ASSETS_DISCOVERY // Find asset in registry if (Cache.FindAsset(path, info)) return true; const auto extension = FileSystem::GetExtension(path).ToLower(); // Check if it's a binary asset if (ContentStorageManager::IsFlaxStorageExtension(extension)) { // Skip packages in editor (results in conflicts with build game packages if deployed inside project folder) #if USE_EDITOR if (extension == PACKAGE_FILES_EXTENSION) return false; #endif // Open storage auto storage = ContentStorageManager::GetStorage(path); if (storage) { #if BUILD_DEBUG ASSERT(storage->GetPath() == path); #endif // Register assets from the storage container (will handle duplicated IDs) Cache.RegisterAssets(storage); return Cache.FindAsset(path, info); } } // Check for json resource else if (JsonStorageProxy::IsValidExtension(extension)) { // Check Json storage layer Guid jsonId; String jsonTypeName; if (JsonStorageProxy::GetAssetInfo(path, jsonId, jsonTypeName)) { // Register asset Cache.RegisterAsset(jsonId, jsonTypeName, path); return Cache.FindAsset(path, info); } } return false; #else // Find asset in registry return Cache.FindAsset(path, info); #endif } String Content::GetEditorAssetPath(const Guid& id) { return Cache.GetEditorAssetPath(id); } Array Content::GetAllAssets() { Array result; Cache.GetAll(result); return result; } Array Content::GetAllAssetsByType(const MClass* type) { Array result; CHECK_RETURN(type, result); Cache.GetAllByTypeName(String(type->GetFullName()), result); return result; } IAssetFactory* Content::GetAssetFactory(const StringView& typeName) { IAssetFactory* result = nullptr; IAssetFactory::Get().TryGet(typeName, result); return result; } IAssetFactory* Content::GetAssetFactory(const AssetInfo& assetInfo) { IAssetFactory* result = nullptr; if (!IAssetFactory::Get().TryGet(assetInfo.TypeName, result)) { // Check if it's a json asset (in editor only) // In build game all asset factories are valid and json assets are cooked into binary packages #if USE_EDITOR if (assetInfo.Path.EndsWith(DEFAULT_JSON_EXTENSION_DOT)) #endif { IAssetFactory::Get().TryGet(JsonAsset::TypeName, result); } } return result; } String Content::CreateTemporaryAssetPath() { return Globals::TemporaryFolder / (Guid::New().ToString(Guid::FormatType::N) + ASSET_FILES_EXTENSION_WITH_DOT); } ContentStats Content::GetStats() { ContentStats stats; AssetsLocker.Lock(); stats.AssetsCount = Assets.Count(); int32 loadFailedCount = 0; for (const auto& e : Assets) { if (e.Value->IsLoaded()) stats.LoadedAssetsCount++; else if (e.Value->LastLoadFailed()) loadFailedCount++; if (e.Value->IsVirtual()) stats.VirtualAssetsCount++; } stats.LoadingAssetsCount = stats.AssetsCount - loadFailedCount - stats.LoadedAssetsCount; AssetsLocker.Unlock(); return stats; } Asset* Content::LoadAsyncInternal(const StringView& internalPath, MClass* type) { CHECK_RETURN(type, nullptr); const auto scriptingType = Scripting::FindScriptingType(type->GetFullName()); if (scriptingType) return LoadAsyncInternal(internalPath, scriptingType); LOG(Error, "Failed to find asset type '{0}'.", String(type->GetFullName())); return nullptr; } Asset* Content::LoadAsyncInternal(const StringView& internalPath, const ScriptingTypeHandle& type) { #if USE_EDITOR const String path = Globals::EngineContentFolder / internalPath + ASSET_FILES_EXTENSION_WITH_DOT; if (!FileSystem::FileExists(path)) { LOG(Error, "Missing file \'{0}\'", path); return nullptr; } #else const String path = Globals::ProjectContentFolder / internalPath + ASSET_FILES_EXTENSION_WITH_DOT; #endif const auto asset = LoadAsync(path, type); if (asset == nullptr) { LOG(Error, "Failed to load \'{0}\' (type: {1})", internalPath, type.ToString()); } return asset; } Asset* Content::LoadAsyncInternal(const Char* internalPath, const ScriptingTypeHandle& type) { return LoadAsyncInternal(StringView(internalPath), type); } FLAXENGINE_API Asset* LoadAsset(const Guid& id, const ScriptingTypeHandle& type) { return Content::LoadAsync(id, type); } Asset* Content::LoadAsync(const StringView& path, MClass* type) { CHECK_RETURN(type, nullptr); const auto scriptingType = Scripting::FindScriptingType(type->GetFullName()); if (scriptingType) return LoadAsync(path, scriptingType); LOG(Error, "Failed to find asset type '{0}'.", String(type->GetFullName())); return nullptr; } Asset* Content::LoadAsync(const StringView& path, const ScriptingTypeHandle& type) { // Ensure path is in a valid format String pathNorm(path); StringUtils::PathRemoveRelativeParts(pathNorm); #if USE_EDITOR if (!FileSystem::FileExists(pathNorm)) { LOG(Error, "Missing file \'{0}\'", pathNorm); return nullptr; } #endif AssetInfo assetInfo; if (GetAssetInfo(pathNorm, assetInfo)) { return LoadAsync(assetInfo.ID, type); } return nullptr; } Array Content::GetAssets() { Array assets; AssetsLocker.Lock(); Assets.GetValues(assets); AssetsLocker.Unlock(); return assets; } const Dictionary& Content::GetAssetsRaw() { AssetsLocker.Lock(); AssetsLocker.Unlock(); return Assets; } Asset* Content::LoadAsync(const Guid& id, const MClass* type) { CHECK_RETURN(type, nullptr); const auto scriptingType = Scripting::FindScriptingType(type->GetFullName()); if (scriptingType) return LoadAsync(id, scriptingType); LOG(Error, "Failed to find asset type '{0}'.", String(type->GetFullName())); return nullptr; } Asset* Content::GetAsset(const StringView& outputPath) { if (outputPath.IsEmpty()) return nullptr; ScopeLock lock(AssetsLocker); for (auto i = Assets.Begin(); i.IsNotEnd(); ++i) { if (i->Value->GetPath() == outputPath) { return i->Value; } } return nullptr; } Asset* Content::GetAsset(const Guid& id) { Asset* result = nullptr; AssetsLocker.Lock(); Assets.TryGet(id, result); AssetsLocker.Unlock(); return result; } void Content::DeleteAsset(Asset* asset) { ScopeLock locker(AssetsLocker); // Validate if (asset == nullptr || asset->_deleteFileOnUnload) { // Back return; } LOG(Info, "Deleting asset {0}...", asset->ToString()); // Mark asset for delete queue (delete it after auto unload) asset->_deleteFileOnUnload = true; // Unload UnloadAsset(asset); } void Content::DeleteAsset(const StringView& path) { ScopeLock locker(AssetsLocker); // Check if is loaded Asset* asset = GetAsset(path); if (asset != nullptr) { // Delete asset DeleteAsset(asset); return; } // Remove from registry AssetInfo info; if (Cache.DeleteAsset(path, &info)) { LOG(Info, "Deleting asset '{0}':{1}({2})", path, info.ID, info.TypeName); } else { LOG(Info, "Deleting asset '{0}':{1}({2})", path, TEXT("?"), TEXT("?")); info.ID = Guid::Empty; } // Delete file deleteFileSafety(path, info.ID); } void Content::deleteFileSafety(const StringView& path, const Guid& id) { // Check if given id is invalid if (!id.IsValid()) { // Cancel operation LOG(Warning, "Cannot remove file \'{0}\'. Given ID is invalid.", path); return; } // Ensure that file has the same ID (prevent from deleting different assets) auto storage = ContentStorageManager::TryGetStorage(path); if (storage) { storage->CloseFileHandles(); // Close file handle to allow removing it if (!storage->HasAsset(id)) { // Skip removing LOG(Warning, "Cannot remove file \'{0}\'. It doesn\'t contain asset {1}.", path, id); return; } } #if PLATFORM_WINDOWS // Safety way - move file to the recycle bin if (FileSystem::MoveFileToRecycleBin(path)) { LOG(Warning, "Failed to move file to Recycle Bin. Path: \'{0}\'", path); } #else // Remove file if (FileSystem::DeleteFile(path)) { LOG(Warning, "Failed to delete file Path: \'{0}\'", path); } #endif } #if USE_EDITOR bool Content::RenameAsset(const StringView& oldPath, const StringView& newPath) { ASSERT(IsInMainThread()); // Cache data Asset* oldAsset = GetAsset(oldPath); Asset* newAsset = GetAsset(newPath); // Validate name if (newAsset != nullptr && newAsset != oldAsset) { LOG(Error, "Invalid name '{0}' when trying to rename '{1}'.", newPath, oldPath); return true; } // Ensure asset is ready for renaming if (oldAsset) { // Wait for data loaded if (oldAsset->WaitForLoaded()) { LOG(Error, "Failed to load asset '{0}'.", oldAsset->ToString()); return true; } // Unload // Don't unload asset fully, only release ref to file, don't call OnUnload so managed asset and all refs will remain alive oldAsset->releaseStorage(); //oldAsset->onUnload_MainThread(); //ScopeLock lock(oldAsset->Locker); //oldAsset->unload(true); } // Ensure to unlock file ContentStorageManager::EnsureAccess(oldPath); // Move file if (FileSystem::MoveFile(newPath, oldPath)) { LOG(Error, "Cannot move file '{0}' to '{1}'", oldPath, newPath); return true; } // Update cache Cache.RenameAsset(oldPath, newPath); ContentStorageManager::OnRenamed(oldPath, newPath); // Check if is loaded if (oldAsset) { // Rename internal call oldAsset->onRename(newPath); // Load //ScopeLock lock(oldAsset->Locker); //oldAsset->startLoading(); } return false; } bool Content::FastTmpAssetClone(const StringView& path, String& resultPath) { ASSERT(path.HasChars()); const String dstPath = Globals::TemporaryFolder / Guid::New().ToString(Guid::FormatType::D) + ASSET_FILES_EXTENSION_WITH_DOT; if (CloneAssetFile(dstPath, path, Guid::New())) return true; resultPath = dstPath; return false; } bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPath, const Guid& dstId) { ASSERT(FileSystem::AreFilePathsEqual(srcPath, dstPath) == false && dstId.IsValid()); LOG(Info, "Cloning asset \'{0}\' to \'{1}\'({2}).", srcPath, dstPath, dstId); // Check source file if (!FileSystem::FileExists(srcPath)) { LOG(Warning, "Missing source file."); return true; } // Special case for json resources if (JsonStorageProxy::IsValidExtension(FileSystem::GetExtension(srcPath).ToLower())) { if (FileSystem::CopyFile(dstPath, srcPath)) { LOG(Warning, "Cannot copy file to destination."); return true; } if (JsonStorageProxy::ChangeId(dstPath, dstId)) { LOG(Warning, "Cannot change asset ID."); return true; } return false; } // Check if destination file is missing if (!FileSystem::FileExists(dstPath)) { // Use quick file copy if (FileSystem::CopyFile(dstPath, srcPath)) { LOG(Warning, "Cannot copy file to destination."); return true; } // Change ID auto storage = ContentStorageManager::GetStorage(dstPath); FlaxStorage::Entry e; storage->GetEntry(0, e); if (storage == nullptr || storage->ChangeAssetID(e, dstId)) { LOG(Warning, "Cannot change asset ID."); return true; } } else { // Use temporary file String tmpPath = Globals::TemporaryFolder / Guid::New().ToString(Guid::FormatType::D); if (FileSystem::CopyFile(tmpPath, srcPath)) { LOG(Warning, "Cannot copy file."); return true; } // Change asset ID { auto storage = ContentStorageManager::GetStorage(tmpPath); FlaxStorage::Entry e; storage->GetEntry(0, e); if (!storage || storage->ChangeAssetID(e, dstId)) { LOG(Warning, "Cannot change asset ID."); return true; } } // Unlock destination file ContentStorageManager::EnsureAccess(dstPath); // Copy temp file to the destination if (FileSystem::CopyFile(dstPath, tmpPath)) { LOG(Warning, "Cannot copy file to destination."); return true; } // Cleanup FileSystem::DeleteFile(tmpPath); // Reload storage { auto storage = ContentStorageManager::GetStorage(dstPath); if (storage) { storage->Reload(); } } } return false; } #endif void Content::UnloadAsset(Asset* asset) { // Check input if (asset == nullptr) return; asset->DeleteObject(); } Asset* Content::CreateVirtualAsset(MClass* type) { CHECK_RETURN(type, nullptr); const auto scriptingType = Scripting::FindScriptingType(type->GetFullName()); if (scriptingType) return CreateVirtualAsset(scriptingType); LOG(Error, "Failed to find asset type '{0}'.", String(type->GetFullName())); return nullptr; } Asset* Content::CreateVirtualAsset(const ScriptingTypeHandle& type) { auto& assetType = type.GetType(); // Init mock asset info AssetInfo info; info.ID = Guid::New(); info.TypeName = String(assetType.Fullname.Get(), assetType.Fullname.Length()); info.Path = CreateTemporaryAssetPath(); // Find asset factory based in its type auto factory = GetAssetFactory(info); if (factory == nullptr) { LOG(Error, "Cannot find virtual asset factory."); return nullptr; } if (!factory->SupportsVirtualAssets()) { LOG(Error, "Cannot create virtual asset of type '{0}'.", info.TypeName); return nullptr; } // Create asset object auto asset = factory->NewVirtual(info); if (asset == nullptr) { LOG(Error, "Cannot create virtual asset object."); return nullptr; } // Call initializer function asset->InitAsVirtual(); // Register asset AssetsLocker.Lock(); ASSERT(!Assets.ContainsKey(asset->GetID())); Assets.Add(asset->GetID(), asset); AssetsLocker.Unlock(); return asset; } void Content::tryCallOnLoaded(Asset* asset) { ScopeLock lock(LoadedAssetsToInvokeLocker); const int32 index = LoadedAssetsToInvoke.Find(asset); if (index != -1) { LoadedAssetsToInvoke.RemoveAtKeepOrder(index); asset->onLoaded_MainThread(); } } void Content::onAssetLoaded(Asset* asset) { // This is called by the asset on loading end ASSERT(asset && asset->IsLoaded()); ScopeLock locker(LoadedAssetsToInvokeLocker); LoadedAssetsToInvoke.Add(asset); } void Content::onAssetUnload(Asset* asset) { // This is called by the asset on unloading ScopeLock locker(AssetsLocker); Assets.Remove(asset->GetID()); UnloadQueue.Remove(asset); LoadedAssetsToInvoke.Remove(asset); } void Content::onAssetChangeId(Asset* asset, const Guid& oldId, const Guid& newId) { ScopeLock locker(AssetsLocker); Assets.Remove(oldId); Assets.Add(newId, asset); } bool Content::IsAssetTypeIdInvalid(const ScriptingTypeHandle& type, const ScriptingTypeHandle& assetType) { // Skip if no restrictions for the type if (!type || !assetType) return false; #if BUILD_DEBUG // Peek types for debugging const auto& typeObj = type.GetType(); const auto& assetTypeObj = assetType.GetType(); #endif // Early out if type matches if (type == assetType) return false; // Check if the asset type inherited from the requested type ScriptingTypeHandle it = assetType.GetType().GetBaseType(); while (it) { if (type == it) return false; it = it.GetType().GetBaseType(); } return true; } Asset* Content::LoadAsync(const Guid& id, const ScriptingTypeHandle& type) { // Early out if (!id.IsValid()) { // Back return nullptr; } // Check if asset has been already loaded Asset* result = GetAsset(id); if (result) { // Validate type if (IsAssetTypeIdInvalid(type, result->GetTypeHandle()) && !result->Is(type)) { LOG(Warning, "Different loaded asset type! Asset: \'{0}\'. Expected type: {1}", result->ToString(), type.ToString()); return nullptr; } return result; } // Check if that asset is during loading LoadCallAssetsLocker.Lock(); if (LoadCallAssets.Contains(id)) { LoadCallAssetsLocker.Unlock(); // Wait for load end // TODO: dont use active waiting and prevent deadlocks if running on a main thread //while (!Engine::ShouldExit()) while (true) { LoadCallAssetsLocker.Lock(); const bool contains = LoadCallAssets.Contains(id); LoadCallAssetsLocker.Unlock(); if (!contains) { return GetAsset(id); } Platform::Sleep(1); } } else { // Mark asset as loading LoadCallAssets.Add(id); LoadCallAssetsLocker.Unlock(); } // Load asset AssetInfo assetInfo; result = load(id, type, assetInfo); // End loading LoadCallAssetsLocker.Lock(); LoadCallAssets.Remove(id); LoadCallAssetsLocker.Unlock(); return result; } Asset* Content::load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo& assetInfo) { // Get cached asset info (from registry) if (!GetAssetInfo(id, assetInfo)) { LOG(Warning, "Invalid asset ID ({0}).", id.ToString(Guid::FormatType::N)); return nullptr; } #if ASSETS_LOADING_EXTRA_VERIFICATION // Ensure we have valid asset info ASSERT(assetInfo.TypeName.HasChars() && assetInfo.Path.HasChars()); // Check if file exists if (!FileSystem::FileExists(assetInfo.Path)) { LOG(Error, "Cannot find file '{0}'", assetInfo.Path); return nullptr; } #endif // Find asset factory based in its type auto factory = GetAssetFactory(assetInfo); if (factory == nullptr) { LOG(Error, "Cannot find asset factory. Info: {0}", assetInfo.ToString()); return nullptr; } // Create asset object auto result = factory->New(assetInfo); if (result == nullptr) { LOG(Error, "Cannot create asset object. Info: {0}", assetInfo.ToString()); return nullptr; } #if ASSETS_LOADING_EXTRA_VERIFICATION // Validate type if (IsAssetTypeIdInvalid(type, result->GetTypeHandle()) && !result->Is(type)) { LOG(Error, "Different loaded asset type! Asset: '{0}'. Expected type: {1}", assetInfo.ToString(), type.ToString()); result->DeleteObject(); return nullptr; } #endif // Register asset ASSERT(result->GetID() == id); AssetsLocker.Lock(); #if ASSETS_LOADING_EXTRA_VERIFICATION // Asset id has to be unique if (Assets.ContainsKey(id)) { CRASH; } #endif Assets.Add(id, result); AssetsLocker.Unlock(); // Start asset loading result->startLoading(); return result; } #if ENABLE_ASSETS_DISCOVERY bool findAsset(const Guid& id, const String& directory, Array& tmpCache, AssetInfo& info) { // Get all asset files tmpCache.Clear(); if (FileSystem::DirectoryGetFiles(tmpCache, directory, TEXT("*"), DirectorySearchOption::AllDirectories)) { LOG(Error, "Cannot query files in folder '{0}'.", directory); return false; } // Start searching for asset with given ID bool result = false; LOG(Info, "Start searching asset with ID: {0} in '{1}'. {2} potential files to check...", id, directory, tmpCache.Count()); for (int32 i = 0; i < tmpCache.Count(); i++) { String& path = tmpCache[i]; // Check if not already in registry // Note: maybe we could disable this check? it would slow down searching but we will find more workspace problems if (!Cache.HasAsset(path)) { auto extension = FileSystem::GetExtension(path).ToLower(); // Check if it's a binary asset if (ContentStorageManager::IsFlaxStorageExtension(extension)) { // Skip packages in editor (results in conflicts with build game packages if deployed inside project folder) #if USE_EDITOR if (extension == PACKAGE_FILES_EXTENSION) continue; #endif // Open storage auto storage = ContentStorageManager::GetStorage(path); if (storage) { // Register assets Cache.RegisterAssets(storage); // Check if that is a missing asset if (storage->HasAsset(id)) { // Found result = Cache.FindAsset(id, info); LOG(Info, "Found {1} at '{0}'!", id, path); } } else { LOG(Error, "Cannot open file '{0}' error code: {1}", path, 0); } } // Check for json resource else if (JsonStorageProxy::IsValidExtension(extension)) { // Check Json storage layer Guid jsonId; String jsonTypeName; if (JsonStorageProxy::GetAssetInfo(path, jsonId, jsonTypeName)) { // Register asset Cache.RegisterAsset(jsonId, jsonTypeName, path); // Check if that is a missing asset if (id == jsonId) { // Found result = Cache.FindAsset(id, info); LOG(Info, "Found {1} at '{0}'!", id, path); } } } } } return result; } #endif