2023-03-14 18:05:18 +01:00
#!/usr/bin/env python3
2023-04-12 22:31:37 +02:00
from configparser import ConfigParser
2023-03-14 18:05:18 +01:00
import logging
2023-04-12 22:31:37 +02:00
from pathlib import Path
2023-03-14 18:05:18 +01:00
2023-04-13 09:34:32 +02:00
__version__ = " 0.0.20 "
2023-03-14 18:05:18 +01:00
__author__ = " rigodron, algoflash, GGLinnk, CrystalPixel "
__license__ = " MIT "
__status__ = " developpement "
2023-04-12 22:31:37 +02:00
def align ( offset : int , align : int ) :
2023-03-14 18:05:18 +01:00
"""
2023-04-12 22:31:37 +02:00
Give the upper rounded offset aligned using the align value .
input : offset = int
input : align = int
return offset = int
2023-03-14 18:05:18 +01:00
"""
2023-04-12 22:31:37 +02:00
if offset % align == 0 : return offset
return offset + align - ( offset % align )
class MotFile :
2023-04-13 09:34:32 +02:00
" Unpack and pack groups of motions in the motFile. "
2023-04-12 22:31:37 +02:00
__groups_offsets = None
__GROUPS_HEADER_LEN = 0x40
__TOTAL_HEADER_ALIGN = 0x20
__MOTION_FILE_ALIGN = 0x20
def unpack ( self , motfile_path : Path , folder_path : Path ) :
2023-04-13 09:34:32 +02:00
"""
Unpack read the global groups_header and each group_header to unpack group and theirs motions in theirs folders .
* groups_header has a fixed length of 0x20
* each group_header is - 1 terminated and the all header block is aligned to 0x20
* each motion file is aligned to 0x20
"""
2023-04-12 22:31:37 +02:00
logging . info ( f " Unpacking { motfile_path } in { folder_path } ... " )
2023-04-13 09:34:32 +02:00
self . __groups_offsets = [ ]
2023-04-12 22:31:37 +02:00
with motfile_path . open ( " rb " ) as motfile_file :
for _ in range ( self . __GROUPS_HEADER_LEN / / 4 ) :
2023-04-13 09:34:32 +02:00
group_header_offset = int . from_bytes ( motfile_file . read ( 4 ) , " big " )
self . __groups_offsets . append ( group_header_offset )
2023-04-12 22:31:37 +02:00
folder_path . mkdir ( parents = True )
logging . debug ( f " Total of groups: { len ( self . __groups_offsets ) : 02 } " )
2023-04-13 09:34:32 +02:00
for group_index , group_header_offset in enumerate ( self . __groups_offsets ) :
# For each non-empty group we create theirs folder with theirs 2 digits index name.
if group_header_offset == 0 :
2023-04-12 22:31:37 +02:00
continue
2023-04-13 08:04:45 +02:00
group_path = folder_path / f " { group_index : 02 } "
group_path . mkdir ( )
2023-04-12 22:31:37 +02:00
2023-04-13 09:34:32 +02:00
# Now we read the group_header and put all motions offsets in a list.
2023-04-12 22:31:37 +02:00
motions_offsets = [ ]
2023-04-13 09:34:32 +02:00
motfile_file . seek ( group_header_offset )
2023-04-12 22:31:37 +02:00
last_motion_offset = int . from_bytes ( motfile_file . read ( 4 ) , " big " )
while last_motion_offset != - 1 :
motions_offsets . append ( last_motion_offset )
last_motion_offset = int . from_bytes ( motfile_file . read ( 4 ) , " big " , signed = True )
for motion_index , motion_offset in enumerate ( motions_offsets ) :
logging . debug ( f " [unpacking] group: { group_index : 02 } motion: { motion_index : 04 } " )
2023-04-13 09:34:32 +02:00
# Now we extract each motion at theirs offsets for the current group.
# We just create an empty file for null offsets to keep ending empty motion offsets in the group_header for repack.
2023-04-12 22:31:37 +02:00
new_motion_path = ( group_path / f " { motion_index : 04 } " ) . with_suffix ( " .mot " )
if motion_offset == 0 :
# We create an empty file.
new_motion_path . touch ( )
continue
2023-04-13 09:34:32 +02:00
# The first uint32 of motion is the motion total length
2023-04-12 22:31:37 +02:00
motfile_file . seek ( motion_offset )
motionfile_len = int . from_bytes ( motfile_file . read ( 4 ) , " big " )
motfile_file . seek ( motion_offset )
new_motion_path . write_bytes ( motfile_file . read ( motionfile_len ) )
def pack ( self , folder_path : Path , motfile_path : Path ) :
2023-04-13 09:34:32 +02:00
" Pack create the header and then pack files following to it. "
2023-04-12 22:31:37 +02:00
logging . info ( f " Packing { folder_path } in { motfile_path } ... " )
2023-04-13 09:34:32 +02:00
# At first we have to count motions for each groups for creating and add length of -1 to the end of each group_header for aligning the header to 0x20 and get the first file offset.
# Then with the first file offset we can populate each group_header
2023-04-12 22:31:37 +02:00
2023-04-13 09:34:32 +02:00
# groups_count is the last folder index because groups_header can contains empty offsets.
2023-04-13 08:04:45 +02:00
groups_count = int ( list ( folder_path . glob ( " * " ) ) [ - 1 ] . name ) + 1
2023-04-12 22:31:37 +02:00
group_motion_len_list = [ [ ] ] * groups_count
2023-03-14 18:05:18 +01:00
2023-04-13 09:34:32 +02:00
# group_motion_len_list contains a list of groups with a list of motions in each and the length of motion for each [group_index][motion_index].
# groups are initialized with empty list to track empty groups using len()
2023-04-12 22:31:37 +02:00
for group_index in range ( groups_count ) :
2023-04-13 09:34:32 +02:00
# test if the group is empty
2023-04-13 08:04:45 +02:00
if not ( folder_path / f " { group_index : 02 } " ) . is_dir ( ) :
continue
2023-04-12 22:31:37 +02:00
motions_count = len ( list ( ( folder_path / f " { group_index : 02 } " ) . glob ( " * " ) ) )
if motions_count == 0 :
continue
group_motion_len_list [ group_index ] = [ None ] * motions_count
for motion_index in range ( motions_count ) :
group_motion_len_list [ group_index ] [ motion_index ] = ( folder_path / f " { group_index : 02 } " / f " { motion_index : 04 } " ) . with_suffix ( " .mot " ) . stat ( ) . st_size
logging . debug ( f " group: { group_index : 02 } motion: { motion_index : 04 } len: { group_motion_len_list [ group_index ] [ motion_index ] : 08x } " )
2023-04-13 09:34:32 +02:00
# We create groups_header with 0x20 fixed length.
2023-04-12 22:31:37 +02:00
current_offset = self . __GROUPS_HEADER_LEN
motfile_headers_data = b " "
for group_index , group in enumerate ( group_motion_len_list ) :
if len ( group ) == 0 :
motfile_headers_data + = b " \x00 \x00 \x00 \x00 "
continue
motfile_headers_data + = current_offset . to_bytes ( 4 , " big " )
current_offset + = len ( group_motion_len_list [ group_index ] ) * 4 + 4
2023-03-14 18:05:18 +01:00
2023-04-12 22:31:37 +02:00
current_offset = align ( current_offset , self . __TOTAL_HEADER_ALIGN )
motfile_headers_data + = b " \x00 " * ( self . __GROUPS_HEADER_LEN - len ( motfile_headers_data ) )
# current_offset point now to the first file position and motfile_headers_data contains groups header data
with motfile_path . open ( " wb " ) as motfile_file :
for group_index , group in enumerate ( group_motion_len_list ) :
if len ( group ) == 0 :
2023-03-14 18:05:18 +01:00
continue
2023-04-12 22:31:37 +02:00
group_header_data = b " "
for motion_index , motion_len in enumerate ( group ) :
if motion_len == 0 :
group_header_data + = b " \x00 \x00 \x00 \x00 "
continue
group_header_data + = current_offset . to_bytes ( 4 , " big " )
motfile_file . seek ( current_offset )
motfile_file . write ( ( folder_path / f " { group_index : 02 } " / f " { motion_index : 04 } " ) . with_suffix ( " .mot " ) . read_bytes ( ) )
current_offset + = align ( motion_len , self . __MOTION_FILE_ALIGN )
motfile_headers_data + = group_header_data + b " \xff \xff \xff \xff "
motfile_headers_data + = b " \x00 " * ( align ( len ( motfile_headers_data ) , self . __TOTAL_HEADER_ALIGN ) - len ( motfile_headers_data ) )
# current_offset = motfile_len
motfile_file . seek ( 0 )
motfile_file . write ( motfile_headers_data )
motfile_file . seek ( current_offset - 1 )
motfile_file . write ( b " \x00 " )
2023-03-14 18:05:18 +01:00
def get_argparser ( ) :
import argparse
2023-04-12 22:31:37 +02:00
parser = argparse . ArgumentParser ( description = ' Gotcha Force MOT packer & unpacker - [GameCube] v ' + __version__ )
2023-03-14 18:05:18 +01:00
parser . add_argument ( ' --version ' , action = ' version ' , version = ' %(prog)s ' + __version__ )
parser . add_argument ( ' -v ' , ' --verbose ' , action = ' store_true ' , help = ' verbose mode ' )
2023-04-12 22:31:37 +02:00
parser . add_argument ( ' -c ' , ' --charset ' , type = str , help = ' -c=USA: use USA charset when unpacking. ' , default = " " )
parser . add_argument ( ' input_path ' , metavar = ' INPUT ' , help = ' ' )
2023-03-14 18:05:18 +01:00
parser . add_argument ( ' output_path ' , metavar = ' OUTPUT ' , help = ' ' , nargs = ' ? ' , default = " " )
group = parser . add_mutually_exclusive_group ( required = True )
2023-04-12 22:31:37 +02:00
group . add_argument ( ' -p ' , ' --pack ' , action = ' store_true ' , help = " -p source_folder (dest_file.bin): Pack source_folder in new file source_folder.bin or dest_file.bin if specified. " )
group . add_argument ( ' -u ' , ' --unpack ' , action = ' store_true ' , help = " -u source_file.bin (dest_folder): Unpack the motFile file in new folder source_file or dest_folder if specified. " )
2023-03-14 18:05:18 +01:00
return parser
2023-04-12 22:31:37 +02:00
2023-03-14 18:05:18 +01:00
if __name__ == ' __main__ ' :
logging . basicConfig ( format = ' %(levelname)s : %(message)s ' , level = logging . INFO )
args = get_argparser ( ) . parse_args ( )
p_input = Path ( args . input_path )
p_output = Path ( args . output_path )
2023-04-12 22:31:37 +02:00
motFile = MotFile ( )
2023-03-14 18:05:18 +01:00
if args . verbose :
logging . getLogger ( ) . setLevel ( logging . DEBUG )
2023-04-12 22:31:37 +02:00
if args . pack :
logging . info ( " ### Pack " )
if not p_input . is_dir ( ) :
raise Exception ( " Error - Invalid unpacked motFile folder path. " )
if p_output == Path ( " . " ) :
p_output = p_input . with_suffix ( " .bin " )
if p_output . is_file ( ) or p_output . is_dir ( ) :
raise Exception ( f " Error - { p_output } already exist. Please remove it before packing. " )
motFile . pack ( p_input , p_output )
elif args . unpack :
logging . info ( " ### Unpack " )
if not p_input . is_file ( ) :
raise Exception ( " Error - Invalid motFile file path. " )
if p_output == Path ( " . " ) :
p_output = p_input . parent / p_input . stem
if p_output . is_file ( ) or p_output . is_dir ( ) :
raise Exception ( f " Error - { p_output } already exist. Please remove it before unpacking. " )
motFile . unpack ( p_input , p_output )