3
0

LegacyConversionHelpers.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  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. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. Helper classes for legacy conversion scripts
  6. """
  7. import os
  8. import platform
  9. import xml.etree.ElementTree
  10. class Stats_Collector(object):
  11. """
  12. Stuff any extra info you want to keep track of during conversion into this object
  13. """
  14. def __init__(self):
  15. self.materialOverrideCount = 0
  16. self.noMaterialOverrideCount = 0
  17. class File_Info(object):
  18. """
  19. Keep track of both the filename and the directory of the project
  20. (not the directory the file is in, but the LY project itself)
  21. """
  22. def __init__(self, filename, projectDir):
  23. self.filename = filename
  24. self.normalizedProjectDir = os.path.normpath(projectDir)
  25. class Log_File(object):
  26. """
  27. Simple class to create, open & save a log file.
  28. """
  29. def __init__(self, filename, include_previous = True):
  30. self.filename = filename
  31. self.logFile = None
  32. self.include_previous = include_previous
  33. self.lines = []
  34. self.create_log_file()
  35. def create_log_file(self):
  36. """
  37. Will either generate a new log file or open the existing log file and read its contents
  38. """
  39. if os.path.exists(self.filename):
  40. self.logFile = open(self.filename, 'r+')
  41. if self.include_previous:
  42. for line in self.logFile.readlines():
  43. self.add_line(line)
  44. else:
  45. self.logFile = open(self.filename, 'w')
  46. def save(self):
  47. """
  48. Saves the log file back to disk.
  49. To ensure that filenames are not added twice, this method first saves an empty
  50. log to disc. Then saves it again with the list of filenames (self.lines)
  51. stored in this class.
  52. """
  53. # First clear the log file's contents so they can be written back in
  54. self.logFile.close()
  55. open(self.filename, 'w').close()
  56. self.logFile = open(self.filename, 'w')
  57. for line in self.lines:
  58. self.logFile.write("{0}\n".format(line))
  59. self.logFile.close()
  60. def get_lines(self):
  61. return self.lines
  62. def add_line(self, line):
  63. self.lines.append(line)
  64. def add_line_no_duplicates(self, line):
  65. """
  66. Adds a line to the log, and prevents adding duplicates
  67. Useful for keeping track of files that have already been converted
  68. The line will be all lowercase for simplicity
  69. """
  70. # rstrip with no arguments will remove trailing whitespace
  71. lowercase = line.lower().rstrip()
  72. if not lowercase in self.lines:
  73. self.lines.append(lowercase)
  74. def has_line(self, line):
  75. """
  76. Checks to see if a specific line already exists in the log
  77. """
  78. # rstrip with no arguments will remove trailing whitespace
  79. lowercase = line.lower().rstrip()
  80. if lowercase in self.lines:
  81. return True
  82. return False
  83. class Common_Command_Line_Options(object):
  84. """
  85. Some common options/parsing
  86. """
  87. def __init__(self, args):
  88. self.projectName = ""
  89. self.assetCatalogOverridePath = ""
  90. self.includeGems = False
  91. self.useP4 = False
  92. self.endsWithStr = ""
  93. self.isHelp = False
  94. self.helpString = f"usage: {args[0]} -project=<project name> -include_gems -ends_with=<filter> -use_p4 -assetCatalogOverridePath=<path>\n\
  95. E.g.:\n\
  96. -project=StarterGame -include_gems\n\
  97. -project is required. Path is relative\n\
  98. -include_gems is optional, and by default gems will not be included.\n\
  99. -ends_with is optional. It could be used to filter for a specific file (--ends_with=default.mtl)\n\
  100. -use_p4 is optional. It will use the p4 command line to check out files that are edited in your default changelist\n\
  101. -assetCatalogOverridePath is optional. It will use asset ids from this file instead of using the current project asset catalog\n\
  102. (match via relative asset path to the project root folder)"
  103. for argument in args:
  104. argument = argument.rstrip(" ").lstrip("-")
  105. if argument == "h" or argument == "help" or argument == "?":
  106. self.isHelp = True
  107. elif argument.startswith("project"):
  108. projectArgs = argument.split("=")
  109. if len(projectArgs) > 1:
  110. self.projectName = projectArgs[1]
  111. elif argument.startswith("assetCatalogOverridePath"):
  112. projectArgs = argument.split("=")
  113. if len(projectArgs) > 1:
  114. self.assetCatalogOverridePath = projectArgs[1]
  115. elif argument == "include_gems":
  116. self.includeGems = True
  117. elif argument == "use_p4":
  118. self.useP4 = True
  119. elif argument.startswith("ends_with"):
  120. endsWithArgs = argument.split("=")
  121. if len(endsWithArgs) > 1:
  122. self.endsWithStr = endsWithArgs[1]
  123. def get_file_list(projectName, includeGems, extensionList, buildPath, gemsPath):
  124. """
  125. The main difference between this and any other way to walk a directory
  126. looking for files is that it keeps track of the lumberyard project
  127. or gems folder the file is in, which can later be used to figure out
  128. the relative path that is used by the engine for the file
  129. """
  130. print("Gathering Files...")
  131. # Lower the project name for easier matching
  132. projectName = projectName.lower()
  133. # First, gather a list of all project folders in the dev root.
  134. # This will help to reduce the amount of files that the script
  135. # has to walk through when searching for files.
  136. projectFolders = []
  137. for root, dirs, files in os.walk(buildPath):
  138. if root != buildPath:
  139. break
  140. for d in dirs:
  141. projectFile = "{0}\\{1}\\project.json".format(root, d)
  142. if os.path.exists(projectFile) and d.lower() == projectName:
  143. projectFolders.append(os.path.join(root, d))
  144. # Add all gems to the list of project folders.
  145. if includeGems:
  146. for root, dirs, files in os.walk(gemsPath):
  147. if root != gemsPath:
  148. break
  149. while len(dirs) >= 1:
  150. d = dirs[0]
  151. gemsFile = "{0}\\{1}\\gem.json".format(root, d)
  152. if os.path.exists(gemsFile):
  153. projectFolders.append(os.path.join(os.path.join(root, d), "Assets"))
  154. for subroot, subdirs, subfiles in os.walk(os.path.join(gemsPath, d)):
  155. if subroot != os.path.join(gemsPath, d):
  156. break
  157. for subd in subdirs:
  158. dirs.append(os.path.join(d, subd))
  159. dirs.remove(d)
  160. fileInfoList = []
  161. for projPath in projectFolders:
  162. for root, dirs, files in os.walk(projPath):
  163. if 'Cache' in root.split(os.sep):
  164. continue
  165. for f in files:
  166. for extension in extensionList:
  167. if f.endswith(extension):
  168. fileInfoList.append(File_Info("{0}\\{1}".format(root, f), projPath))
  169. return fileInfoList
  170. class Asset_Catalog_Dictionaries(object):
  171. """
  172. Some dictionaries from the asset catalog
  173. """
  174. def __init__(self, relativePathToAssetIdDict, assetIdToRelativePathDict, assetUuidToAssetIdsDict):
  175. self.relativePathToAssetIdDict = relativePathToAssetIdDict
  176. self.assetIdToRelativePathDict = assetIdToRelativePathDict
  177. self.assetUuidToAssetIdsDict = assetUuidToAssetIdsDict
  178. def get_asset_id_from_relative_path(self, relativePath):
  179. # parse the asset catalog and find the assetId
  180. if relativePath in self.relativePathToAssetIdDict:
  181. return self.relativePathToAssetIdDict[relativePath]
  182. return ""
  183. def get_asset_catalog_dictionaries(assetCatalogPath):
  184. """
  185. This function pre-supposes that you have modified AssetCatalog::SaveRegistry_Impl
  186. in Code/Tools/AssetProcessor/native/AssetManager/AssetCatalog.cpp
  187. to use AZ::ObjectStream::ST_XML instead of AZ::ObjectStream::ST_BINARY
  188. and subsequently deleted Cache/<project name>/pc/<project name>/assetcatalog.xml
  189. and let it re-build so that it is a parseable xml file
  190. """
  191. print("Parsing Asset Catalog...")
  192. relativePathToAssetIdDict = {}
  193. relativePathToAssetIdDict[""] = "{00000000-0000-0000-0000-000000000000}:0"
  194. assetIdToRelativePathDict = {}
  195. assetUuidToAssetIdsDict = {}
  196. assetCatalogXml = xml.etree.ElementTree.parse(assetCatalogPath)
  197. for possibleAssetInfo in assetCatalogXml.getroot().iter('Class'):
  198. if "name" in possibleAssetInfo.keys() and possibleAssetInfo.get("name") == "AssetInfo":
  199. # We found some AssetInfo
  200. relativePath = ""
  201. assetId = ""
  202. for child in possibleAssetInfo:
  203. if "field" in child.keys() and child.get("field") == "relativePath" and "value" in child.keys():
  204. relativePath = child.get("value")
  205. if "name" in child.keys() and child.get("name") == "AssetId":
  206. guid = ""
  207. subId = ""
  208. for assetIdPart in child:
  209. if "field" in assetIdPart.keys() and assetIdPart.get("field") == "guid" and "value" in assetIdPart.keys():
  210. guid = assetIdPart.get("value")
  211. if "field" in assetIdPart.keys() and assetIdPart.get("field") == "subId" and "value" in assetIdPart.keys():
  212. subId = assetIdPart.get("value")
  213. assetId = "".join((guid, ":", subId))
  214. relativePathToAssetIdDict[relativePath] = assetId
  215. assetIdToRelativePathDict[assetId] = relativePath
  216. if not guid in assetUuidToAssetIdsDict:
  217. assetUuidToAssetIdsDict[guid] = []
  218. assetUuidToAssetIdsDict[guid].append(assetId)
  219. return Asset_Catalog_Dictionaries(relativePathToAssetIdDict, assetIdToRelativePathDict, assetUuidToAssetIdsDict)
  220. def create_xml_element_from_string(xmlString):
  221. # copy paste a line from a .slice or other serialization file from lumberyard, add a \ before the " marks, and this function will turn it into an xml element
  222. # e.g. <Class name=\"EditorMaterialComponent\" field=\"element\" version=\"3\" type=\"{02B60E9D-470B-447D-A6EE-7D635B154183}\">
  223. # does not protect against malformed strings.
  224. # relies heavily on things like starting with <, no space between < and the tag, etc.
  225. # but works in a pinch
  226. rawXmlList = xmlString.split()
  227. # strip the < from the first element to get the tag
  228. element = xml.etree.ElementTree.Element(rawXmlList[0].lstrip("<"))
  229. for itemIndex in range(len(rawXmlList)):
  230. item = rawXmlList[itemIndex]
  231. if itemIndex == 0:
  232. # ignore the tag because we've already handled it
  233. continue
  234. elif itemIndex == (len(rawXmlList) - 1):
  235. # strip the end tag if it exists
  236. item = item.rstrip('>')
  237. item = item.rstrip('/')
  238. #parse the item
  239. itemList = item.split('=')
  240. attribute = itemList[0]
  241. value = itemList[1].lstrip('\"').rstrip('\"')
  242. element.set(attribute, value)
  243. return element
  244. def get_uuid_from_assetId(assetId):
  245. separatorIndex = assetId.find(":")
  246. return assetId[:separatorIndex]
  247. def get_subid_from_assetId(assetId):
  248. separatorIndex = assetId.find(":") + 1
  249. return assetId[separatorIndex:]
  250. def get_default_asset_platform():
  251. host_platform_to_asset_platform_map = { 'windows': 'pc',
  252. 'linux': 'linux',
  253. 'darwin': 'mac' }
  254. return host_platform_to_asset_platform_map.get(platform.system().lower(), "")
  255. class Component_Converter(object):
  256. """
  257. Converter base class
  258. """
  259. def __init__(self, assetCatalogHelper, statsCollector):
  260. self.assetCatalogHelper = assetCatalogHelper
  261. self.statsCollector = statsCollector
  262. def is_this_the_component_im_looking_for(self, xmlElement, parent):
  263. pass
  264. def gather_info_for_conversion(self, xmlElement, parent):
  265. pass
  266. def convert(self, xmlElement, parent):
  267. pass
  268. def reset(self):
  269. pass