2021-05-13 21:00:38 -04:00
#!/usr/bin/env python3
2021-12-28 01:07:41 +00:00
import argparse , json , os , signal , time , colorama , multiprocessing
2021-05-13 21:00:38 -04:00
2021-12-28 01:07:41 +00:00
colorama . init ()
2021-12-16 04:05:29 +00:00
2021-07-28 21:55:02 -04:00
EXTRACTED_ASSETS_NAMEFILE = ".extracted-assets.json"
def SignalHandler ( sig , frame ):
print ( f 'Signal { sig } received. Aborting...' )
mainAbort . set ()
# Don't exit immediately to update the extracted assets file.
2021-05-13 21:00:38 -04:00
2021-08-04 04:21:31 +01:00
def ExtractFile ( xmlPath , outputPath , outputSourcePath ):
2021-05-13 21:00:38 -04:00
if globalAbort . is_set ():
# Don't extract if another file wasn't extracted properly.
return
2021-12-16 04:05:29 +00:00
execStr = f "tools/ZAPD/ZAPD.out e -eh -i { xmlPath } -b baserom/ -o { outputPath } -osf { outputSourcePath } -gsf 1 -rconf tools/ZAPDConfigs/MM/Config.xml { ZAPDArgs } "
2021-08-04 04:21:31 +01:00
2021-05-13 21:00:38 -04:00
if globalUnaccounted :
2021-12-16 04:05:29 +00:00
execStr += " -Wunaccounted"
2021-05-13 21:00:38 -04:00
print ( execStr )
exitValue = os . system ( execStr )
if exitValue != 0 :
globalAbort . set ()
print ( " \n " )
print ( "Error when extracting from file " + xmlPath , file = os . sys . stderr )
print ( "Aborting..." , file = os . sys . stderr )
print ( " \n " )
def ExtractFunc ( fullPath ):
* pathList , xmlName = fullPath . split ( os . sep )
objectName = os . path . splitext ( xmlName )[ 0 ]
2021-07-28 21:55:02 -04:00
if "scenes" in pathList :
outPath = os . path . join ( "assets" , * pathList [ 2 :])
else :
outPath = os . path . join ( "assets" , * pathList [ 2 :], objectName )
2021-05-13 21:00:38 -04:00
outSourcePath = outPath
2021-07-28 21:55:02 -04:00
if fullPath in globalExtractedAssetsTracker :
timestamp = globalExtractedAssetsTracker [ fullPath ][ "timestamp" ]
modificationTime = int ( os . path . getmtime ( fullPath ))
if modificationTime < timestamp :
# XML has not been modified since last extraction.
2021-05-13 21:00:38 -04:00
return
2021-07-28 21:55:02 -04:00
currentTimeStamp = int ( time . time ())
2021-08-04 04:21:31 +01:00
ExtractFile ( fullPath , outPath , outSourcePath )
2021-05-13 21:00:38 -04:00
2021-07-28 21:55:02 -04:00
if not globalAbort . is_set ():
# Only update timestamp on succesful extractions
if fullPath not in globalExtractedAssetsTracker :
globalExtractedAssetsTracker [ fullPath ] = globalManager . dict ()
globalExtractedAssetsTracker [ fullPath ][ "timestamp" ] = currentTimeStamp
def initializeWorker ( abort , unaccounted : bool , extractedAssetsTracker : dict , manager ):
2021-05-13 21:00:38 -04:00
global globalAbort
global globalUnaccounted
2021-07-28 21:55:02 -04:00
global globalExtractedAssetsTracker
global globalManager
2021-05-13 21:00:38 -04:00
globalAbort = abort
globalUnaccounted = unaccounted
2021-07-28 21:55:02 -04:00
globalExtractedAssetsTracker = extractedAssetsTracker
globalManager = manager
2021-05-13 21:00:38 -04:00
def main ():
parser = argparse . ArgumentParser ( description = "baserom asset extractor" )
parser . add_argument ( "-s" , "--single" , help = "asset path relative to assets/, e.g. objects/gameplay_keep" )
parser . add_argument ( "-f" , "--force" , help = "Force the extraction of every xml instead of checking the touched ones." , action = "store_true" )
2021-12-28 01:07:41 +00:00
parser . add_argument ( "-j" , "--jobs" , help = "Number of cpu cores to extract with." )
2021-05-13 21:00:38 -04:00
parser . add_argument ( "-u" , "--unaccounted" , help = "Enables ZAPD unaccounted detector warning system." , action = "store_true" )
2021-12-16 04:05:29 +00:00
parser . add_argument ( "-Z" , help = "Pass the argument on to ZAPD, e.g. `-ZWunaccounted` to warn about unaccounted blocks in XMLs. Each argument should be passed separately, *without* the leading dash." , metavar = "ZAPD_ARG" , action = "append" )
2021-05-13 21:00:38 -04:00
args = parser . parse_args ()
2021-12-28 01:07:41 +00:00
global ZAPDArgs
ZAPDArgs = ""
2021-12-16 04:05:29 +00:00
if args . Z is not None :
2021-12-28 01:07:41 +00:00
badZAPDArg = False
2021-12-16 04:05:29 +00:00
for i in range ( len ( args . Z )):
z = args . Z [ i ]
if z [ 0 ] == '-' :
2021-12-28 01:07:41 +00:00
print ( f " { colorama . Fore . LIGHTRED_EX } error { colorama . Fore . RESET } : argument \" { z } \" starts with \" - \" , which is not supported." , file = os . sys . stderr )
badZAPDArg = True
2021-12-16 04:05:29 +00:00
else :
2021-12-28 01:07:41 +00:00
args . Z [ i ] = "-" + z
2021-12-16 04:05:29 +00:00
if badZAPDArg :
2021-12-28 01:07:41 +00:00
exit ( 1 )
2021-12-16 04:05:29 +00:00
2021-12-28 01:07:41 +00:00
ZAPDArgs = " " . join ( args . Z )
print ( "Using extra ZAPD arguments: " + ZAPDArgs )
2021-12-16 04:05:29 +00:00
2021-07-28 21:55:02 -04:00
global mainAbort
2021-12-28 01:07:41 +00:00
mainAbort = multiprocessing . Event ()
manager = multiprocessing . Manager ()
2021-07-28 21:55:02 -04:00
signal . signal ( signal . SIGINT , SignalHandler )
extractedAssetsTracker = manager . dict ()
if os . path . exists ( EXTRACTED_ASSETS_NAMEFILE ) and not args . force :
with open ( EXTRACTED_ASSETS_NAMEFILE , encoding = 'utf-8' ) as f :
extractedAssetsTracker . update ( json . load ( f , object_hook = manager . dict ))
2021-05-13 21:00:38 -04:00
asset_path = args . single
if asset_path is not None :
fullPath = os . path . join ( "assets" , "xml" , asset_path + ".xml" )
2021-07-28 21:55:02 -04:00
if not os . path . exists ( fullPath ):
2021-12-16 04:05:29 +00:00
print ( f "Error. File { fullPath } does not exist." , file = os . sys . stderr )
2021-07-28 21:55:02 -04:00
exit ( 1 )
initializeWorker ( mainAbort , args . unaccounted , extractedAssetsTracker , manager )
# Always extract if -s is used.
if fullPath in extractedAssetsTracker :
del extractedAssetsTracker [ fullPath ]
2021-05-13 21:00:38 -04:00
ExtractFunc ( fullPath )
else :
xmlFiles = []
2021-07-28 21:55:02 -04:00
for currentPath , _ , files in os . walk ( os . path . join ( "assets" , "xml" )):
2021-05-13 21:00:38 -04:00
for file in files :
fullPath = os . path . join ( currentPath , file )
if file . endswith ( ".xml" ):
xmlFiles . append ( fullPath )
2021-12-28 01:07:41 +00:00
try :
numCores = int ( args . jobs or 0 )
if numCores <= 0 :
numCores = 1
print ( "Extracting assets with " + str ( numCores ) + " CPU core" + ( "s" if numCores > 1 else "" ) + "." )
with multiprocessing . get_context ( "fork" ) . Pool ( numCores , initializer = initializeWorker , initargs = ( mainAbort , args . unaccounted , extractedAssetsTracker , manager )) as p :
p . map ( ExtractFunc , xmlFiles )
except ( multiprocessing . ProcessError , TypeError ):
print ( "Warning: Multiprocessing exception ocurred." , file = os . sys . stderr )
print ( "Disabling mutliprocessing." , file = os . sys . stderr )
initializeWorker ( mainAbort , args . unaccounted , extractedAssetsTracker , manager )
for singlePath in xmlFiles :
ExtractFunc ( singlePath )
2021-05-13 21:00:38 -04:00
2021-07-28 21:55:02 -04:00
with open ( EXTRACTED_ASSETS_NAMEFILE , 'w' , encoding = 'utf-8' ) as f :
serializableDict = dict ()
for xml , data in extractedAssetsTracker . items ():
serializableDict [ xml ] = dict ( data )
json . dump ( dict ( serializableDict ), f , ensure_ascii = False , indent = 4 )
if mainAbort . is_set ():
2021-05-13 21:00:38 -04:00
exit ( 1 )
if __name__ == "__main__" :
main ()