Improve FBNeo dat generator script (#1677)

* FBNeo_dat_gen: change text encoding, change year to releaseyear and pass parser where missing

* FBNeo_dat_gen: standardize cli args

* gitattributes: Add .py EOL LF

---------

Co-authored-by: Rob Loach <[email protected]>
This commit is contained in:
AramisandRob Loach authored and GitHub committed 2026-06-19 11:37:59 -04:00
1 parent 864ff2b27a
commit 4e74868001
2 files changed
+240 -235

No files matched your search

+1
View File
@@ -2,3 +2,4 @@
*.dat text eol=lf *.dat text eol=lf
*.xml text eol=lf *.xml text eol=lf
*.cht text eol=lf *.cht text eol=lf
*.py text eol=lf
+239 -235
View File
@@ -1,235 +1,239 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# FBNeo_dat_gen.py # FBNeo_dat_gen.py
# Written in 2017 by SpiralBrad <[email protected]> # Written in 2017 by SpiralBrad <[email protected]>
## usage: FBNeo_dat_gen.py [-h] -dat DAT -path PATH [-output_file OUTPUT_FILE] ## usage: FBNeo_dat_gen.py [-h] --dat DAT --path PATH [--output-file OUTPUT_FILE]
## [-header_name HEADER_NAME] ## [--header-name HEADER_NAME]
## [-header_description HEADER_DESCRIPTION] ## [--header-description HEADER_DESCRIPTION]
## [-header_version HEADER_VERSION] ## [--header-version HEADER_VERSION]
## ##
## Generates Final Burn Neo .dat file needed for RetroArch/libretro- ## Generates Final Burn Neo .dat file needed for RetroArch/libretro-
## db/c_converter ## db/c_converter
## ##
## optional arguments: ## optional arguments:
## -h, --help show this help message and exit ## -h, --help show this help message and exit
## -output_file OUTPUT_FILE ## -o, --output_file OUTPUT_FILE
## Path to the target output file; example: FBNeo - ## Path to the target output file; example: FBNeo -
## Arcade games.dat ## Arcade games.dat
## -header_name HEADER_NAME ## --header-name HEADER_NAME
## Override the clrmamepro(name) in the output .dat ## Override the clrmamepro(name) in the output .dat
## -header_description HEADER_DESCRIPTION ## --header-description HEADER_DESCRIPTION
## Override the clrmamepro(description) in the output ## Override the clrmamepro(description) in the output
## .dat ## .dat
## -header_version HEADER_VERSION ## --header-version HEADER_VERSION
## Override the clrmamepro(version) in the output .dat ## Override the clrmamepro(version) in the output .dat
## ##
## required arguments: ## required arguments:
## -dat DAT Misc -> Generate dat file -> Generate dat (Arcade ## -d, --dat DAT Misc -> Generate dat file -> Generate dat (Arcade
## only) ## only)
## -path PATH Path to a split, ClrMamePro verified and TorrentZipped ## -p, --path PATH Path to a split, ClrMamePro verified and TorrentZipped
## ROM set matching the Arcade only dat ## ROM set matching the Arcade only dat
## ##
## example usage: python3 FBA_dat_gen.py -dat "FBNeo v0.2.97.42 (ClrMame Pro ## example usage: python3 FBA_dat_gen.py --dat "FBNeo v0.2.97.42 (ClrMame Pro
## XML).dat" -path "/path/to/split/verified/torrentzipped/roms/" ## XML).dat" --path "/path/to/split/verified/torrentzipped/roms/"
## -output_file "FBNeo - Arcade Games.dat" ## --output-file "FBNeo - Arcade Games.dat"
#- To the extent possible under law, the author(s) have dedicated all #- To the extent possible under law, the author(s) have dedicated all
#- copyright and related and neighboring rights to this software to the #- copyright and related and neighboring rights to this software to the
#- public domain worldwide. This software is distributed without any #- public domain worldwide. This software is distributed without any
#- warranty. #- warranty.
#- #-
#- You should have received a copy of the CC0 Public Domain Dedication #- You should have received a copy of the CC0 Public Domain Dedication
#- along with this software. If not, see #- along with this software. If not, see
#- <http://creativecommons.org/publicdomain/zero/1.0/>. #- <http://creativecommons.org/publicdomain/zero/1.0/>.
import argparse import argparse
import hashlib import hashlib
import operator import operator
import os import os
import os.path import os.path
import sys import sys
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import zlib import zlib
def main(): def main():
parser = setup_argparse() parser = setup_argparse()
args = parser.parse_args() args = parser.parse_args()
dat_root = get_datroot(args.dat, parser) if parser == None:
header_name = get_header_name(args) print("parser is none")
header_version = get_header_version(args, dat_root, parser) dat_root = get_datroot(args.dat, parser)
header_description = get_header_description(args, header_version) header_name = get_header_name(args)
header = generate_dat_header(header_name, header_description, header_version) header_version = get_header_version(args, dat_root, parser)
game_list = generate_game_list(dat_root, args.path) header_description = get_header_description(args, header_version)
output(args, header, game_list) header = generate_dat_header(header_name, header_description, header_version)
game_list = generate_game_list(dat_root, args.path, parser)
def setup_argparse(): output(args, header, game_list)
"""Set up the argparse arguments and return the argparse instance"""
parser = argparse.ArgumentParser(prog='FBgen.py', description='Generates Final Burn Neo .dat file needed for RetroArch/libretro-db/c_converter') def setup_argparse():
required_arguments = parser.add_argument_group('required arguments') """Set up the argparse arguments and return the argparse instance"""
required_arguments.add_argument('-dat', help='Misc -> Generate dat file -> Generate dat (Arcade only)', required=True) parser = argparse.ArgumentParser(prog='FBgen.py', description='Generates Final Burn Neo .dat file needed for RetroArch/libretro-db/c_converter')
required_arguments.add_argument('-path', help='Path to a split, ClrMamePro verified and TorrentZipped ROM set matching the Arcade only dat', required=True) required_arguments = parser.add_argument_group('required arguments')
parser.add_argument('-output_file', help='Path to the target output file; example: FBNeo - Arcade games.dat') required_arguments.add_argument('-d', '--dat', help='Misc -> Generate dat file -> Generate dat (Arcade only)', required=True)
parser.add_argument('-header_name', help='Override the clrmamepro(name) in the output .dat') required_arguments.add_argument('-p', '--path', help='Path to a split, ClrMamePro verified and TorrentZipped ROM set matching the Arcade only dat', required=True)
parser.add_argument('-header_description', help='Override the clrmamepro(description) in the output .dat') parser.add_argument('-o', '--output-file', help='Path to the target output file; example: FBNeo - Arcade games.dat')
parser.add_argument('-header_version', help='Override the clrmamepro(version) in the output .dat') parser.add_argument('--header-name', help='Override the clrmamepro(name) in the output .dat')
parser.add_argument('--header-description', help='Override the clrmamepro(description) in the output .dat')
if len(sys.argv[1:])==0: parser.add_argument('--header-version', help='Override the clrmamepro(version) in the output .dat')
parser.print_help()
print('\n'.join(['', if len(sys.argv) == 1:
'example usage: python3 FBA_dat_gen.py -dat "FBNeo v0.2.97.42 (ClrMame Pro ', parser.print_help()
' XML).dat" -path "/path/to/split/verified/torrentzipped/roms/" ', print('\n'.join(['',
' -output_file "FBNeo - Arcade Games.dat"'])) 'example usage: python3 FBA_dat_gen.py --dat "FBNeo v0.2.97.42 (ClrMame Pro ',
parser.exit() ' XML).dat" -path "/path/to/split/verified/torrentzipped/roms/" ',
return parser ' --output-file "FBNeo - Arcade Games.dat"']))
parser.exit()
def get_datroot(dat, parser): return parser
"""Get the ElementTree root from the specified <dat> file and return the ElementTree root element"""
if not os.path.isfile(dat): def get_datroot(dat, parser):
print('File not found: ' + dat) """Get the ElementTree root from the specified <dat> file and return the ElementTree root element"""
print('') if not os.path.isfile(dat):
parser.print_help() print('File not found: ' + dat)
parser.exit() print('')
else: parser.print_help()
dat_tree = ET.parse(dat) parser.exit()
dat_root = dat_tree.getroot() else:
return dat_root dat_tree = ET.parse(dat)
dat_root = dat_tree.getroot()
def get_header_name(args): return dat_root
"""Return the default header name or the -header_name argument"""
if not args.header_name: def get_header_name(args):
header_name = 'FBNeo - Arcade Games' """Return the default header name or the -header_name argument"""
else: if not args.header_name:
header_name = args.header_name header_name = 'FBNeo - Arcade Games'
return header_name else:
header_name = args.header_name
def get_header_version(args, dat_root, parser): return header_name
"""Return the version from the <dat_root> header version, or the -header_version argument"""
if not args.header_version: def get_header_version(args, dat_root, parser):
header_version = dat_root.find('header').find('version').text """Return the version from the <dat_root> header version, or the -header_version argument"""
if not header_version: if not args.header_version:
print('Version not found in ClrMame Pro XML dat. Please specify with -header_version') header_version = dat_root.find('header').find('version').text
print('') if not header_version:
parser.print_help() print('Version not found in ClrMame Pro XML dat. Please specify with -header_version')
parser.exit() print('')
raise SystemExit() parser.print_help()
else: parser.exit()
header_version = args.header_version raise SystemExit()
return header_version else:
header_version = args.header_version
def get_header_description(args, version): return header_version
"""Return the description by generationg via the <version> or the -header_description argument"""
if not args.header_description: def get_header_description(args, version):
header_description = 'FBNeo v' + version + ' Arcade Games' """Return the description by generationg via the <version> or the -header_description argument"""
else: if not args.header_description:
header_description = args.header_description header_description = 'FBNeo v' + version + ' Arcade Games'
return header_description else:
header_description = args.header_description
def generate_dat_header(name, description, version): return header_description
"""Return the textual DAT header from a given <name>, <description>, and <version>"""
header = ['clrmamepro (', def generate_dat_header(name, description, version):
'\t' + 'name "%s"' % name, """Return the textual DAT header from a given <name>, <description>, and <version>"""
'\t' + 'description "%s"' % description, header = ['clrmamepro (',
'\t' + 'version %s' % version, '\t' + 'name "%s"' % name,
')'] '\t' + 'description "%s"' % description,
return '\n'.join(header) '\t' + 'version %s' % version,
')']
def generate_game_list(dat_root, path): return '\n'.join(header)
"""Generate the sorted list of games with all metadata and return a textual dat list"""
game_list = [] def generate_game_list(dat_root, path, parser):
"""Generate the sorted list of games with all metadata and return a textual dat list"""
if not os.path.isdir(path): game_list = []
print('Path not found: ' + path)
print('') if not os.path.isdir(path):
parser.print_help() print(f'Path is not dir or not found: {path}\n')
parser.exit() parser.print_help()
else: parser.exit()
game_entries = [] else:
for game in dat_root.iter('game'): game_entries = []
# 'gpriders.zip' kludge hack, as of at least 0.2.97.42 for game in dat_root.iter('game'):
# - rom is exact to 'gprider.zip' and does not exist in a split set # 'gpriders.zip' kludge hack, as of at least 0.2.97.42
if game.get('name') != 'gpriders': # - rom is exact to 'gprider.zip' and does not exist in a split set
# same 0.2.97.44 if game.get('name') != 'gpriders':
if game.get('name') != 'natodefa': # same 0.2.97.44
if game.get('name') != 'marioo': if game.get('name') != 'natodefa':
entry = GameEntry() if game.get('name') != 'marioo':
# set name ('description' in FBA-generated-dat) entry = GameEntry()
entry.name = game.find('description').text # set name ('description' in FBA-generated-dat)
# set year entry.name = game.find('description').text
entry.year = game.find('year').text # set year
# set publisher ('manufacturer' in FBA-generated-dat) entry.year = game.find('year').text
entry.publisher = game.find('manufacturer').text # set publisher ('manufacturer' in FBA-generated-dat)
# set zip filename entry.publisher = game.find('manufacturer').text
entry.zip = game.get('name') + '.zip' # set zip filename
zip_path = os.path.join(path, entry.zip) entry.zip = game.get('name') + '.zip'
# set zip size zip_path = os.path.join(path, entry.zip)
entry.size = os.path.getsize(zip_path) if not os.path.exists(zip_path):
# set zip crc print(f'Did not find "{zip_path}", skipping.')
entry.crc = get_crc(zip_path) continue
# set zip md5 # set zip size
entry.md5 = get_md5(zip_path) entry.size = os.path.getsize(zip_path)
# set zip sha1 # set zip crc
entry.sha1 = get_sha1(zip_path) entry.crc = get_crc(zip_path)
# Add to game_entries list # set zip md5
game_entries.append(entry) entry.md5 = get_md5(zip_path)
# set zip sha1
# Sort game_entries list entry.sha1 = get_sha1(zip_path)
game_entries.sort(key=operator.attrgetter('name')) # Add to game_entries list
game_entries.append(entry)
# Generate formatted textual list
for entry in game_entries: # Sort game_entries list
text_entry = ['game (', game_entries.sort(key=operator.attrgetter('name'))
'\t' + 'name "%s"' % entry.name,
'\t' + 'year "%s"' % entry.year, # Generate formatted textual list
'\t' + 'publisher "%s"' % entry.publisher, for entry in game_entries:
'\t' + 'rom ( name %s size %s crc %s md5 %s sha1 %s )' % (entry.zip, entry.size, entry.crc, entry.md5, entry.sha1), text_entry = ['game (',
')', '\t' + 'name "%s"' % entry.name,
''] '\t' + 'releaseyear "%s"' % entry.year,
game_list.append('\n'.join(text_entry)) '\t' + 'publisher "%s"' % entry.publisher,
return '\n'.join(game_list) '\t' + 'rom ( name %s size %s crc %s md5 %s sha1 %s )' % (entry.zip, entry.size, entry.crc, entry.md5, entry.sha1),
')',
def output(args, header, game_list): '']
"""Combine <header> and <game_list> and print to stdout, or write to specified -output_file""" game_list.append('\n'.join(text_entry))
if not args.output_file: return '\n'.join(game_list)
# print to stdout
print('\n'.join([header, '', game_list]).rstrip()) def output(args, header, game_list):
else: """Combine <header> and <game_list> and print to stdout, or write to specified --output-file"""
# write to args.output_file if not args.output_file:
with open(args.output_file, 'w') as f: # print to stdout
f.write('\n'.join([header, '', game_list])) print('\n'.join([header, '', game_list]).rstrip())
else:
class GameEntry: # write to args.output_file
name = None with open(args.output_file, 'w') as f:
year = None f.write('\n'.join([header, '', game_list]))
publisher = None
zip = None class GameEntry:
size = None name = None
crc = None year = None
md5 = None publisher = None
sha1 = None zip = None
size = None
def get_crc(file): crc = None
"""Return the CRC32 hash of <file>""" md5 = None
prev = 0 sha1 = None
for eachLine in open(file, 'rb'):
prev = zlib.crc32(eachLine, prev) def get_crc(file):
return str("%X"%(prev & 0xFFFFFFFF)).zfill(8) """Return the CRC32 hash of <file>"""
prev = 0
def get_md5(file): for eachLine in open(file, 'rb'):
"""Return the MD5 hash of <file>""" prev = zlib.crc32(eachLine, prev)
m = hashlib.md5() return str("%X"%(prev & 0xFFFFFFFF)).zfill(8)
with open(file, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b""): def get_md5(file):
m.update(chunk) """Return the MD5 hash of <file>"""
return m.hexdigest().zfill(32) m = hashlib.md5()
with open(file, 'rb') as f:
def get_sha1(file): for chunk in iter(lambda: f.read(4096), b""):
"""Return the SHA1 hash of <file>""" m.update(chunk)
m = hashlib.sha1() return m.hexdigest().zfill(32)
with open(file, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b""): def get_sha1(file):
m.update(chunk) """Return the SHA1 hash of <file>"""
return m.hexdigest().zfill(40) m = hashlib.sha1()
with open(file, 'rb') as f:
if __name__ == '__main__': for chunk in iter(lambda: f.read(4096), b""):
main() m.update(chunk)
return m.hexdigest().zfill(40)
if __name__ == '__main__':
main()