#!/usr/bin/env python # -*- coding: UTF-8 -*- # # Created by Praetorian (ShadowNate) for Classic Adventures in Greek # classic.adventures.in.greek@gmail.com # # DONE Add code and switch option: to get the blade runner installation directory as input, then find the TLK files and export them with proper naming # DONE fix proper names for sheets as per latest code changes # osLibFound = False sysLibFound = False shutilLibFound = False xlwtLibFound = False csvLibFound = False try: import os except ImportError: print "[Error] os python library is required to be installed!" else: osLibFound = True try: import sys except ImportError: print "[Error] sys python library is required to be installed!" else: sysLibFound = True try: import shutil except ImportError: print "[Error] Shutil python library is required to be installed!" else: shutilLibFound = True try: import xlwt except ImportError: print "[Error] xlwt python library is required to be installed!" else: xlwtLibFound = True try: import csv except ImportError: print "[Error] csv python library is required to be installed!" else: csvLibFound = True if (not osLibFound) \ or (not sysLibFound) \ or (not shutilLibFound) \ or (not xlwtLibFound) \ or (not csvLibFound): sys.stdout.write("[Error] Errors were found when trying to import required python libraries\n") sys.exit(1) from os import walk, errno, path from xlwt import * from audFileLib import * from treFileLib import * from pogoTextResource import * from devCommentaryText import * # encoding=utf8 #reload(sys) #sys.setdefaultencoding('utf8') COMPANY_EMAIL = "classic.adventures.in.greek@gmail.com" APP_VERSION = "0.80" APP_NAME = "sortBladeRunnerWavs" APP_WRAPPER_NAME = "quotesSpreadsheetCreator.py" APP_NAME_SPACED = "Blade Runner Transcript Excel Creator (bare bones)" APP_SHORT_DESC = "* Create an Excel (.XLS) for transcribing Blade Runner.\n* (Optional) Extract TRx (x is the language code) files as additional sheets in the Excel\n* (Optional) Export speech files from the game's TLK resources to WAV format." OUTPUT_XLS_FILENAME = 'out' OUTPUT_XLS_FILENAME_EXT = '.xls' OUTPUT_XLS_QUOTES_SHEET = 'INGQUO_' # needs suffix x.TRx where x is the language code SUPPORTED_TLK_INPUT_FILES = [('1.TLK', 'TLK01'), ('2.TLK', 'TLK02'), ('3.TLK', 'TLK03'), ('A.TLK', 'TLK0A'), ('SPCHSFX.TLK', 'TLKSPCHSFX')] SUPPORTED_MIX_INPUT_FOR_EXTRA_SPEECH_EXPORT_FILES = [('SFX.MIX', 'SFX')] SUPPORTED_EXPORTED_AUD_FROM_MIX_FILES = [('COLONY.AUD', 'SFX.MIX'), \ ('MA04VO1A.AUD', 'SFX.MIX'), \ ('CT01VO1A.AUD', 'SFX.MIX'), \ ('HC01VO1A.AUD', 'SFX.MIX'), \ ('67_0470R.AUD', 'SFX.MIX'), \ ('67_0480R.AUD', 'SFX.MIX'), \ ('67_0500R.AUD', 'SFX.MIX'), \ ('67_0540R.AUD', 'SFX.MIX'), \ ('67_0560R.AUD', 'SFX.MIX'), \ ('67_0870R.AUD', 'SFX.MIX'), \ ('67_0880R.AUD', 'SFX.MIX'), \ ('67_0900R.AUD', 'SFX.MIX'),\ ('67_0940R.AUD', 'SFX.MIX'), \ ('67_0960R.AUD', 'SFX.MIX'), \ ('67_1070R.AUD', 'SFX.MIX'), \ ('67_1080R.AUD', 'SFX.MIX'), \ ('67_1100R.AUD', 'SFX.MIX'), \ ('67_1140R.AUD', 'SFX.MIX'), \ ('67_1160R.AUD', 'SFX.MIX') \ ] SUPPORTED_MIX_INPUT_FOR_TRX_EXPORT_FILES = ['STARTUP.MIX'] # 15 TRx files SUPPORTED_EXPORTED_TRx_FILES = ['CLUES.TR','ACTORS.TR','CRIMES.TR','CLUETYPE.TR','KIA.TR','SPINDEST.TR','VK.TR','OPTIONS.TR','DLGMENU.TR','ENDCRED.TR','HELP.TR','SCORERS.TR','KIACRED.TR','ERRORMSG.TR','AUTOSAVE.TR'] SUPPORTED_PLACEHOLDER_VQA_ENGLISH_FILES = [ ('WSTLGO_', 'Westwood Studios Partnership Intro'), ('BRLOGO_', 'Blade Runner Logo')] SUPPORTED_PLACEHOLDER_VQA_LOCALIZED_FILES = [ ('INTRO_', 'Intro cutscene - Prologue'), ('MW_A_', 'Eisenduller murder scene'), ('MW_B01_', 'Act 3 intro part - Start part'), ('MW_B02_', 'Act 3 intro part - Lucy is Replicant'), ('MW_B03_', 'Act 3 intro part - Dektora is Replicant'), ('MW_B04_', 'Act 3 intro part - Lucy and Dektora are human'), ('MW_B05_', 'Act 3 intro part - End part '), ('INTRGT_', 'Interrogation scene - Baker, Holloway, McCoy'), ('MW_C01_', 'Clovis and Sadik pay a visit to the Twins - Start Part'), ('MW_C02_', 'Clovis and Sadik pay a visit to the Twins - Clovis has an INCEPT PHOTO'), ('MW_C03_', 'Clovis and Sadik pay a visit to the Twins - Clovis DOES NOT have an INCEPT PHOTO'), ('MW_D_', 'Tyrell, Kolvig and Clovis meet-up at Tyrell Corp'), ('END01A_', 'Underground getaway - Lucy Human'), ('END01B_', 'Underground getaway - Lucy with DNA'), ('END01C_', 'Underground getaway - Lucy not enough DNA'), ('END01D_', 'Underground getaway - Dektora Human'), ('END01E_', 'Underground getaway - Dektora with DNA'), ('END01F_', 'Underground getaway - Dektora, not enough DNA'), ('END03_', 'Underground getaway - McCoy alone'), ('END04A_', 'Finale - McCoy on Moonbus'), ('END04B_', 'Finale - McCoy with Lucy on Moonbus'), ('END04C_', 'Finale - McCoy with Dektora on Moonbus'), ('END06_', 'Underground getaway - Steele Ending') ] SUPPORTED_SPECIAL_POGO_FILE = 'POGO.TR' SUPPORTED_LANGUAGES_DESCRIPTION_CODE_TLIST = [('EN_ANY', 'E', 'English'), ('DE_DEU', 'G', 'German'), ('FR_FRA', 'F', 'French'), ('IT_ITA', 'I', 'Italian'), ('ES_ESP', 'S', 'Spanish'), ('RU_RUS', 'R', 'Russian')] DEFAULT_LANG_DESC_CODE = SUPPORTED_LANGUAGES_DESCRIPTION_CODE_TLIST[0] gTraceModeEnabled = False gActiveLanguageDescriptionCodeTuple = '' gStringReplacementForRootFolderWithExportedFiles = "" gNumReplaceStartingCharacters = 0 gMIXWavFiles = [] gMIXWavFilesNoDups = [] gTLKWavFiles = [] gTLKWavFilesNoDups = [] gActorPropertyEntries = [] #[0]:id, [1]:ShortHand Name [2]:Full Name gActorPropertyEntriesWasInit = False # strFileName should be the full file name (including extension) def calculateFoldHash(strFileName): i = 0 hash = 0 strParam = strFileName.upper() lenFileName = len(strParam); while i < lenFileName and i < 12: groupSum = 0 # work in groups of 4 bytes for j in range(0, 4): # LSB first, so the four letters in the string are re-arranged (first letter goes to lower place) groupSum >>= 8; if (i < lenFileName): groupSum |= (ord(strParam[i]) << 24) i += 1 else: # if i >= lenFileName but still haven't completed the four byte loop add 0s groupSum |= 0 hash = ((hash << 1) | ((hash >> 31) & 1)) + groupSum hash &= 0xFFFFFFFF # mask here! #print (strParam +': ' +''.join('{:08X}'.format(hash))) return hash # Fill the gActorPropertyEntries table def initActorPropertyEntries(thePathToActorNamesTxt): global gActorPropertyEntriesWasInit global gActorPropertyEntries firstLine = True gActorPropertyEntriesWasInit = False #print "[Debug] opening actornames" if thePathToActorNamesTxt is None or not thePathToActorNamesTxt: actorNamesTextFile = u'actornames.txt' relPath = u'.' thePathToActorNamesTxt = os.path.join(relPath, actorNamesTextFile) print "[Warning] Actor names text file %s not found in arguments. Attempting to open local file if it exists" % (thePathToActorNamesTxt) if os.access(thePathToActorNamesTxt, os.F_OK): print "[Info] Actor names text file found: {0}.".format(thePathToActorNamesTxt) with open(thePathToActorNamesTxt) as tsv: if gTraceModeEnabled: print "[Debug] Parsing Override Encoding file info..." for line in csv.reader(tsv, dialect="excel-tab"): #skip first line header if firstLine == True: #if gTraceModeEnabled: # print "[Debug] Skipping Header line in Excel sheet" firstLine = False else: gActorPropertyEntries.append(line) gActorPropertyEntriesWasInit = True tsv.close() else: ## error print "[Error] Actor names text file not found: {0}.".format(thePathToActorNamesTxt) sys.exit(1) # terminate if finding actor names file failed (Blade Runner) def getActorShortNameById(lookupActorId): if not gActorPropertyEntriesWasInit: return '' else: for actorEntryTmp in gActorPropertyEntries: if int(actorEntryTmp[0]) == int(lookupActorId): return actorEntryTmp[1] return '' def getActorFullNameById(lookupActorId): if not gActorPropertyEntriesWasInit: return '' else: for actorEntryTmp in gActorPropertyEntries: if int(actorEntryTmp[0]) == int(lookupActorId): return actorEntryTmp[2] return '' def getActorIdByShortName(lookupActorShortName): if not gActorPropertyEntriesWasInit: return '' else: for actorEntryTmp in gActorPropertyEntries: if actorEntryTmp[1] == lookupActorShortName: return actorEntryTmp[0].zfill(2) return '' def getActorShortNameAndLocalQuoteIdByAUDHashID(audHashId): actorId = 0 actorShortName = '' actorLocalQuoteId = 0 if not gActorPropertyEntriesWasInit: print "[Error] actor properties were not initialized!" return (actorId, actorShortName, actorLocalQuoteId) for actorEntryTmp in gActorPropertyEntries: if( (audHashId - (int(actorEntryTmp[0]) * 10000) ) >= 0) and ((audHashId - (int(actorEntryTmp[0]) * 10000)) < 10000): actorId = int(actorEntryTmp[0]) actorShortName = actorEntryTmp[1] actorLocalQuoteId = audHashId - (actorId * 10000) return (actorId, actorShortName, actorLocalQuoteId) return (actorId, actorShortName, actorLocalQuoteId) # Aux. Ensure existence of output directory def ensure_dir(directory): try: os.makedirs(directory) except OSError as e: if e.errno != errno.EEXIST: raise # # Reading in the INPUT TLK files and checking all the AUD file properties def inputTLKsExport(inputTLKpath, outputWAVpath, pExportWavFilesMode, pExtractDevCommAndExtraSFXMode): # try to open all TLK, MIX file entries from SUPPORTED_TLK_INPUT_FILES + SUPPORTED_MIX_INPUT_FOR_EXTRA_SPEECH_EXPORT_FILES # then per TLK file # create an output folder in the OUTPUT PATH named TLK## for the 1, 2, 3 TLK and TLKSPCHSFX for the SPCHSFX.TLK # printout: # total entries # total data size # and per entry the # fileID # segment offset # file size if not pExportWavFilesMode and not pExtractDevCommAndExtraSFXMode: return # nothing to do print "[Info] Checking in:\n\t%s\n\tfor TLK or MIX files containing supported speech audio (AUD) to export as WAV to:\n\t%s" % (inputTLKpath, outputWAVpath) inputTLKFilesForSpeechFound = [] inputMIXFilesForSpeechFound = [] # breaking after first for loop yields only the top directory files, which is what we want for (dirpath, dirnames, filenames) in walk(inputTLKpath): for filename in filenames: if pExportWavFilesMode: for tlkTuple in (SUPPORTED_TLK_INPUT_FILES): if filename.upper() == tlkTuple[0]: inputTLKFilesForSpeechFound.append(tlkTuple) if pExtractDevCommAndExtraSFXMode: for tlkTuple in SUPPORTED_MIX_INPUT_FOR_EXTRA_SPEECH_EXPORT_FILES: if filename.upper() == tlkTuple[0]: inputMIXFilesForSpeechFound.append(tlkTuple) break if pExportWavFilesMode: if len(inputTLKFilesForSpeechFound) == 0: print "[Error] No valid speech audio files (TLK) were found in the specified input path (-ip switch)" sys.exit(1) if pExtractDevCommAndExtraSFXMode: if len(inputMIXFilesForSpeechFound) == 0: print "[Error] No valid extras speech audio files (MIX) were found in the specified input path (-ip switch)" sys.exit(1) # get the supported AUD files from TLK or MIX archives for tmpTLKorMIXFileTuple in (inputTLKFilesForSpeechFound + inputMIXFilesForSpeechFound): if gTraceModeEnabled: print "[Info] Found supported audio file: %s" % ('"' + inputTLKpath + tmpTLKorMIXFileTuple[0] + '"') errorFound = False inTLKorMIXFile = None fileIsMIX = False #print "[Trace] Type of file is: %s" % (tmpTLKorMIXFileTuple[0][-3:]) if tmpTLKorMIXFileTuple[0][-3:].upper() == 'MIX': fileIsMIX = True print "[Info] Checking MIX file %s for speech audio (-xdevs mode)... " % (tmpTLKorMIXFileTuple[0]) # # Create output folder if not exists at output path if gTraceModeEnabled: print "[Debug] Ensuring output directory %s" % (os.path.join(outputWAVpath, tmpTLKorMIXFileTuple[1] )) ensure_dir(os.path.join(outputWAVpath, tmpTLKorMIXFileTuple[1] ) ) try: inTLKorMIXFile = open(os.path.join(inputTLKpath,tmpTLKorMIXFileTuple[0]), 'rb') except: errorFound = True print "[Error] Unexpected event:", sys.exc_info()[0] raise if not errorFound: tmpBuff = inTLKorMIXFile.read(2) # H: unsigned short (2 bytes) followed by I: unsigned int (4 bytes) tlkFileEntriesNumTuple = struct.unpack('H', tmpBuff) numOfAUDEntriesToExtract = tlkFileEntriesNumTuple[0] tmpBuff = inTLKorMIXFile.read(4) tlkFileDataSegmentSizeTuple = struct.unpack('I', tmpBuff) allTlkFileSize = tlkFileDataSegmentSizeTuple[0] inTLKorMIXFile.seek(0, 2) # go to file end allActualBytesInMixFile = inTLKorMIXFile.tell() inTLKorMIXFile.seek(6, 0) # go to start of table of TLK file entries (right after the 6 bytes header) # 2 + 4 = 6 bytes short MIX header # 12 bytes per TLK entry in entries table # quick size validation if gTraceModeEnabled: print "[Debug] Entries: %d, Data segment size: %d bytes" % (numOfAUDEntriesToExtract, allTlkFileSize) if allActualBytesInMixFile != 2 + 4 + 12 * numOfAUDEntriesToExtract + allTlkFileSize: print "[Error] Audio Archive file size mismatch with reported size in header for %s!" % (tmpTLKorMIXFileTuple[0]) else: # # 12 bytes per entry # 4 bytes: ID # 4 bytes: Offset in data segment # 4 bytes: Size of data # for i in range(0, numOfAUDEntriesToExtract): inTLKorMIXFile.seek(2 + 4 + 12*i) tmpBuff = inTLKorMIXFile.read(4) tmpRdTuple = struct.unpack('I', tmpBuff) idOfAUDEntry = tmpRdTuple[0] tmpBuff = inTLKorMIXFile.read(4) tmpRdTuple = struct.unpack('I', tmpBuff) offsetOfAUDEntry = tmpRdTuple[0] tmpBuff = inTLKorMIXFile.read(4) tmpRdTuple = struct.unpack('I', tmpBuff) sizeOfAUDEntry = tmpRdTuple[0] targetSimpleAudFileName = ''.join('{:08X}'.format(idOfAUDEntry)).upper()+'.AUD' foundSupportedAUDFromMIXFile = False if fileIsMIX: # MIX file like SFX.MIX which has some spoken dialogue #print 'idOfEntry: %d' % (idOfAUDEntry) for suppAUDFromMIXFileName in zip(*SUPPORTED_EXPORTED_AUD_FROM_MIX_FILES)[0]: #print 'checked with: %d' % (calculateFoldHash(suppAUDFromMIXFileName)) if(idOfAUDEntry == calculateFoldHash(suppAUDFromMIXFileName)): foundSupportedAUDFromMIXFile = True targetSimpleAudFileName = suppAUDFromMIXFileName break if foundSupportedAUDFromMIXFile: if gTraceModeEnabled: print "[Debug] Entry: %s, offset: %s, Data segment size: %s bytes" % (''.join('{:08X}'.format(idOfAUDEntry)), ''.join('{:08X}'.format(offsetOfAUDEntry)),''.join('{:08X}'.format(sizeOfAUDEntry))) #print "[Debug] Filename: " + targetSimpleAudFileName else: # TLK file if gTraceModeEnabled: print "[Debug] Entry: %s, offset: %s, Data segment size: %s bytes" % (''.join('{:08X}'.format(idOfAUDEntry)), ''.join('{:08X}'.format(offsetOfAUDEntry)),''.join('{:08X}'.format(sizeOfAUDEntry))) # # put file in AUD object # do we need AUD decode? # create WAV from AUD # write WAV to appropriate output folder # Figure out proper naming for file # then: # AudFile aud; # aud.loadAudFile(fs); (fs is file stream) # aud.export_as_wav(fs, offset, offset + int(sizeof(AudHeader)) + aud.header().size_in, target); # # if foundSupportedAUDFromMIXFile or not fileIsMIX: inTLKorMIXFile.seek(2 + 4 + 12*numOfAUDEntriesToExtract + offsetOfAUDEntry) if(offsetOfAUDEntry + sizeOfAUDEntry > allTlkFileSize): print "[Error] audio file (AUD) file size mismatch with reported size in entry header!" else: targetSimpleWavFileName = targetSimpleAudFileName[:-4] + '.WAV' # remove the .AUD before adding the .WAV if not fileIsMIX: # TLK file (actorID, actorSName, localQuoteId) = getActorShortNameAndLocalQuoteIdByAUDHashID(idOfAUDEntry) targetSimpleWavFileName = actorSName + '_' + str(localQuoteId).zfill(4) + '_' + ''.join('{:08X}'.format(idOfAUDEntry)).upper()+'.WAV' audFileBuffer = inTLKorMIXFile.read(sizeOfAUDEntry) if (len(audFileBuffer) == sizeOfAUDEntry): # load audio file (AUD) file thisAudFile = audFile(gTraceModeEnabled) if (thisAudFile.loadAudFile(audFileBuffer, allTlkFileSize, targetSimpleAudFileName)): if gTraceModeEnabled: print "[Debug] Audio file (AUD) file %s was loaded successfully!" % (targetSimpleAudFileName) #print os.path.join(outputWAVpath, tmpTLKorMIXFileTuple[1], targetSimpleWavFileName) # tmpTLKorMIXFileTuple[1] is the subfolder where the AUD -> WAV files for this archive are written if not os.path.isfile(os.path.join(outputWAVpath, tmpTLKorMIXFileTuple[1], targetSimpleWavFileName) ): thisAudFile.export_as_wav(audFileBuffer, os.path.join(outputWAVpath, tmpTLKorMIXFileTuple[1], targetSimpleWavFileName) ) else: if gTraceModeEnabled: print "[Info] Output file %s already exists. Skipping..." % (os.path.join(outputWAVpath, tmpTLKorMIXFileTuple[1], targetSimpleWavFileName)) else: print "[Warning] Failed to load a proper audio file (AUD) %s (to export it to %s)! Size of input is: %d" % (targetSimpleAudFileName, targetSimpleWavFileName, sizeOfAUDEntry) else: print "[Error] while reading audio file (AUD) file %s into mem buffer" % (targetSimpleAudFileName) #print "[Error] while reading audio file (AUD) file %s into mem buffer" % (''.join('{:08X}'.format(idOfMIXEntry))) inTLKorMIXFile.close() # SYS EXIT IS HERE ONLY FOR DEBUG PURPOSES OF PARSING TLK FILES - SHOULD BE COMMENTED OUT NORMALLY # sys.exit(0) return def appendVQAPlaceHolderSheets(excelOutBook = None): if excelOutBook != None: print "[Info] Appending placeholder sheets for supported video cutscenes (VQA)..." currVQAFileNameLclzd = None for currVQAFileNameDesc in SUPPORTED_PLACEHOLDER_VQA_ENGLISH_FILES + SUPPORTED_PLACEHOLDER_VQA_LOCALIZED_FILES: if currVQAFileNameDesc in SUPPORTED_PLACEHOLDER_VQA_ENGLISH_FILES: currVQAFileNameLclzd = currVQAFileNameDesc[0] + 'E.VQA' elif currVQAFileNameDesc in SUPPORTED_PLACEHOLDER_VQA_LOCALIZED_FILES: currVQAFileNameLclzd = currVQAFileNameDesc[0] + ('%s' % (gActiveLanguageDescriptionCodeTuple[1])) + '.VQA' if currVQAFileNameLclzd is not None: if gTraceModeEnabled: print "[Debug] Creating placeholder sheet %s: %s!" % (currVQAFileNameLclzd, currVQAFileNameDesc[1]) sh = excelOutBook.add_sheet(currVQAFileNameLclzd) # First Row n = 0 # keeps track of rows col_names = ['VQA File: %s' % (currVQAFileNameLclzd), '22050 audio sampling rate', currVQAFileNameDesc[1] ] colIdx = 0 for colNameIt in col_names: sh.write(n, colIdx, colNameIt) colIdx+=1 # Second Row n = 1 col_names = ['Start (YT)', 'End (YT)', 'Subtitle', 'By Actor', 'StartTime', 'Time Diff-SF', 'TimeDiff-SF(ms)', 'TimeDiff-EF', 'TimeDiff-EF(ms)', 'Frame Start', 'Frame End', 'Notes'] colIdx = 0 for colNameIt in col_names: sh.write(n, colIdx, colNameIt) colIdx+=1 #n+=1 return def appendPOGOTextSheet(excelOutBook = None): if excelOutBook != None: print "[Info] Appending POGO sheet..." currTreFileName = '%s%s' % (SUPPORTED_SPECIAL_POGO_FILE, gActiveLanguageDescriptionCodeTuple[1]) # POGO if gTraceModeEnabled: print "[Debug] TR%s file %s was loaded successfully!" % (gActiveLanguageDescriptionCodeTuple[1], currTreFileName) # POGO sh = excelOutBook.add_sheet(currTreFileName) n = 0 # keeps track of rows col1_name = 'Text Resource File: %s' % (currTreFileName) sh.write(n, 0, col1_name) # Second Row n = 1 col1_name = 'TextId' col2_name = 'Text' sh.write(n, 0, col1_name) sh.write(n, 1, col2_name) n+=1 pogoTRInstance = pogoTextResource(gTraceModeEnabled) objUTF8Unicode = None for m, e1 in enumerate(pogoTRInstance.getPogoEntriesList(), n): sh.write(m, 0, e1[0]) objStr = e1[1] try: # We assume utf-8 charset for POGO (since we get the text from a python script) objUTF8Unicode = unicode(objStr, 'utf-8') except Exception as e: print '[Error] Failed to create unicode string: ' + str(e) objUTF8Unicode = unicode("???", 'utf-8') sh.write(m, 1, objUTF8Unicode) # aux: populate a row in extra audio sheets def auxPopulateExtraSpeechAudioRow(sh = None, n = 0, pFilenameStr = '', pTextStr = '', pListOfExtraWavsForDevSheet = None): if sh is None \ or not pFilenameStr \ or pListOfExtraWavsForDevSheet is None \ or len(pListOfExtraWavsForDevSheet) == 0: return False foundMatch = False for dcAUDFilename, dcArchiveMIXFile in SUPPORTED_EXPORTED_AUD_FROM_MIX_FILES: if dcAUDFilename == pFilenameStr: sh.write(n, 0, pFilenameStr) # Filename # We assume utf-8 charset for Audio Commentary and extra text entries (since we get the text from a python script) try: objUTF8Unicode = unicode(pTextStr, 'utf-8') except Exception as e: print '[Error] Failed to create unicode string: ' + str(e) objUTF8Unicode = unicode("???", 'utf-8') sh.write(n, 1, objUTF8Unicode) # Quote sh.write(n, 2, 'N/A') # By Actor sh.write(n, 3, '') # Notes sh.write(n, 4, 'N/A') # To Actor smArchiveMIXFile = '' for smArchiveMIXFile, smArchiveMIXShort in SUPPORTED_MIX_INPUT_FOR_EXTRA_SPEECH_EXPORT_FILES: if smArchiveMIXFile == dcArchiveMIXFile: sh.write(n, 5, smArchiveMIXShort) # Resource - proper break for extraWavFDS in pListOfExtraWavsForDevSheet: twoTokensOfRelDirnameAndFilenameXLS = extraWavFDS.split('&', 2) if len(twoTokensOfRelDirnameAndFilenameXLS) == 3: tokensOfFilename = twoTokensOfRelDirnameAndFilenameXLS[0].split('#', 3) #ShortHandFileName if (tokensOfFilename[0][:-4]+'.AUD') in zip(*SUPPORTED_EXPORTED_AUD_FROM_MIX_FILES)[0] \ and (tokensOfFilename[0][:-4]+'.AUD') == dcAUDFilename: shortHandFileName = tokensOfFilename[0] # real path of filename realPathOfFileNameToLink = twoTokensOfRelDirnameAndFilenameXLS[2] # checks if not empty if gStringReplacementForRootFolderWithExportedFiles and gNumReplaceStartingCharacters > 0: realPathOfFileNameToLink = realPathOfFileNameToLink.replace(realPathOfFileNameToLink[:gNumReplaceStartingCharacters], gStringReplacementForRootFolderWithExportedFiles) hyperlinkAudioFormula = 'HYPERLINK("file://%s","%s")' % (realPathOfFileNameToLink, shortHandFileName) sh.write(n, 6, Formula(hyperlinkAudioFormula)) break foundMatch = True break return foundMatch def appendDevCommentarySheet(excelOutBook = None, listOfExtraWavsForDevSheet = None ): if excelOutBook != None \ and listOfExtraWavsForDevSheet != None \ and len(listOfExtraWavsForDevSheet) > 0: objUTF8Unicode = None print "[Info] Appending Developer Commentary sheet..." sh = excelOutBook.add_sheet("DEV AUDIO") devCommentaryTextInstance = devCommentaryText(gTraceModeEnabled) # # # First Sheet n = 0 # keeps track of rows col_names = ["DEVELOPERS' AUDIO COMMENTARY"] colIdx = 0 for colNameIt in col_names: sh.write(n, colIdx, colNameIt) colIdx+=1 n+= 1 col_names = ['Filename', 'Quote', 'By Actor', 'Notes', 'To Actor', 'Resource', 'ShortHandFileName'] colIdx = 0 for colNameIt in col_names: sh.write(n, colIdx, colNameIt) colIdx+=1 n+= 1 # populate audio commentary rows for pFilenameStr, pTextStr in devCommentaryTextInstance.getAudioCommentaryTextEntriesList(): if auxPopulateExtraSpeechAudioRow(sh, n, pFilenameStr, pTextStr, listOfExtraWavsForDevSheet) == True: n+=1 # change row # # # Second Sheet (EXTRA SPEECH) sh = excelOutBook.add_sheet("SPEECH (SFX)") n = 0 # keeps track of rows col_names = ['EXTRA SPEECH (SFX)'] colIdx = 0 for colNameIt in col_names: sh.write(n, colIdx, colNameIt) colIdx+=1 n+= 1 col_names = ['Filename', 'Quote', 'By Actor', 'Notes', 'To Actor', 'Resource', 'ShortHandFileName'] colIdx = 0 for colNameIt in col_names: sh.write(n, colIdx, colNameIt) colIdx+=1 n+= 1 # populate extra speech audio rows for pFilenameStr, pTextStr in devCommentaryTextInstance.getExtraSpeechAudioEntriesList(): if auxPopulateExtraSpeechAudioRow(sh, n, pFilenameStr, pTextStr, listOfExtraWavsForDevSheet) == True: n+=1 # change row # # # Third Sheet sh = excelOutBook.add_sheet("DEV I_SEZ") n = 0 # keeps track of rows col_names = ['I_SEZ QUOTES'] colIdx = 0 for colNameIt in col_names: sh.write(n, colIdx, colNameIt) colIdx+=1 n+= 1 col_names = ['AssignedID', 'Quote', 'By Actor', 'Notes', 'To Actor', 'Resource', 'ShortHandFileName'] colIdx = 0 for colNameIt in col_names: sh.write(n, colIdx, colNameIt) colIdx+=1 n+= 1 for idStr, textStr in devCommentaryTextInstance.getISEZTextEntriesList(): sh.write(n, 0, idStr) # We assume utf-8 charset for I_SEZ Commentary (since we get the text from a python script) try: objUTF8Unicode = unicode(textStr, 'utf-8') except Exception as e: print '[Error] Failed to create unicode string: ' + str(e) objUTF8Unicode = unicode("???", 'utf-8') sh.write(n, 1, objUTF8Unicode) sh.write(n, 2, 'N/A') # by actor sh.write(n, 3, '') # notes sh.write(n, 4, 'N/A') # to actor sh.write(n, 5, 'N/A') # Resources sh.write(n, 6, 'N/A') # ShortHandFilename and URL n+=1 return def inputMIXExtractTREs(inputMIXpath, excelOutBook = None): print "[Info] Checking in %s for MIX files to extract Text Resources (TR%ss) from..." % (inputMIXpath, gActiveLanguageDescriptionCodeTuple[1]) inputMIXFilesFound = [] objUTF8Unicode = None # breaking after first for loop yields only the top directory files, which is what we want for (dirpath, dirnames, filenames) in walk(inputMIXpath): for filename in filenames: for mixFileName in SUPPORTED_MIX_INPUT_FOR_TRX_EXPORT_FILES: if filename.upper() == mixFileName: inputMIXFilesFound.append(mixFileName) break if len(inputMIXFilesFound) == 0: print "[Error] No supported game archive resource files (MIX) were found in the specified input path (-ip switch)" sys.exit(1) for tmpMIXfileName in inputMIXFilesFound: if gTraceModeEnabled: print "[Info] Found MIX file: %s" % ('"' + tmpMIXfileName + '"') errorFound = False inMIXFile = None # try: inMIXFile = open(os.path.join(inputMIXpath,tmpMIXfileName), 'rb') except: errorFound = True print "[Error] Unexpected event:", sys.exc_info()[0] raise if not errorFound: totalTREs = 0 tmpBuff = inMIXFile.read(2) # H: unsigned short (2 bytes) followed by I: unsigned int (4 bytes) mixFileEntriesNumTuple = struct.unpack('H', tmpBuff) numOfTREEntriesToExtract = mixFileEntriesNumTuple[0] tmpBuff = inMIXFile.read(4) mixFileDataSegmentSizeTuple = struct.unpack('I', tmpBuff) allMixFileSize = mixFileDataSegmentSizeTuple[0] inMIXFile.seek(0, 2) # go to file end allActualBytesInMixFile = inMIXFile.tell() inMIXFile.seek(6, 0) # go to start of table of MIX file entries (right after the 6 bytes header) # 2 + 4 = 6 bytes short MIX header # 12 bytes per MIX entry in entries table # quick size validation if gTraceModeEnabled: print "[Debug] Entries: %d, Data segment size: %d bytes" % (numOfTREEntriesToExtract, allMixFileSize) if allActualBytesInMixFile != 2 + 4 + 12 * numOfTREEntriesToExtract + allMixFileSize: print "[Error] MIX file size mismatch with reported size in header for %s!" % (tmpMIXfileName) else: # # 12 bytes per entry # 4 bytes: ID # 4 bytes: Offset in data segment # 4 bytes: Size of data # for i in range(0, numOfTREEntriesToExtract): foundTREFile = False currTreFileName = 'UNKNOWN.TR%s' % (gActiveLanguageDescriptionCodeTuple[1]) inMIXFile.seek(2 + 4 + 12*i) tmpBuff = inMIXFile.read(4) tmpRdTuple = struct.unpack('I', tmpBuff) idOfMIXEntry = tmpRdTuple[0] tmpBuff = inMIXFile.read(4) tmpRdTuple = struct.unpack('I', tmpBuff) offsetOfMIXEntry = tmpRdTuple[0] tmpBuff = inMIXFile.read(4) tmpRdTuple = struct.unpack('I', tmpBuff) sizeOfMIXEntry = tmpRdTuple[0] for suppTREFileName in SUPPORTED_EXPORTED_TRx_FILES: suppTREFileName = suppTREFileName + gActiveLanguageDescriptionCodeTuple[1] if(idOfMIXEntry == calculateFoldHash(suppTREFileName)): foundTREFile = True currTreFileName = suppTREFileName break if (foundTREFile == True): if gTraceModeEnabled: print "[Debug] Entry Name: %s, Entry ID: %s, offset: %s, Data segment size: %s bytes" % (currTreFileName, ''.join('{:08X}'.format(idOfMIXEntry)), ''.join('{:08X}'.format(offsetOfMIXEntry)),''.join('{:08X}'.format(sizeOfMIXEntry))) # # IF TRE FILE: # put file in TRE object # # inMIXFile.seek(2 + 4 + 12*numOfTREEntriesToExtract + offsetOfMIXEntry) if(offsetOfMIXEntry + sizeOfMIXEntry > allMixFileSize): print "[Error] TR%s file size mismatch with reported size in entry header!" % (gActiveLanguageDescriptionCodeTuple[1]) else: treFileBuffer = inMIXFile.read(sizeOfMIXEntry) if (len(treFileBuffer) == sizeOfMIXEntry): # load TRE file thisTreFile = treFile(gTraceModeEnabled) if (thisTreFile.loadTreFile(treFileBuffer, allMixFileSize, currTreFileName)): if gTraceModeEnabled: print "[Debug] TR%s file %s was loaded successfully!" % (gActiveLanguageDescriptionCodeTuple[1], currTreFileName) if excelOutBook != None: sh = excelOutBook.add_sheet(currTreFileName) n = 0 # keeps track of rows col1_name = 'Text Resource File: %s' % (currTreFileName) sh.write(n, 0, col1_name) # Second Row n = 1 col1_name = 'TextId' col2_name = 'Text' sh.write(n, 0, col1_name) sh.write(n, 1, col2_name) n+=1 for m, e1 in enumerate(thisTreFile.stringEntriesLst, n): sh.write(m, 0, e1[0]) objStr = e1[1] if ( currTreFileName[:-1] == 'ERRORMSG.TR'): # ERRORMSG.TRx uses different font than the others # and there are conflicts (with the other in-game fonts) # Supposedly these messages would be printed by an OS font # They seem to mostly use the Latin-1 codepage, although there are a few # characters that don't encode right (but they do with a IBM437 like the rest of the TRx # Probably, these are forgotten (buggy) characters from a previous plan (print error messages with in-game font?) # So we treat them as bugs and fix them. # Further proof that they are buggy, there are language versions that have two codes for the same character # in ERRORMSG TRx. # Eg. in Spanish version they use \xa2 (correct with IBM437) and \xf3 (correct with Latin-1) for the 'ó' character # or in French version 'è' uses both \x8a and \xe8 objEncodeSafeStr = "" for i in range(0, len(objStr)): # ENG version has no buggy character exceptions # GER version has no buggy character exceptions # FRA version if (objStr[i] == '\x82'): # FRA - ERRORMSG.TRF objEncodeSafeStr += '\xe9' # fixing buggy character 'é' elif (objStr[i] == '\x85'): # FRA - ERRORMSG.TRF (also used in ITA ERRORMSG.TRI - same glyph) objEncodeSafeStr += '\xe0' # fixing buggy character 'à' elif (objStr[i] == '\x8a'): # FRA - ERRORMSG.TRF (also used in ITA ERRORMSG.TRI - same glyph) objEncodeSafeStr += '\xe8' # fixing buggy character 'è' # ITA version #elif (objStr[i] == '\x85'): # ITA - ERRORMSG.TRI [commented out: already covered in FRA check above] # objEncodeSafeStr += '\xe0' # buggy - fixed above #elif (objStr[i] == '\x8a'): # ITA - ERRORMSG.TRI [commented out: already covered in FRA check above] # objEncodeSafeStr += '\xe8' # buggy - fixed above elif (objStr[i] == '\x97'): # ITA - ERRORMSG.TRI objEncodeSafeStr += '\xf9' # fixing buggy character 'ù' # ESP version elif (objStr[i] == '\xa2'): # ESP - ERRORMSG.TRS objEncodeSafeStr += '\xf3' # fixing buggy character 'ó' else: objEncodeSafeStr += objStr[i] # try: objUTF8Unicode = unicode(objEncodeSafeStr, 'latin-1') except Exception as e: print '[Error] Failed to create unicode string: ' + str(e) objUTF8Unicode = unicode("???", 'latin-1') else: # all the other official game TRx use the KIA6PT.FON # There could be variances of the KIA6PT.FON per Blade Runner version # TODO: For now, we assume that there aren't significant variances that warrant a more elaborate approach # TODO: Tested for EN, DEU, FRA, ITA, ESP. Pending testing for Russian version. try: objUTF8Unicode = unicode(objStr, 'cp437') except Exception as e: print '[Error] Failed to create unicode string: ' + str(e) objUTF8Unicode = unicode("???", 'cp437') sh.write(m, 1, objUTF8Unicode) #for tupleIdString in thisTreFile.stringEntriesLst: # #print "[Debug] Id: %d\t Text: %s" % (tupleIdString[0], tupleIdString[1]) # pass totalTREs = totalTREs + 1 else: print "[Error] while loading TR%s %s file!" % (gActiveLanguageDescriptionCodeTuple[1], currTreFileName) else: print "[Error] while reading TR%s file %s into mem buffer" % (gActiveLanguageDescriptionCodeTuple[1], currTreFileName) #print "[Error] while reading TR%s file %s into mem buffer" % (gActiveLanguageDescriptionCodeTuple[1], ''.join('{:08X}'.format(idOfMIXEntry))) inMIXFile.close() print "[Info] Total TR%ss processed: %d " % (gActiveLanguageDescriptionCodeTuple[1], totalTREs) return # # Creating the OUTPUT XLS file with one sheet named as the @param sheet with entries based on the # listTlkWavs (wav files from TLK files, without duplicates) # and /or # listDevsWavs (extra (supported) wav files practicaclly from the SFX.MIX file), without duplicates) # def outputXLS(filename, sheet, listTlkWavs, listDevsWavs, parseTREResourcesAlso = False, addDevCommAndExtraSFXSheetEnabled = False, addPOGOTextSheetEnabled = False, mixInputFolderPath = ''): global gStringReplacementForRootFolderWithExportedFiles global gNumReplaceStartingCharacters book = xlwt.Workbook() sh = book.add_sheet(sheet) # First Row n = 0 # keeps track of rows #variables = [x, y, z] #x_desc = 'Display' #y_desc = 'Dominance' #z_desc = 'Test' #desc = [x_desc, y_desc, z_desc] # # # You may need to group the variables together #for n, (v_desc, v) in enumerate(zip(desc, variables)): #sh.write(n, 0, v_desc) #sh.write(n, 1, v) col1_name = 'BladeRunnerTLK In-Game dialogue / voiceover quotes' sh.write(n, 0, col1_name) # Second Row n = 1 col1_name = 'Filename' col2_name = 'Quote' col3_name = 'By Actor' col4_name = 'Notes' col5_name = 'To Actor' col6_name = 'Resource' col7_name = 'ShortHandFileName' sh.write(n, 0, col1_name) sh.write(n, 1, col2_name) sh.write(n, 2, col3_name) sh.write(n, 3, col4_name) sh.write(n, 4, col5_name) sh.write(n, 5, col6_name) sh.write(n, 6, col7_name) n+=1 for m, e1 in enumerate(listTlkWavs, n): twoTokensOfRelDirnameAndFilenameXLS = e1.split('&', 2) if len(twoTokensOfRelDirnameAndFilenameXLS) == 3: fourTokensOfFilename = twoTokensOfRelDirnameAndFilenameXLS[0].split('#', 3) if len(fourTokensOfFilename) == 4: # fix rogue _ chars in 3rd token of filename (split at '_') tmpAUDFileName = fourTokensOfFilename[0] + '-' + fourTokensOfFilename[1] + '.AUD' #ActorId-QuoteId.AUD sh.write(m, 0, tmpAUDFileName) twoTokensOfQuote = fourTokensOfFilename[2].split('-', 1) if len(twoTokensOfQuote) == 2: #Quote sh.write(m, 1, twoTokensOfQuote[1]) else: #Quote sh.write(m, 1, fourTokensOfFilename[2]) #Notes sh.write(m, 3, 'TODO') #byActor sh.write(m, 2, fourTokensOfFilename[3]) #ResourceFolder sh.write(m, 5, twoTokensOfRelDirnameAndFilenameXLS[1]) #ShortHandFileName tmpActorShortHand = getActorShortNameById(fourTokensOfFilename[0]) shortHandFileName = tmpActorShortHand + '_' + fourTokensOfFilename[1] + '_' + fourTokensOfFilename[2] + '.WAV' # real path of filename realPathOfFileNameToLink = twoTokensOfRelDirnameAndFilenameXLS[2] # checks if not empty if gStringReplacementForRootFolderWithExportedFiles and gNumReplaceStartingCharacters > 0: realPathOfFileNameToLink = realPathOfFileNameToLink.replace(realPathOfFileNameToLink[:gNumReplaceStartingCharacters], gStringReplacementForRootFolderWithExportedFiles) #works in Linux + Libreoffice # also works in Windows + LibreOffice (run from msys) -- tried something like: # python sortBladeRunnerWavs.py -op /g/WORKSPACE/BladeRunnerWorkspace/br-mixer-master/data/WAV -m "G:/WORKSPACE/BladeRunnerWorkspace/br-mixer-master/data/WAV" # put real full path for each file as FILE URL, and real (or approximate shorthand file name as alias) hyperlinkAudioFormula = 'HYPERLINK("file://%s","%s")' % (realPathOfFileNameToLink, shortHandFileName) sh.write(m, 6, Formula(hyperlinkAudioFormula)) else: sh.write(m, 0, e1) #Notes sh.write(m, 3, 'error') else: sh.write(m, 0, e1) #Notes sh.write(m, 3, 'error') # works for filenames where some rogue greek characters exist #sh.write(m, 0, str.decode("%s" % e1, 'utf-8')) #for m, e2 in enumerate(list2, n+1): # sh.write(m, 1, e2) appendVQAPlaceHolderSheets(book) if parseTREResourcesAlso == True and mixInputFolderPath != '': inputMIXExtractTREs(mixInputFolderPath, book) if addPOGOTextSheetEnabled == True: appendPOGOTextSheet(book) if addDevCommAndExtraSFXSheetEnabled == True: appendDevCommentarySheet(book, listDevsWavs) try: book.save(filename) print "[Info] Done." except Exception as e: print "[Error] Could not save the output Excel file:: " + str(e) # # Aux function to weed out duplicates from wav file lists # valid listTypes: 'TLKWAVS', 'EXTRAMIXWAVS' def wavDupsRemove(listOfWavFiles, listType): if listType != 'TLKWAVS' and listType != 'EXTRAMIXWAVS': print '[Error] Invalid listtype %s specified to remove duplicate wav files' % (listType) sys.exit(1) listOfWavFilesNoDups = [] for filenameSrcTmp in listOfWavFiles: duplicateFound = False #print "[Debug]Converted %s" % (filenameSrcTmp) # Weed out duplicates by copying to another table (quick and dirty) twoTokensOfRelDirnameAndFilenameSrc = filenameSrcTmp.split('&', 2) tmpRelDirNameSrc = twoTokensOfRelDirnameAndFilenameSrc[1] # eg. TLK1 keyForDuplicateSearchSrc = None keyForDuplicateSearchTarg = None if (listType == 'EXTRAMIXWAVS'): keyForDuplicateSearchSrc = twoTokensOfRelDirnameAndFilenameSrc[0] else: threeTokensOfQuoteFilenameSrc = twoTokensOfRelDirnameAndFilenameSrc[0].split('#', 2) #concatenate actorID and quoteID for search key keyForDuplicateSearchSrc = threeTokensOfQuoteFilenameSrc[0] + threeTokensOfQuoteFilenameSrc[1] for fileTargIdx, filenameTargTmp in enumerate(listOfWavFilesNoDups): twoTokensOfRelDirnameAndFilenameTarg = filenameTargTmp.split('&', 2) tmpRelDirNameTarg = twoTokensOfRelDirnameAndFilenameTarg[1] if (listType == 'EXTRAMIXWAVS'): keyForDuplicateSearchTarg = twoTokensOfRelDirnameAndFilenameTarg[0] else: threeTokensOfQuoteFilenameTarg = twoTokensOfRelDirnameAndFilenameTarg[0].split('#', 2) #concatenate actorID and quoteID for search key keyForDuplicateSearchTarg = threeTokensOfQuoteFilenameTarg[0] + threeTokensOfQuoteFilenameTarg[1] if(keyForDuplicateSearchSrc == keyForDuplicateSearchTarg): #print "[Debug] Found duplicate %s" % (filenameSrcTmp) duplicateFound = True listOfWavFilesNoDups[fileTargIdx] = twoTokensOfRelDirnameAndFilenameTarg[0] + '&' + tmpRelDirNameSrc + ',' + tmpRelDirNameTarg + '&' + twoTokensOfRelDirnameAndFilenameTarg[2] break if(duplicateFound == False): listOfWavFilesNoDups.append(filenameSrcTmp) #for filenameSrcTmp in listOfWavFilesNoDups: # print "[Debug] Unique %s" % (filenameSrcTmp) return listOfWavFilesNoDups # # Aux function to validate input language description # def getLanguageDescCodeTuple(candidateLangDescriptionStr): if (candidateLangDescriptionStr is None or not candidateLangDescriptionStr ): resultTuple = DEFAULT_LANG_DESC_CODE else: tmpMatchTuplesList = [ (x,y,z) for (x,y,z) in SUPPORTED_LANGUAGES_DESCRIPTION_CODE_TLIST if x == candidateLangDescriptionStr] if tmpMatchTuplesList is not None and len(tmpMatchTuplesList) > 0: resultTuple = tmpMatchTuplesList[0] else: resultTuple = None return resultTuple def printInfoMessageForLanguageSelectionSyntax(): tmpCSVSupportedLangDescValues = ", ".join( zip(*SUPPORTED_LANGUAGES_DESCRIPTION_CODE_TLIST)[0] ) print "Valid values for language selection are: %s" % (tmpCSVSupportedLangDescValues) print "Default value is: %s (%s)" % (DEFAULT_LANG_DESC_CODE[0], DEFAULT_LANG_DESC_CODE[2]) return # # # # ######################## # main def main(argsCL): # TODO parse arguments using argparse? https://docs.python.org/3/library/argparse.html#module-argparse global gTraceModeEnabled global gStringReplacementForRootFolderWithExportedFiles global gNumReplaceStartingCharacters global gActiveLanguageDescriptionCodeTuple global gMIXWavFiles global gMIXWavFilesNoDups global gTLKWavFiles global gTLKWavFilesNoDups gTraceModeEnabled = False gActiveLanguageDescriptionCodeTuple = DEFAULT_LANG_DESC_CODE pathToActorNamesTxt = "" candidateLangDescriptionTxt = "" TMProotFolderWithExportedFiles = "" TMProotFolderWithInputTLKFiles = "" exportWavFilesMode = False extractTreFilesMode = False extractDevCommAndExtraSFXMode = False extractPOGOTextMode = False invalidSyntax = False print "Running %s (%s)..." % (APP_NAME_SPACED, APP_VERSION) #print "Len of sysargv = %s" % (len(argsCL)) if len(argsCL) == 2: if(argsCL[1] == '--help'or argsCL[1] == '-h'): print "%s %s supports Westwood's Blade Runner PC Game (1997)." % (APP_NAME_SPACED, APP_VERSION) print APP_SHORT_DESC print "Created by Praetorian of the classic adventures in Greek team." print "Always keep backups!" print "--------------------" print "%s takes has one mandatory argument, ie. the folder of the exported WAV files:" % (APP_WRAPPER_NAME) print "Valid syntax: %s -op folderpath_for_exported_wav_Files [-ip folderpath_for_TLK_Files] [-ian path_to_actornames_txt] [-m stringPathToReplaceFolderpathInExcelLinks] [-ld gameInputLanguageDescription] [-xwav] [-xtre] [-xdevs] [-xpogo] [--trace]" % (APP_WRAPPER_NAME) print "The -op switch has an argument that is the path for exported WAV files folder. The -op switch is REQUIRED always." print "The -ip switch has an argument that is the path for the input (TLK or MIX) files folder (can be the same as the Blade Runner installation folder)." print "The -ian switch is followed by the path to actornames.txt, if it's not in the current working directory." print "The -m switch has an argument that is a replacement string for the path to the folder of exported WAV files which will be used as a prefix for the links in the output XLS file." print "The -ld switch has an argument that is the language description of the original game files that you use as input." printInfoMessageForLanguageSelectionSyntax() print "The -xwav switch enables the WAV audio export mode from the TLK files. It requires an INPUT path to be set with the -ip switch." print "The -xtre switch enables the TRx parsing mode from the original MIX files. It requires an INPUT path to be set with the -ip switch." print "The -xdevs switch will add a sheet for Developer Commentary text and some additional voice-overs from SFX.MIX." print "The -xpogo switch will add a sheet for the POGO text." print "The --trace switch enables more debug messages being printed during execution." print "--------------------" print "If the app finishes successfully, a file named %s-(language)%s will be created in the current working folder." % (OUTPUT_XLS_FILENAME, OUTPUT_XLS_FILENAME_EXT) print "--------------------" print "Thank you for using this app." print "Please provide any feedback to: %s " % (COMPANY_EMAIL) sys.exit() elif(argsCL[1] == '--version' or argsCL[1] == '-v'): print "%s %s supports Westwood's Blade Runner PC Game (1997)." % (APP_NAME_SPACED, APP_VERSION) print "Please provide any feedback to: %s " % (COMPANY_EMAIL) sys.exit() else: invalidSyntax = True elif len(argsCL) > 2: for i in range(1, len(argsCL)): if( i < (len(argsCL) - 1) and argsCL[i][:1] == '-' and argsCL[i+1][:1] != '-'): if (argsCL[i] == '-op'): TMProotFolderWithExportedFiles = argsCL[i+1] gNumReplaceStartingCharacters = len(TMProotFolderWithExportedFiles) elif (argsCL[i] == '-ip'): TMProotFolderWithInputTLKFiles = argsCL[i+1] elif (argsCL[i] == '-m'): gStringReplacementForRootFolderWithExportedFiles = argsCL[i+1] elif (argsCL[i] == '-ian'): pathToActorNamesTxt = argsCL[i+1] elif (argsCL[i] == '-ld'): candidateLangDescriptionTxt = argsCL[i+1] elif (argsCL[i] == '-xwav'): print "[Info] Export WAVs from TLK files mode enabled (only missing files will be exported)." exportWavFilesMode = True elif (argsCL[i] == '-xtre'): print "[Info] Extract Text Resources (TRx) mode enabled." extractTreFilesMode = True elif (argsCL[i] == '-xdevs'): print "[Info] Additional Sheet for developer commentary and extra SFX mode enabled." extractDevCommAndExtraSFXMode = True elif (argsCL[i] == '-xpogo'): print "[Info] Additional Sheet for POGO text mode enabled." extractPOGOTextMode = True elif argsCL[i] == '--trace': print "[Info] Trace mode enabled (more debug messages)." gTraceModeEnabled = True elif argsCL[i][:1] == '-': invalidSyntax = True break if (not exportWavFilesMode): print "[Info] Export WAVs from TLK files mode disabled." if (not extractTreFilesMode): print "[Info] Extract Text Resources (TRx) mode disabled." if (not extractDevCommAndExtraSFXMode): print "[Info] Additional Sheet for developer commentary and extra SFX mode disabled." if (not extractPOGOTextMode): print "[Info] Additional Sheet for POGO text mode disabled." if not TMProotFolderWithExportedFiles: # this argument is mandatory print "[Error] The output path for exported files (-op switch) argument is mandatory!" invalidSyntax = True if (not invalidSyntax) \ and (exportWavFilesMode == True or extractDevCommAndExtraSFXMode == True or extractTreFilesMode == True) \ and (TMProotFolderWithInputTLKFiles == ''): print "[Error] No game input path (-ip switch) specified, while any of the export audio to WAV mode (-xwav), the export extras (-xdevs) or the extract Text Resources mode (-xtre) is enabled." invalidSyntax = True if (not invalidSyntax) \ and (exportWavFilesMode == False and extractDevCommAndExtraSFXMode == False and extractTreFilesMode == False) \ and (TMProotFolderWithInputTLKFiles != ''): print "[Warning] Specified game input path (-ip switch) will be ignored, since the export audio to WAV mode (-xwav), the export extras (-xdevs) and the extract Text Resources mode (-xtre) are disabled." # not invalid syntax though gActiveLanguageDescriptionCodeTuple = getLanguageDescCodeTuple(candidateLangDescriptionTxt) if (not invalidSyntax) and gActiveLanguageDescriptionCodeTuple is None: print "[Error] Invalid language code was specified" printInfoMessageForLanguageSelectionSyntax() invalidSyntax = True if not invalidSyntax: print "[Info] Game Language Selected: %s (%s)" % (gActiveLanguageDescriptionCodeTuple[0], gActiveLanguageDescriptionCodeTuple[2]) # parse Actors files: initActorPropertyEntries(pathToActorNamesTxt) #for actorEntryTmp in gActorPropertyEntries: # print "[Debug] Found actor: %s %s %s" % (actorEntryTmp[0], actorEntryTmp[1], actorEntryTmp[2]) # # Early checks for invalid cases # 1. if TMProotFolderWithInputTLKFiles is not valid and -xtre or -xwav -> error! # use os.path.isdir? # # 2. if not -wav and -op path is invalid then empty INGQUOT sheet -> error # 3 [We check for this further bellow, before call for outputXLS]. if not -wav and -op path is empty (has no WAVs) then empty INGQUOT sheet -> Treat as an error case! if ((exportWavFilesMode == True or extractDevCommAndExtraSFXMode == True or extractTreFilesMode == True) and (not os.path.isdir(TMProotFolderWithInputTLKFiles))): print "[Error] Invalid game input path (-ip switch) was specified, while the export audio to WAV mode (-xwav), export extras (-xdevs) or the extract Text Resources mode (-xtre) is enabled." sys.exit(1) if ((exportWavFilesMode == False and extractDevCommAndExtraSFXMode == False) and (not os.path.isdir(TMProotFolderWithExportedFiles))): print "[Error] Invalid output path for exported files (-op switch) was specified, while the export audio to WAV mode (-xwav) and export extras (-xdevs) are disabled (if enabled, it would create the path)." sys.exit(1) # # Checking for the optional case of parsing the input TLK files to export to WAV # if TMProotFolderWithInputTLKFiles != '': if (exportWavFilesMode == True or extractDevCommAndExtraSFXMode == True): inputTLKsExport(TMProotFolderWithInputTLKFiles, TMProotFolderWithExportedFiles, exportWavFilesMode, extractDevCommAndExtraSFXMode) #if (extractTreFilesMode == True): # inputMIXExtractTREs(TMProotFolderWithInputTLKFiles) # # Parsing the exported WAV files # NO: depending on the mode don't parse the irrellevant WAVs? print "[Info] Parsing the (exported) WAV audio files in the -op path.\n\tPlease wait (this could take a while)..." # The broad search here is intentional for (dirpath, dirnames, filenames) in walk(TMProotFolderWithExportedFiles): for nameIdx, nameTmp in enumerate(filenames): if (len(nameTmp) > 4) \ and nameTmp.upper()[-4:] == '.WAV' \ and len(nameTmp.split('.', 1)) == 2: # radical heal (but practical) - don't allow cases like .AUD.WAV or any intermediate dots in the filename! relDirName = '' # os.path.split would Split the pathname path into a pair, # (head, tail) where tail is the last pathname component and head is everything leading up to that. The tail part will never contain a slash pathTokens = dirpath.split(os.path.sep) idxOfLastTLKToken = -1 idxOfLastSFXToken = -1 for tokenIdx, pTokenTmp in enumerate(pathTokens): posOfTLKWithinRightmostToken = pTokenTmp.find("TLK") posOfSFXWithinRightmostToken = pTokenTmp.find("SFX") if posOfTLKWithinRightmostToken != -1: idxOfLastTLKToken = tokenIdx if posOfSFXWithinRightmostToken != -1: idxOfLastSFXToken = tokenIdx if (posOfTLKWithinRightmostToken != -1 \ or posOfSFXWithinRightmostToken != -1): relDirName = pTokenTmp # don't break, we are interested in right-most matching token in the path #if (nameTmp.upper()[:-4] + '.AUD') in zip(*SUPPORTED_EXPORTED_AUD_FROM_MIX_FILES)[0]: # print '[Trace] 1 file: %s' % (nameTmp.upper()) # print 'TLKss %d vs SFXss:%d ' % (idxOfLastTLKToken, idxOfLastSFXToken) if (idxOfLastSFXToken > idxOfLastTLKToken \ and (nameTmp.upper()[:-4] + '.AUD') in zip(*SUPPORTED_EXPORTED_AUD_FROM_MIX_FILES)[0]): gMIXWavFiles.append(filenames[nameIdx] +'&' + relDirName + '&' + os.path.join(dirpath, nameTmp)) else: gTLKWavFiles.append(filenames[nameIdx] +'&' + relDirName + '&' + os.path.join(dirpath, nameTmp)) #break for fileIdx, filenameTmp in enumerate(gTLKWavFiles): twoTokensOfFilenameAndRelDirname = filenameTmp.split('&', 1) if len(twoTokensOfFilenameAndRelDirname) != 2: print "[Error] While attempting filename and relative dirname split: %s" % (filenameTmp) sys.exit(0) # # Remove WAV extension here # AUD file from TLK archive threeTokensOfFilename = twoTokensOfFilenameAndRelDirname[0][:-4].split('_', 2) if len(threeTokensOfFilename) == 3: # fix rogue _ chars in 3rd token of filename (split at '_') threeTokensOfFilename[2] = threeTokensOfFilename[2].replace("_", "-") # Replace first token # replace actor name shorthand with ActorID in first part tmpActorId = getActorIdByShortName(threeTokensOfFilename[0]) tmpActorFullName = '' if(tmpActorId != '' and tmpActorId is not None): tmpActorFullName = getActorFullNameById(tmpActorId) if(tmpActorFullName != '' and tmpActorFullName is not None): threeTokensOfFilename[0] = tmpActorId.zfill(2) threeTokensOfFilename.append(tmpActorFullName) else: #fatal error if something cannot convert to spot it immediately print "[Error] in actorIdMatch match: %s %s" % (tmpActorId, twoTokensOfFilenameAndRelDirname[0][:-4]) sys.exit(0) else: #fatal error if something cannot convert to spot it immediately print "[Error] in shorthand match: %s %s" % (threeTokensOfFilename[0], twoTokensOfFilenameAndRelDirname[0][:-4]) sys.exit(0) # # #foundMatchForActorShortHand = False #for actorEntryTmp in gActorPropertyEntries: # if actorEntryTmp[1] == threeTokensOfFilename[0]: # threeTokensOfFilename[0] = actorEntryTmp[0].zfill(2) # threeTokensOfFilename.append(actorEntryTmp[2]) # foundMatchForActorShortHand = True # break # end of replace actor name shorthand tmpHealedSimpleFilename = '#'.join(threeTokensOfFilename) filenameTmp = tmpHealedSimpleFilename + '&' + twoTokensOfFilenameAndRelDirname[1] gTLKWavFiles[fileIdx] = filenameTmp else: print "[Error] in splitting tokens on _: %s" % (filenameTmp) sys.exit(0) # # Now handle the extra speech files (-xdevs mode) for fileIdx, filenameTmp in enumerate(gMIXWavFiles): # # if it's a AUD file from a MIX archive, we treat it differently here # (we will put it in another sheet) twoTokensOfFilenameAndRelDirname = filenameTmp.split('&', 1) if len(twoTokensOfFilenameAndRelDirname) != 2: print "[Error] While attempting filename and relative dirname split on extra speech file: %s" % (filenameTmp) sys.exit(0) if (gTraceModeEnabled): print "[Debug] No healing action needed on: %s" % (twoTokensOfFilenameAndRelDirname[0]) #sort in-place # # gTLKWavFiles.sort() gMIXWavFiles.sort() # # # Remove duplicates from TLK AUD speech files (-xwav mode) # gTLKWavFilesNoDups = wavDupsRemove(gTLKWavFiles, 'TLKWAVS') if len(gTLKWavFilesNoDups) == 0: print "[Error] No supported speech audio files (WAV) were found in the output folder path (-op switch)." sys.exit(1) # # Remove duplicates from extra speech files (-xdevs mode) # gMIXWavFilesNoDups = wavDupsRemove(gMIXWavFiles, 'EXTRAMIXWAVS') if extractDevCommAndExtraSFXMode and len(gMIXWavFilesNoDups) == 0: print "[Error] No supported extra speech audio files (WAV) were found in the output folder path (-op switch)." sys.exit(1) constructedOutputFilename = "%s-%s%s" % (OUTPUT_XLS_FILENAME, gActiveLanguageDescriptionCodeTuple[2], OUTPUT_XLS_FILENAME_EXT) print "[Info] Creating output excel %s file..." % (constructedOutputFilename) outputXLS(constructedOutputFilename, OUTPUT_XLS_QUOTES_SHEET + gActiveLanguageDescriptionCodeTuple[1] + '.TR' + gActiveLanguageDescriptionCodeTuple[1], gTLKWavFilesNoDups, gMIXWavFilesNoDups, extractTreFilesMode, extractDevCommAndExtraSFXMode, extractPOGOTextMode, TMProotFolderWithInputTLKFiles) else: invalidSyntax = True if invalidSyntax == True: print "[Error] Invalid syntax!\n Try: \n %s --help for more info \n %s --version for version info " % (APP_WRAPPER_NAME, APP_WRAPPER_NAME) print "Valid syntax: %s -op folderpath_for_exported_wav_Files [-ip folderpath_for_TLK_Files] [-ian path_to_actornames_txt] [-m stringPathToReplaceFolderpathInExcelLinks] [-ld gameInputLanguageDescription] [-xwav] [-xtre] [-xdevs] [-xpogo] [--trace]" % (APP_WRAPPER_NAME) print "\nDetected arguments:" tmpi = 0 for tmpArg in argsCL: if tmpi==0: #skip first argument tmpi+=1 continue print "Argument: %s" % (tmpArg) tmpi+=1 # 00_0000 -- DealsInInsects dupl TLK01, TLK0A # 00_0510 -- ThinkingOfChangingJobs-Leon dupl TLK02, TLK03 # 00-8520 -- WhatDoYouKnow dupl TLK01, TLK0A # Total unique quotes seems to be 5495! # TODO rename files in folders to conform to the underscore '_' and '-' format (a few don't -- let's have them all conforming!) # # ######################### # if __name__ == '__main__': main(sys.argv[0:]) else: ## debug #print '[Debug] %s was imported from another module' % (APP_WRAPPER_NAME) pass