mirror of
https://github.com/spicyjpeg/573in1.git
synced 2025-02-02 12:37:19 +01:00
234 lines
5.8 KiB
Python
Executable File
234 lines
5.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
__version__ = "0.4.2"
|
|
__author__ = "spicyjpeg"
|
|
|
|
import json, logging, os, re
|
|
from argparse import ArgumentParser, FileType, Namespace
|
|
from pathlib import Path
|
|
from typing import ByteString, Mapping, TextIO
|
|
|
|
from common.cart import DumpFlag, ROMHeaderDump
|
|
from common.cartdata import *
|
|
from common.games import GameDB, GameDBEntry
|
|
from common.util import InterleavedFile, setupLogger
|
|
|
|
## Flash dump "parser"
|
|
|
|
_ROM_HEADER_LENGTH: int = 0x20
|
|
_MAME_SYSTEM_ID: bytes = bytes.fromhex("01 12 34 56 78 9a bc 3d")
|
|
|
|
def parseFlashDump(dump: bytes) -> ROMHeaderDump:
|
|
return ROMHeaderDump(
|
|
DumpFlag.DUMP_HAS_SYSTEM_ID | DumpFlag.DUMP_SYSTEM_ID_OK,
|
|
_MAME_SYSTEM_ID,
|
|
dump[0:_ROM_HEADER_LENGTH]
|
|
)
|
|
|
|
## Dump processing
|
|
|
|
def processDump(
|
|
dump: ROMHeaderDump, gameDB: GameDB, nameHints: Sequence[str] = [],
|
|
exportFile: TextIO | None = None
|
|
) -> ROMHeaderDBEntry:
|
|
parser: ROMHeaderParser = newROMHeaderParser(dump)
|
|
|
|
# If the parser could not find a valid game code in the dump, attempt to
|
|
# parse it from the provided hints.
|
|
if parser.region is None:
|
|
raise RuntimeError("can't parse game region from dump")
|
|
|
|
if parser.code is None:
|
|
for hint in nameHints:
|
|
code: re.Match | None = GAME_CODE_REGEX.search(
|
|
hint.upper().encode("ascii")
|
|
)
|
|
|
|
if code is not None:
|
|
parser.code = code.group().decode("ascii")
|
|
break
|
|
|
|
if parser.code is None:
|
|
raise RuntimeError(
|
|
"can't parse game code from dump nor from filename"
|
|
)
|
|
|
|
matches: list[GameDBEntry] = sorted(
|
|
gameDB.lookupByCode(parser.code, parser.region)
|
|
)
|
|
|
|
if exportFile:
|
|
_, flags = str(parser.flags).split(".", 1)
|
|
matchList: str = " ".join(
|
|
(game.mameID or f"[{game}]") for game in matches
|
|
)
|
|
|
|
exportFile.write(
|
|
f"{' '.join(nameHints)},"
|
|
f"{parser.code},"
|
|
f"{parser.region},"
|
|
f"{matchList},"
|
|
f"{parser.getFormatType().name},"
|
|
f"{flags}\n"
|
|
)
|
|
if not matches:
|
|
raise RuntimeError(
|
|
f"{parser.code} {parser.region} not found in game list"
|
|
)
|
|
|
|
# If more than one match is found, use the first result.
|
|
game: GameDBEntry = matches[0]
|
|
|
|
if game.hasSystemID() and game.flashLockedToIOBoard:
|
|
if not (parser.flags & DataFlag.DATA_HAS_SYSTEM_ID):
|
|
raise RuntimeError("game has a system ID but dump has no signature")
|
|
else:
|
|
if parser.flags & DataFlag.DATA_HAS_SYSTEM_ID:
|
|
raise RuntimeError("dump has a signature but game has no system ID")
|
|
|
|
logging.info(f"imported: {game.getFullName()}")
|
|
return ROMHeaderDBEntry(parser.code, parser.region, game.name, parser)
|
|
|
|
## Main
|
|
|
|
_FULL_DUMP_SIZE: int = 0x1000000
|
|
_EVEN_ODD_DUMP_SIZE: int = 0x200000
|
|
|
|
def createParser() -> ArgumentParser:
|
|
parser = ArgumentParser(
|
|
description = \
|
|
"Recursively scans a directory for subdirectories containing MAME "
|
|
"flash dumps, analyzes them and generates .db files.",
|
|
add_help = False
|
|
)
|
|
|
|
group = parser.add_argument_group("Tool options")
|
|
group.add_argument(
|
|
"-h", "--help",
|
|
action = "help",
|
|
help = "Show this help message and exit"
|
|
)
|
|
group.add_argument(
|
|
"-v", "--verbose",
|
|
action = "count",
|
|
help = "Enable additional logging levels"
|
|
)
|
|
|
|
group = parser.add_argument_group("File paths")
|
|
group.add_argument(
|
|
"-o", "--output",
|
|
type = Path,
|
|
default = os.curdir,
|
|
help = "Path to output directory (current directory by default)",
|
|
metavar = "dir"
|
|
)
|
|
group.add_argument(
|
|
"-e", "--export",
|
|
type = FileType("wt"),
|
|
help = "Export CSV table of all dumps parsed to specified path",
|
|
metavar = "file"
|
|
)
|
|
group.add_argument(
|
|
"gameList",
|
|
type = FileType("rt"),
|
|
help = "Path to JSON file containing game list"
|
|
)
|
|
group.add_argument(
|
|
"input",
|
|
type = Path,
|
|
nargs = "+",
|
|
help = "Paths to input directories"
|
|
)
|
|
|
|
return parser
|
|
|
|
def main():
|
|
parser: ArgumentParser = createParser()
|
|
args: Namespace = parser.parse_args()
|
|
setupLogger(args.verbose)
|
|
|
|
with args.gameList as _file:
|
|
gameList: Sequence[Mapping[str, Any]] = json.load(_file)
|
|
|
|
gameDB: GameDB = GameDB(gameList)
|
|
|
|
failures: int = 0
|
|
entries: list[ROMHeaderDBEntry] = []
|
|
|
|
if args.export:
|
|
args.export.write(
|
|
"# nameHints,code,region,matchList,formatType,flags\n"
|
|
)
|
|
|
|
for inputPath in args.input:
|
|
for rootDir, _, files in os.walk(inputPath):
|
|
root: Path = Path(rootDir)
|
|
|
|
for dumpName in files:
|
|
path: Path = root / dumpName
|
|
size: int = os.stat(path).st_size
|
|
|
|
match path.suffix.lower():
|
|
case ".31m":
|
|
oddPath: Path = Path(rootDir, f"{path.stem}.27m")
|
|
|
|
if not oddPath.is_file():
|
|
logging.warning(f"ignoring: {path}, no .27m file")
|
|
continue
|
|
if size != _EVEN_ODD_DUMP_SIZE:
|
|
logging.warning(f"ignoring: {path}, invalid size")
|
|
continue
|
|
|
|
with \
|
|
open(path, "rb") as even, \
|
|
open(oddPath, "rb") as odd:
|
|
data: ByteString = InterleavedFile(even, odd) \
|
|
.read(_ROM_HEADER_LENGTH)
|
|
|
|
case ".27m":
|
|
evenPath: Path = Path(rootDir, f"{path.stem}.31m")
|
|
|
|
if not evenPath.is_file():
|
|
logging.warning(f"ignoring: {path}, no .31m file")
|
|
|
|
continue
|
|
|
|
case _:
|
|
if size != _FULL_DUMP_SIZE:
|
|
logging.warning(f"ignoring: {path}, invalid size")
|
|
continue
|
|
|
|
with open(path, "rb") as _file:
|
|
data: ByteString = _file.read(_ROM_HEADER_LENGTH)
|
|
|
|
dump: ROMHeaderDump = parseFlashDump(data)
|
|
hints: Sequence[str] = dumpName, root.name
|
|
|
|
try:
|
|
entries.append(
|
|
processDump(dump, gameDB, hints, args.export)
|
|
)
|
|
except RuntimeError as exc:
|
|
logging.error(f"failed to import: {path}, {exc}")
|
|
failures += 1
|
|
|
|
if args.export:
|
|
args.export.close()
|
|
|
|
# Sort all entries and generate the .db file.
|
|
if not entries:
|
|
logging.warning("no entries generated")
|
|
return
|
|
|
|
entries.sort()
|
|
|
|
with open(args.output / "flash.db", "wb") as _file:
|
|
for entry in entries:
|
|
_file.write(entry.serialize())
|
|
|
|
logging.info(f"{len(entries)} entries saved, {failures} failures")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|