2021-02-12 21:57:06 +00:00
#!/usr/bin/env python3
2022-04-29 19:15:00 +00:00
import argparse
import json
import os
import signal
import time
import multiprocessing
2023-05-06 21:31:30 +00:00
from pathlib import Path
2022-01-17 00:43:07 +00:00
2021-05-23 00:00:10 +00:00
2021-05-26 23:40:48 +00:00
EXTRACTED_ASSETS_NAMEFILE = " .extracted-assets.json "
2021-11-09 01:51:45 +00:00
2021-05-26 23:40:48 +00:00
def SignalHandler ( sig , frame ) :
print ( f ' Signal { sig } received. Aborting... ' )
mainAbort . set ( )
# Don't exit immediately to update the extracted assets file.
2020-03-17 04:31:30 +00:00
2021-05-30 15:09:59 +00:00
def ExtractFile ( xmlPath , outputPath , outputSourcePath ) :
2021-05-23 00:00:10 +00:00
if globalAbort . is_set ( ) :
# Don't extract if another file wasn't extracted properly.
return
2023-05-06 21:31:30 +00:00
zapdPath = Path ( " tools " ) / " ZAPD " / " ZAPD.out "
configPath = Path ( " tools " ) / " ZAPDConfigs " / " MqDbg " / " Config.xml "
Path ( outputPath ) . mkdir ( parents = True , exist_ok = True )
Path ( outputSourcePath ) . mkdir ( parents = True , exist_ok = True )
2024-03-01 21:12:22 +00:00
execStr = f " { zapdPath } e -eh -i { xmlPath } -b baseroms/gc-eu-mq-dbg/segments -o { outputPath } -osf { outputSourcePath } -gsf 1 -rconf { configPath } --cs-float both { ZAPDArgs } "
2022-01-17 00:43:07 +00:00
2021-10-17 11:32:09 +00:00
if " overlays " in xmlPath :
execStr + = " --static "
2022-01-23 23:09:02 +00:00
2021-05-23 00:00:10 +00:00
if globalUnaccounted :
2022-01-17 00:43:07 +00:00
execStr + = " -Wunaccounted "
2020-03-17 04:31:30 +00:00
2021-05-23 00:00:10 +00: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 " )
2020-03-17 04:31:30 +00:00
2020-12-26 11:39:52 +00:00
def ExtractFunc ( fullPath ) :
2021-05-23 00:00:10 +00:00
* pathList , xmlName = fullPath . split ( os . sep )
objectName = os . path . splitext ( xmlName ) [ 0 ]
outPath = os . path . join ( " assets " , * pathList [ 2 : ] , objectName )
outSourcePath = outPath
2020-03-17 04:31:30 +00:00
2021-07-28 02:16:03 +00: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.
return
2021-05-26 23:40:48 +00:00
2021-05-28 04:03:47 +00:00
currentTimeStamp = int ( time . time ( ) )
2021-05-23 00:00:10 +00:00
2021-05-30 15:09:59 +00:00
ExtractFile ( fullPath , outPath , outSourcePath )
2021-05-23 00:00:10 +00:00
2021-05-26 23:40:48 +00: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
2021-07-28 02:16:03 +00:00
def initializeWorker ( abort , unaccounted : bool , extractedAssetsTracker : dict , manager ) :
2021-05-23 00:00:10 +00:00
global globalAbort
global globalUnaccounted
2021-05-26 23:40:48 +00:00
global globalExtractedAssetsTracker
global globalManager
2021-05-23 00:00:10 +00:00
globalAbort = abort
globalUnaccounted = unaccounted
2021-05-26 23:40:48 +00:00
globalExtractedAssetsTracker = extractedAssetsTracker
globalManager = manager
2020-05-26 16:53:53 +00:00
2022-01-17 00:43:07 +00:00
def processZAPDArgs ( argsZ ) :
badZAPDArg = False
for z in argsZ :
if z [ 0 ] == ' - ' :
2022-04-29 19:15:00 +00:00
print ( f ' error: argument " { z } " starts with " - " , which is not supported. ' , file = os . sys . stderr )
2022-01-17 00:43:07 +00:00
badZAPDArg = True
if badZAPDArg :
exit ( 1 )
ZAPDArgs = " " . join ( f " - { z } " for z in argsZ )
print ( " Using extra ZAPD arguments: " + ZAPDArgs )
return ZAPDArgs
2020-12-28 23:37:52 +00:00
def main ( ) :
2021-02-12 21:57:06 +00:00
parser = argparse . ArgumentParser ( description = " baserom asset extractor " )
parser . add_argument ( " -s " , " --single " , help = " asset path relative to assets/, e.g. objects/gameplay_keep " )
2024-02-06 01:40:31 +00:00
parser . add_argument ( " -f " , " --force " , help = " Force the extraction of every xml instead of checking the touched ones (overwriting current files). " , action = " store_true " )
2022-01-17 00:43:07 +00:00
parser . add_argument ( " -j " , " --jobs " , help = " Number of cpu cores to extract with. " )
2021-05-23 00:00:10 +00:00
parser . add_argument ( " -u " , " --unaccounted " , help = " Enables ZAPD unaccounted detector warning system. " , action = " store_true " )
2022-01-17 00:43:07 +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-02-12 21:57:06 +00:00
args = parser . parse_args ( )
2022-01-17 00:43:07 +00:00
global ZAPDArgs
ZAPDArgs = processZAPDArgs ( args . Z ) if args . Z else " "
2021-05-26 23:40:48 +00:00
global mainAbort
2022-01-17 00:43:07 +00:00
mainAbort = multiprocessing . Event ( )
manager = multiprocessing . Manager ( )
2021-05-26 23:40:48 +00:00
signal . signal ( signal . SIGINT , SignalHandler )
extractedAssetsTracker = manager . dict ( )
2021-07-28 02:16:03 +00:00
if os . path . exists ( EXTRACTED_ASSETS_NAMEFILE ) and not args . force :
2021-05-30 15:09:59 +00:00
with open ( EXTRACTED_ASSETS_NAMEFILE , encoding = ' utf-8 ' ) as f :
2021-05-26 23:40:48 +00:00
extractedAssetsTracker . update ( json . load ( f , object_hook = manager . dict ) )
2021-05-23 00:00:10 +00:00
2021-02-12 21:57:06 +00:00
asset_path = args . single
if asset_path is not None :
2021-05-23 00:00:10 +00:00
fullPath = os . path . join ( " assets " , " xml " , asset_path + " .xml " )
2021-07-28 02:16:03 +00:00
if not os . path . exists ( fullPath ) :
2022-01-17 00:43:07 +00:00
print ( f " Error. File { fullPath } does not exist. " , file = os . sys . stderr )
2021-07-28 02:16:03 +00:00
exit ( 1 )
initializeWorker ( mainAbort , args . unaccounted , extractedAssetsTracker , manager )
# Always extract if -s is used.
if fullPath in extractedAssetsTracker :
del extractedAssetsTracker [ fullPath ]
2021-05-23 00:00:10 +00:00
ExtractFunc ( fullPath )
2021-02-12 21:57:06 +00:00
else :
xmlFiles = [ ]
2021-05-26 23:40:48 +00:00
for currentPath , _ , files in os . walk ( os . path . join ( " assets " , " xml " ) ) :
2021-05-23 00:00:10 +00:00
for file in files :
fullPath = os . path . join ( currentPath , file )
if file . endswith ( " .xml " ) :
xmlFiles . append ( fullPath )
2021-02-12 21:57:06 +00:00
2023-05-06 21:31:30 +00:00
class CannotMultiprocessError ( Exception ) :
pass
2021-08-30 00:19:52 +00:00
try :
2022-01-17 00:43:07 +00:00
numCores = int ( args . jobs or 0 )
if numCores < = 0 :
numCores = 1
print ( " Extracting assets with " + str ( numCores ) + " CPU core " + ( " s " if numCores > 1 else " " ) + " . " )
2023-05-06 21:31:30 +00:00
try :
mp_context = multiprocessing . get_context ( " fork " )
except ValueError as e :
raise CannotMultiprocessError ( ) from e
with mp_context . Pool ( numCores , initializer = initializeWorker , initargs = ( mainAbort , args . unaccounted , extractedAssetsTracker , manager ) ) as p :
2021-08-30 00:19:52 +00:00
p . map ( ExtractFunc , xmlFiles )
2023-05-06 21:31:30 +00:00
except ( multiprocessing . ProcessError , TypeError , CannotMultiprocessError ) :
2021-08-30 00:19:52 +00:00
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-23 00:00:10 +00:00
2021-05-26 23:40:48 +00: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-23 00:00:10 +00:00
exit ( 1 )
2020-12-28 23:37:52 +00:00
if __name__ == " __main__ " :
2022-01-17 00:43:07 +00:00
main ( )