UiCanvasFileObject.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "UiCanvasFileObject.h"
  9. #include "UiSerialize.h"
  10. #include <AzCore/Serialization/Utils.h>
  11. #include <LyShine/UiSerializeHelpers.h>
  12. #include "UiCanvasComponent.h"
  13. #include "UiElementComponent.h"
  14. ////////////////////////////////////////////////////////////////////////////////////////////////
  15. // Load a serialized stream that may be in an older format that may require massaging the steam
  16. UiCanvasFileObject* UiCanvasFileObject::LoadCanvasFromStream(AZ::IO::GenericStream& stream, const AZ::ObjectStream::FilterDescriptor& filterDesc)
  17. {
  18. // get the size of the file
  19. size_t fileSize = stream.GetLength();
  20. if (fileSize == 0)
  21. {
  22. AZ_Error("UI", false, "UI Canvas file: %s is zero bytes on disk, and cannot be loaded.", stream.GetFilename());
  23. return nullptr;
  24. }
  25. // read in the entire file into a char buffer. Note that files are not 0-truncated!
  26. char* buffer = new char[fileSize + 1];
  27. size_t bytesRead = stream.Read(fileSize, buffer);
  28. // null terminate in case we perform string operations.
  29. // this is not necessary on ObjectStream, but loading legacy files often requires string ops.
  30. buffer[bytesRead] = 0;
  31. // if ReadRaw read the file ok then load the entity from the buffer using AZ
  32. // serialization
  33. UiCanvasFileObject* canvas = nullptr;
  34. if (bytesRead == fileSize)
  35. {
  36. // Check to see if this is an old format canvas file that cannot be handled simply in the
  37. // version convert functions
  38. enum class FileFormat
  39. {
  40. ReallyOld, Old, CanvasObject
  41. };
  42. FileFormat fileFormat = FileFormat::CanvasObject;
  43. // All canvas files start with this (at least up to the introduction of the UiCanvasFileObject)
  44. const char* objectStreamPrefix =
  45. "<ObjectStream version=\"1\">";
  46. // This is what canvas files looked like prior to the introduction of the UiCanvasFileObject
  47. const char* oldStylePrefix =
  48. "<Class name=\"AZ::Entity\"";
  49. // This is what canvas files looked like in Fall 2015 (prior to R1)
  50. const char* reallyOldStylePrefix =
  51. "<Entity type=\"{";
  52. // See if we can identify the buffer as one of the old formats
  53. if (strncmp(buffer, objectStreamPrefix, strlen(objectStreamPrefix)) == 0)
  54. {
  55. // Is started with the usually ObjectStream prefix
  56. // find the start of the next tag
  57. const char* secondTag = buffer + strlen(objectStreamPrefix);
  58. secondTag = strchr(secondTag, '<');
  59. if (secondTag)
  60. {
  61. if (strncmp(secondTag, oldStylePrefix, strlen(oldStylePrefix)) == 0)
  62. {
  63. fileFormat = FileFormat::Old;
  64. }
  65. else if (strncmp(secondTag, reallyOldStylePrefix, strlen(reallyOldStylePrefix)) == 0)
  66. {
  67. fileFormat = FileFormat::ReallyOld;
  68. }
  69. }
  70. }
  71. if (fileFormat == FileFormat::Old)
  72. {
  73. // We can load this format but copying all of the entities from the canvas component (and children)
  74. // to the root slice is not efficient. So write a warning to the log that load times are impacted.
  75. AZ_Warning("UI", false, "UI canvas file: %s is in an old format, load times will be faster if you resave it.",
  76. stream.GetFilename());
  77. // Read this as an old format canvas file
  78. canvas = LoadCanvasEntitiesFromOldFormatFile(buffer, fileSize, filterDesc);
  79. if (!canvas)
  80. {
  81. AZ_Warning("UI", false, "Old format UI canvas file: %s could not be loaded. It may be corrupted.",
  82. stream.GetFilename());
  83. }
  84. }
  85. else
  86. {
  87. // This does not look like an old format canvas file so treat it as new format
  88. AZ::IO::MemoryStream newFormatStream(buffer, fileSize);
  89. canvas = LoadCanvasFromNewFormatStream(newFormatStream, filterDesc);
  90. if (!canvas)
  91. {
  92. AZ_Warning("UI", false, "UI canvas file: %s could not be loaded. It may be corrupted.",
  93. newFormatStream.GetFilename());
  94. }
  95. }
  96. }
  97. delete [] buffer;
  98. return canvas;
  99. }
  100. ////////////////////////////////////////////////////////////////////////////////////////////////////
  101. void UiCanvasFileObject::SaveCanvasToStream(AZ::IO::GenericStream& stream, UiCanvasFileObject* canvasFileObject)
  102. {
  103. AZ::Utils::SaveObjectToStream<UiCanvasFileObject>(stream, AZ::DataStream::ST_XML, canvasFileObject);
  104. }
  105. ////////////////////////////////////////////////////////////////////////////////////////////////////
  106. AZ::Entity* UiCanvasFileObject::LoadCanvasEntitiesFromStream(AZ::IO::GenericStream& stream, AZ::Entity*& rootSliceEntity)
  107. {
  108. AZ::Entity* canvasEntity = nullptr;
  109. UiCanvasFileObject* fileObject = AZ::Utils::LoadObjectFromStream<UiCanvasFileObject>(stream);
  110. if (fileObject && fileObject->m_canvasEntity && fileObject->m_rootSliceEntity)
  111. {
  112. canvasEntity = fileObject->m_canvasEntity;
  113. rootSliceEntity = fileObject->m_rootSliceEntity;
  114. }
  115. SAFE_DELETE(fileObject);
  116. return canvasEntity;
  117. }
  118. ////////////////////////////////////////////////////////////////////////////////////////////////////
  119. void UiCanvasFileObject::Reflect(AZ::ReflectContext* context)
  120. {
  121. AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
  122. if (serializeContext)
  123. {
  124. serializeContext->Class<UiCanvasFileObject>()
  125. ->Version(2, &UiCanvasFileObject::VersionConverter)
  126. ->Field("CanvasEntity", &UiCanvasFileObject::m_canvasEntity)
  127. ->Field("RootSliceEntity", &UiCanvasFileObject::m_rootSliceEntity);
  128. }
  129. }
  130. ////////////////////////////////////////////////////////////////////////////////////////////////////
  131. UiCanvasFileObject* UiCanvasFileObject::LoadCanvasEntitiesFromOldFormatFile(const char* buffer, size_t bufferSize, const AZ::ObjectStream::FilterDescriptor& filterDesc)
  132. {
  133. // This function attempts to read an old format (pre root slice) canvas file.
  134. // This is a little complex for a VersionConvert function to do. If we tried to do it in the version
  135. // converter for the UiCanvasComponent it would be hard because the root slice entity is saved as a
  136. // sibling of the entity with the UiCanvasComponent on it so it is not accessible within the
  137. // UiCanvasComponent version converter. Trying to save things into a static list for processing later
  138. // would be messy and would fail if two canvases were being loaded at the same time on different threads.
  139. // So we want to do the version conversion in the next level up - which is the CanvasFileObject
  140. // However, there is no CanvasFileObject level in an old style canvas file. So what we do is modify the buffer
  141. // so that it looks (just at the top level) like a new style file - with a CanvasFileObject.
  142. // Then we can handle the conversion in the CanvasFileObject version converter.
  143. // These are the prefix and suffix for the new style file:
  144. const char* prefixToAdd1 =
  145. "<ObjectStream version=\"1\">\n"
  146. "\t<Class name=\"CanvasFileObject\" version=\"1\" type=\"{1F02632F-F113-49B1-85AD-8CD0FA78B8AA}\">\n"
  147. "\t\t<Class name=\"AZ::Entity\" field=\"CanvasEntity\" version=\"2\" type=\"{75651658-8663-478D-9090-2432DFCAFA44}\">\n";
  148. const char* prefixToAdd2 =
  149. "<ObjectStream version=\"1\">\n"
  150. "\t<Class name=\"CanvasFileObject\" version=\"1\" type=\"{1F02632F-F113-49B1-85AD-8CD0FA78B8AA}\">\n"
  151. "\t\t<Class name=\"AZ::Entity\" field=\"CanvasEntity\" type=\"{75651658-8663-478D-9090-2432DFCAFA44}\">\n";
  152. const char* suffixToAdd =
  153. "\t\t</Class>\n"
  154. "\t</Class>\n"
  155. "</ObjectStream>\n";
  156. const char* prefixToAdd = prefixToAdd1;
  157. // These are the prefix and suffix for an old style file. Note that the use of \r\n versus \n only is inconsistent
  158. // sometimes it comes in with one and sometimes the other.
  159. const char* prefixToRemove1 =
  160. "<ObjectStream version=\"1\">\n"
  161. "\t<Class name=\"AZ::Entity\" version=\"2\" type=\"{75651658-8663-478D-9090-2432DFCAFA44}\">\n";
  162. const char* prefixToRemove2 =
  163. "<ObjectStream version=\"1\">\r\n"
  164. "\t<Class name=\"AZ::Entity\" version=\"2\" type=\"{75651658-8663-478D-9090-2432DFCAFA44}\">\r\n";
  165. const char* typeString =
  166. "type=\"{75651658-8663-478D-9090-2432DFCAFA44}\">";
  167. const char* suffixToRemove1 =
  168. "\t</Class>\n"
  169. "</ObjectStream>\n";
  170. const char* suffixToRemove2 =
  171. "\t</Class>\r\n"
  172. "</ObjectStream>\r\n";
  173. // Do a sanity check that the buffer does start with the prefix that we will remove
  174. // Also, determine how newlines are represented in the file.
  175. const char* suffixToRemove = nullptr;
  176. size_t prefixToRemoveLen = 0;
  177. if (strncmp(buffer, prefixToRemove1, strlen(prefixToRemove1)) == 0)
  178. {
  179. prefixToRemoveLen = strlen(prefixToRemove1);
  180. suffixToRemove = suffixToRemove1;
  181. }
  182. else if (strncmp(buffer, prefixToRemove2, strlen(prefixToRemove2)) == 0)
  183. {
  184. prefixToRemoveLen = strlen(prefixToRemove2);
  185. suffixToRemove = suffixToRemove2;
  186. }
  187. else
  188. {
  189. // not an exact match - this can happen, for example if the entity version is not 2
  190. // it can have a missing version
  191. // This is a more forgiving way to do the test. It could replace the code above but
  192. // that code has been working for a while so we add this code as a backup
  193. const char* typeStart = strstr(buffer, typeString);
  194. if (!typeStart)
  195. {
  196. // We can't convert this file.
  197. if (bufferSize < strlen(prefixToRemove2))
  198. {
  199. // Something is very wrong. The file is shorter that the expected prefix.
  200. // note that we must use AZ_Warning here as this code is shared in tools which don't have gEnv.
  201. AZ_Warning("UI", false, "Error converting canvas file. File appears to be truncated.");
  202. }
  203. else
  204. {
  205. // Print out the start of the file for help in debugging
  206. // user reported issues
  207. AZStd::string messageBuffer(buffer, strlen(prefixToRemove2));
  208. AZ_Warning("UI", false, "Error converting canvas file. Prefix is:\r\n%s", messageBuffer.c_str());
  209. }
  210. return nullptr;
  211. }
  212. prefixToAdd = prefixToAdd2;
  213. suffixToRemove = suffixToRemove1;
  214. const char* p = typeStart + strlen(typeString);
  215. if (*p == '\r')
  216. {
  217. ++p;
  218. suffixToRemove = suffixToRemove2;
  219. }
  220. if (*p == '\n')
  221. {
  222. ++p;
  223. }
  224. // the prefix length is up to the \n after the typeString
  225. prefixToRemoveLen = p - buffer;
  226. }
  227. // work out some lengths here for the strings we want to mess with
  228. const size_t prefixToAddLen = strlen(prefixToAdd);
  229. const size_t suffixToAddLen = strlen(suffixToAdd);
  230. const size_t suffixToRemoveLen = strlen(suffixToRemove);
  231. // This allows for not knowing exactly how many extra chars will be at the end of the file.
  232. // We search backward for some arbitrary character in the suffixToRemove ('<') and line things
  233. // up using that.
  234. const char* lastOpenAngleInBuffer = strrchr(buffer, '<');
  235. const char* lastOpenAngleInSuffixToRemove = strrchr(suffixToRemove, '<');
  236. const char* suffixToRemoveStart = lastOpenAngleInBuffer - (lastOpenAngleInSuffixToRemove - suffixToRemove);
  237. // sanity check to check that the suffix matches
  238. if (strncmp(suffixToRemoveStart, suffixToRemove, suffixToRemoveLen) != 0)
  239. {
  240. AZ_Warning("UI", false, "Error converting canvas file. File appears to be truncated at the end.");
  241. return nullptr;
  242. }
  243. // Compute the start and length of the part we want to copy from the old buffer to the new buffer
  244. const char* oldBufferCoreStart = buffer + prefixToRemoveLen;
  245. const size_t oldBufferCoreLen = suffixToRemoveStart - oldBufferCoreStart;
  246. // allocate the new buffer
  247. size_t newBufferSize = prefixToAddLen + oldBufferCoreLen + suffixToAddLen + 1;
  248. char* newBuffer = new char[newBufferSize];
  249. // fill the new buffer with the new prefix, the old core and the new suffix
  250. char* insertPoint = newBuffer;
  251. azstrncpy(insertPoint, newBufferSize, prefixToAdd, prefixToAddLen);
  252. insertPoint += prefixToAddLen;
  253. azstrncpy(insertPoint, newBufferSize - prefixToAddLen, oldBufferCoreStart, oldBufferCoreLen);
  254. insertPoint += oldBufferCoreLen;
  255. azstrncpy(insertPoint, newBufferSize - prefixToAddLen - oldBufferCoreLen, suffixToAdd, suffixToAddLen);
  256. insertPoint += suffixToAddLen;
  257. insertPoint[0] = '\0';
  258. // Now try loading from this new buffer, the rest of the conversion is done in CanvasFileObject::VersionConverter
  259. AZ::IO::MemoryStream stream(newBuffer, newBufferSize);
  260. UiCanvasFileObject* canvas = LoadCanvasFromNewFormatStream(stream, filterDesc);
  261. delete [] newBuffer;
  262. return canvas;
  263. }
  264. ////////////////////////////////////////////////////////////////////////////////////////////////////
  265. UiCanvasFileObject* UiCanvasFileObject::LoadCanvasFromNewFormatStream(AZ::IO::GenericStream& stream, const AZ::ObjectStream::FilterDescriptor& filterDesc)
  266. {
  267. UiCanvasFileObject* fileObject =
  268. AZ::Utils::LoadObjectFromStream<UiCanvasFileObject>(stream, nullptr, filterDesc);
  269. return fileObject;
  270. }
  271. ////////////////////////////////////////////////////////////////////////////////////////////////////
  272. // Helper function to find the root element node in a canvas entity node
  273. AZ::SerializeContext::DataElementNode* UiCanvasFileObject::FindRootElementInCanvasEntity(
  274. [[maybe_unused]] AZ::SerializeContext& context,
  275. AZ::SerializeContext::DataElementNode& canvasEntityNode)
  276. {
  277. // Find the UiCanvasComponent in the CanvasEntity
  278. AZ::SerializeContext::DataElementNode* canvasComponentNode =
  279. LyShine::FindComponentNode(canvasEntityNode, UiCanvasComponent::TYPEINFO_Uuid());
  280. if (!canvasComponentNode)
  281. {
  282. return nullptr;
  283. }
  284. // Find the RootElement entity in the UiCanvasComponent
  285. int rootElementIndex = canvasComponentNode->FindElement(AZ_CRC("RootElement", 0x9ac9557b));
  286. if (rootElementIndex == -1)
  287. {
  288. return nullptr;
  289. }
  290. AZ::SerializeContext::DataElementNode& rootElementNode = canvasComponentNode->GetSubElement(rootElementIndex);
  291. return &rootElementNode;
  292. }
  293. ////////////////////////////////////////////////////////////////////////////////////////////////////
  294. // Helper function to create the root slice entity node and all its sub nodes and then copy
  295. // the entities representing all the UI elements in the canvas into the SliceComponent node
  296. bool UiCanvasFileObject::CreateRootSliceNodeAndCopyInEntities(
  297. AZ::SerializeContext& context,
  298. AZ::SerializeContext::DataElementNode& canvasFileObjectNode,
  299. AZStd::vector<AZ::SerializeContext::DataElementNode>& copiedEntities)
  300. {
  301. // Create an entity node for the root slice
  302. int entityIndex = canvasFileObjectNode.AddElement<AZ::Entity>(context, "RootSliceEntity");
  303. if (entityIndex == -1)
  304. {
  305. return false;
  306. }
  307. AZ::SerializeContext::DataElementNode& entityNode = canvasFileObjectNode.GetSubElement(entityIndex);
  308. // create the entity Id node
  309. if (!LyShine::CreateEntityIdNode(context, entityNode))
  310. {
  311. return false;
  312. }
  313. // Do not create a name node.
  314. // EntityContext::CreateRootSlice creates an Entity with no name for the root slice entity
  315. // This means that it defaults to the EntityId. If we don't create a name node here it seems to get a random
  316. // value. That doesn't seem to matter though since the name of this entity is not used for anything.
  317. // create the IsDependencyReady node
  318. bool isDependencyReady = true;
  319. int isDependencyReadyIndex = entityNode.AddElementWithData(context, "IsDependencyReady", isDependencyReady);
  320. if (isDependencyReadyIndex == -1)
  321. {
  322. return false;
  323. }
  324. // create the components vector node (which is a generic vector)
  325. using componentsVector = AZ::Entity::ComponentArrayType;
  326. AZ::SerializeContext::ClassData* componentVectorClassData = AZ::SerializeGenericTypeInfo<componentsVector>::GetGenericInfo()->GetClassData();
  327. int componentsIndex = entityNode.AddElement(context, "Components", *componentVectorClassData);
  328. if (componentsIndex == -1)
  329. {
  330. return false;
  331. }
  332. AZ::SerializeContext::DataElementNode& componentsNode = entityNode.GetSubElement(componentsIndex);
  333. // create the slice component node
  334. int sliceComponentIndex = componentsNode.AddElement(context, "element", AZ::SliceComponent::TYPEINFO_Uuid());
  335. if (sliceComponentIndex == -1)
  336. {
  337. return false;
  338. }
  339. AZ::SerializeContext::DataElementNode& sliceComponentNode = componentsNode.GetSubElement(sliceComponentIndex);
  340. // create the component base class
  341. if (!LyShine::CreateComponentBaseClassNode(context, sliceComponentNode))
  342. {
  343. return false;
  344. }
  345. // create the Entities vector
  346. using entityVector = AZStd::vector<AZ::Entity*>;
  347. AZ::SerializeContext::ClassData* entityVectorClassData = AZ::SerializeGenericTypeInfo<entityVector>::GetGenericInfo()->GetClassData();
  348. int entitiesIndex = sliceComponentNode.AddElement(context, "Entities", *entityVectorClassData);
  349. if (entitiesIndex == -1)
  350. {
  351. return false;
  352. }
  353. AZ::SerializeContext::DataElementNode& entitiesNode = sliceComponentNode.GetSubElement(entitiesIndex);
  354. // Add the entities to the entities vector
  355. for (AZ::SerializeContext::DataElementNode& entityElement : copiedEntities)
  356. {
  357. entityElement.SetName("element"); // all elements in the Vector should have this name
  358. entitiesNode.AddElement(entityElement);
  359. }
  360. // No need to create the empty Slices node
  361. // create the IsDynamic node
  362. bool isDynamic = true;
  363. int isDynamicIndex = sliceComponentNode.AddElementWithData(context, "IsDynamic", isDynamic);
  364. if (isDynamicIndex == -1)
  365. {
  366. return false;
  367. }
  368. return true;
  369. }
  370. ////////////////////////////////////////////////////////////////////////////////////////////////////
  371. bool UiCanvasFileObject::VersionConverter(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& canvasFileObjectNode)
  372. {
  373. if (canvasFileObjectNode.GetVersion() == 1)
  374. {
  375. // this is a pre-slice dummy CanvasFileObject programatically created on load
  376. // we need to change all Entity* references (m_rootElement in UiCanvasComponent
  377. // and m_children in UiElementComponent) into EntityId's instead and move
  378. // the entities data into the slice component.
  379. // find the CanvasEntity in the CanvasFileObject
  380. int canvasEntityIndex = canvasFileObjectNode.FindElement(AZ_CRC("CanvasEntity", 0x87ff30ab));
  381. if (canvasEntityIndex == -1)
  382. {
  383. return false;
  384. }
  385. AZ::SerializeContext::DataElementNode& canvasEntityNode = canvasFileObjectNode.GetSubElement(canvasEntityIndex);
  386. // Find the m_rootElement member in the UiCanvasComponent on the canvas entity
  387. AZ::SerializeContext::DataElementNode* rootElementNode = FindRootElementInCanvasEntity(context, canvasEntityNode);
  388. if (!rootElementNode)
  389. {
  390. return false;
  391. }
  392. // All UI element entities will be copied to this container and then added to the slice component
  393. AZStd::vector<AZ::SerializeContext::DataElementNode> copiedEntities;
  394. // recursively process the root element and all of its child elements, copying their child entities to the
  395. // entities container and replacing them with EntityIds
  396. if (!UiElementComponent::MoveEntityAndDescendantsToListAndReplaceWithEntityId(context, *rootElementNode, -1, copiedEntities))
  397. {
  398. return false;
  399. }
  400. // Create the RootSliceEntity in the CanvasFileObject and copy the entities into it
  401. if (!CreateRootSliceNodeAndCopyInEntities(context, canvasFileObjectNode, copiedEntities))
  402. {
  403. return false;
  404. }
  405. }
  406. return true;
  407. }