You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.cpp

532 lines
24 KiB
C++

/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzToolsFramework/Prefab/PrefabLoader.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/StringFunc/StringFunc.h>
#include <AzFramework/Asset/AssetSystemBus.h>
#include <AzFramework/FileFunc/FileFunc.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/Prefab/PrefabDomUtils.h>
#include <AzToolsFramework/Prefab/PrefabSystemComponentInterface.h>
namespace AzToolsFramework
{
namespace Prefab
{
void PrefabLoader::RegisterPrefabLoaderInterface()
{
m_prefabSystemComponentInterface = AZ::Interface<PrefabSystemComponentInterface>::Get();
AZ_Assert(
m_prefabSystemComponentInterface != nullptr,
"Prefab System Component Interface could not be found. "
"It is a requirement for the PrefabLoader class. "
"Check that it is being correctly initialized.");
auto settingsRegistry = AZ::SettingsRegistry::Get();
AZ_Assert(settingsRegistry, "Settings registry is not set");
[[maybe_unused]] bool result =
settingsRegistry->Get(m_projectPathWithOsSeparator.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectPath);
AZ_Warning("Prefab", result, "Couldn't retrieve project root path");
m_projectPathWithSlashSeparator = AZ::IO::Path(m_projectPathWithOsSeparator.Native(), '/').MakePreferred();
AZ::Interface<PrefabLoaderInterface>::Register(this);
}
void PrefabLoader::UnregisterPrefabLoaderInterface()
{
AZ::Interface<PrefabLoaderInterface>::Unregister(this);
}
TemplateId PrefabLoader::LoadTemplateFromFile(AZ::IO::PathView filePath)
{
AZStd::unordered_set<AZ::IO::Path> progressedFilePathsSet;
TemplateId newTemplateId = LoadTemplateFromFile(filePath, progressedFilePathsSet);
return newTemplateId;
}
TemplateId PrefabLoader::LoadTemplateFromFile(AZ::IO::PathView filePath, AZStd::unordered_set<AZ::IO::Path>& progressedFilePathsSet)
{
if (!IsValidPrefabPath(filePath))
{
AZ_Error(
"Prefab", false,
"PrefabLoader::LoadTemplateFromFile - "
"Invalid file path: '%.*s'.",
AZ_STRING_ARG(filePath.Native())
);
return InvalidTemplateId;
}
auto readResult = AZ::Utils::ReadFile(GetFullPath(filePath).Native(), MaxPrefabFileSize);
if (!readResult.IsSuccess())
{
AZ_Error(
"Prefab", false,
"PrefabLoader::LoadTemplate - Failed to load Prefab file from '%.*s'."
"Error message: '%s'",
AZ_STRING_ARG(filePath.Native()),
readResult.GetError().c_str()
);
return InvalidTemplateId;
}
return LoadTemplateFromString(readResult.GetValue(), filePath, progressedFilePathsSet);
}
TemplateId PrefabLoader::LoadTemplateFromString(
AZStd::string_view content, AZ::IO::PathView originPath)
{
AZStd::unordered_set<AZ::IO::Path> progressedFilePathsSet;
TemplateId newTemplateId = LoadTemplateFromString(content, originPath, progressedFilePathsSet);
return newTemplateId;
}
TemplateId PrefabLoader::LoadTemplateFromString(
AZStd::string_view fileContent,
AZ::IO::PathView originPath,
AZStd::unordered_set<AZ::IO::Path>& progressedFilePathsSet)
{
if (!IsValidPrefabPath(originPath))
{
AZ_Error(
"Prefab", false,
"PrefabLoader::LoadTemplateFromString - "
"Invalid origin path: '%.*s'",
AZ_STRING_ARG(originPath.Native())
);
return InvalidTemplateId;
}
AZ::IO::Path relativePath = GenerateRelativePath(originPath);
// Cyclical dependency detected if the prefab file is already part of the progressed
// file path set.
if (progressedFilePathsSet.contains(relativePath))
{
AZ_Error(
"Prefab", false,
"PrefabLoader::LoadTemplateFromString - "
"Prefab file '%.*s' has been detected to directly or indirectly depend on itself."
"Terminating any further loading of this branch of its prefab hierarchy.",
AZ_STRING_ARG(originPath.Native())
);
return InvalidTemplateId;
}
// Directly return loaded Template id.
TemplateId loadedTemplateId = m_prefabSystemComponentInterface->GetTemplateIdFromFilePath(relativePath);
if (loadedTemplateId != InvalidTemplateId)
{
return loadedTemplateId;
}
// Read Template's prefab file from disk and parse Prefab DOM from file.
AZ::Outcome<PrefabDom, AZStd::string> readPrefabFileResult = AzFramework::FileFunc::ReadJsonFromString(fileContent);
if (!readPrefabFileResult.IsSuccess())
{
AZ_Error(
"Prefab", false,
"PrefabLoader::LoadTemplate - Failed to load Prefab file from '%.*s'."
"Error message: '%s'",
AZ_STRING_ARG(originPath.Native()),
readPrefabFileResult.GetError().c_str());
return InvalidTemplateId;
}
// Create new Template with the Prefab DOM.
TemplateId newTemplateId = m_prefabSystemComponentInterface->AddTemplate(relativePath, readPrefabFileResult.TakeValue());
if (newTemplateId == InvalidTemplateId)
{
AZ_Error(
"Prefab", false,
"PrefabLoader::LoadTemplate - "
"Failed to create a template from instance with source file path '%.*s': "
"invalid template id returned.",
AZ_STRING_ARG(originPath.Native())
);
return InvalidTemplateId;
}
TemplateReference newTemplateReference = m_prefabSystemComponentInterface->FindTemplate(newTemplateId);
Template& newTemplate = newTemplateReference->get();
// Mark the file as being in progress.
progressedFilePathsSet.emplace(relativePath);
// Get 'Instances' value from Template.
bool isLoadedWithErrors = false;
PrefabDomValueReference instancesReference = newTemplate.GetInstancesValue();
if (instancesReference.has_value())
{
PrefabDomValue& instances = instancesReference->get();
// For each instance value in 'instances', try to create source Templates for target Template's nested instance data.
// Also create Links between source/target Templates if source Template loaded successfully.
for (PrefabDomValue::MemberIterator instanceIterator = instances.MemberBegin(); instanceIterator != instances.MemberEnd();
++instanceIterator)
{
if (!LoadNestedInstance(instanceIterator, newTemplateId, progressedFilePathsSet))
{
isLoadedWithErrors = true;
AZ_Error(
"Prefab", false,
"PrefabLoader::LoadTemplate - "
"Loading nested instance '%s' in target Template '%u' from Prefab file '%.*s' failed.",
instanceIterator->name.GetString(), newTemplateId,
AZ_STRING_ARG(originPath.Native())
);
}
}
}
newTemplate.MarkAsLoadedWithErrors(isLoadedWithErrors);
// Un-mark the file as being in progress.
progressedFilePathsSet.erase(originPath);
// Return target Template id.
return newTemplateId;
}
bool PrefabLoader::LoadNestedInstance(
PrefabDomValue::MemberIterator& instanceIterator, TemplateId targetTemplateId,
AZStd::unordered_set<AZ::IO::Path>& progressedFilePathsSet)
{
const PrefabDomValue& instance = instanceIterator->value;
AZ::IO::PathView instancePath = AZStd::string_view(instanceIterator->name.GetString(), instanceIterator->name.GetStringLength());
if (!IsValidPrefabPath(instancePath))
{
AZ_Error(
"Prefab", false,
"PrefabLoader::LoadNestedInstance - "
"There's an Instance with an invalid path '%s' in the target Template on file path '%s'.",
instanceIterator->name.GetString(),
m_prefabSystemComponentInterface->FindTemplate(targetTemplateId)->get().GetFilePath().c_str());
return false;
}
// Get source Template's path for getting nested instance data.
PrefabDomValueConstReference sourceReference = PrefabDomUtils::FindPrefabDomValue(instance, PrefabDomUtils::SourceName);
if (!sourceReference.has_value() || !sourceReference->get().IsString() || sourceReference->get().GetStringLength() == 0)
{
AZ_Error(
"Prefab", false,
"PrefabLoader::LoadNestedInstance - "
"Can't get '%s' string value in Instance value '%s' of Template's Prefab DOM from file '%s'.",
PrefabDomUtils::SourceName, instanceIterator->name.GetString(),
m_prefabSystemComponentInterface->FindTemplate(targetTemplateId)->get().GetFilePath().c_str());
return false;
}
const PrefabDomValue& source = sourceReference->get();
AZStd::string_view nestedTemplatePath(source.GetString(), source.GetStringLength());
// Get Template id of nested instance from its path.
// If source Template is already loaded, get the id from Template File Path To Id Map,
// else load the source Template by calling 'LoadTemplate' recursively.
TemplateId nestedTemplateId = LoadTemplateFromFile(nestedTemplatePath, progressedFilePathsSet);
TemplateReference nestedTemplateReference = m_prefabSystemComponentInterface->FindTemplate(nestedTemplateId);
if (!nestedTemplateReference.has_value() || !nestedTemplateReference->get().IsValid())
{
AZ_Error(
"Prefab", false,
"PrefabLoader::LoadNestedInstance - "
"Error occurred while loading nested Prefab file '%.*s' from Prefab file '%s'.",
AZ_STRING_ARG(nestedTemplatePath),
m_prefabSystemComponentInterface->FindTemplate(targetTemplateId)->get().GetFilePath().c_str()
);
return false;
}
// After source template has been loaded, create Link between source/target Template.
LinkId newLinkId =
m_prefabSystemComponentInterface->AddLink(nestedTemplateId, targetTemplateId, instanceIterator, AZStd::nullopt);
if (newLinkId == InvalidLinkId)
{
AZ_Error(
"Prefab", false,
"PrefabLoader::LoadNestedInstance - "
"Failed to add a new Link to Nested Template Instance '%s' which connects source Template '%.*s' and target Template "
"'%s'.",
instanceIterator->name.GetString(), AZ_STRING_ARG(nestedTemplatePath),
m_prefabSystemComponentInterface->FindTemplate(targetTemplateId)->get().GetFilePath().c_str()
);
return false;
}
// Let the new Template carry up the error flag of its nested Prefab.
return !nestedTemplateReference->get().IsLoadedWithErrors();
}
bool PrefabLoader::SaveTemplate(TemplateId templateId)
{
const auto& domAndFilepath = StoreTemplateIntoFileFormat(templateId);
if (!domAndFilepath)
{
return false;
}
auto outcome = AzFramework::FileFunc::WriteJsonFile(domAndFilepath->first, GetFullPath(domAndFilepath->second));
if (!outcome.IsSuccess())
{
AZ_Error(
"Prefab", false,
"PrefabLoader::SaveTemplate - "
"Failed to save template '%s'."
"Error: %s",
domAndFilepath->second.c_str(),
outcome.GetError().c_str()
);
return false;
}
m_prefabSystemComponentInterface->SetTemplateDirtyFlag(templateId, false);
return true;
}
bool PrefabLoader::SaveTemplateToFile(TemplateId templateId, AZ::IO::PathView absolutePath)
{
AZ_Assert(absolutePath.IsAbsolute(), "SaveTemplateToFile requires an absolute path for saving the initial prefab file.");
const auto& domAndFilepath = StoreTemplateIntoFileFormat(templateId);
if (!domAndFilepath)
{
return false;
}
// Verify that the absolute path provided to this matches the relative path saved in the template.
// Otherwise, the saved prefab won't be able to be loaded.
auto relativePath = GenerateRelativePath(absolutePath);
if (relativePath != domAndFilepath->second)
{
AZ_Error(
"Prefab", false,
"PrefabLoader::SaveTemplateToFile - "
"Failed to save template '%s' to location '%.*s'."
"Error: Relative path '%.*s' for location didn't match template name.",
domAndFilepath->second.c_str(), AZ_STRING_ARG(absolutePath.Native()), AZ_STRING_ARG(relativePath.Native()));
return false;
}
auto outcome = AzFramework::FileFunc::WriteJsonFile(domAndFilepath->first, absolutePath);
if (!outcome.IsSuccess())
{
AZ_Error(
"Prefab", false,
"PrefabLoader::SaveTemplateToFile - "
"Failed to save template '%s' to location '%.*s'."
"Error: %s",
domAndFilepath->second.c_str(), AZ_STRING_ARG(absolutePath.Native()), outcome.GetError().c_str());
return false;
}
m_prefabSystemComponentInterface->SetTemplateDirtyFlag(templateId, false);
return true;
}
bool PrefabLoader::SaveTemplateToString(TemplateId templateId, AZStd::string& output)
{
const auto& domAndFilepath = StoreTemplateIntoFileFormat(templateId);
if (!domAndFilepath)
{
return false;
}
auto outcome = AzFramework::FileFunc::WriteJsonToString(domAndFilepath->first, output);
if (!outcome.IsSuccess())
{
AZ_Error(
"Prefab", false,
"PrefabLoader::SaveTemplateToString - "
"Failed to serialize template '%s' into a string."
"Error: %s",
domAndFilepath->second.c_str(),
outcome.GetError().c_str()
);
return false;
}
return true;
}
AZStd::optional<AZStd::pair<PrefabDom, AZ::IO::Path>> PrefabLoader::StoreTemplateIntoFileFormat(TemplateId templateId)
{
// Acquire the template we are saving
TemplateReference templateToSaveReference = m_prefabSystemComponentInterface->FindTemplate(templateId);
if (!templateToSaveReference.has_value())
{
AZ_Warning(
"Prefab", false,
"PrefabLoader::SaveTemplate - Unable to save prefab template with id: '%llu'. "
"Template with that id could not be found",
templateId
);
return AZStd::nullopt;
}
Template& templateToSave = templateToSaveReference->get();
if (!templateToSave.IsValid())
{
AZ_Warning(
"Prefab", false,
"PrefabLoader::SaveTemplate - Unable to save Prefab Template with id: %llu. "
"Template with that id is invalid",
templateId);
return AZStd::nullopt;
}
// Make a copy of a our prefab DOM where nested instances become file references with patch data
PrefabDom templateDomToSave;
if (!templateToSave.CopyTemplateIntoPrefabFileFormat(templateDomToSave))
{
AZ_Error(
"Prefab", false,
"PrefabLoader::SaveTemplate - Unable to store a collapsed version of prefab Template while attempting to save to %s."
"Save cannot continue",
templateToSave.GetFilePath().c_str()
);
return AZStd::nullopt;
}
return { { AZStd::move(templateDomToSave), templateToSave.GetFilePath() } };
}
bool PrefabLoader::IsValidPrefabPath(AZ::IO::PathView path)
{
// Check for OS invalid character and paths ending on '/' '\\' separators as final char
AZStd::string_view pathStr = path.Native();
return !path.empty() &&
(pathStr.find_first_of(AZ_FILESYSTEM_INVALID_CHARACTERS) == AZStd::string::npos) &&
(pathStr.back() != '\\' && pathStr.back() != '/');
}
AZ::IO::Path PrefabLoader::GetFullPath(AZ::IO::PathView path)
{
AZ::IO::Path pathWithOSSeparator = AZ::IO::Path(path).MakePreferred();
if (pathWithOSSeparator.IsAbsolute())
{
// If an absolute path was passed in, just return it as-is.
return path;
}
// A relative path was passed in, so try to turn it back into an absolute path.
AZ::IO::Path fullPath;
bool pathFound = false;
AZ::Data::AssetInfo assetInfo;
AZStd::string rootFolder;
AZStd::string inputPath(path.Native());
// Given an input path that's expected to exist, try to look it up.
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
pathFound, &AzToolsFramework::AssetSystemRequestBus::Events::GetSourceInfoBySourcePath,
inputPath.c_str(), assetInfo, rootFolder);
if (pathFound)
{
// The asset system provided us with a valid root folder and relative path, so return it.
fullPath = AZ::IO::Path(rootFolder) / assetInfo.m_relativePath;
}
else
{
// If for some reason the Asset system couldn't provide a relative path, provide some fallback logic.
// Check to see if the AssetProcessor is ready. If it *is* and we didn't get a path, print an error then follow
// the fallback logic. If it's *not* ready, we're probably either extremely early in a tool startup flow or inside
// a unit test, so just execute the fallback logic without an error.
[[maybe_unused]] bool assetProcessorReady = false;
AzFramework::AssetSystemRequestBus::BroadcastResult(
assetProcessorReady, &AzFramework::AssetSystemRequestBus::Events::AssetProcessorIsReady);
AZ_Error(
"Prefab", !assetProcessorReady, "Full source path for '%.*s' could not be determined. Using fallback logic.",
AZ_STRING_ARG(path.Native()));
// If a relative path was passed in, make it relative to the project root.
fullPath = AZ::IO::Path(m_projectPathWithOsSeparator).Append(pathWithOSSeparator);
}
return fullPath;
}
AZ::IO::Path PrefabLoader::GenerateRelativePath(AZ::IO::PathView path)
{
bool pathFound = false;
AZStd::string relativePath;
AZStd::string rootFolder;
AZ::IO::Path finalPath;
// The asset system allows for paths to be relative to multiple root folders, using a priority system.
// This request will make the input path relative to the most appropriate, highest-priority root folder.
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
pathFound, &AzToolsFramework::AssetSystemRequestBus::Events::GenerateRelativeSourcePath, path.Native(),
relativePath, rootFolder);
if (pathFound && !relativePath.empty())
{
// A relative path was generated successfully, so return it.
finalPath = relativePath;
}
else
{
// If for some reason the Asset system couldn't provide a relative path, provide some fallback logic.
// Check to see if the AssetProcessor is ready. If it *is* and we didn't get a path, print an error then follow
// the fallback logic. If it's *not* ready, we're probably either extremely early in a tool startup flow or inside
// a unit test, so just execute the fallback logic without an error.
[[maybe_unused]] bool assetProcessorReady = false;
AzFramework::AssetSystemRequestBus::BroadcastResult(
assetProcessorReady, &AzFramework::AssetSystemRequestBus::Events::AssetProcessorIsReady);
AZ_Error("Prefab", !assetProcessorReady,
"Relative source path for '%.*s' could not be determined. Using project path as relative root.",
AZ_STRING_ARG(path.Native()));
AZ::IO::Path pathWithOSSeparator = AZ::IO::Path(path.Native()).MakePreferred();
if (pathWithOSSeparator.IsAbsolute())
{
// If an absolute path was passed in, make it relative to the project path.
finalPath = AZ::IO::Path(path.Native(), '/').MakePreferred().LexicallyRelative(m_projectPathWithSlashSeparator);
}
else
{
// If a relative path was passed in, just return it.
finalPath = path;
}
}
return finalPath;
}
AZ::IO::Path PrefabLoaderInterface::GeneratePath()
{
return AZStd::string::format("Prefab_%s", AZ::Entity::MakeId().ToString().c_str());
}
} // namespace Prefab
} // namespace AzToolsFramework