import os import copy from subprocess import Popen, PIPE from xml.etree import ElementTree as ET from xml.dom import minidom import pyfbsdk as mobu import RS.Perforce as P4 def RunTerminalCommand(cmdTxt): process = Popen(cmdTxt, shell=True, stdout=PIPE) output = process.communicate()[0] return output def WriteCleanXml(rootNode, xmlPath): outputString = ET.tostring(rootNode, encoding="utf-8") xmlDoc = minidom.parseString(outputString) prettyXml = '\n'.join([line for line in xmlDoc.toprettyxml(indent=' '*2, encoding='utf-8').split('\n') if line.strip()]) with open(xmlPath, "wb") as fileHandle: fileHandle.write(prettyXml) def SetupGameContent(changeListNumber=None): # Get Project Root and Name projectRoot = mobu.FBApplication().FBXFileName.split("{0}art{0}".format(os.path.sep))[0] projectName = projectRoot.split(os.path.sep)[1].upper() # Get Mission Name fbxName = os.path.split(mobu.FBApplication().FBXFileName)[1][:-4].upper() missionText = fbxName.split("_")[0].lower() if projectName == "GTA5_DLC": missionText = "_".join(fbxName.split("_")[:2]).lower() # Create List of Xml Paths xmlList = [] if projectName == "RDR3": xmlList.append(os.path.join(projectRoot, "build", "dev", "common", "data", "common_cutscene.meta")) xmlList.append(os.path.join(projectRoot, "build", "dev", "common", "data", "anim", "anim.meta")) if projectName == "GTA5_DLC": xmlList.append(os.path.join(projectRoot, "build", "dev_ng", "content.xml")) # Loop Through Each Xml Path - and set search text for xmlPath in xmlList: searchText = "/anim/animscenes/cuts@" if xmlPath.endswith("anim.meta") else "/anim/cutscene/cuts_" # Sync Latest Xml, rootNode, and dataFilesNode P4.Sync(xmlPath, force=True) tree = ET.parse(xmlPath) rootNode = tree.getroot() if rootNode.tag == "dataFiles": dataFilesNode = rootNode else: dataFilesNode = tree.find("dataFiles") # Search For Cutscene RPFs for Matching Nodes rpfNodes = [node for node in dataFilesNode.findall(".//Item/filename/..") if searchText in node.find("filename").text.lower()] matchedNode = [node for node in rpfNodes if node.find("filename").text.lower().endswith("{0}{1}.rpf".format(searchText, missionText))] # Scene RPF Not Found - copy and add a new node if rpfNodes and matchedNode == []: platformText = rpfNodes[0].find("filename").text.lower().split(searchText)[0] rpfText = "{0}{1}{2}.rpf".format(platformText, searchText, missionText) newNode = copy.deepcopy(rpfNodes[0]) filenameNode = newNode.find("filename") filenameNode.text = rpfText dataFilesNode.append(newNode) # RDR Specific - Sort And Cleanup Nodes if projectName == "RDR3": # Sort And Cleanup Nodes container = dataFilesNode data = [] keyList = [] for elem in container: nameNode = elem.find("filename") key = nameNode.text.lower() nameNode.text = key if key not in keyList: keyList.append(key) data.append((key, elem)) data.sort() container[:] = [item[-1] for item in data] # GTA5 DLC Specific - Add extra fileToEnable node else: dlcPackName = rpfText.split("dlc_")[1].split(":")[0].upper() changeSetText = "{}_AUTOGEN".format(dlcPackName) enableFilesNode = rootNode.find(".//*[changeSetName='{}']/filesToEnable".format(changeSetText)) if enableFilesNode != []: itemNodes = [node for node in enableFilesNode.findall("Item") if "/anim/cutscene/cuts_" in node.text] matchedNode = [node for node in enableFilesNode.findall("Item") if node.text.lower() == rpfText.lower()] if rpfNodes and matchedNode == []: newNode = copy.deepcopy(itemNodes[0]) newNode.text = rpfText enableFilesNode.append(newNode) # Write Out Updated Xml - and add to changelist if provided P4.Edit(xmlPath) # tree.write(xmlPath, encoding="UTF-8") WriteCleanXml(rootNode, xmlPath) if changeListNumber: os.chdir(projectRoot) cmdTxt = "p4 reopen -c {0} {1}".format(changeListNumber, xmlPath) RunTerminalCommand(cmdTxt) def SetupToolsContent(changeListNumber=None): # Get Project Paths and Name projectRoot = os.environ.get("RS_PROJROOT") toolsRoot = os.environ.get("RS_TOOLSROOT") assetRoot = os.path.join(projectRoot, "assets") projectName = os.path.basename(projectRoot).upper() # Get Content Files if projectName == "RDR3": contentFiles = [os.path.join(toolsRoot, "etc", "content", "content_animation_cutscene.xml"), os.path.join(toolsRoot, "etc", "content", "content_animation_cutscene_fbx.xml")] contentToolPath = os.path.join(assetRoot, "metadata", "content", "generator", "GenerateCutsceneContent.bat") cmdTxt = "{} < nul".format(contentToolPath) elif projectName == "GTA5_DLC": contentFiles = [os.path.join(projectRoot, "content_animation_cutscene.xml"), os.path.join(projectRoot, "content_animation_cutscene_fbx.xml")] contentToolPath = os.path.join(toolsRoot, "bin", "Cutscene", "content", "cutscene_content_generator.exe") cmdTxt = "{0} --fbx --target={1}".format(contentToolPath, projectRoot.split(os.path.sep)[-1]) else: errorMsg = "Error: This FBX is from a project the content update tool doesn't support." print errorMsg return errorMsg # Sync Latest Content Xml Files P4.Revert(contentFiles) P4.Sync(contentFiles, force=True) # Run Content Tool Command os.chdir(projectRoot) RunTerminalCommand(cmdTxt) # Move Files to Our ChangeList - if they were updated for eachXml in contentFiles: eachP4Obj = P4.GetFileState(eachXml) if P4.IsOpenForEdit(eachP4Obj): cmdTxt = "p4 diff -ds {}".format(eachXml) output = RunTerminalCommand(cmdTxt) diffText = output.split("====\r")[1] # File Identical - we revert it if len(diffText) < 5: P4.Revert(eachXml) # File Updated and ChangeList Provided - move to our changelist elif changeListNumber: cmdTxt = "p4 reopen -c {0} {1}".format(changeListNumber, eachXml) RunTerminalCommand(cmdTxt) def Run(): fbxPath = mobu.FBApplication().FBXFileName.lower() animationFolder = "{0}art{0}animation{0}".format(os.path.sep) if animationFolder in fbxPath and os.path.splitext(fbxPath)[1] == ".fbx": SetupGameContent() SetupToolsContent()