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.
485 lines
20 KiB
C++
485 lines
20 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
#include "UiCanvasFileObject.h"
|
|
#include "UiSerialize.h"
|
|
#include <AzCore/Serialization/Utils.h>
|
|
#include <LyShine/UiSerializeHelpers.h>
|
|
#include "UiCanvasComponent.h"
|
|
#include "UiElementComponent.h"
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Load a serialized stream that may be in an older format that may require massaging the steam
|
|
UiCanvasFileObject* UiCanvasFileObject::LoadCanvasFromStream(AZ::IO::GenericStream& stream, const AZ::ObjectStream::FilterDescriptor& filterDesc)
|
|
{
|
|
// get the size of the file
|
|
size_t fileSize = stream.GetLength();
|
|
|
|
if (fileSize == 0)
|
|
{
|
|
AZ_Error("UI", false, "UI Canvas file: %s is zero bytes on disk, and cannot be loaded.", stream.GetFilename());
|
|
return nullptr;
|
|
}
|
|
|
|
// read in the entire file into a char buffer. Note that files are not 0-truncated!
|
|
char* buffer = new char[fileSize + 1];
|
|
size_t bytesRead = stream.Read(fileSize, buffer);
|
|
|
|
// null terminate in case we perform string operations.
|
|
// this is not necessary on ObjectStream, but loading legacy files often requires string ops.
|
|
buffer[bytesRead] = 0;
|
|
|
|
// if ReadRaw read the file ok then load the entity from the buffer using AZ
|
|
// serialization
|
|
UiCanvasFileObject* canvas = nullptr;
|
|
if (bytesRead == fileSize)
|
|
{
|
|
// Check to see if this is an old format canvas file that cannot be handled simply in the
|
|
// version convert functions
|
|
|
|
enum class FileFormat
|
|
{
|
|
ReallyOld, Old, CanvasObject
|
|
};
|
|
FileFormat fileFormat = FileFormat::CanvasObject;
|
|
|
|
// All canvas files start with this (at least up to the introduction of the UiCanvasFileObject)
|
|
const char* objectStreamPrefix =
|
|
"<ObjectStream version=\"1\">";
|
|
|
|
// This is what canvas files looked like prior to the introduction of the UiCanvasFileObject
|
|
const char* oldStylePrefix =
|
|
"<Class name=\"AZ::Entity\"";
|
|
|
|
// This is what canvas files looked like in Fall 2015 (prior to R1)
|
|
const char* reallyOldStylePrefix =
|
|
"<Entity type=\"{";
|
|
|
|
// See if we can identify the buffer as one of the old formats
|
|
if (strncmp(buffer, objectStreamPrefix, strlen(objectStreamPrefix)) == 0)
|
|
{
|
|
// Is started with the usually ObjectStream prefix
|
|
// find the start of the next tag
|
|
const char* secondTag = buffer + strlen(objectStreamPrefix);
|
|
secondTag = strchr(secondTag, '<');
|
|
if (secondTag)
|
|
{
|
|
if (strncmp(secondTag, oldStylePrefix, strlen(oldStylePrefix)) == 0)
|
|
{
|
|
fileFormat = FileFormat::Old;
|
|
}
|
|
else if (strncmp(secondTag, reallyOldStylePrefix, strlen(reallyOldStylePrefix)) == 0)
|
|
{
|
|
fileFormat = FileFormat::ReallyOld;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fileFormat == FileFormat::Old)
|
|
{
|
|
// We can load this format but copying all of the entities from the canvas component (and children)
|
|
// to the root slice is not efficient. So write a warning to the log that load times are impacted.
|
|
AZ_Warning("UI", false, "UI canvas file: %s is in an old format, load times will be faster if you resave it.",
|
|
stream.GetFilename());
|
|
|
|
// Read this as an old format canvas file
|
|
canvas = LoadCanvasEntitiesFromOldFormatFile(buffer, fileSize, filterDesc);
|
|
|
|
if (!canvas)
|
|
{
|
|
AZ_Warning("UI", false, "Old format UI canvas file: %s could not be loaded. It may be corrupted.",
|
|
stream.GetFilename());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// This does not look like an old format canvas file so treat it as new format
|
|
AZ::IO::MemoryStream newFormatStream(buffer, fileSize);
|
|
canvas = LoadCanvasFromNewFormatStream(newFormatStream, filterDesc);
|
|
|
|
if (!canvas)
|
|
{
|
|
AZ_Warning("UI", false, "UI canvas file: %s could not be loaded. It may be corrupted.",
|
|
newFormatStream.GetFilename());
|
|
}
|
|
}
|
|
}
|
|
|
|
delete [] buffer;
|
|
return canvas;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiCanvasFileObject::SaveCanvasToStream(AZ::IO::GenericStream& stream, UiCanvasFileObject* canvasFileObject)
|
|
{
|
|
AZ::Utils::SaveObjectToStream<UiCanvasFileObject>(stream, AZ::DataStream::ST_XML, canvasFileObject);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::Entity* UiCanvasFileObject::LoadCanvasEntitiesFromStream(AZ::IO::GenericStream& stream, AZ::Entity*& rootSliceEntity)
|
|
{
|
|
AZ::Entity* canvasEntity = nullptr;
|
|
|
|
UiCanvasFileObject* fileObject = AZ::Utils::LoadObjectFromStream<UiCanvasFileObject>(stream);
|
|
if (fileObject && fileObject->m_canvasEntity && fileObject->m_rootSliceEntity)
|
|
{
|
|
canvasEntity = fileObject->m_canvasEntity;
|
|
rootSliceEntity = fileObject->m_rootSliceEntity;
|
|
}
|
|
|
|
SAFE_DELETE(fileObject);
|
|
|
|
return canvasEntity;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiCanvasFileObject::Reflect(AZ::ReflectContext* context)
|
|
{
|
|
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
|
|
|
|
if (serializeContext)
|
|
{
|
|
serializeContext->Class<UiCanvasFileObject>()
|
|
->Version(2, &UiCanvasFileObject::VersionConverter)
|
|
->Field("CanvasEntity", &UiCanvasFileObject::m_canvasEntity)
|
|
->Field("RootSliceEntity", &UiCanvasFileObject::m_rootSliceEntity);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiCanvasFileObject* UiCanvasFileObject::LoadCanvasEntitiesFromOldFormatFile(const char* buffer, size_t bufferSize, const AZ::ObjectStream::FilterDescriptor& filterDesc)
|
|
{
|
|
// This function attempts to read an old format (pre root slice) canvas file.
|
|
// This is a little complex for a VersionConvert function to do. If we tried to do it in the version
|
|
// converter for the UiCanvasComponent it would be hard because the root slice entity is saved as a
|
|
// sibling of the entity with the UiCanvasComponent on it so it is not accessible within the
|
|
// UiCanvasComponent version converter. Trying to save things into a static list for processing later
|
|
// would be messy and would fail if two canvases were being loaded at the same time on different threads.
|
|
// So we want to do the version conversion in the next level up - which is the CanvasFileObject
|
|
// However, there is no CanvasFileObject level in an old style canvas file. So what we do is modify the buffer
|
|
// so that it looks (just at the top level) like a new style file - with a CanvasFileObject.
|
|
// Then we can handle the conversion in the CanvasFileObject version converter.
|
|
|
|
// These are the prefix and suffix for the new style file:
|
|
const char* prefixToAdd1 =
|
|
"<ObjectStream version=\"1\">\n"
|
|
"\t<Class name=\"CanvasFileObject\" version=\"1\" type=\"{1F02632F-F113-49B1-85AD-8CD0FA78B8AA}\">\n"
|
|
"\t\t<Class name=\"AZ::Entity\" field=\"CanvasEntity\" version=\"2\" type=\"{75651658-8663-478D-9090-2432DFCAFA44}\">\n";
|
|
|
|
const char* prefixToAdd2 =
|
|
"<ObjectStream version=\"1\">\n"
|
|
"\t<Class name=\"CanvasFileObject\" version=\"1\" type=\"{1F02632F-F113-49B1-85AD-8CD0FA78B8AA}\">\n"
|
|
"\t\t<Class name=\"AZ::Entity\" field=\"CanvasEntity\" type=\"{75651658-8663-478D-9090-2432DFCAFA44}\">\n";
|
|
|
|
const char* suffixToAdd =
|
|
"\t\t</Class>\n"
|
|
"\t</Class>\n"
|
|
"</ObjectStream>\n";
|
|
|
|
const char* prefixToAdd = prefixToAdd1;
|
|
|
|
// These are the prefix and suffix for an old style file. Note that the use of \r\n versus \n only is inconsistent
|
|
// sometimes it comes in with one and sometimes the other.
|
|
const char* prefixToRemove1 =
|
|
"<ObjectStream version=\"1\">\n"
|
|
"\t<Class name=\"AZ::Entity\" version=\"2\" type=\"{75651658-8663-478D-9090-2432DFCAFA44}\">\n";
|
|
|
|
const char* prefixToRemove2 =
|
|
"<ObjectStream version=\"1\">\r\n"
|
|
"\t<Class name=\"AZ::Entity\" version=\"2\" type=\"{75651658-8663-478D-9090-2432DFCAFA44}\">\r\n";
|
|
|
|
const char* typeString =
|
|
"type=\"{75651658-8663-478D-9090-2432DFCAFA44}\">";
|
|
|
|
const char* suffixToRemove1 =
|
|
"\t</Class>\n"
|
|
"</ObjectStream>\n";
|
|
|
|
const char* suffixToRemove2 =
|
|
"\t</Class>\r\n"
|
|
"</ObjectStream>\r\n";
|
|
|
|
// Do a sanity check that the buffer does start with the prefix that we will remove
|
|
// Also, determine how newlines are represented in the file.
|
|
const char* suffixToRemove = nullptr;
|
|
|
|
size_t prefixToRemoveLen = 0;
|
|
if (strncmp(buffer, prefixToRemove1, strlen(prefixToRemove1)) == 0)
|
|
{
|
|
prefixToRemoveLen = strlen(prefixToRemove1);
|
|
suffixToRemove = suffixToRemove1;
|
|
}
|
|
else if (strncmp(buffer, prefixToRemove2, strlen(prefixToRemove2)) == 0)
|
|
{
|
|
prefixToRemoveLen = strlen(prefixToRemove2);
|
|
suffixToRemove = suffixToRemove2;
|
|
}
|
|
else
|
|
{
|
|
// not an exact match - this can happen, for example if the entity version is not 2
|
|
// it can have a missing version
|
|
// This is a more forgiving way to do the test. It could replace the code above but
|
|
// that code has been working for a while so we add this code as a backup
|
|
const char* typeStart = strstr(buffer, typeString);
|
|
if (!typeStart)
|
|
{
|
|
// We can't convert this file.
|
|
if (bufferSize < strlen(prefixToRemove2))
|
|
{
|
|
// Something is very wrong. The file is shorter that the expected prefix.
|
|
// note that we must use AZ_Warning here as this code is shared in tools which don't have gEnv.
|
|
AZ_Warning("UI", false, "Error converting canvas file. File appears to be truncated.");
|
|
}
|
|
else
|
|
{
|
|
// Print out the start of the file for help in debugging
|
|
// user reported issues
|
|
AZStd::string messageBuffer(buffer, strlen(prefixToRemove2));
|
|
AZ_Warning("UI", false, "Error converting canvas file. Prefix is:\r\n%s", messageBuffer.c_str());
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
prefixToAdd = prefixToAdd2;
|
|
suffixToRemove = suffixToRemove1;
|
|
|
|
const char* p = typeStart + strlen(typeString);
|
|
if (*p == '\r')
|
|
{
|
|
++p;
|
|
suffixToRemove = suffixToRemove2;
|
|
}
|
|
if (*p == '\n')
|
|
{
|
|
++p;
|
|
}
|
|
|
|
// the prefix length is up to the \n after the typeString
|
|
prefixToRemoveLen = p - buffer;
|
|
}
|
|
|
|
// work out some lengths here for the strings we want to mess with
|
|
const size_t prefixToAddLen = strlen(prefixToAdd);
|
|
const size_t suffixToAddLen = strlen(suffixToAdd);
|
|
|
|
const size_t suffixToRemoveLen = strlen(suffixToRemove);
|
|
|
|
// This allows for not knowing exactly how many extra chars will be at the end of the file.
|
|
// We search backward for some arbitrary character in the suffixToRemove ('<') and line things
|
|
// up using that.
|
|
const char* lastOpenAngleInBuffer = strrchr(buffer, '<');
|
|
const char* lastOpenAngleInSuffixToRemove = strrchr(suffixToRemove, '<');
|
|
const char* suffixToRemoveStart = lastOpenAngleInBuffer - (lastOpenAngleInSuffixToRemove - suffixToRemove);
|
|
|
|
// sanity check to check that the suffix matches
|
|
if (strncmp(suffixToRemoveStart, suffixToRemove, suffixToRemoveLen) != 0)
|
|
{
|
|
AZ_Warning("UI", false, "Error converting canvas file. File appears to be truncated at the end.");
|
|
return nullptr;
|
|
}
|
|
|
|
// Compute the start and length of the part we want to copy from the old buffer to the new buffer
|
|
const char* oldBufferCoreStart = buffer + prefixToRemoveLen;
|
|
const size_t oldBufferCoreLen = suffixToRemoveStart - oldBufferCoreStart;
|
|
|
|
// allocate the new buffer
|
|
size_t newBufferSize = prefixToAddLen + oldBufferCoreLen + suffixToAddLen + 1;
|
|
char* newBuffer = new char[newBufferSize];
|
|
|
|
// fill the new buffer with the new prefix, the old core and the new suffix
|
|
char* insertPoint = newBuffer;
|
|
|
|
azstrncpy(insertPoint, newBufferSize, prefixToAdd, prefixToAddLen);
|
|
insertPoint += prefixToAddLen;
|
|
|
|
azstrncpy(insertPoint, newBufferSize - prefixToAddLen, oldBufferCoreStart, oldBufferCoreLen);
|
|
insertPoint += oldBufferCoreLen;
|
|
|
|
azstrncpy(insertPoint, newBufferSize - prefixToAddLen - oldBufferCoreLen, suffixToAdd, suffixToAddLen);
|
|
insertPoint += suffixToAddLen;
|
|
|
|
insertPoint[0] = '\0';
|
|
|
|
// Now try loading from this new buffer, the rest of the conversion is done in CanvasFileObject::VersionConverter
|
|
AZ::IO::MemoryStream stream(newBuffer, newBufferSize);
|
|
UiCanvasFileObject* canvas = LoadCanvasFromNewFormatStream(stream, filterDesc);
|
|
|
|
delete [] newBuffer;
|
|
|
|
return canvas;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiCanvasFileObject* UiCanvasFileObject::LoadCanvasFromNewFormatStream(AZ::IO::GenericStream& stream, const AZ::ObjectStream::FilterDescriptor& filterDesc)
|
|
{
|
|
UiCanvasFileObject* fileObject =
|
|
AZ::Utils::LoadObjectFromStream<UiCanvasFileObject>(stream, nullptr, filterDesc);
|
|
return fileObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Helper function to find the root element node in a canvas entity node
|
|
AZ::SerializeContext::DataElementNode* UiCanvasFileObject::FindRootElementInCanvasEntity(
|
|
[[maybe_unused]] AZ::SerializeContext& context,
|
|
AZ::SerializeContext::DataElementNode& canvasEntityNode)
|
|
{
|
|
// Find the UiCanvasComponent in the CanvasEntity
|
|
AZ::SerializeContext::DataElementNode* canvasComponentNode =
|
|
LyShine::FindComponentNode(canvasEntityNode, UiCanvasComponent::TYPEINFO_Uuid());
|
|
if (!canvasComponentNode)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// Find the RootElement entity in the UiCanvasComponent
|
|
int rootElementIndex = canvasComponentNode->FindElement(AZ_CRC("RootElement", 0x9ac9557b));
|
|
if (rootElementIndex == -1)
|
|
{
|
|
return nullptr;
|
|
}
|
|
AZ::SerializeContext::DataElementNode& rootElementNode = canvasComponentNode->GetSubElement(rootElementIndex);
|
|
|
|
return &rootElementNode;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Helper function to create the root slice entity node and all its sub nodes and then copy
|
|
// the entities representing all the UI elements in the canvas into the SliceComponent node
|
|
bool UiCanvasFileObject::CreateRootSliceNodeAndCopyInEntities(
|
|
AZ::SerializeContext& context,
|
|
AZ::SerializeContext::DataElementNode& canvasFileObjectNode,
|
|
AZStd::vector<AZ::SerializeContext::DataElementNode>& copiedEntities)
|
|
{
|
|
// Create an entity node for the root slice
|
|
int entityIndex = canvasFileObjectNode.AddElement<AZ::Entity>(context, "RootSliceEntity");
|
|
if (entityIndex == -1)
|
|
{
|
|
return false;
|
|
}
|
|
AZ::SerializeContext::DataElementNode& entityNode = canvasFileObjectNode.GetSubElement(entityIndex);
|
|
|
|
// create the entity Id node
|
|
if (!LyShine::CreateEntityIdNode(context, entityNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Do not create a name node.
|
|
// EntityContext::CreateRootSlice creates an Entity with no name for the root slice entity
|
|
// This means that it defaults to the EntityId. If we don't create a name node here it seems to get a random
|
|
// value. That doesn't seem to matter though since the name of this entity is not used for anything.
|
|
|
|
// create the IsDependencyReady node
|
|
bool isDependencyReady = true;
|
|
int isDependencyReadyIndex = entityNode.AddElementWithData(context, "IsDependencyReady", isDependencyReady);
|
|
if (isDependencyReadyIndex == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// create the components vector node (which is a generic vector)
|
|
using componentsVector = AZStd::vector<AZ::Component*>;
|
|
AZ::SerializeContext::ClassData* componentVectorClassData = AZ::SerializeGenericTypeInfo<componentsVector>::GetGenericInfo()->GetClassData();
|
|
int componentsIndex = entityNode.AddElement(context, "Components", *componentVectorClassData);
|
|
if (componentsIndex == -1)
|
|
{
|
|
return false;
|
|
}
|
|
AZ::SerializeContext::DataElementNode& componentsNode = entityNode.GetSubElement(componentsIndex);
|
|
|
|
// create the slice component node
|
|
int sliceComponentIndex = componentsNode.AddElement(context, "element", AZ::SliceComponent::TYPEINFO_Uuid());
|
|
if (sliceComponentIndex == -1)
|
|
{
|
|
return false;
|
|
}
|
|
AZ::SerializeContext::DataElementNode& sliceComponentNode = componentsNode.GetSubElement(sliceComponentIndex);
|
|
|
|
// create the component base class
|
|
if (!LyShine::CreateComponentBaseClassNode(context, sliceComponentNode))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// create the Entities vector
|
|
using entityVector = AZStd::vector<AZ::Entity*>;
|
|
AZ::SerializeContext::ClassData* entityVectorClassData = AZ::SerializeGenericTypeInfo<entityVector>::GetGenericInfo()->GetClassData();
|
|
int entitiesIndex = sliceComponentNode.AddElement(context, "Entities", *entityVectorClassData);
|
|
if (entitiesIndex == -1)
|
|
{
|
|
return false;
|
|
}
|
|
AZ::SerializeContext::DataElementNode& entitiesNode = sliceComponentNode.GetSubElement(entitiesIndex);
|
|
|
|
// Add the entities to the entities vector
|
|
for (AZ::SerializeContext::DataElementNode& entityElement : copiedEntities)
|
|
{
|
|
entityElement.SetName("element"); // all elements in the Vector should have this name
|
|
entitiesNode.AddElement(entityElement);
|
|
}
|
|
|
|
// No need to create the empty Slices node
|
|
|
|
// create the IsDynamic node
|
|
bool isDynamic = true;
|
|
int isDynamicIndex = sliceComponentNode.AddElementWithData(context, "IsDynamic", isDynamic);
|
|
if (isDynamicIndex == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiCanvasFileObject::VersionConverter(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& canvasFileObjectNode)
|
|
{
|
|
if (canvasFileObjectNode.GetVersion() == 1)
|
|
{
|
|
// this is a pre-slice dummy CanvasFileObject programatically created on load
|
|
// we need to change all Entity* references (m_rootElement in UiCanvasComponent
|
|
// and m_children in UiElementComponent) into EntityId's instead and move
|
|
// the entities data into the slice component.
|
|
|
|
// find the CanvasEntity in the CanvasFileObject
|
|
int canvasEntityIndex = canvasFileObjectNode.FindElement(AZ_CRC("CanvasEntity", 0x87ff30ab));
|
|
if (canvasEntityIndex == -1)
|
|
{
|
|
return false;
|
|
}
|
|
AZ::SerializeContext::DataElementNode& canvasEntityNode = canvasFileObjectNode.GetSubElement(canvasEntityIndex);
|
|
|
|
// Find the m_rootElement member in the UiCanvasComponent on the canvas entity
|
|
AZ::SerializeContext::DataElementNode* rootElementNode = FindRootElementInCanvasEntity(context, canvasEntityNode);
|
|
if (!rootElementNode)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// All UI element entities will be copied to this container and then added to the slice component
|
|
AZStd::vector<AZ::SerializeContext::DataElementNode> copiedEntities;
|
|
|
|
// recursively process the root element and all of its child elements, copying their child entities to the
|
|
// entities container and replacing them with EntityIds
|
|
if (!UiElementComponent::MoveEntityAndDescendantsToListAndReplaceWithEntityId(context, *rootElementNode, -1, copiedEntities))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Create the RootSliceEntity in the CanvasFileObject and copy the entities into it
|
|
if (!CreateRootSliceNodeAndCopyInEntities(context, canvasFileObjectNode, copiedEntities))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|