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
2024-06-24 13:22:39 +00:00
from tools import version_config
2021-05-26 23:40:48 +00:00
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
2024-06-24 13:22:39 +00:00
def ExtractFile ( assetConfig : version_config . AssetConfig , outputPath : Path , outputSourcePath : Path ) :
2024-08-08 04:27:02 +00:00
name = assetConfig . name
2024-06-24 13:22:39 +00:00
xmlPath = assetConfig . xml_path
version = globalVersionConfig . version
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 "
2024-06-24 13:22:39 +00:00
configPath = Path ( " tools " ) / " ZAPDConfigs " / version / " Config.xml "
outputPath . mkdir ( parents = True , exist_ok = True )
outputSourcePath . mkdir ( parents = True , exist_ok = True )
2024-09-04 18:49:16 +00:00
execStr = f " { zapdPath } e -eh -i { xmlPath } -b { globalBaseromSegmentsDir } -o { outputPath } -osf { outputSourcePath } -gsf 1 -rconf { configPath } --cs-float both { ZAPDArgs } "
2023-05-06 21:31:30 +00:00
2024-09-07 18:37:15 +00:00
if name . startswith ( " code/ " ) or name . startswith ( " n64dd/ " ) or name . startswith ( " overlays/ " ) :
2024-06-24 13:22:39 +00:00
assert assetConfig . start_offset is not None
assert assetConfig . end_offset is not None
2023-05-06 21:31:30 +00:00
2024-06-24 13:22:39 +00:00
execStr + = f " --start-offset 0x { assetConfig . start_offset : X } "
execStr + = f " --end-offset 0x { assetConfig . end_offset : X } "
2022-01-17 00:43:07 +00:00
2024-08-08 04:27:02 +00:00
if name . startswith ( " overlays/ " ) :
overlayName = name . split ( " / " ) [ 1 ]
2024-06-24 13:22:39 +00:00
baseAddress = globalVersionConfig . dmadata_segments [ overlayName ] . vram + assetConfig . start_offset
execStr + = f " --base-address 0x { baseAddress : X } "
2021-10-17 11:32:09 +00:00
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 " )
2024-07-17 22:13:39 +00:00
print ( f " Error when extracting from file { xmlPath } " , file = os . sys . stderr )
2021-05-23 00:00:10 +00:00
print ( " Aborting... " , file = os . sys . stderr )
print ( " \n " )
2020-03-17 04:31:30 +00:00
2024-06-24 13:22:39 +00:00
def ExtractFunc ( assetConfig : version_config . AssetConfig ) :
objectName = assetConfig . name
xml_path = assetConfig . xml_path
xml_path_str = str ( xml_path )
2021-05-23 00:00:10 +00:00
2024-09-04 18:49:16 +00:00
outPath = globalOutputDir / objectName
2021-05-23 00:00:10 +00:00
outSourcePath = outPath
2020-03-17 04:31:30 +00:00
2024-06-24 13:22:39 +00:00
if xml_path_str in globalExtractedAssetsTracker :
timestamp = globalExtractedAssetsTracker [ xml_path_str ] [ " timestamp " ]
modificationTime = int ( os . path . getmtime ( xml_path ) )
2021-07-28 02:16:03 +00:00
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
2024-06-24 13:22:39 +00:00
ExtractFile ( assetConfig , outPath , outSourcePath )
2021-05-23 00:00:10 +00:00
2021-05-26 23:40:48 +00:00
if not globalAbort . is_set ( ) :
2024-09-26 04:21:00 +00:00
# Only update timestamp on successful extractions
2024-06-24 13:22:39 +00:00
if xml_path_str not in globalExtractedAssetsTracker :
globalExtractedAssetsTracker [ xml_path_str ] = globalManager . dict ( )
globalExtractedAssetsTracker [ xml_path_str ] [ " timestamp " ] = currentTimeStamp
2021-05-26 23:40:48 +00:00
2024-09-04 18:49:16 +00:00
def initializeWorker ( versionConfig : version_config . VersionConfig , abort , unaccounted : bool , extractedAssetsTracker : dict , manager , baseromSegmentsDir : Path , outputDir : Path ) :
2024-06-24 13:22:39 +00:00
global globalVersionConfig
2021-05-23 00:00:10 +00:00
global globalAbort
global globalUnaccounted
2021-05-26 23:40:48 +00:00
global globalExtractedAssetsTracker
global globalManager
2024-09-04 18:49:16 +00:00
global globalBaseromSegmentsDir
global globalOutputDir
2024-06-24 13:22:39 +00:00
globalVersionConfig = versionConfig
2021-05-23 00:00:10 +00:00
globalAbort = abort
globalUnaccounted = unaccounted
2021-05-26 23:40:48 +00:00
globalExtractedAssetsTracker = extractedAssetsTracker
globalManager = manager
2024-09-04 18:49:16 +00:00
globalBaseromSegmentsDir = baseromSegmentsDir
globalOutputDir = outputDir
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 " )
2024-09-04 18:49:16 +00:00
parser . add_argument (
" baserom_segments_dir " ,
type = Path ,
help = " Directory of uncompressed ROM segments " ,
)
parser . add_argument (
" output_dir " ,
type = Path ,
help = " Output directory to place files in " ,
)
parser . add_argument ( " -v " , " --version " , dest = " oot_version " , help = " OOT game version " , default = " gc-eu-mq-dbg " )
2024-06-24 13:22:39 +00:00
parser . add_argument ( " -s " , " --single " , help = " Extract a single asset by name, 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 ( )
2024-09-04 18:49:16 +00:00
baseromSegmentsDir : Path = args . baserom_segments_dir
2024-06-24 13:22:39 +00:00
version : str = args . oot_version
2024-09-04 18:49:16 +00:00
outputDir : Path = args . output_dir
2024-09-11 13:51:53 +00:00
args . output_dir . mkdir ( parents = True , exist_ok = True )
2024-06-24 13:22:39 +00:00
versionConfig = version_config . load_version_config ( version )
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 )
2024-09-04 18:49:16 +00:00
extraction_times_p = outputDir / " assets_extraction_times.json "
2021-05-26 23:40:48 +00:00
extractedAssetsTracker = manager . dict ( )
2024-06-24 13:22:39 +00:00
if extraction_times_p . exists ( ) and not args . force :
with extraction_times_p . open ( 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
2024-06-24 13:22:39 +00:00
singleAssetName = args . single
if singleAssetName is not None :
assetConfig = None
for asset in versionConfig . assets :
if asset . name == singleAssetName :
assetConfig = asset
break
else :
print ( f " Error. Asset { singleAssetName } not found in config. " , file = os . sys . stderr )
2021-07-28 02:16:03 +00:00
exit ( 1 )
2024-09-04 18:49:16 +00:00
initializeWorker ( versionConfig , mainAbort , args . unaccounted , extractedAssetsTracker , manager , baseromSegmentsDir , outputDir )
2021-07-28 02:16:03 +00:00
# Always extract if -s is used.
2024-06-26 14:30:55 +00:00
xml_path_str = str ( assetConfig . xml_path )
if xml_path_str in extractedAssetsTracker :
del extractedAssetsTracker [ xml_path_str ]
2024-06-24 13:22:39 +00:00
ExtractFunc ( assetConfig )
2021-02-12 21:57:06 +00:00
else :
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
2024-09-04 18:49:16 +00:00
with mp_context . Pool ( numCores , initializer = initializeWorker , initargs = ( versionConfig , mainAbort , args . unaccounted , extractedAssetsTracker , manager , baseromSegmentsDir , outputDir ) ) as p :
2024-06-24 13:22:39 +00:00
p . map ( ExtractFunc , versionConfig . assets )
2023-05-06 21:31:30 +00:00
except ( multiprocessing . ProcessError , TypeError , CannotMultiprocessError ) :
2024-09-26 04:21:00 +00:00
print ( " Warning: Multiprocessing exception occurred. " , file = os . sys . stderr )
2021-08-30 00:19:52 +00:00
print ( " Disabling mutliprocessing. " , file = os . sys . stderr )
2024-09-04 18:49:16 +00:00
initializeWorker ( versionConfig , mainAbort , args . unaccounted , extractedAssetsTracker , manager , baseromSegmentsDir , outputDir )
2024-06-24 13:22:39 +00:00
for assetConfig in versionConfig . assets :
ExtractFunc ( assetConfig )
2021-05-23 00:00:10 +00:00
2024-06-24 13:22:39 +00:00
with extraction_times_p . open ( ' w ' , encoding = ' utf-8 ' ) as f :
2021-05-26 23:40:48 +00:00
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 ( )