123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- /*
- * 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 = AZ::Entity::ComponentArrayType;
- 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;
- }
|