Fix errors, add gamedb builder and PCMCIA card info

This commit is contained in:
spicyjpeg 2024-12-23 00:24:56 +01:00
parent 813f939bde
commit 4b57169e64
No known key found for this signature in database
GPG Key ID: 5CC87404C01DF393
17 changed files with 919 additions and 1203 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -128,7 +128,7 @@
"source": "${PROJECT_SOURCE_DIR}/data/fpga.bit"
},
{
"type": "db",
"type": "gamedb",
"name": "data/games.db",
"source": "${PROJECT_SOURCE_DIR}/data/games.json"
},

View File

@ -116,7 +116,7 @@
"source": "${PROJECT_SOURCE_DIR}/data/fpga.bit"
},
{
"type": "db",
"type": "gamedb",
"name": "data/games.db",
"source": "${PROJECT_SOURCE_DIR}/data/games.json"
},

View File

@ -23,7 +23,7 @@
"properties": {
"type": {
"title": "Entry type",
"description": "Must be 'empty', 'text', 'binary', 'tim', 'metrics', 'palette', 'strings' or 'db'.",
"description": "Must be 'empty', 'text', 'binary', 'tim', 'metrics', 'palette', 'strings' or 'gamedb'.",
"type": "string",
"enum": [
@ -34,7 +34,7 @@
"metrics",
"palette",
"strings",
"db"
"gamedb"
]
},
"name": {
@ -172,7 +172,7 @@
"additionalProperties": false,
"properties": {
"type": { "pattern": "^metrics|palette|strings|db$" },
"type": { "pattern": "^metrics|palette|strings|gamedb$" },
"name": { "type": "string" },
"compLevel": {},
@ -234,18 +234,18 @@
}
},
{
"required": [ "db" ],
"required": [ "gamedb" ],
"additionalProperties": false,
"properties": {
"type": { "const": "db" },
"type": { "const": "gamedb" },
"name": { "type": "string" },
"compLevel": {},
"strings": {
"title": "Game database",
"description": "Game database root object. If not specified, the source attribute must be a path to a JSON file containing this object.",
"type": "object"
"$ref": "gamedb.json"
}
}
}

View File

@ -151,7 +151,7 @@ size_t PackageProvider::loadData(util::Data &output, const char *path) {
if (!output.allocate(uncompLength + margin))
return 0;
auto compPtr = &output.as<uint8_t>() + margin;
auto compPtr = output.as<uint8_t>() + margin;
if (
(_file->seek(offset) != offset) ||

View File

@ -268,7 +268,7 @@ PortError Port::memoryCardWrite(const void *data, uint16_t lba) const {
if (exchangeBytes(
&checksum,
response,
ackResponse,
sizeof(checksum),
sizeof(ackResponse)
) < sizeof(ackResponse))

View File

@ -91,8 +91,8 @@ const char *const IDE_MOUNT_POINTS[]{ "ide0:", "ide1:" };
FileIOManager::FileIOManager(void)
: _resourceFile(nullptr), resourcePtr(nullptr), resourceLength(0) {
__builtin_memset(ideDevices, 0, sizeof(ideDevices));
__builtin_memset(ideProviders, 0, sizeof(ideProviders));
util::clear(ideDevices);
util::clear(ideProviders);
vfs.mount("resource:", &resource);
#ifdef ENABLE_PCDRV

View File

@ -34,6 +34,7 @@
#include "main/cart/cart.hpp"
#include "main/cart/cartdata.hpp"
#include "main/cart/cartio.hpp"
#include "main/formats.hpp"
#include "main/uibase.hpp"
#include "ps1/system.h"
@ -225,10 +226,12 @@ private:
ui::ScreenshotOverlay _screenshotOverlay;
ui::Context &_ctx;
fs::StringTable _stringTable;
FileIOManager _fileIO;
AudioStreamManager _audioStream;
formats::GameDB _gameDB;
formats::StringTable _stringTable;
Thread _workerThread;
util::Data _workerStack;
WorkerStatus _workerStatus;

View File

@ -91,17 +91,28 @@ enum SignatureFlag : uint8_t {
SIGNATURE_PAD_WITH_FF = 1 << 2
};
enum IOBoardType : uint8_t {
IO_BOARD_NONE = 0,
IO_BOARD_ANALOG = 1,
IO_BOARD_KICK = 2,
IO_BOARD_FISHING_REEL = 3,
IO_BOARD_DIGITAL = 4,
IO_BOARD_DDR_KARAOKE = 5,
IO_BOARD_GUNMANIA = 6
};
enum PCMCIADeviceType : uint8_t {
PCMCIA_NONE = 0,
PCMCIA_NETWORK_PCB = 1,
PCMCIA_FLASH_CARD_8 = 2,
PCMCIA_FLASH_CARD_16 = 3,
PCMCIA_FLASH_CARD_32 = 4,
PCMCIA_FLASH_CARD_64 = 5
};
enum GameFlag : uint8_t {
GAME_IO_BOARD_BITMASK = 7 << 0,
GAME_IO_BOARD_NONE = 0 << 0,
GAME_IO_BOARD_ANALOG = 1 << 0,
GAME_IO_BOARD_KICK = 2 << 0,
GAME_IO_BOARD_FISHING_REEL = 3 << 0,
GAME_IO_BOARD_DIGITAL = 4 << 0,
GAME_IO_BOARD_DDR_KARAOKE = 5 << 0,
GAME_IO_BOARD_GUNMANIA = 6 << 0,
GAME_INSTALL_RTC_HEADER_REQUIRED = 1 << 3,
GAME_RTC_HEADER_REQUIRED = 1 << 4
GAME_INSTALL_RTC_HEADER_REQUIRED = 1 << 0,
GAME_RTC_HEADER_REQUIRED = 1 << 1
};
/* Game database structures */
@ -130,12 +141,15 @@ struct GameInfo {
public:
char specifications[MAX_SPECIFICATIONS];
char regions[MAX_REGIONS][3];
char code[3];
char code[4];
uint8_t flags;
uint16_t nameOffset;
uint16_t year;
IOBoardType ioBoard;
PCMCIADeviceType pcmcia[2];
uint8_t flags;
ROMHeaderInfo rtcHeader, flashHeader;
CartInfo installCart, gameCart;
};
@ -145,15 +159,16 @@ public:
static constexpr size_t NUM_SORT_TABLES = 4;
enum SortOrder : uint8_t {
SORT_CODE = 0,
SORT_NAME = 1,
SORT_YEAR = 2
SORT_CODE = 0,
SORT_NAME = 1,
SORT_SERIES = 2,
SORT_YEAR = 3
};
class GameDBHeader {
public:
uint32_t magic[2];
uint16_t sortTableOffsets[NUM_SORT_TABLES];
uint16_t numEntries, numSortOrders;
inline bool validateMagic(void) const {
return (magic[0] == "573g"_c) && (magic[1] == "medb"_c);

View File

@ -26,30 +26,49 @@ from typing import Any
from common.analysis import MAMENVRAMDump, getBootloaderVersion
from common.cartparser import parseCartHeader, parseROMHeader
from common.decompile import AnalysisError
from common.gamedb import GameInfo
from common.gamedb import GameInfo, PCMCIADeviceType
from common.util import \
JSONFormatter, JSONGroupedArray, JSONGroupedObject, setupLogger
## Game analysis
_PCMCIA_CARD_TYPES: dict[int | None, PCMCIADeviceType] = {
None: PCMCIADeviceType.PCMCIA_NONE,
8: PCMCIADeviceType.PCMCIA_FLASH_CARD_8,
16: PCMCIADeviceType.PCMCIA_FLASH_CARD_16,
32: PCMCIADeviceType.PCMCIA_FLASH_CARD_32,
64: PCMCIADeviceType.PCMCIA_FLASH_CARD_64
}
def analyzeGame(game: GameInfo, nvramDir: Path, reanalyze: bool = False):
dump: MAMENVRAMDump = MAMENVRAMDump(nvramDir)
dump: MAMENVRAMDump = MAMENVRAMDump.fromDirectory(nvramDir)
if (reanalyze or game.bootloaderVersion is None) and dump.bootloader:
try:
game.bootloaderVersion = getBootloaderVersion(dump.bootloader)
except AnalysisError:
pass
if reanalyze or not game.pcmcia1:
game.pcmcia1 = _PCMCIA_CARD_TYPES[dump.pcmcia1Size]
if reanalyze or not game.pcmcia2:
game.pcmcia2 = _PCMCIA_CARD_TYPES[dump.pcmcia2Size]
if (reanalyze or game.rtcHeader is None) and dump.rtcHeader:
game.rtcHeader = parseROMHeader(dump.rtcHeader, True)
if (reanalyze or game.flashHeader is None) and dump.flashHeader:
game.flashHeader = parseROMHeader(dump.flashHeader)
if reanalyze or game.bootloaderVersion is None:
game.bootloaderVersion = None
if (reanalyze or game.installCart is None) and dump.installCart:
game.installCart = parseCartHeader(dump.installCart)
if (reanalyze or game.gameCart is None) and dump.gameCart:
game.gameCart = parseCartHeader(dump.gameCart)
if dump.bootloader:
try:
game.bootloaderVersion = getBootloaderVersion(dump.bootloader)
except AnalysisError:
pass
if reanalyze or game.rtcHeader is None:
game.rtcHeader = \
parseROMHeader(dump.rtcHeader, True) if dump.rtcHeader else None
if reanalyze or game.flashHeader is None:
game.flashHeader = \
parseROMHeader(dump.flashHeader) if dump.flashHeader else None
if reanalyze or game.installCart is None:
game.installCart = \
parseCartHeader(dump.installCart) if dump.installCart else None
if reanalyze or game.gameCart is None:
game.gameCart = \
parseCartHeader(dump.gameCart) if dump.gameCart else None
## Main

View File

@ -26,26 +26,40 @@ from typing import Any
import lz4.block
from common.assets import *
from common.util import normalizeFileName
from PIL import Image
## Asset conversion
def getJSONObject(asset: Mapping[str, Any], sourceDir: Path, key: str) -> dict:
if key in asset:
return asset[key]
with open(sourceDir / asset["source"], "rt", encoding = "utf-8") as file:
return json.load(file)
def processAsset(asset: Mapping[str, Any], sourceDir: Path) -> ByteString:
match asset.get("type", "file").strip():
case "empty":
return bytes(int(asset.get("size", 0)))
case "text" | "binary":
with open(sourceDir / asset["source"], "rb") as file:
data: ByteString = file.read()
case "text":
# The file is read in text mode and then encoded back to binary
# manually in order to translate any CRLF line endings to LF only.
with open(
sourceDir / asset["source"], "rt", encoding = "utf-8"
) as file:
return file.read().encode("utf-8")
return data
case "binary":
with open(sourceDir / asset["source"], "rb") as file:
return file.read()
case "tim":
ix: int = int(asset["imagePos"]["x"])
iy: int = int(asset["imagePos"]["y"])
cx: int = int(asset["clutPos"]["x"])
cy: int = int(asset["clutPos"]["y"])
cx: int = int(asset["clutPos"] ["x"])
cy: int = int(asset["clutPos"] ["y"])
image: Image.Image = Image.open(sourceDir / asset["source"])
image.load()
@ -58,49 +72,22 @@ def processAsset(asset: Mapping[str, Any], sourceDir: Path) -> ByteString:
return generateIndexedTIM(image, ix, iy, cx, cy)
case "metrics":
if "metrics" in asset:
metrics: dict = asset["metrics"]
else:
with open(
sourceDir / asset["source"], "rt", encoding = "utf-8"
) as file:
metrics: dict = json.load(file)
return generateFontMetrics(metrics)
return generateFontMetrics(
getJSONObject(asset, sourceDir, "metrics")
)
case "palette":
if "palette" in asset:
palette: dict = asset["palette"]
else:
with open(
sourceDir / asset["source"], "rt", encoding = "utf-8"
) as file:
palette: dict = json.load(file)
return generateColorPalette(palette)
return generateColorPalette(
getJSONObject(asset, sourceDir, "palette")
)
case "strings":
if "strings" in asset:
strings: dict = asset["strings"]
else:
with open(
sourceDir / asset["source"], "rt", encoding = "utf-8"
) as file:
strings: dict = json.load(file)
return generateStringTable(
getJSONObject(asset, sourceDir, "strings")
)
return generateStringTable(strings)
case "db":
if "db" in asset:
db: dict = asset["db"]
else:
with open(
sourceDir / asset["source"], "rt", encoding = "utf-8"
) as file:
db: dict = json.load(file)
# TODO: implement
return b""
case "gamedb":
return generateGameDB(getJSONObject(asset, sourceDir, "gamedb"))
case _type:
raise KeyError(f"unsupported asset type '{_type}'")
@ -153,6 +140,13 @@ def createParser() -> ArgumentParser:
"resource list by default)",
metavar = "dir"
)
group.add_argument(
"-e", "--export",
type = Path,
help = \
"Dump generated files (before compression) to specified path",
metavar = "dir"
)
group.add_argument(
"configFile",
type = FileType("rt", encoding = "utf-8"),
@ -179,11 +173,19 @@ def main():
fileData: bytearray = bytearray()
for asset in configFile["resources"]:
data: ByteString = processAsset(asset, sourceDir)
entry: PackageIndexEntry = \
PackageIndexEntry(len(fileData), 0, len(data))
name: str = asset["name"]
data: ByteString = processAsset(asset, sourceDir)
compLevel: int | None = asset.get("compLevel", args.compress_level)
if data and args.export:
args.export.mkdir(parents = True, exist_ok = True)
with open(args.export / normalizeFileName(name), "wb") as file:
file.write(data)
entry: PackageIndexEntry = \
PackageIndexEntry(len(fileData), 0, len(data))
compLevel: int | None = \
asset.get("compLevel", args.compress_level)
if data and compLevel:
data = lz4.block.compress(
@ -194,8 +196,8 @@ def main():
)
entry.compLength = len(data)
entries[asset["name"]] = entry
fileData += data
entries[name] = entry
fileData += data
while len(fileData) % args.align:
fileData.append(0)

View File

@ -16,6 +16,7 @@
import logging, re
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
from .cart import *
@ -25,16 +26,61 @@ from .util import InterleavedFile
## MAME NVRAM directory reader
_PCMCIA_CARD_SIZES: Sequence[int] = 8, 16, 32, 64
def _getPCMCIACardSize(path: Path, card: int) -> int | None:
for size in _PCMCIA_CARD_SIZES:
if (
(path / f"pccard{card}_konami_dual_slot3_{size}mb_1l").is_file() and
(path / f"pccard{card}_konami_dual_slot4_{size}mb_1l").is_file()
):
return size * 2
if (path / f"pccard{card}_{size}mb_1l").is_file():
return size
return None
def _loadCartDump(path: Path) -> CartDump | None:
try:
with open(path, "rb") as file:
return parseMAMECartDump(file.read())
except FileNotFoundError:
return None
@dataclass
class MAMENVRAMDump:
def __init__(self, nvramDir: Path):
pcmcia1Size: int | None = None
pcmcia2Size: int | None = None
rtcHeader: ROMHeaderDump | None = None
flashHeader: ROMHeaderDump | None = None
bootloader: PSEXEAnalyzer | None = None
installCart: CartDump | None = None
gameCart: CartDump | None = None
@staticmethod
def fromDirectory(path: Path):
try:
with open(path / "m48t58", "rb") as file:
file.seek(RTC_HEADER_OFFSET)
rtcHeader: ROMHeaderDump | None = ROMHeaderDump(
DumpFlag.DUMP_PUBLIC_DATA_OK,
b"",
file.read(RTC_HEADER_LENGTH)
)
except FileNotFoundError:
rtcHeader: ROMHeaderDump | None = None
try:
with InterleavedFile(
open(nvramDir / "29f016a.31m", "rb"),
open(nvramDir / "29f016a.27m", "rb")
open(path / "29f016a.31m", "rb"),
open(path / "29f016a.27m", "rb")
) as file:
file.seek(FLASH_HEADER_OFFSET)
self.flashHeader: ROMHeaderDump | None = ROMHeaderDump(
flashHeader: ROMHeaderDump | None = ROMHeaderDump(
DumpFlag.DUMP_PUBLIC_DATA_OK,
b"",
file.read(FLASH_HEADER_LENGTH)
@ -44,41 +90,27 @@ class MAMENVRAMDump:
file.seek(FLASH_EXECUTABLE_OFFSET)
try:
self.bootloader: PSEXEAnalyzer | None = PSEXEAnalyzer(file)
bootloader: PSEXEAnalyzer | None = PSEXEAnalyzer(file)
except AnalysisError:
self.bootloader: PSEXEAnalyzer | None = None
bootloader: PSEXEAnalyzer | None = None
except FileNotFoundError:
self.flashHeader: ROMHeaderDump | None = None
self.bootloader: PSEXEAnalyzer | None = None
flashHeader: ROMHeaderDump | None = None
bootloader: PSEXEAnalyzer | None = None
try:
with open(nvramDir / "m48t58", "rb") as file:
file.seek(RTC_HEADER_OFFSET)
self.rtcHeader: ROMHeaderDump | None = ROMHeaderDump(
DumpFlag.DUMP_PUBLIC_DATA_OK,
b"",
file.read(RTC_HEADER_LENGTH)
)
except FileNotFoundError:
self.rtcHeader: ROMHeaderDump | None = None
self.installCart: CartDump | None = \
self._loadCartDump(nvramDir / "cassette_install_eeprom")
self.gameCart: CartDump | None = \
self._loadCartDump(nvramDir / "cassette_game_eeprom")
def _loadCartDump(self, path: Path) -> CartDump | None:
try:
with open(path, "rb") as file:
return parseMAMECartDump(file.read())
except FileNotFoundError:
return None
return MAMENVRAMDump(
_getPCMCIACardSize(path, 1),
_getPCMCIACardSize(path, 2),
rtcHeader,
flashHeader,
bootloader,
_loadCartDump(path / "cassette_install_eeprom"),
_loadCartDump(path / "cassette_game_eeprom")
)
## Bootloader executable analysis
_BOOT_VERSION_REGEX: re.Pattern = \
re.compile(rb"\0BOOT VER[. ]*(1\.[0-9A-Z]+)\0")
re.compile(rb"\0BOOT VER\.? *(1\.[0-9A-Z]+)\0")
def getBootloaderVersion(exe: PSEXEAnalyzer) -> str:
for matched in _BOOT_VERSION_REGEX.finditer(exe.body):

View File

@ -17,12 +17,13 @@
from collections.abc import Generator, Mapping, Sequence
from dataclasses import dataclass
from struct import Struct
from typing import Any
from typing import Any, Callable
import numpy
from numpy import ndarray
from PIL import Image
from .util import \
from numpy import ndarray
from PIL import Image
from .gamedb import GAME_INFO_STRUCT, GameInfo
from .util import \
HashTableBuilder, StringBlobBuilder, colorFromString, hashData, \
roundUpToMultiple
@ -263,7 +264,7 @@ def generateStringTable(
for keyHash, string in _walkStringTree(strings):
hashTable.addEntry(keyHash, blob.addString(string))
tableLength: int = 0 \
blobOffset: int = 0 \
+ _STRING_TABLE_HEADER_STRUCT.size \
+ _STRING_TABLE_ENTRY_STRUCT.size * len(hashTable.entries)
@ -280,12 +281,62 @@ def generateStringTable(
else:
tableData += _STRING_TABLE_ENTRY_STRUCT.pack(
entry.fullHash,
tableLength + entry.data,
blobOffset + entry.data,
entry.chainIndex
)
return tableData + blob.data
## Game database generator
_GAMEDB_HEADER_STRUCT: Struct = Struct("< 8s 2H")
_GAMEDB_HEADER_MAGIC: bytes = b"573gmedb"
_GAMEDB_STRING_ALIGNMENT: int = 4
_GAMEDB_SORT_ORDERS: Sequence[Callable[[ GameInfo ], tuple]] = (
lambda game: ( game.code, game.name ), # SORT_CODE
lambda game: ( game.name, game.code ), # SORT_NAME
lambda game: ( game.series or "", game.code, game.name ), # SORT_SERIES
lambda game: ( game.year, game.code, game.name ) # SORT_YEAR
)
def generateGameDB(gamedb: Mapping[str, Any]) -> bytearray:
numEntries: int = len(gamedb["games"])
gameListOffset: int = 0 \
+ _GAMEDB_HEADER_STRUCT.size \
+ len(_GAMEDB_SORT_ORDERS) * 2 * numEntries
blobOffset: int = 0 \
+ gameListOffset \
+ GAME_INFO_STRUCT.size * numEntries
games: list[GameInfo] = []
blob: StringBlobBuilder = StringBlobBuilder(_GAMEDB_STRING_ALIGNMENT)
gameListData: bytearray = bytearray()
sortTableData: bytearray = bytearray()
sortTableData += _GAMEDB_HEADER_STRUCT.pack(
_GAMEDB_HEADER_MAGIC,
numEntries,
len(_GAMEDB_SORT_ORDERS)
)
for info in gamedb["games"]:
game: GameInfo = GameInfo.fromJSONObject(info)
name: bytes = game.name.encode("utf-8") + b"\0"
games.append(game)
gameListData += game.toBinary(blobOffset + blob.addString(name))
for sortOrder in _GAMEDB_SORT_ORDERS:
indices: list[int] = \
sorted(range(numEntries), key = lambda i: sortOrder(games[i]))
for index in indices:
offset: int = gameListOffset + GAME_INFO_STRUCT.size * index
sortTableData += offset.to_bytes(2, "little")
return sortTableData + gameListData + blob.data
## Package header generator
_PACKAGE_INDEX_HEADER_STRUCT: Struct = Struct("< 8s I 2H")

View File

@ -24,7 +24,7 @@ from .util import JSONGroupedObject
## Utilities
def _makeJSONObject(*groups: Mapping[str, Any]) -> JSONGroupedObject:
def _makeJSONObject(*groups: Mapping[str, Any]) -> JSONGroupedObject | None:
jsonObj: JSONGroupedObject = JSONGroupedObject()
for group in groups:
@ -37,7 +37,7 @@ def _makeJSONObject(*groups: Mapping[str, Any]) -> JSONGroupedObject:
if dest:
jsonObj.groups.append(dest)
return jsonObj
return jsonObj if jsonObj.groups else None
## Flags
@ -108,7 +108,7 @@ class HeaderFlag(IntFlag):
@staticmethod
def fromJSONObject(obj: Mapping[str, Any]) -> Self:
flags: HeaderFlag = 0
flags: HeaderFlag = HeaderFlag(0)
flags |= {
None: HeaderFlag.FORMAT_NONE,
@ -168,7 +168,7 @@ class ChecksumFlag(IntFlag):
@staticmethod
def fromJSONObject(obj: Mapping[str, Any]) -> Self:
flags: ChecksumFlag = 0
flags: ChecksumFlag = ChecksumFlag(0)
flags |= {
None: ChecksumFlag.CHECKSUM_WIDTH_NONE,
@ -222,7 +222,7 @@ class IdentifierFlag(IntFlag):
@staticmethod
def fromJSONObject(obj: Mapping[str, Any]) -> Self:
flags: IdentifierFlag = 0
flags: IdentifierFlag = IdentifierFlag(0)
flags |= {
None: IdentifierFlag.PRIVATE_TID_TYPE_NONE,
@ -275,7 +275,7 @@ class SignatureFlag(IntFlag):
@staticmethod
def fromJSONObject(obj: Mapping[str, Any]) -> Self:
flags: SignatureFlag = 0
flags: SignatureFlag = SignatureFlag(0)
flags |= {
None: SignatureFlag.SIGNATURE_TYPE_NONE,
@ -306,31 +306,74 @@ class SignatureFlag(IntFlag):
}
)
class IOBoardType(IntEnum):
IO_BOARD_NONE = 0
IO_BOARD_ANALOG = 1
IO_BOARD_KICK = 2
IO_BOARD_FISHING_REEL = 3
IO_BOARD_DIGITAL = 4
IO_BOARD_DDR_KARAOKE = 5
IO_BOARD_GUNMANIA = 6
@staticmethod
def fromJSONObject(obj: str | None) -> Self:
return {
None: IOBoardType.IO_BOARD_NONE,
"GX700-PWB(F)": IOBoardType.IO_BOARD_ANALOG,
"GX700-PWB(K)": IOBoardType.IO_BOARD_KICK,
"GE765-PWB(B)A": IOBoardType.IO_BOARD_FISHING_REEL,
"GX894-PWB(B)A": IOBoardType.IO_BOARD_DIGITAL,
"GX921-PWB(B)": IOBoardType.IO_BOARD_DDR_KARAOKE,
"PWB0000073070": IOBoardType.IO_BOARD_GUNMANIA
}[obj]
def toJSONObject(self) -> str | None:
return {
IOBoardType.IO_BOARD_NONE: None,
IOBoardType.IO_BOARD_ANALOG: "GX700-PWB(F)",
IOBoardType.IO_BOARD_KICK: "GX700-PWB(K)",
IOBoardType.IO_BOARD_FISHING_REEL: "GE765-PWB(B)A",
IOBoardType.IO_BOARD_DIGITAL: "GX894-PWB(B)A",
IOBoardType.IO_BOARD_DDR_KARAOKE: "GX921-PWB(B)",
IOBoardType.IO_BOARD_GUNMANIA: "PWB0000073070"
}[self]
class PCMCIADeviceType(IntEnum):
PCMCIA_NONE = 0
PCMCIA_NETWORK_PCB = 1
PCMCIA_FLASH_CARD_8 = 2
PCMCIA_FLASH_CARD_16 = 3
PCMCIA_FLASH_CARD_32 = 4
PCMCIA_FLASH_CARD_64 = 5
@staticmethod
def fromJSONObject(obj: str | None) -> Self:
return {
None: PCMCIADeviceType.PCMCIA_NONE,
"PWB0000100991": PCMCIADeviceType.PCMCIA_NETWORK_PCB,
"flashCard8MB": PCMCIADeviceType.PCMCIA_FLASH_CARD_8,
"flashCard16MB": PCMCIADeviceType.PCMCIA_FLASH_CARD_16,
"flashCard32MB": PCMCIADeviceType.PCMCIA_FLASH_CARD_32,
"flashCard64MB": PCMCIADeviceType.PCMCIA_FLASH_CARD_64
}[obj]
def toJSONObject(self) -> str | None:
return {
PCMCIADeviceType.PCMCIA_NONE: None,
PCMCIADeviceType.PCMCIA_NETWORK_PCB: "PWB0000100991",
PCMCIADeviceType.PCMCIA_FLASH_CARD_8: "flashCard8MB",
PCMCIADeviceType.PCMCIA_FLASH_CARD_16: "flashCard16MB",
PCMCIADeviceType.PCMCIA_FLASH_CARD_32: "flashCard32MB",
PCMCIADeviceType.PCMCIA_FLASH_CARD_64: "flashCard64MB"
}[self]
class GameFlag(IntFlag):
GAME_IO_BOARD_BITMASK = 7 << 0
GAME_IO_BOARD_NONE = 0 << 0
GAME_IO_BOARD_ANALOG = 1 << 0
GAME_IO_BOARD_KICK = 2 << 0
GAME_IO_BOARD_FISHING_REEL = 3 << 0
GAME_IO_BOARD_DIGITAL = 4 << 0
GAME_IO_BOARD_DDR_KARAOKE = 5 << 0
GAME_IO_BOARD_GUNMANIA = 6 << 0
GAME_INSTALL_RTC_HEADER_REQUIRED = 1 << 3
GAME_RTC_HEADER_REQUIRED = 1 << 4
GAME_INSTALL_RTC_HEADER_REQUIRED = 1 << 0
GAME_RTC_HEADER_REQUIRED = 1 << 1
@staticmethod
def fromJSONObject(obj: Mapping[str, Any]) -> Self:
flags: GameFlag = 0
flags |= {
None: GameFlag.GAME_IO_BOARD_NONE,
"GX700-PWB(F)": GameFlag.GAME_IO_BOARD_ANALOG,
"GX700-PWB(K)": GameFlag.GAME_IO_BOARD_KICK,
"GE765-PWB(B)A": GameFlag.GAME_IO_BOARD_FISHING_REEL,
"GX894-PWB(B)A": GameFlag.GAME_IO_BOARD_DIGITAL,
"GX921-PWB(B)": GameFlag.GAME_IO_BOARD_DDR_KARAOKE,
"PWB0000073070": GameFlag.GAME_IO_BOARD_GUNMANIA
}[obj.get("ioBoard", None)]
flags: GameFlag = GameFlag(0)
for key, flag in {
"installRequiresRTCHeader": GameFlag.GAME_INSTALL_RTC_HEADER_REQUIRED,
@ -344,16 +387,6 @@ class GameFlag(IntFlag):
def toJSONObject(self) -> JSONGroupedObject:
return _makeJSONObject(
{
"ioBoard": {
GameFlag.GAME_IO_BOARD_NONE: None,
GameFlag.GAME_IO_BOARD_ANALOG: "GX700-PWB(F)",
GameFlag.GAME_IO_BOARD_KICK: "GX700-PWB(K)",
GameFlag.GAME_IO_BOARD_FISHING_REEL: "GE765-PWB(B)A",
GameFlag.GAME_IO_BOARD_DIGITAL: "GX894-PWB(B)A",
GameFlag.GAME_IO_BOARD_DDR_KARAOKE: "GX921-PWB(B)",
GameFlag.GAME_IO_BOARD_GUNMANIA: "PWB0000073070"
}[self & GameFlag.GAME_IO_BOARD_BITMASK]
}, {
"installRequiresRTCHeader":
(GameFlag.GAME_INSTALL_RTC_HEADER_REQUIRED in self),
"requiresRTCHeader": (GameFlag.GAME_RTC_HEADER_REQUIRED in self)
@ -362,11 +395,12 @@ class GameFlag(IntFlag):
## Data structures
_ROM_HEADER_INFO_STRUCT: Struct = Struct("< 4s 2s 3B x")
_CART_INFO_STRUCT: Struct = Struct("< 8s 2s 6B")
_GAME_INFO_STRUCT: Struct = Struct("< 4s 36s 3s B 2H 10s 10s 16s 16s")
_MAX_SPECIFICATIONS: int = 4
_MAX_REGIONS: int = 12
ROM_HEADER_INFO_STRUCT: Struct = Struct("< 4s 2s 3B x")
CART_INFO_STRUCT: Struct = Struct("< 8s 2s 6B")
GAME_INFO_STRUCT: Struct = Struct("< 4s 36s 4s 2H 4B 10s 10s 16s 16s")
_MAX_SPECIFICATIONS: int = 4
_MAX_REGIONS: int = 12
@dataclass
class ROMHeaderInfo:
@ -401,7 +435,7 @@ class ROMHeaderInfo:
)
def toBinary(self) -> bytes:
return _ROM_HEADER_INFO_STRUCT.pack(
return ROM_HEADER_INFO_STRUCT.pack(
self.signatureField,
self.yearField,
self.headerFlags,
@ -454,7 +488,7 @@ class CartInfo:
)
def toBinary(self) -> bytes:
return _CART_INFO_STRUCT.pack(
return CART_INFO_STRUCT.pack(
self.dataKey,
self.yearField,
self.pcb,
@ -476,7 +510,10 @@ class GameInfo:
series: str | None
year: int
flags: GameFlag
ioBoard: IOBoardType
pcmcia1: PCMCIADeviceType
pcmcia2: PCMCIADeviceType
flags: GameFlag
bootloaderVersion: str | None = None
@ -502,7 +539,10 @@ class GameInfo:
obj.get("series", None),
obj["year"],
GameFlag.fromJSONObject(obj.get("flags", {})),
IOBoardType .fromJSONObject(obj.get("ioBoard", None)),
PCMCIADeviceType.fromJSONObject(obj.get("pcmcia1", None)),
PCMCIADeviceType.fromJSONObject(obj.get("pcmcia2", None)),
GameFlag .fromJSONObject(obj.get("flags", {})),
obj.get("bootloaderVersion", None),
@ -524,7 +564,10 @@ class GameInfo:
"series": self.series,
"year": self.year
}, {
"flags": self.flags.toJSONObject()
"ioBoard": self.ioBoard.toJSONObject(),
"pcmcia1": self.pcmcia1.toJSONObject(),
"pcmcia2": self.pcmcia2.toJSONObject(),
"flags": self.flags .toJSONObject()
}, {
"bootloaderVersion": self.bootloaderVersion
}, {
@ -552,18 +595,21 @@ class GameInfo:
# FIXME: identifiers, series and bootloaderVersion are not currently
# included in the binary format
return _GAME_INFO_STRUCT.pack(
b"".join(sorted(ord(spec[1]) for spec in self.specifications)),
return GAME_INFO_STRUCT.pack(
bytes(sorted(ord(spec[1]) for spec in self.specifications)),
b"".join(sorted(
region.encode("ascii").ljust(3, b"\0")
for region in self.regions
)),
self.code.encode("ascii"),
self.flags,
nameOffset,
self.year,
self.rtcHeader .toBinary(),
self.flashHeader.toBinary(),
self.installCart.toBinary(),
self.gameCart .toBinary()
self.ioBoard,
self.pcmcia1,
self.pcmcia2,
self.flags,
self.rtcHeader .toBinary() if self.rtcHeader else b"",
self.flashHeader.toBinary() if self.flashHeader else b"",
self.installCart.toBinary() if self.installCart else b"",
self.gameCart .toBinary() if self.gameCart else b""
)

View File

@ -51,7 +51,9 @@ def decodeSigned(value: int, bitLength: int) -> int:
# characters (' ', '$', '%', '*') excluded.
_BASE41_CHARSET: str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./:"
_COLOR_REGEX: re.Pattern = re.compile(r"^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$")
_NAME_INVALID_REGEX: re.Pattern = re.compile(r"[^0-9A-Z+,-._]", re.IGNORECASE)
_COLOR_REGEX: re.Pattern = \
re.compile(r"^#?([0-9A-F]{3}|[0-9A-F]{6})$", re.IGNORECASE)
def toPrintableChar(value: int) -> str:
if (value < 0x20) or (value > 0x7e):
@ -89,6 +91,9 @@ def decodeBase41(data: str) -> bytearray:
return output
def normalizeFileName(value: str) -> str:
return _NAME_INVALID_REGEX.sub("_", value)
def colorFromString(value: str) -> tuple[int, int, int]:
matched: re.Match | None = _COLOR_REGEX.match(value)

View File

@ -107,6 +107,8 @@ def main():
converted: bool = False
args.output.mkdir(parents = True, exist_ok = True)
#if os.path.isfile(args.input / "bios.bin"):
#copyfile(args.input / "bios.bin", args.output / "700a01.22g")
#converted = True